Files
DECNET/development/api-audit.md

1001 lines
46 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/**/*.py` for `/api/v1` calls
- **Workers**: Searched `decnet/<worker>/**/*.py` (excluding CLI)
- **Tests**: Searched `tests/**/*.py` for 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 `AgentClient` class
- Master→Updater client calls via `UpdaterClient` class
- 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:
1. **`api_deploy_swarm.py`**: Deploy topology to all enrolled hosts
2. **`api_teardown_swarm.py`**: Teardown fleet
3. **`api_check_hosts.py`**: Active mTLS probe of all hosts (for dashboard health)
4. **`api_decommission_host.py`** (swarm): Calls agent `/self-destruct`
5. **`api_decommission_host.py`** (swarm_mgmt): Calls agent `/self-destruct`
6. **`api_teardown_host.py`** (swarm_mgmt): Calls agent `/self-destruct`
7. **`api_list_hosts.py`** (swarm_mgmt): Calls agent `/health` on every list request
8. **Engine `deployer.py`**: Direct `/deploy` + `/topology/apply` calls 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:
1. **`api_push_update.py`**: Upload new release to updater
2. **`api_push_update_self.py`**: Update the updater binary itself
3. **`api_rollback_host.py`**: Rollback updater to previous release
4. **`api_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.health` heartbeat to local bus (separate from mTLS heartbeat)
- Updater publishes `system.updater.health` to 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`, `/health` are 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**:
1. **`/swarm/enroll` vs `/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).
2. **Agent `/deploy` + `/teardown` vs `/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.
3. **Agent `/mutate` returns 501**: Placeholder for future worker-side mutation.
- Currently master re-sends `/deploy` with updated config.
- Safe to leave as-is (fails closed); can implement later.
---
### 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:
1. **`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)
2. **`decnet swarm enroll`**
- Hits: `POST /swarm/enroll`
3. **`decnet swarm list`**
- Hits: `GET /swarm/hosts`
4. **`decnet swarm check`**
- Hits: `POST /swarm/check`
5. **`decnet swarm update`**
- Hits: `GET /swarm/hosts` + direct mTLS to updater port 8766
6. **`decnet swarm deckies`**
- Hits: `GET /swarm/deckies`
7. **`decnet swarm decommission`**
- Hits: `DELETE /swarm/hosts/{uuid}`
**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 `@deprecated` in docstrings.
- No old "v1" flavors replaced by newer flows (e.g., no `decnet deploy-v1` vs `decnet deploy-v2`).
- All commands in `MASTER_ONLY_COMMANDS` + `MASTER_ONLY_GROUPS` are 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 --help` on agents (when `DECNET_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 `/topologies` CRUD.
- **`decnet bus`**: New ServiceBus worker. UNIX-socket pub/sub, not HTTP.
- **Worker supervisors** (`probe`, `collect`, `correlate`, `sniffer`, `profiler`): Field microservices. Spawned by `decnet deploy` as 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:
- `--json` flag 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:
1. Phase 1 grepped Python/test files only; it did **not systematically scan TypeScript/TSX**.
2. Dynamic path construction (e.g., `` api.post(`/topologies/${id}/teardown`) ``) requires careful regex; simple string matching misses them.
3. 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:
```bash
# 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.tsx` calls **ONLY** `POST /swarm/enroll-bundle` (new flow).
- No web caller for `POST /swarm/enroll` (old flow) found.
**CLI caller analysis**:
- `decnet swarm enroll` (Phase 3 audit) calls `POST /swarm/enroll` (line 572 of Phase 3 summary).
**Recommendation**: **DEPRECATE simple `/swarm/enroll`**
1. Keep both endpoints for now (CLI still uses simple).
2. Mark `POST /swarm/enroll` as `@deprecated` in docstring; note that new deployments should use `POST /swarm/enroll-bundle`.
3. Update CLI (`decnet swarm enroll`) to call `/swarm/enroll-bundle` in a follow-up PR.
4. Only DELETE simple `/swarm/enroll` **after** 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:
```python
# 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**:
```bash
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**:
```bash
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:
```python
# 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 call `status-events`.
**Blast radius**: ~40 lines (one function + docstring + route decorator).
**Verification before deleting**:
```bash
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**:
```bash
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:
```python
# 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 call `next-ip`.
**Blast radius**: ~60 lines (one function + route + docstring).
**Verification before deleting**:
```bash
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**:
```bash
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**: 1015 minutes
**Total estimated project time**: 1 hour (including test runs)
---
### Why This Order
1. **PR #1** removes the most isolated endpoint (dedicated handler module + test). No shared code, lowest risk.
2. **PR #2** modifies a shared catalog module but removes only one function. Can be reviewed with test edits.
3. **PR #3** similar scope to #2 (catalog module). Groups naturally with #2's test file edit strategy.
4. **Enroll consolidation deferred**: Requires CLI change first (`decnet swarm enroll` → `/swarm/enroll-bundle`). Plan for Phase 5.
---
### Testing Strategy for Each PR
1. **Before deletion**: Run the verification grep command above. Should return zero results except in files being deleted.
2. **After deletion**:
- Run `pytest tests/api/ -v` to verify no regressions in other routes.
- Spot-check web UI in dev (`decnet web`, then visit `/topologies` page).
- 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).
---
### Critical Lessons for Future Audits
1. **Phase 1 methodology is insufficient**: Future audits must:
- Grep TypeScript/TSX sources **systematically** (not as an afterthought in Phase 4).
- Audit `decnet_web/src` for 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.
2. **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.
3. **Consolidation opportunities**: The simple `/swarm/enroll` is 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):
1. `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.
2. Determine whether the canonical handler is `get_deckies` or `api_list_deckies` (check which the web frontend's response shape matches).
3. 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](#enroll-flow-consolidation) above. Reiterated here so all redundancies live in one place.
- **`POST /swarm/enroll`** — legacy, simple, still called by `decnet swarm enroll` CLI.
- **`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` + `/teardown` vs `/topology/apply` + `/topology/teardown`** — fleet-wide vs single-topology scopes. Both serve agent, different purposes. Keep.
- **`POST /deckies/deploy` vs `POST /{topology_id}/deploy`** — same as above: fleet-wide deploy vs topology-scoped deploy. Keep.