REST surface: add the route that drifted, and a Features entry that points here
Chapter 15 was written on 2026-08-31 and was correct for all 14 routes that existed then. GET /member-credentials shipped three days later (#111) and the page did not follow it. - ch.15: route count 14 -> 15, a table row for GET /member-credentials, and a per-route detail section (field meanings, and that blockedCount is the policy's own blocked set, not knownCount - allowedCount). - ch.11: a short entry for the REST face — what it is for, the bind: knob, why it exists, the drain-on-read gotcha — linking to ch.15. Deliberately no route table: a second copy is the defect, not the errors it collects. The "not an agent channel" point is stated accurately: REST does not skip the authorization gate. 14 of 15 routes resolve the caller through the same CallerResolver and Authz table MCP uses, and /healthz is open on purpose as a liveness probe. The real reason is that identity comes from the connection, and a member's own child process is a connection the daemon must reason about — which was wrong before (#161). fleetd #252. A test in the repo now enumerates FleetApp's registrations and fails when they no longer match, naming this page as the one to update.
+36
@@ -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
|
||||
|
||||
+20
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user