The REST surface is a supported operator fallback and is documented nowhere — 14 routes, 0 entries #252

Closed
opened 2026-09-03 07:50:04 +02:00 by ltms · 1 comment
Owner

Split out of #114 point 3, so that ticket can close on the doc rewrite it was really about.

The finding

The daemon serves 14 REST routes. I enumerated them from the code today rather than from the old design doc, which had them wrong:

GET    /healthz
GET    /metrics
GET    /agents
GET    /members
POST   /members
DELETE /members/{paneId}
GET    /profiles
GET    /sessions
GET    /sessions/{id}/status
GET    /sessions/{id}/replies
POST   /sessions/{id}/message
POST   /sessions/{id}/reply
POST   /sessions/{id}/ask
GET    /tasks/{ticket}

None of them appears in any document. CLAUDE.md documents no REST at all, deliberately — it is the agent instruction surface and MCP is the agent channel. docs/MCP-Contract.md used to list a few, and had them wrong (POST /workers, DELETE /workers/{paneId}); that page is now flows-only and names no routes by design.

The decision #114 asked for, and my answer

#114 put it as a fork: "If REST is a supported operator surface it belongs in wiki/11-Features.md; if it is internal, say so once and stop treating its absence as debt."

It is a supported operator surface. I am not inferring that — I have used it as one. When the MCP mount drops, the only way to drive the fleet is 127.0.0.1:8765 directly, and that has happened often enough that I keep a note on how to do it. Two of these routes have behaviour an operator must know before touching them:

  • GET /sessions/{id}/replies drains on first read. Read it into a file. Piping it through head or a script that exits early loses the payload permanently. A timed-out ticket's real answer lands here, so this is the route you reach for at exactly the moment you cannot afford to lose it.
  • GET /tasks/{ticket} is the REST form of fleet_poll, and it is subject to the same ticket TTL.

An operator surface with a destructive read and no documentation is the gap worth closing.

What this is not

Not a request to document REST for agents. CLAUDE.md deliberately says nothing about REST and should keep saying nothing: the bridge is the only channel for a member, and inviting agents to call REST would route around the authorization gate. This is documentation for the human operator, in the operator's own chapter.

Scope

  1. One entry in wiki/11-Features.md covering the REST face: what it is for (operator fallback and dashboards), the route list, the knob that binds it, why it exists, and the drain-on-read gotcha.
  2. Say explicitly that it is not an agent channel, and why.
  3. Do not add a route table to docs/MCP-Contract.md. A second copy of the route list is exactly the drift #114 spent a month accumulating; if the wiki entry needs a guard against the same rot, give it one the way McpContractDocTest guards the tool names.

Note on route naming

GET /agents and GET /members both exist and look like near-synonyms from the outside. Whoever writes this should check what each actually returns and say so, or file a follow-up if one of them is vestigial. I have not checked.

Related: #114 (the doc rewrite this came from) · #113 (second copies drifting).

Split out of #114 point 3, so that ticket can close on the doc rewrite it was really about. ## The finding The daemon serves 14 REST routes. I enumerated them from the code today rather than from the old design doc, which had them wrong: ``` GET /healthz GET /metrics GET /agents GET /members POST /members DELETE /members/{paneId} GET /profiles GET /sessions GET /sessions/{id}/status GET /sessions/{id}/replies POST /sessions/{id}/message POST /sessions/{id}/reply POST /sessions/{id}/ask GET /tasks/{ticket} ``` **None of them appears in any document.** `CLAUDE.md` documents no REST at all, deliberately — it is the agent instruction surface and MCP is the agent channel. `docs/MCP-Contract.md` used to list a few, and had them wrong (`POST /workers`, `DELETE /workers/{paneId}`); that page is now flows-only and names no routes by design. ## The decision #114 asked for, and my answer #114 put it as a fork: *"If REST is a supported operator surface it belongs in `wiki/11-Features.md`; if it is internal, say so once and stop treating its absence as debt."* **It is a supported operator surface.** I am not inferring that — I have used it as one. When the MCP mount drops, the only way to drive the fleet is `127.0.0.1:8765` directly, and that has happened often enough that I keep a note on how to do it. Two of these routes have behaviour an operator must know before touching them: - **`GET /sessions/{id}/replies` drains on first read.** Read it into a file. Piping it through `head` or a script that exits early loses the payload permanently. A timed-out ticket's real answer lands here, so this is the route you reach for at exactly the moment you cannot afford to lose it. - **`GET /tasks/{ticket}`** is the REST form of `fleet_poll`, and it is subject to the same ticket TTL. An operator surface with a destructive read and no documentation is the gap worth closing. ## What this is not Not a request to document REST *for agents*. `CLAUDE.md` deliberately says nothing about REST and should keep saying nothing: the bridge is the only channel for a member, and inviting agents to call REST would route around the authorization gate. This is documentation for the human operator, in the operator's own chapter. ## Scope 1. One entry in `wiki/11-Features.md` covering the REST face: what it is for (operator fallback and dashboards), the route list, the knob that binds it, **why it exists**, and the drain-on-read gotcha. 2. Say explicitly that it is not an agent channel, and why. 3. Do **not** add a route table to `docs/MCP-Contract.md`. A second copy of the route list is exactly the drift #114 spent a month accumulating; if the wiki entry needs a guard against the same rot, give it one the way `McpContractDocTest` guards the tool names. ## Note on route naming `GET /agents` and `GET /members` both exist and look like near-synonyms from the outside. Whoever writes this should check what each actually returns and say so, or file a follow-up if one of them is vestigial. I have not checked. Related: #114 (the doc rewrite this came from) · #113 (second copies drifting).
Author
Owner

