fleetd #480 Unit C: fleet_handover MCP tool #485

Merged
ltms merged 2 commits from worker/480-c-handover-tool-8ed764-6 into main 2026-09-11 02:27:56 +02:00
Member

fleetd #480 Unit C — wires the fleet_handover MCP tool onto the already-merged LeadRollover
executor (PR #483).

What changed

  • fleet_handover tool (FleetMcp.java): actions open/confirm/cancel, thin adapter over
    LeadRollover.open/confirm/cancel.
  • Registered unconditionally. FleetMcp takes a nullable LeadRollover (new trailing
    constructor parameter). When it is null (no leadRollover: configured), every action returns a
    clean, structured refusal naming NOT_CONFIGURED instead of throwing — same for LeadRollover's
    own IllegalStateException from a config removed by a later hot reload. This keeps the
    registered tool surface stable regardless of config, per the fleetd #474 charter tool-surface
    gate and McpContractDocTest.
  • Authorization: new Authz.Action.HANDOVER, gated on caller.isPrimary() — folded into the
    same case SPAWN, STOP, DRAIN, HANDOVER -> caller.isPrimary(); line as the other lifecycle
    actions.
  • No caller-identity parameter. The tool's input schema carries only action, reason,
    token, operatorConfirmed — no terminal/sessionId/leadTerminal/anything like it. The
    lead pane is always callerTerminal(exchange), resolved by the MCP layer from the connection —
    never a request field, per LeadRollover's class javadoc (fleetd #480 correction 2) and this
    project's charter invariant 3.
  • Fleetd.java: the existing leadRollover local (previously unused after Unit A) is now
    passed into FleetMcp's construction. The LeadRollover leadRollover = leadRollover(cfg, router.leadAgents(), config); line itself is untouched — FleetdLeadRolloverWiringTest still
    passes unchanged.
  • Added a package-private FleetMcp.registeredTools() accessor (delegates to
    McpSyncServer#listTools()) so a test can assert against the actually-registered wire schema
    rather than scraping source text.

Explicitly out of scope (left alone, not touched)

  • LeadRollover.java itself.
  • CLAUDE.md, wiki/, fleetd.yaml.
  • No "return fleet state" field added to open.

Defect found in LeadRollover.java, NOT fixed (out of scope for this unit)

waitUntilInjectable (used by confirm()'s deferred continuation to decide whether the calling
lead's own turn has ended) gates on AgentStatus.injectable(), which is IDLE || BLOCKED || DONE.
BLOCKED is a live, paused turn (e.g. a pane sitting at an open approval prompt) — not an
ended one. So if the lead's turn hits an approval prompt right after calling confirm(), the
continuation can read that as "settled" within one turnSettleSeconds window and send /clear
into a live, paused turn, destroying context — the exact failure this guard exists to prevent.
This is a real, verified finding (recorded in this session's own memory before this unit started,
dated 2026-09-11), not a hypothesis. Per the brief for this unit, LeadRollover.java was left
untouched; flagging for the lead to open as a follow-up.

Build

mvn clean install in fleetd/, unpiped, full output read: BUILD SUCCESS (1 occurrence),
BUILD FAILURE (0 occurrences), Tests run: 1659, Failures: 0, Errors: 0, Skipped: 0.

New tests added (FleetMcpHandoverTest, 8 tests, all passing):

  • registeredEitherWay — fleet_handover registered whether leadRollover: is configured or not.
  • nullLeadRolloverRefusesCleanlyForEveryAction — null LeadRollover, all 3 actions: no throw,
    clean NOT_CONFIGURED.
  • unknownActionIsACleanError — missing/bogus action is a clean tool error, not an exception.
  • onlyThePrimaryIsPermitted — toolAction maps to Authz.Action.HANDOVER; worker and architect
    refused, primary permitted, via denyFor (the authorization gate itself).
  • schemaCarriesNoCallerIdentityParameter — asserts against registeredTools()'s actual wire
    schema that no terminal/sessionId/leadTerminal/callerTerminal/target property exists.
  • openThenConfirmRoundTripsAndOwnershipIsEnforced — open then confirm on the same terminal
    succeeds; a different terminal gets NOT_YOUR_ROLLOVER.
  • cancelUnknownTokenIsCleanNotAFailure / cancelKnownTokenSucceeds.
fleetd #480 Unit C — wires the `fleet_handover` MCP tool onto the already-merged `LeadRollover` executor (PR #483). ## What changed - **`fleet_handover` tool** (`FleetMcp.java`): actions `open`/`confirm`/`cancel`, thin adapter over `LeadRollover.open`/`confirm`/`cancel`. - **Registered unconditionally.** `FleetMcp` takes a nullable `LeadRollover` (new trailing constructor parameter). When it is `null` (no `leadRollover:` configured), every action returns a clean, structured refusal naming `NOT_CONFIGURED` instead of throwing — same for `LeadRollover`'s own `IllegalStateException` from a config removed by a later hot reload. This keeps the registered tool surface stable regardless of config, per the fleetd #474 charter tool-surface gate and `McpContractDocTest`. - **Authorization**: new `Authz.Action.HANDOVER`, gated on `caller.isPrimary()` — folded into the same `case SPAWN, STOP, DRAIN, HANDOVER -> caller.isPrimary();` line as the other lifecycle actions. - **No caller-identity parameter.** The tool's input schema carries only `action`, `reason`, `token`, `operatorConfirmed` — no `terminal`/`sessionId`/`leadTerminal`/anything like it. The lead pane is always `callerTerminal(exchange)`, resolved by the MCP layer from the connection — never a request field, per `LeadRollover`'s class javadoc (fleetd #480 correction 2) and this project's charter invariant 3. - **`Fleetd.java`**: the existing `leadRollover` local (previously unused after Unit A) is now passed into `FleetMcp`'s construction. The `LeadRollover leadRollover = leadRollover(cfg, router.leadAgents(), config);` line itself is untouched — `FleetdLeadRolloverWiringTest` still passes unchanged. - Added a package-private `FleetMcp.registeredTools()` accessor (delegates to `McpSyncServer#listTools()`) so a test can assert against the actually-registered wire schema rather than scraping source text. ## Explicitly out of scope (left alone, not touched) - `LeadRollover.java` itself. - `CLAUDE.md`, `wiki/`, `fleetd.yaml`. - No "return fleet state" field added to `open`. ## Defect found in `LeadRollover.java`, NOT fixed (out of scope for this unit) `waitUntilInjectable` (used by `confirm()`'s deferred continuation to decide whether the calling lead's own turn has ended) gates on `AgentStatus.injectable()`, which is `IDLE || BLOCKED || DONE`. `BLOCKED` is a **live, paused** turn (e.g. a pane sitting at an open approval prompt) — not an ended one. So if the lead's turn hits an approval prompt right after calling `confirm()`, the continuation can read that as "settled" within one `turnSettleSeconds` window and send `/clear` into a live, paused turn, destroying context — the exact failure this guard exists to prevent. This is a real, verified finding (recorded in this session's own memory before this unit started, dated 2026-09-11), not a hypothesis. Per the brief for this unit, `LeadRollover.java` was left untouched; flagging for the lead to open as a follow-up. ## Build `mvn clean install` in `fleetd/`, unpiped, full output read: **BUILD SUCCESS** (1 occurrence), **BUILD FAILURE** (0 occurrences), `Tests run: 1659, Failures: 0, Errors: 0, Skipped: 0`. New tests added (`FleetMcpHandoverTest`, 8 tests, all passing): - `registeredEitherWay` — fleet_handover registered whether `leadRollover:` is configured or not. - `nullLeadRolloverRefusesCleanlyForEveryAction` — null `LeadRollover`, all 3 actions: no throw, clean `NOT_CONFIGURED`. - `unknownActionIsACleanError` — missing/bogus `action` is a clean tool error, not an exception. - `onlyThePrimaryIsPermitted` — `toolAction` maps to `Authz.Action.HANDOVER`; worker and architect refused, primary permitted, via `denyFor` (the authorization gate itself). - `schemaCarriesNoCallerIdentityParameter` — asserts against `registeredTools()`'s actual wire schema that no `terminal`/`sessionId`/`leadTerminal`/`callerTerminal`/`target` property exists. - `openThenConfirmRoundTripsAndOwnershipIsEnforced` — open then confirm on the same terminal succeeds; a different terminal gets `NOT_YOUR_ROLLOVER`. - `cancelUnknownTokenIsCleanNotAFailure` / `cancelKnownTokenSucceeds`.
agent added 1 commit 2026-09-11 02:07:59 +02:00
fleetd #480 Unit C: wire fleet_handover MCP tool onto LeadRollover
CI / contract (pull_request) Successful in 44s
CI / build (pull_request) Successful in 1m56s
62646957ea
Adds the fleet_handover tool (open/confirm/cancel) as a thin adapter over
LeadRollover, registered unconditionally so the charter tool-surface gate
sees a stable set regardless of whether leadRollover: is configured. With a
null LeadRollover every action degrades to a clean NOT_CONFIGURED refusal
instead of throwing. Gated on a new Authz.Action.HANDOVER (primary-only,
same as SPAWN/STOP/DRAIN). The caller's own connection-resolved terminal is
the only lead identity ever used — the tool's input schema carries no
terminal/session/leadTerminal parameter, so a lead can only ever roll
itself. Fleetd.main now passes its existing leadRollover local into FleetMcp
via a new trailing constructor parameter.
agent added 1 commit 2026-09-11 02:25:16 +02:00
fleetd #480 correction round: collapse FleetMcp to one required constructor
CI / contract (pull_request) Successful in 55s
CI / build (pull_request) Successful in 1m32s
eb0557621e
FleetMcp had a defaulted 15-argument constructor that delegated to the new
16-argument one with an implicit null for leadRollover. Dropping the
leadRollover argument from Fleetd.main's FleetMcp(...) call fell back to that
shorter overload, compiled fine, and left all 1659 tests green — the live
daemon would then answer NOT_CONFIGURED to fleet_handover forever with
nothing going red.

Delete every overload that could reach the 16-arg constructor with a
silently-defaulted leadRollover (11/12/13/14/15-arg forms all chained to it),
leaving the 16-arg constructor as FleetMcp's sole public constructor. Update
FleetMcpAuthzTest's call site to pass every parameter explicitly (leadChannel
null, OutageSource.none(), LeadSeatSource.none(), List.of(), leadRollover
null) — Fleetd.java and FleetMcpHandoverTest already called the full form.
Proved with mvn -o -q compile: removing the leadRollover argument from
Fleetd.main now fails to compile instead of silently defaulting.

No behaviour changes — NOT_CONFIGURED refusals are unchanged.
ltms merged commit 9494a6b99a into main 2026-09-11 02:27:56 +02:00
ltms deleted branch worker/480-c-handover-tool-8ed764-6 2026-09-11 02:27:56 +02:00
Sign in to join this conversation.