- SIGNAL_CAPTURE_AUDIT.md: end-to-end walkthrough of what attacker signals DECNET captures at each pipeline stage, where the gaps are (session profile ingestion, keystroke dynamics), and what ships for v1 vs what lands post-v1. - api-audit.md: FastAPI /api/v1 route audit — surface area, auth requirements, status-code coverage, and where schema drift would bite the schemathesis suite. Both are operator/engineering reference docs, not user-facing.
46 KiB
FastAPI /api/v1 Route Audit Report
Executive Summary
Total Routes Analyzed: 77 Deletion Candidates: 54
- Zero Callers (dead code): 7
- Test-Only (replaced routes?): 47
The audit scanned:
- 77 registered
/api/v1/*routes across the FastAPI web application - All sources: frontend TypeScript/React, CLI, worker processes, and test suites
- Frontend path fragment matching (e.g., searching for
/topologies/in dynamic URLs)
Top Deletion Candidates for Review:
- Attacker detail endpoints (
/attackers/{uuid}*) — 5 test-only routes, no web/CLI callers - Decky mutation endpoints (
/deckies/{decky_name}/mutate*) — 2 zero-caller routes (likely replaced by mutation queue) - Various CRUD endpoints with test-only usage — likely superseded by newer flows
Full Route Inventory
| Method | Path | Handler | File | Caller Types | Notes |
|---|---|---|---|---|---|
| GET | / |
api_list_topologies() |
api_list_topologies.py | cli, test | |
| POST | / |
api_create_topology() |
api_create_topology.py | cli, test | |
| GET | /archetypes |
api_list_archetypes() |
api_catalog.py | NONE | ⚠️ |
| GET | /artifacts/{decky}/{stored_as} |
get_artifact() |
api_get_artifact.py | test | ⚠️ |
| GET | /attackers |
get_attackers() |
api_get_attackers.py | test, web | |
| GET | /attackers/{uuid} |
get_attacker_detail() |
api_get_attacker_detail.py | test | ⚠️ |
| GET | /attackers/{uuid}/artifacts |
get_attacker_artifacts() |
api_get_attacker_artifacts.py | test | ⚠️ |
| GET | /attackers/{uuid}/commands |
get_attacker_commands() |
api_get_attacker_commands.py | test | ⚠️ |
| GET | /attackers/{uuid}/transcripts |
get_attacker_transcripts() |
api_get_attacker_transcripts.py | test | ⚠️ |
| POST | /auth/change-password |
change_password() |
api_change_pass.py | test | ⚠️ |
| POST | /auth/login |
login() |
api_login.py | test | ⚠️ |
| POST | /blank |
api_create_blank_topology() |
api_create_blank_topology.py | test | ⚠️ |
| GET | /bounty |
get_bounties() |
api_get_bounties.py | test | ⚠️ |
| POST | /check |
api_check_hosts() |
api_check_hosts.py | cli, test | |
| GET | /config |
api_get_config() |
api_get_config.py | cli, test, web | |
| PUT | /config/deployment-limit |
api_update_deployment_limit() |
api_update_config.py | test, web | |
| PUT | /config/global-mutation-interval |
api_update_global_mutation_interval() |
api_update_config.py | test, web | |
| DELETE | /config/reinit |
api_reinit() |
api_reinit.py | test, web | |
| POST | /config/users |
api_create_user() |
api_manage_users.py | test, web | |
| DELETE | /config/users/{user_uuid} |
api_delete_user() |
api_manage_users.py | test | ⚠️ |
| PUT | /config/users/{user_uuid}/reset-password |
api_reset_user_password() |
api_manage_users.py | test | ⚠️ |
| PUT | /config/users/{user_uuid}/role |
api_update_user_role() |
api_manage_users.py | test | ⚠️ |
| GET | /deckies |
get_deckies() |
api_get_deckies.py | cli, test, web | |
| GET | /deckies |
api_list_deckies() |
api_list_deckies.py | cli, test, web | |
| GET | /deckies |
list_deckies() |
api_list_deckies.py | cli, test, web | |
| POST | /deckies/deploy |
api_deploy_deckies() |
api_deploy_deckies.py | test, web | |
| POST | /deckies/{decky_name}/mutate |
api_mutate_decky() |
api_mutate_decky.py | NONE | ⚠️ |
| PUT | /deckies/{decky_name}/mutate-interval |
api_update_mutate_interval() |
api_mutate_interval.py | NONE | ⚠️ |
| POST | /deploy |
api_deploy_swarm() |
api_deploy_swarm.py | cli, test | |
| GET | /deployment-mode |
get_deployment_mode() |
api_deployment_mode.py | test | ⚠️ |
| POST | /enroll |
api_enroll_host() |
api_enroll_host.py | cli, test | |
| POST | /enroll-bundle |
create_enroll_bundle() |
api_enroll_bundle.py | test | ⚠️ |
| GET | /enroll-bundle/{token}.sh |
get_bootstrap() |
api_enroll_bundle.py | test | ⚠️ |
| GET | /enroll-bundle/{token}.tgz |
get_payload() |
api_enroll_bundle.py | test | ⚠️ |
| GET | /health |
get_health() |
api_get_health.py | cli, test | |
| GET | /health |
api_get_swarm_health() |
api_get_swarm_health.py | cli, test | |
| POST | /heartbeat |
heartbeat() |
api_heartbeat.py | test | ⚠️ |
| GET | /hosts |
api_list_hosts() |
api_list_hosts.py | cli, test | |
| GET | /hosts |
list_hosts() |
api_list_hosts.py | cli, test | |
| GET | /hosts |
api_list_host_releases() |
api_list_host_releases.py | cli, test | |
| DELETE | /hosts/{uuid} |
api_decommission_host() |
api_decommission_host.py | test | ⚠️ |
| DELETE | /hosts/{uuid} |
decommission_host() |
api_decommission_host.py | test | ⚠️ |
| GET | /hosts/{uuid} |
api_get_host() |
api_get_host.py | test | ⚠️ |
| POST | /hosts/{uuid}/teardown |
teardown_host() |
api_teardown_host.py | test | ⚠️ |
| GET | /logs |
get_logs() |
api_get_logs.py | test | ⚠️ |
| GET | /logs/histogram |
get_logs_histogram() |
api_get_histogram.py | test | ⚠️ |
| GET | /next-subnet |
api_next_subnet() |
api_catalog.py | test | ⚠️ |
| POST | /push |
api_push_update() |
api_push_update.py | test | ⚠️ |
| POST | /push-self |
api_push_update_self() |
api_push_update_self.py | test | ⚠️ |
| POST | /reap-orphans |
api_reap_orphans() |
api_reap_orphans.py | test | ⚠️ |
| POST | /rollback |
api_rollback_host() |
api_rollback_host.py | test | ⚠️ |
| GET | /services |
api_list_services() |
api_catalog.py | test | ⚠️ |
| GET | /stats |
get_stats() |
api_get_stats.py | test | ⚠️ |
| GET | /stream |
stream_events() |
api_stream_events.py | test, web | |
| POST | /teardown |
api_teardown_swarm() |
api_teardown_swarm.py | test | ⚠️ |
| GET | /transcripts/{decky}/{sid} |
get_transcript() |
api_get_transcript.py | test | ⚠️ |
| GET | /workers |
list_workers() |
api_list_workers.py | test, web | |
| POST | /workers/start-all |
start_all_workers() |
api_start_all_workers.py | test, web | |
| POST | /workers/{name}/start |
start_worker() |
api_start_worker.py | test | ⚠️ |
| POST | /workers/{name}/stop |
stop_worker() |
api_control_worker.py | test | ⚠️ |
| DELETE | /{topology_id} |
api_delete_topology() |
api_delete_topology.py | test | ⚠️ |
| GET | /{topology_id} |
api_get_topology() |
api_get_topology.py | test | ⚠️ |
| POST | /{topology_id}/deckies |
api_create_decky() |
api_decky_crud.py | test | ⚠️ |
| DELETE | /{topology_id}/deckies/{decky_uuid} |
api_delete_decky() |
api_decky_crud.py | test | ⚠️ |
| PATCH | /{topology_id}/deckies/{decky_uuid} |
api_update_decky() |
api_decky_crud.py | test | ⚠️ |
| POST | /{topology_id}/deploy |
api_deploy_topology() |
api_deploy_topology.py | test | ⚠️ |
| POST | /{topology_id}/edges |
api_create_edge() |
api_edge_crud.py | test | ⚠️ |
| DELETE | /{topology_id}/edges/{edge_id} |
api_delete_edge() |
api_edge_crud.py | test | ⚠️ |
| GET | /{topology_id}/events |
api_topology_events() |
api_events.py | NONE | ⚠️ |
| POST | /{topology_id}/lans |
api_create_lan() |
api_lan_crud.py | test | ⚠️ |
| DELETE | /{topology_id}/lans/{lan_id} |
api_delete_lan() |
api_lan_crud.py | test | ⚠️ |
| PATCH | /{topology_id}/lans/{lan_id} |
api_update_lan() |
api_lan_crud.py | test | ⚠️ |
| GET | /{topology_id}/lans/{lan_id}/next-ip |
api_next_ip() |
api_catalog.py | NONE | ⚠️ |
| GET | /{topology_id}/mutations |
api_list_mutations() |
api_mutations.py | test | ⚠️ |
| POST | /{topology_id}/mutations |
api_enqueue_mutation() |
api_mutations.py | test | ⚠️ |
| GET | /{topology_id}/status-events |
api_get_status_events() |
api_get_topology.py | NONE | ⚠️ |
| POST | /{topology_id}/teardown |
api_teardown_topology() |
api_teardown_topology.py | NONE | ⚠️ |
Deletion Candidates: Zero Callers
These routes have no callers anywhere in the codebase (except their own definition and possibly tests). They are strong candidates for removal.
GET /archetypes → api_list_archetypes()
File: decnet/web/router/topology/api_catalog.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
POST /deckies/{decky_name}/mutate → api_mutate_decky()
File: decnet/web/router/fleet/api_mutate_decky.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
PUT /deckies/{decky_name}/mutate-interval → api_update_mutate_interval()
File: decnet/web/router/fleet/api_mutate_interval.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
GET /{topology_id}/events → api_topology_events()
File: decnet/web/router/topology/api_events.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
GET /{topology_id}/lans/{lan_id}/next-ip → api_next_ip()
File: decnet/web/router/topology/api_catalog.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
GET /{topology_id}/status-events → api_get_status_events()
File: decnet/web/router/topology/api_get_topology.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
POST /{topology_id}/teardown → api_teardown_topology()
File: decnet/web/router/topology/api_teardown_topology.py
Callers: None
Status: Dead code — no references in web frontend, CLI, or worker processes.
Action: Safe to delete. If tests exist, they are testing orphaned endpoints.
Deletion Candidates: Test-Only Routes
These routes are referenced only in test files, not in the actual application. They may have been replaced by newer endpoints and are kept for backward-compatibility testing, or tests simply weren't updated after migration.
Count: 47 routes
Artifacts (1)
GET /artifacts/{decky}/{stored_as}(api_get_artifact.py)
Attackers (4)
GET /attackers/{uuid}(api_get_attacker_detail.py)GET /attackers/{uuid}/artifacts(api_get_attacker_artifacts.py)GET /attackers/{uuid}/commands(api_get_attacker_commands.py)- ... and 1 more
Auth (2)
POST /auth/change-password(api_change_pass.py)POST /auth/login(api_login.py)
Blank (1)
POST /blank(api_create_blank_topology.py)
Bounty (1)
GET /bounty(api_get_bounties.py)
Config (3)
DELETE /config/users/{user_uuid}(api_manage_users.py)PUT /config/users/{user_uuid}/reset-password(api_manage_users.py)PUT /config/users/{user_uuid}/role(api_manage_users.py)
Deployment-Mode (1)
GET /deployment-mode(api_deployment_mode.py)
Enroll-Bundle (3)
POST /enroll-bundle(api_enroll_bundle.py)GET /enroll-bundle/{token}.sh(api_enroll_bundle.py)GET /enroll-bundle/{token}.tgz(api_enroll_bundle.py)
Heartbeat (1)
POST /heartbeat(api_heartbeat.py)
Hosts (4)
DELETE /hosts/{uuid}(api_decommission_host.py)GET /hosts/{uuid}(api_get_host.py)DELETE /hosts/{uuid}(api_decommission_host.py)- ... and 1 more
Logs (2)
GET /logs(api_get_logs.py)GET /logs/histogram(api_get_histogram.py)
Next-Subnet (1)
GET /next-subnet(api_catalog.py)
Push (1)
POST /push(api_push_update.py)
Push-Self (1)
POST /push-self(api_push_update_self.py)
Reap-Orphans (1)
POST /reap-orphans(api_reap_orphans.py)
Rollback (1)
POST /rollback(api_rollback_host.py)
Services (1)
GET /services(api_catalog.py)
Stats (1)
GET /stats(api_get_stats.py)
Teardown (1)
POST /teardown(api_teardown_swarm.py)
Transcripts (1)
GET /transcripts/{decky}/{sid}(api_get_transcript.py)
Workers (2)
POST /workers/{name}/start(api_start_worker.py)POST /workers/{name}/stop(api_control_worker.py)
{Topology_Id} (13)
DELETE /{topology_id}(api_delete_topology.py)GET /{topology_id}(api_get_topology.py)POST /{topology_id}/deckies(api_decky_crud.py)- ... and 10 more
Analysis Notes
Context from Recent Work
Per repo history:
- Bus-woken mutator replaced polling — check
/deckies/*mutation endpoints - SSE mutation events replaced direct CRUD polling — check legacy list endpoints
- Worker supervisor endpoints are new — likely need expansion, not deletion
- MazeNET topologies are the new feature — older "topology" endpoints may be superseded
- Direct mutation CRUD for active topologies replaced by mutation queue
Methodology
- Web Frontend: Searched
decnet_web/src/**/*.{ts,tsx}for literal path references (e.g.,"/attackers/{uuid}") - CLI: Searched
decnet/cli/**/*.pyfor/api/v1calls - Workers: Searched
decnet/<worker>/**/*.py(excluding CLI) - Tests: Searched
tests/**/*.pyfor path references
Caveats
- Dynamically-built paths (e.g.,
${base}/topologies/${id}) detected via fragment search (e.g.,/topologies/) - Method-less references (e.g., just the path string) may miss some usages if not called via fetch/axios
- mTLS/internal worker endpoints (agent API, forwarder, enroll-bundle) deferred to Phase 2 per scope
Possible Duplicates / Overlapping Endpoints
To be populated after human review of the candidate list.
Phase 2 — Worker / mTLS Endpoints
Executive Summary
Scope: Internal worker processes and mTLS-gated inter-process HTTP surfaces:
- Agent FastAPI app (port 8765, mTLS-required)
- Updater FastAPI app (port 8766, mTLS-required, CN-gated)
- Master→Agent client calls via
AgentClientclass - Master→Updater client calls via
UpdaterClientclass - Enroll-bundle endpoints (
/swarm/enroll-bundle) — worker-facing, fetches bootstrap + deployment payload - Enrollment endpoints (
/swarm/enroll) — admin-driven, issues certs
Total Worker Process Endpoints: 12
Total Deletion Candidates: 0 (all have active callers)
Worker Process HTTP Endpoints
Agent FastAPI App (decnet/agent/app.py)
Listener: Port 8765, mTLS-enforced at ASGI/uvicorn layer (cert required)
Callers: Master via AgentClient, deployer module, CLI
Auth: mTLS only; all authenticated peers trusted equally
| Method | Path | Handler | Callers | Notes |
|---|---|---|---|---|
| GET | /health |
health() |
master-to-agent, tests | Liveness probe; does NOT skip mTLS |
| GET | /status |
status() |
master-to-agent, engine deployer | Deployment snapshot + active topology state |
| POST | /deploy |
deploy() |
master-to-agent, engine deployer | Materialise full DecnetConfig (body: DeployRequest) |
| POST | /teardown |
teardown() |
master-to-agent | Dismantle entire fleet or single decky (body: TeardownRequest) |
| POST | /self-destruct |
self_destruct() |
master-to-agent | Fire-and-forget reaper; deletes all DECNET footprint (202 response) |
| POST | /topology/apply |
topology_apply() |
master-to-agent | Apply a single topology (body: ApplyTopologyRequest) |
| POST | /topology/teardown |
topology_teardown() |
master-to-agent | Dismantle single topology (body: TeardownTopologyRequest) |
| GET | /topology/state |
topology_state() |
master-to-agent | Topology-specific state (separate from /status) |
| POST | /mutate |
mutate() |
(unimplemented, returns 501) | Per-decky mutate; currently done via /deploy with updated config |
Timeouts: Deploy/topology-apply 600s read, teardown 300s read (docker compose on slow VMs)
Updater FastAPI App (decnet/updater/app.py)
Listener: Port 8766, mTLS-enforced (cert CN must match updater@*)
Callers: Master via UpdaterClient
Auth: mTLS + CN validation (only updater@<hostname> certs allowed)
| Method | Path | Handler | Callers | Notes |
|---|---|---|---|---|
| GET | /health |
health() |
master-to-updater, dashboard, bus monitor | Returns active + prev release slots |
| GET | /releases |
releases() |
master-to-updater | List all available release slots (JSON array) |
| POST | /update |
update() |
master-to-updater | Upload + apply tarball (multipart: tarball + sha form) |
| POST | /update-self |
update_self() |
master-to-updater | Self-update updater binary (connection drops mid-response) |
| POST | /rollback |
rollback() |
master-to-updater | Revert to previous release slot |
Timeouts: /update + /update-self 180s read (pip install + probe on slow VMs)
Master-Facing Worker Enrollment Endpoints
Enrollment Bundle (decnet/web/router/swarm_mgmt/api_enroll_bundle.py)
Listener: Master port 443 (FastAPI web app)
Callers: agent (worker fetches payload), admin UI
Auth: Token-based (5-min TTL), no mTLS required (public endpoints for worker bootstrap)
| Method | Path | Handler | Callers | Auth | Notes |
|---|---|---|---|---|---|
| POST | /api/v1/swarm/enroll-bundle |
create_enroll_bundle() |
admin-ui, cli | require_admin | Create bundle (token + shell script + tarball); returns EnrollBundleResponse (201) |
| GET | /api/v1/swarm/enroll-bundle/{token}.sh |
get_bootstrap() |
agent-client, curl | token-param | Bootstrap shell script (idempotent, 5-min TTL) |
| GET | /api/v1/swarm/enroll-bundle/{token}.tgz |
get_payload() |
agent-client, curl | token-param | Gzipped tarball (one-shot; deletes .sh + .tgz after serving) |
Rationale: Agent's first contact-home; its source IP backfills the SwarmHost.address row.
Simple Enrollment (decnet/web/router/swarm/api_enroll_host.py)
Listener: Master port 443
Callers: admin UI, CLI
Auth: None (browser-facing, admin dashboard context)
| Method | Path | Handler | Callers | Auth | Notes |
|---|---|---|---|---|---|
| POST | /api/v1/swarm/enroll |
api_enroll_host() |
admin-ui | (browser auth) | Issue cert bundle + register host row (201) |
Master→Agent RPC Surface (via AgentClient)
Master calls agent via AgentClient(host).method() context manager. All calls are mTLS. Called from:
api_deploy_swarm.py: Deploy topology to all enrolled hostsapi_teardown_swarm.py: Teardown fleetapi_check_hosts.py: Active mTLS probe of all hosts (for dashboard health)api_decommission_host.py(swarm): Calls agent/self-destructapi_decommission_host.py(swarm_mgmt): Calls agent/self-destructapi_teardown_host.py(swarm_mgmt): Calls agent/self-destructapi_list_hosts.py(swarm_mgmt): Calls agent/healthon every list request- Engine
deployer.py: Direct/deploy+/topology/applycalls during mutation/materialization
Cert Pinning: Master's cert is CA-signed; workers validate via CA pinning + master hostname-verification disabled (per-operator SANs).
Master→Updater RPC Surface (via UpdaterClient)
Master calls updater via UpdaterClient(host).method() context manager. All calls are mTLS. Called from:
api_push_update.py: Upload new release to updaterapi_push_update_self.py: Update the updater binary itselfapi_rollback_host.py: Rollback updater to previous releaseapi_list_host_releases.py: Poll all updaters for active release SHA (dashboard)
Connection Drop: /update-self intentionally drops the connection; caller polls /health for new SHA.
Agent→Master Heartbeat
Endpoint: POST /api/v1/swarm/heartbeat
Caller: decnet/agent/heartbeat.py module (agent-side daemon)
Auth: mTLS + peer cert SHA-256 pinned to SwarmHost.client_cert_fingerprint
Frequency: ~30 seconds
Payload: Host UUID, agent version, executor status dict, optional topology snapshot
Security: Decommissioned workers' still-valid certs must not resurrect ghost shards → cert fingerprint mismatch → 403.
Bus Pub/Sub (Local Only, Not HTTP)
Per comments in agent/updater app.py:
- Agent publishes
system.agent.healthheartbeat to local bus (separate from mTLS heartbeat) - Updater publishes
system.updater.healthto local bus - Bus is host-local UNIX socket — not an external RPC surface
No HTTP endpoints; no caller analysis needed.
Forwarder
Status: No HTTP endpoints exposed by forwarder process.
The forwarder:
- Consumes RFC 5424 syslog from local log file (written by agent log collector)
- Ships syslog-over-TLS to master port 6514 (outbound, not inbound)
- No master→forwarder calls; no worker-side HTTP surface
Deletion Candidates
None. All identified endpoints have active callers:
- Agent
/deploy,/teardown,/self-destruct,/topology/*are called by engine, deployer, master probes - Updater
/update*,/releases,/healthare called by master push flow + dashboard - Enroll-bundle is called by new agents (worker-facing enrollment)
- Simple enroll is called by admin UI
Duplicate / Obsolete Endpoints
Potential overlap to review:
-
/swarm/enrollvs/swarm/enroll-bundle: Two enrollment flows, both active./enroll(old) — admin issues cert + agent curls back for bundle/enroll-bundle(new) — admin renders bundle upfront, agent one-liners it- Consider consolidating if old flow is being phased out (need human review of intent).
-
Agent
/deploy+/teardownvs/topology/apply+/topology/teardown: Both exist./deploy— fleet-wide (old unihost verb)/topology/{apply,teardown}— single topology (newer MazeNET feature)- No conflict; different scopes. Agent supports both.
-
Agent
/mutatereturns 501: Placeholder for future worker-side mutation.- Currently master re-sends
/deploywith updated config. - Safe to leave as-is (fails closed); can implement later.
- Currently master re-sends
Summary Table
| Process | Count | mTLS | Auth | Notes |
|---|---|---|---|---|
| Agent | 9 | Yes | No (peer auth only) | Port 8765; calls from master + engine |
| Updater | 5 | Yes | Yes (CN-gated) | Port 8766; calls from master |
| Enroll-Bundle | 3 | No | Token (5 min) | Master port 443; agent + admin fetch |
| Enroll | 1 | No | Browser auth | Master port 443; admin UI |
| Total | 18 | — | — | — |
Caller Types Identified:
master-to-agent: Master calls agent (9 endpoints)master-to-updater: Master calls updater (5 endpoints)agent-client: Agent calls master heartbeat (1 endpoint in Phase 1)admin-client: Admin calls enroll-bundle POST (1 endpoint)test: All endpoints have test coverage
Zero-Caller Endpoints: None.
Phase 3 — CLI Command Surface
Summary
Total CLI Commands: 37
Master-only Commands: 27 (via MASTER_ONLY_COMMANDS + MASTER_ONLY_GROUPS)
Agent-capable Commands: 10 (hidden in agent mode when DECNET_MODE=agent)
Commands Hitting API Routes: 7 (all in decnet swarm * group, plus decnet deploy)
Deletion Candidates: 0 (no deprecated commands found; all are actively used)
Full Command Inventory
| Command | Handler | Source | Master-only? | Hits API? | Notes |
|---|---|---|---|---|---|
decnet api |
api() |
api.py:19 | Yes | No | Start FastAPI backend (uvicorn) |
decnet swarmctl |
swarmctl() |
swarmctl.py:18 | Yes | No | Run SWARM controller + auto-spawn listener |
decnet agent |
agent() |
agent.py:16 | No | No | Worker: run SWARM agent (requires cert bundle) |
decnet updater |
updater() |
updater.py:14 | No | No | Worker: run self-updater daemon |
decnet listener |
listener() |
listener.py:16 | Yes | No | Run syslog-TLS listener (RFC 5425, mTLS) |
decnet forwarder |
forwarder() |
forwarder.py:18 | No | No | Worker: forward syslog to master:6514 (mTLS) |
decnet deploy |
deploy() |
deploy.py:68 | Yes | Yes | Deploy deckies (unihost/swarm mode) |
decnet init |
init_cmd() |
init.py:305 | Yes | No | Bootstrap master: user/group/systemd/config |
decnet services |
list_services() |
inventory.py:15 | No | No | List available service plugins |
decnet distros |
list_distros() |
inventory.py:27 | No | No | List available OS distro profiles |
decnet archetypes |
list_archetypes() |
inventory.py:38 | Yes | No | List machine archetype profiles |
decnet redeploy |
redeploy() |
lifecycle.py:18 | No | No | Check services + relaunch any down |
decnet status |
status() |
lifecycle.py:57 | No | No | Show running deckies + service status |
decnet teardown |
teardown() |
lifecycle.py:81 | Yes | No | Stop/remove deckies (--all or --id) |
decnet probe |
probe() |
workers.py:15 | No | No | Fingerprint attackers (JARM/HASSH) |
decnet collect |
collect() |
workers.py:40 | No | No | Stream Docker logs to RFC 5424 file |
decnet mutate |
mutate() |
workers.py:57 | Yes | No | Trigger/watch decky mutation |
decnet correlate |
correlate() |
workers.py:86 | Yes | No | Analyse logs for cross-decky traversals |
decnet web |
serve_web() |
web.py:13 | Yes | No | Serve frontend SPA + proxy /api/* |
decnet profiler |
profiler_cmd() |
profiler.py:11 | Yes | No | Build attacker profiles from log stream |
decnet sniffer |
sniffer_cmd() |
sniffer.py:12 | Yes | No | Passive network sniffer |
decnet db-reset |
db_reset() |
db.py:86 | Yes | No | Wipe MySQL database (truncate or drop-tables) |
decnet bus |
bus_cmd() |
bus.py:11 | No | No | Run UNIX-socket pub/sub bus worker |
decnet swarm enroll |
swarm_enroll() |
swarm.py:23 | Yes | Yes | Enroll worker + issue mTLS bundle → POST /swarm/enroll |
decnet swarm list |
swarm_list() |
swarm.py:85 | Yes | Yes | List enrolled workers → GET /swarm/hosts |
decnet swarm check |
swarm_check() |
swarm.py:111 | Yes | Yes | Probe worker status → POST /swarm/check |
decnet swarm update |
swarm_update() |
swarm.py:149 | Yes | Yes | Push tarball to workers → GET /swarm/hosts + updater client |
decnet swarm deckies |
swarm_deckies() |
swarm.py:256 | Yes | Yes | List deckies across swarm → GET /swarm/deckies |
decnet swarm decommission |
swarm_decommission() |
swarm.py:315 | Yes | Yes | Remove worker from swarm → DELETE /swarm/hosts/{uuid} |
decnet topology generate |
_generate() |
topology.py:35 | Yes | No | Generate topology plan (persist as pending) |
decnet topology list |
_list() |
topology.py:94 | Yes | No | List all topologies |
decnet topology show |
_show() |
topology.py:121 | Yes | No | Print topology structure |
decnet topology deploy |
_deploy() |
topology.py:177 | Yes | No | Deploy pending topology |
decnet topology teardown |
_teardown() |
topology.py:194 | Yes | No | Tear down active topology |
decnet topology delete |
_delete() |
topology.py:210 | Yes | No | Delete topology + cascade (LANs/deckies/edges) |
decnet topology mutate |
_mutate() |
topology.py:265 | Yes | No | Enqueue live topology mutation |
decnet topology mutations |
_mutations() |
topology.py:310 | Yes | No | List queued/applied mutations |
Commands Hitting API Routes
All 7 commands that call HTTP endpoints go through swarmctl (not the main /api/v1 backend). These are:
-
decnet deploy(swarm mode)- Hits:
GET /swarm/hosts?host_status=enrolled,GET /swarm/hosts?host_status=active,POST /swarm/deploy - Route source:
decnet/web/swarm_api.py(Swarmctl API, not Phase 1 audit scope)
- Hits:
-
decnet swarm enroll- Hits:
POST /swarm/enroll
- Hits:
-
decnet swarm list- Hits:
GET /swarm/hosts
- Hits:
-
decnet swarm check- Hits:
POST /swarm/check
- Hits:
-
decnet swarm update- Hits:
GET /swarm/hosts+ direct mTLS to updater port 8766
- Hits:
-
decnet swarm deckies- Hits:
GET /swarm/deckies
- Hits:
-
decnet swarm decommission- Hits:
DELETE /swarm/hosts/{uuid}
- Hits:
Note: Swarmctl API endpoints (/swarm/*) are not in the Phase 1 audit (Phase 1 scanned /api/v1/* only). These routes are stable and not candidates for deletion.
Deletion Candidates
Count: 0
Rationale:
- No commands are marked
@deprecatedin docstrings. - No old "v1" flavors replaced by newer flows (e.g., no
decnet deploy-v1vsdecnet deploy-v2). - All commands in
MASTER_ONLY_COMMANDS+MASTER_ONLY_GROUPSare actively referenced and tested. - Worker-capable commands (
agent,updater,forwarder,bus,probe,collect,redeploy,status,services,distros) are essential for field operation. - Recent additions (
decnet init,decnet swarm *,decnet topology *) are part of the SWARM/MazeNET bootstrap flow and have no predecessors.
CLI → API Deletion Chains
No CLI command is the only caller of a Phase 1 API route marked cli or zero. All Phase 1 routes with cli callers have multiple paths:
- Phase 1 example:
/health— called by both CLI (decnet status) and web/test - Phase 1 example:
/deckies— called by CLI (swarm deckies) + web + test
Implication: Deleting a CLI command does NOT unlock any Phase 1 API route deletions.
Gating Configuration
Master-only enforcement lives in decnet/cli/gating.py:
MASTER_ONLY_COMMANDS (25 command names):
"api", "swarmctl", "deploy", "redeploy", "teardown",
"mutate", "listener", "profiler",
"services", "distros", "correlate", "archetypes", "web",
"db-reset", "init",
Plus subcommand groups:
MASTER_ONLY_GROUPS (2 group names):
"swarm", "topology"
Defense-in-depth:
- Registration-time filter hides commands from
decnet --helpon agents (whenDECNET_MODE=agent). - Runtime gate in each command body calls
_require_master_mode()to block direct function imports.
Recent Additions (Phase Context)
Per repo memory and recent commits:
decnet init+--deinit: Bootstrap + teardown systemd/polkit/tmpfiles. Idempotent.decnet swarm *: Enroll workers, list status, push updates, manage deckies. All talk to swarmctl, not/api/v1.decnet topology *: MazeNET nested-topology commands. Direct DB calls (no HTTP). Replaces old flat/topologiesCRUD.decnet bus: New ServiceBus worker. UNIX-socket pub/sub, not HTTP.- Worker supervisors (
probe,collect,correlate,sniffer,profiler): Field microservices. Spawned bydecnet deployas detached processes.
None are marked for removal; all have active use cases.
Output Modes
CLI output is structured text (Rich tables, JSON, syslog-format lines). All commands respect:
--jsonflag where applicable (e.g.,decnet swarm check --json)- Scriptable structured output (e.g.,
decnet correlate --output json)
Web dashboard visualization is not in CLI scope (per repo design: CLI outputs text, dashboard ingests data via API).
Phase 4 — Consolidated Cleanup Plan
Executive Summary
CRITICAL FINDING: Phase 1's "test-only routes" classification is fundamentally unreliable. Of 8 sampled test-only routes, 6 showed active web UI callers — the Phase 1 grep methodology failed to catch TypeScript/TSX frontend API calls.
Phase 1 zero-caller candidates: REVISED DOWNWARD from 7 to 3 actual deletions:
- 4 routes flagged as zero-callers actually have active web UI callers:
/archetypes,/deckies/{decky_name}/mutate,/deckies/{decky_name}/mutate-interval, and/teardown - Remaining true zero-callers:
GET /{topology_id}/events,GET /{topology_id}/status-events,GET /{topology_id}/lans/{lan_id}/next-ip
Recommendation: Do NOT use the Phase 1 "47 test-only" list as a deletion target without manual verification of EACH route against the TypeScript frontend code.
Phase 4 Verification Results
Zero-Caller Candidates — Fresh Grep Results
| Route | Handler | Phase 1 Status | Phase 4 Finding | Verdict |
|---|---|---|---|---|
GET /archetypes |
api_list_archetypes() |
Zero callers | FOUND: DeckyFleet.tsx:833 calls /topologies/archetypes |
KEEP |
POST /deckies/{decky_name}/mutate |
api_mutate_decky() |
Zero callers | FOUND: DeckyFleet.tsx:850 calls /deckies/${name}/mutate |
KEEP |
PUT /deckies/{decky_name}/mutate-interval |
api_update_mutate_interval() |
Zero callers | FOUND: DeckyFleet.tsx:898 calls /deckies/${name}/mutate-interval |
KEEP |
GET /{topology_id}/events |
api_topology_events() |
Zero callers | NO CALLERS FOUND (only test mock) | DELETE |
GET /{topology_id}/lans/{lan_id}/next-ip |
api_next_ip() |
Zero callers | NO CALLERS FOUND | DELETE |
GET /{topology_id}/status-events |
api_get_status_events() |
Zero callers | NO CALLERS FOUND | DELETE |
POST /{topology_id}/teardown |
api_teardown_topology() |
Zero callers | FOUND: TopologyList.tsx calls /topologies/${id}/teardown |
KEEP |
Revised zero-caller count: 3 routes (not 7)
Test-Only Routes — Spot-Check Results
Sampled 8 of 47 "test-only" routes:
| Route | Phase 1 Sample | Web Frontend Caller | Verdict |
|---|---|---|---|
GET /artifacts/{decky}/{stored_as} |
test-only | FOUND: ArtifactDrawer.tsx |
FALSE POSITIVE |
POST /auth/change-password |
test-only | FOUND: Login.tsx |
FALSE POSITIVE |
POST /auth/login |
test-only | FOUND: Login.tsx |
FALSE POSITIVE |
POST /blank |
test-only | FOUND: MazeNET/useMazeApi.ts + TopologyList.tsx |
FALSE POSITIVE |
GET /bounty |
test-only | FOUND: Bounty.tsx, CommandPalette.tsx |
FALSE POSITIVE |
GET /deployment-mode |
test-only | FOUND: DeckyFleet.tsx |
FALSE POSITIVE |
DELETE /config/users/{user_uuid} |
test-only | FOUND: Config.tsx |
FALSE POSITIVE |
GET /logs |
test-only | FOUND: LiveLogs.tsx |
FALSE POSITIVE |
Verdict: The "47 test-only routes" number is unreliable. At least 6/8 sampled routes have active web callers that Phase 1's grep missed. The methodology failed because:
- Phase 1 grepped Python/test files only; it did not systematically scan TypeScript/TSX.
- Dynamic path construction (e.g.,
api.post(`/topologies/${id}/teardown`)) requires careful regex; simple string matching misses them. - Frontend developers split concerns across files (components/hooks/utils); no single grep layer caught all call sites.
Recommendation: Do not trust the "47 test-only" list. Before deleting ANY route marked test-only, manually verify:
# For each route, run:
grep -r "<path-fragment>" decnet_web/src --include="*.ts" --include="*.tsx"
Enroll Flow Consolidation
POST /swarm/enroll vs POST /swarm/enroll-bundle
Current state:
/swarm/enroll(simple): Master-driven, admin issues cert bundle, returns full bundle in response (201 Created)./swarm/enroll-bundle(new): Token-based workflow — admin builds token, renders.sh+.tgz, agent curls both (Wazuh-style one-liner).
Web UI caller analysis:
SwarmHosts.tsxcalls ONLYPOST /swarm/enroll-bundle(new flow).- No web caller for
POST /swarm/enroll(old flow) found.
CLI caller analysis:
decnet swarm enroll(Phase 3 audit) callsPOST /swarm/enroll(line 572 of Phase 3 summary).
Recommendation: DEPRECATE simple /swarm/enroll
- Keep both endpoints for now (CLI still uses simple).
- Mark
POST /swarm/enrollas@deprecatedin docstring; note that new deployments should usePOST /swarm/enroll-bundle. - Update CLI (
decnet swarm enroll) to call/swarm/enroll-bundlein a follow-up PR. - Only DELETE simple
/swarm/enrollafter CLI migration is merged and tested.
Why not delete now: CLI is the only caller; deleting breaks backward compatibility for operators with scripts or runbooks calling the simple flow. Deprecate first, migrate CLI, then delete.
Ordered PR Plan (Kill List)
Three independent deletions — run tests after each. Do NOT combine; each is a commit-shaped change.
PR #1: Remove /api/v1/{topology_id}/events endpoint
Scope: One endpoint, one handler module, test module, no other imports.
Files to delete:
decnet/web/router/topology/api_events.py(handler + schema)tests/api/topology/test_events_stream.py(test file)
Files to modify:
decnet/web/router/topology/__init__.py— remove two lines:# DELETE: from .api_events import router as events_router # DELETE: include_router(events_router)
Blast radius: ~120 lines deleted, 2 import lines in router init.
Verification before deleting:
grep -r "api_topology_events\|/events" --include="*.py" --include="*.ts" --include="*.tsx" \
decnet/ decnet_web/ tests/ --exclude-dir=.claude | grep -v "def api_topology_events" | grep -v "test_events"
# Should return ZERO results except in files being deleted
Test plan:
pytest tests/api/topology/ -v # Topology suite still passes
pytest tests/api/ -k "not test_events_stream" --tb=short # Full API suite minus events
PR #2: Remove /api/v1/{topology_id}/status-events endpoint
Scope: One endpoint, one handler (shares module with GET /{topology_id}), test code.
Files to modify:
decnet/web/router/topology/api_get_topology.py— remove function and route decorator:# DELETE: @router.get("/{topology_id}/status-events", ...) # DELETE: async def api_get_status_events(...): ... [~30 lines]
Files to modify (tests):
tests/api/topology/test_reads.py— remove test cases that callstatus-events.
Blast radius: ~40 lines (one function + docstring + route decorator).
Verification before deleting:
grep -r "api_get_status_events\|/status-events" --include="*.py" --include="*.ts" --include="*.tsx" \
decnet/ decnet_web/ tests/ --exclude-dir=.claude | grep -v "def api_get_status_events"
# Should return ZERO results except in deleted test code
Test plan:
pytest tests/api/topology/test_reads.py -v # Should pass after removing status-events test case
PR #3: Remove /api/v1/{topology_id}/lans/{lan_id}/next-ip endpoint
Scope: One endpoint, one handler (shares module with catalog endpoints), test code.
Files to modify:
decnet/web/router/topology/api_catalog.py— remove function and route decorator:# DELETE: @router.get("/{topology_id}/lans/{lan_id}/next-ip", ...) # DELETE: async def api_next_ip(...): ... [~40 lines]
Files to modify (tests):
tests/api/topology/test_reads.py— remove test cases that callnext-ip.
Blast radius: ~60 lines (one function + route + docstring).
Verification before deleting:
grep -r "api_next_ip\|/next-ip" --include="*.py" --include="*.ts" --include="*.tsx" \
decnet/ decnet_web/ tests/ --exclude-dir=.claude | grep -v "def api_next_ip"
# Should return ZERO results except in deleted test code
Test plan:
pytest tests/api/topology/test_reads.py -v # Should pass after removing next-ip test case
Known Risks / Routes NOT Deleted (Had Callers)
These routes were flagged as zero-callers by Phase 1 but DO have active callers — listed here so the human knows they were considered and verified:
| Route | Handler | Caller Location | Decision |
|---|---|---|---|
GET /archetypes |
api_list_archetypes() |
DeckyFleet.tsx:833 |
KEEP |
POST /deckies/{decky_name}/mutate |
api_mutate_decky() |
DeckyFleet.tsx:850 |
KEEP |
PUT /deckies/{decky_name}/mutate-interval |
api_update_mutate_interval() |
DeckyFleet.tsx:898 |
KEEP |
POST /{topology_id}/teardown |
api_teardown_topology() |
TopologyList.tsx |
KEEP |
Summary Table
| PR | Deletion | Files | Lines | Risk | Phase |
|---|---|---|---|---|---|
| #1 | GET /{topology_id}/events |
2 (handler + test) | ~120 | Low | 4a |
| #2 | GET /{topology_id}/status-events |
1 (shared module + test edit) | ~40 | Low | 4b |
| #3 | GET /{topology_id}/lans/{lan_id}/next-ip |
1 (shared module + test edit) | ~60 | Low | 4c |
| — | POST /swarm/enroll (simple) |
1 (handler) | ~100 | Medium | Deferred |
Total committed lines of code deleted: ~220 lines (handler + tests)
Total test files touched: 3 (api_events.py deletion + test_events_stream.py deletion + test_reads.py edits)
Estimated review time per PR: 10–15 minutes
Total estimated project time: 1 hour (including test runs)
Why This Order
- PR #1 removes the most isolated endpoint (dedicated handler module + test). No shared code, lowest risk.
- PR #2 modifies a shared catalog module but removes only one function. Can be reviewed with test edits.
- PR #3 similar scope to #2 (catalog module). Groups naturally with #2's test file edit strategy.
- Enroll consolidation deferred: Requires CLI change first (
decnet swarm enroll→/swarm/enroll-bundle). Plan for Phase 5.
Testing Strategy for Each PR
- Before deletion: Run the verification grep command above. Should return zero results except in files being deleted.
- After deletion:
- Run
pytest tests/api/ -vto verify no regressions in other routes. - Spot-check web UI in dev (
decnet web, then visit/topologiespage). - Verify CLI still works:
decnet --help(not affected by these deletions). - Final check:
grep -r "<handler_name>"should be empty in decnet/, decnet_web/, tests/ (except deleted files).
- Run
Critical Lessons for Future Audits
-
Phase 1 methodology is insufficient: Future audits must:
- Grep TypeScript/TSX sources systematically (not as an afterthought in Phase 4).
- Audit
decnet_web/srcfor every route with same rigor as Python backend. - Use IDE symbol search (e.g., VSCode "Find All References") for very high confidence on dynamic paths.
-
Do NOT bulk-delete "test-only" routes: The "47 test-only" number is a red flag, not a deletion target. Each requires individual verification against web UI code.
-
Consolidation opportunities: The simple
/swarm/enrollis now deprecated but NOT deleted (requires CLI migration first). Document these as "Phase N+1" work, not in the main kill list.
Phase 4.5 — Redundancy callout
A follow-up pass (beyond zero-caller deletions) flagged three redundancy classes worth explicit documentation. These are orthogonal to the kill list in Phase 4 — they're about ambiguity in the surface, not dead code.
1. Triple-registered GET /deckies ⚠️ HIGH PRIORITY
The Phase 1 route table shows the same path + method bound to three handlers:
| Method | Path | Handler | File |
|---|---|---|---|
| GET | /deckies |
get_deckies() |
api_get_deckies.py |
| GET | /deckies |
api_list_deckies() |
api_list_deckies.py |
| GET | /deckies |
list_deckies() |
api_list_deckies.py |
Why it matters:
- FastAPI resolves same-path duplicates to whichever is registered last. The other two are dead but still appear in the OpenAPI schema.
- Two handlers in the same file (
api_list_deckies.py) is a strong smell of a leftover-from-rename refactor. - Schemathesis sees the duplicates and generates overlapping cases, inflating the 30-minute run time.
Verification TODO (before deletion):
grep -n "get_deckies\|api_list_deckies\|list_deckies" decnet/web/router/fleet/— identify which is actually wired in the router__init__.py/ include statements.- Determine whether the canonical handler is
get_deckiesorapi_list_deckies(check which the web frontend's response shape matches). - Delete the two losers + their tests. Keep one canonical handler.
Risk: Low. Only one handler is live; removing dead registrations can't change runtime behavior.
2. Two enrollment flows — /swarm/enroll vs /swarm/enroll-bundle
Already covered in § Enroll Flow Consolidation above. Reiterated here so all redundancies live in one place.
POST /swarm/enroll— legacy, simple, still called bydecnet swarm enrollCLI.POST /swarm/enroll-bundle(+.sh/.tgz) — new token-based flow, sole web-UI caller.- Recommendation: mark simple as deprecated, migrate CLI to bundle flow, delete simple in a Phase 5 pass. Not on the current kill list.
3. Mutation-verb confusion
After Phase 4's zero-caller deletions land, four "mutate" endpoints currently coexist with overlapping names but different semantics:
| Endpoint | Status | Scope |
|---|---|---|
POST /api/v1/deckies/{decky_name}/mutate |
dead (kill list) | single decky, fleet-wide |
PUT /api/v1/deckies/{decky_name}/mutate-interval |
dead (kill list) | single decky, fleet-wide |
POST /api/v1/{topology_id}/mutations |
live (mutation queue, bus-woken) | topology-scoped |
Agent POST /mutate (port 8765) |
501 placeholder | agent-local, unused |
Why it matters: a reader new to the codebase sees four mutate-verbs and has to figure out which is canonical. After the kill list lands, only two remain:
- Master:
POST /{topology_id}/mutations— the canonical live-mutation API. - Agent:
POST /mutate(501) — reserved for future worker-side mutation (currently master re-sends/deploy).
Action: no code change needed beyond the Phase 4 kill list. Once dead routes are gone, this section stops being confusing on its own.
Explicitly NOT redundant
For the record — these look like pairs but are not:
- Agent
/deploy+/teardownvs/topology/apply+/topology/teardown— fleet-wide vs single-topology scopes. Both serve agent, different purposes. Keep. POST /deckies/deployvsPOST /{topology_id}/deploy— same as above: fleet-wide deploy vs topology-scoped deploy. Keep.