diff --git a/11-Features.md b/11-Features.md index 7393253..7366c8c 100644 --- a/11-Features.md +++ b/11-Features.md @@ -3124,6 +3124,42 @@ The first attempt at this shipped **inert** and every test passed: the matcher g members on `sonnet` never matched and zero seats were charged. Every test in that change put the lead on the same profile name as the target — the one shape the live config does not have. +## The REST face — the operator's way in when MCP is not there + +**What.** The daemon serves 15 HTTP routes on the same port as the MCP mount. Every capability +behind an MCP tool is reachable there, so the fleet can be driven with plain `curl`. The full +reference — each route, the `Authz` role it needs, and its matching MCP tool — is +[REST API Reference](15-REST-API-Reference). + +**On.** Always on. The listen address is the `bind:` block in `fleetd.yaml` (`host` and `port`); +this host uses `127.0.0.1:8765`. `GET /metrics` is the one route that can be absent — it is +registered only when metrics are configured. + +**Why it exists.** MCP is the agent channel, but an operator needs a way in that does not depend on +an agent session being healthy. A lead whose MCP mount has dropped cannot call a single `fleet_*` +tool, and that is exactly the moment you most need to see what the fleet is doing. REST is that +door, and it is also what a dashboard or an acceptance-test script would use. + +**The gotcha: `GET /sessions/{id}/replies` destroys what it returns.** It drains the inbox, so the +first read is the only read. Send it to a file. Piping it through `head`, or any script that exits +early, loses the payload for good — and this is the route you reach for when a ticket has timed out +and a member's real answer is sitting in that inbox. There is no second copy. + +**It is not an agent channel, and members should not be pointed at it.** Not because it skips the +authorization gate — it does not. Every route except `/healthz` resolves the caller through the same +`CallerResolver` and the same `Authz` table MCP uses, so a member calling REST gets a member's +rights. The reason is narrower: identity is resolved from the connection, and a member's own child +process is a connection the daemon must reason about. That has been wrong before — a member's `curl` +child was resolved as the primary until it was fixed. One channel for agents is one place to get that +right. `CLAUDE.md` therefore says nothing about REST, deliberately, and should keep saying nothing. + +**Why there is no route table on this page.** There is already one, on chapter 15, and a second copy +is the defect — not the errors it accumulates. Chapter 15 was written on 2026-08-31 and was accurate +for all 14 routes that existed then. `GET /member-credentials` shipped three days later and the page +did not follow it, which is how the drift starts. A test in the repo now enumerates the routes +`FleetApp` registers and fails when they no longer match its inventory, naming chapter 15 as the page +to update. + ### A note for anyone briefing a worker to read this page **A worker cannot see the current version of this file.** `wiki/` is a submodule, and the parent diff --git a/15-REST-API-Reference.md b/15-REST-API-Reference.md index 1352fb7..27c848f 100644 --- a/15-REST-API-Reference.md +++ b/15-REST-API-Reference.md @@ -1,6 +1,6 @@ # REST API Reference -`fleetd` exposes 14 HTTP routes, built once in `FleetApp.build()` +`fleetd` exposes 15 HTTP routes, built once in `FleetApp.build()` (`fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java:126-160`). No other page collects them in one place — each route is currently documented only where it happens to matter for some other topic, spread across seven pages. @@ -35,6 +35,7 @@ the `Authz.Action` each handler checks (`Authz.java:44-71`) — "open" means the | GET | `/agents` | Raw herdr agent list (every agent herdr tracks) | any authenticated caller (`READ`) | none | | GET | `/members` | The fleetd-owned worker roster, joined with live herdr status | any authenticated caller (`READ`) | `fleet_list` (its `members` section only — `fleet_list` also adds peer `leads` and capacity data this route does not have) | | GET | `/profiles` | Configured worker profiles and the default one | any authenticated caller (`READ`) | `fleet_profiles` | +| GET | `/member-credentials` | The member credential policy: which names it knows, which it allows, and how many it blocks — names and counts only, never a value | any authenticated caller (`READ`) | none | | POST | `/members` | Spawn a worker | primary only (`SPAWN`) | `fleet_spawn` | | DELETE | `/members/{paneId}` | Tear a worker down by pane id | primary only (`STOP`) | `fleet_stop` | | POST | `/sessions/{id}/message` | Deliver a turn to a session, or answer a worker's `fleet_ask` | primary or architect (`SEND`) | `fleet_send` | @@ -197,6 +198,24 @@ always answers `204`, with no path that surfaces an unknown `paneId` back to the `sessions.release` itself does anything for an id it does not recognise was not checked for this page. +### `GET /member-credentials` + +Handler: `memberCredentials`, `FleetApp.java:378-391`. No body. Answers `200` with the live +credential policy, read fresh on every call rather than from a snapshot: + +| Field | Meaning | +|---|---| +| `present` | whether a `memberCredentials` policy is configured at all | +| `policy` | the policy mode, e.g. `allow-list`; empty string when none is set | +| `known` | the credential names the policy knows about | +| `allowed` | the names a member is allowed to inherit | +| `knownCount`, `allowedCount`, `blockedCount` | the three counts | + +**It returns names and counts, never a value.** That is the whole point of the route: a probe can +check what a member would inherit without the check itself becoming a way to read secrets. Note +`blockedCount` is the policy's own blocked set, not `knownCount - allowedCount` — the two can +differ, and the field is the one to trust. + ### `GET /healthz` Handler: `healthz`, `FleetApp.java:229-266`. The only route with no `allow()` call at all — it