Correcting this ticket: its central claim is wrong, and the real defect is smaller and more interesting.

I wrote "None of them appears in any document." That is false. wiki/15-REST-API-Reference.md has existed since 2026-08-31 (4912b7a, "#168 §B: add chapters 14, 15 and 16"). It is a good page: it documents every route in one table with the Authz.Action each handler checks and the matching MCP tool, plus per-route detail, plus a warning that a green /healthz does not mean spawning works.

I filed a ticket about a documentation surface drifting without checking the documentation surface. That is the same failure the ticket is about, committed while filing it.

What is actually wrong

Chapter 15 documented 14 routes and was correct for all 14 on the day it was written. The code now registers 15. The single gap is GET /member-credentials, which shipped hours before this ticket was filed (#111) and which the page did not follow.

So the defect is not "documented nowhere". It is "documented once, correctly, and already one route behind after three days" — which is a much better argument for the guard than the one I filed.

Revised scope, and what I have done

  1. One entry in wiki/11-Features.md covering the REST face with the route list — rejected. That would have been a third copy of the route list. Chapter 15 is the reference. Features now carries a short entry: what the surface is for, the bind: knob, why it exists, the drain-on-read gotcha, and a link to chapter 15. No table.
  2. Chapter 15 updated: route count 14 → 15, a table row for GET /member-credentials, and a per-route detail section for it (field meanings, and the note that blockedCount is the policy's own blocked set, not knownCount - allowedCount).
  3. The "not an agent channel" point is kept but restated accurately. I originally wrote that pointing agents at REST would "route around the authorization gate". It would not: 14 of the 15 routes resolve the caller through the same CallerResolver and the same Authz table MCP uses, and /healthz is deliberately open as a liveness probe. The real reason is narrower — identity is resolved from the connection, and a member's own child process is a connection the daemon has to reason about. That has been wrong before (a member's curl child was resolved as primary, #161). One channel for agents is one place to get that right.
  4. Point 3 of the original scope stands unchanged: no route table in docs/MCP-Contract.md.

The /agents vs /members question I left open

Not vestigial, and chapter 15 already had this right. /agents is herdr's raw view — which panes exist. /members is the daemon's own roster joined to that live view by terminalId, so it also carries what only the daemon knows (agentSessionId, the saved work refs). Ask /agents what panes are alive; ask /members what members the daemon believes in.

The guard

An in-repo test is being built that enumerates the routes FleetApp registers and fails when they no longer match its inventory, naming chapter 15 as the page to update. It is in-repo on purpose: wiki/ is a submodule whose pointer is never advanced, so a test reading the wiki would skip silently in CI — trading a drift guard for a guard that cannot run.

It would have caught this. GET /member-credentials is exactly the case: a route added, a page not updated, nothing failing.

**Correcting this ticket: its central claim is wrong, and the real defect is smaller and more interesting.** I wrote "None of them appears in any document." That is false. `wiki/15-REST-API-Reference.md` has existed since **2026-08-31** (`4912b7a`, "#168 §B: add chapters 14, 15 and 16"). It is a good page: it documents every route in one table with the `Authz.Action` each handler checks and the matching MCP tool, plus per-route detail, plus a warning that a green `/healthz` does not mean spawning works. I filed a ticket about a documentation surface drifting **without checking the documentation surface**. That is the same failure the ticket is about, committed while filing it. ## What is actually wrong Chapter 15 documented **14** routes and was correct for all 14 on the day it was written. The code now registers **15**. The single gap is `GET /member-credentials`, which shipped hours before this ticket was filed (#111) and which the page did not follow. So the defect is not "documented nowhere". It is "documented once, correctly, and already one route behind after three days" — which is a much better argument for the guard than the one I filed. ## Revised scope, and what I have done 1. ~~One entry in `wiki/11-Features.md` covering the REST face with the route list~~ — **rejected.** That would have been a third copy of the route list. Chapter 15 is the reference. Features now carries a short entry: what the surface is for, the `bind:` knob, why it exists, the drain-on-read gotcha, and a link to chapter 15. No table. 2. **Chapter 15 updated**: route count 14 → 15, a table row for `GET /member-credentials`, and a per-route detail section for it (field meanings, and the note that `blockedCount` is the policy's own blocked set, not `knownCount - allowedCount`). 3. The "not an agent channel" point is kept but **restated accurately**. I originally wrote that pointing agents at REST would "route around the authorization gate". It would not: 14 of the 15 routes resolve the caller through the same `CallerResolver` and the same `Authz` table MCP uses, and `/healthz` is deliberately open as a liveness probe. The real reason is narrower — identity is resolved from the connection, and a member's own child process is a connection the daemon has to reason about. That has been wrong before (a member's `curl` child was resolved as primary, #161). One channel for agents is one place to get that right. 4. **Point 3 of the original scope stands unchanged**: no route table in `docs/MCP-Contract.md`. ## The `/agents` vs `/members` question I left open Not vestigial, and chapter 15 already had this right. `/agents` is herdr's raw view — which panes exist. `/members` is the daemon's own roster joined to that live view by `terminalId`, so it also carries what only the daemon knows (`agentSessionId`, the saved work refs). Ask `/agents` what panes are alive; ask `/members` what members the daemon believes in. ## The guard An in-repo test is being built that enumerates the routes `FleetApp` registers and fails when they no longer match its inventory, naming chapter 15 as the page to update. It is in-repo on purpose: `wiki/` is a submodule whose pointer is never advanced, so a test reading the wiki would skip silently in CI — trading a drift guard for a guard that cannot run. It would have caught this. `GET /member-credentials` is exactly the case: a route added, a page not updated, nothing failing.
ltms closed this issue 2026-09-03 11:01:06 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#252