fleetd #421: let a lead peek its own held peer mail, primary-only #438
Reference in New Issue
Block a user
Delete Branch "worker/421-lead-peek-held-msgs-cdbad2-10"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #421.
The defect
fleet_listreported held lead-to-lead messages undercoordinator.held[]with only a truncated 80-charpreview— never the full body.fleet_poll{target: coordId}returned[]silently (it drains a worker's reply inbox, not the coordinator mailbox — the wrong route entirely).fleet_ackwould have destroyed the message unread. A lead had no way to read its own held peer mail safely.Tool shape chosen, and why
Added a non-destructive read via
fleet_poll{coordId}— not afleet_listfull-body mode. Afleet_listfull-body mode was raised and withdrawn in the ticket's own comments: it would contradictfleet_list's deliberate truncation (HELD_PREVIEW_MAX_CHARS), which exists specifically so a roster scan never dumps a coordination body. Routing the full read throughfleet_pollinstead keepsfleet_lista cheap, always-safe scan and makes the one place that can return a full body an explicit, separately-authorized call.fleet_poll(target, coordId)now branches on two independent arguments:coordIdset → peek (never ack) this daemon's own held lead-to-lead mailtargetset (nocoordId) → drain a worker's reply inbox (unchanged,DRAIN)READ)pollAction(String target, String coordId)got an actual signature change, not an additive overload — every one of its 9 call sites (production + tests) now states explicitly what it passes. This was deliberate per the ticket: an overload would let a caller keep passing the old single argument and silently miss the new branch.coordIdmust equal the caller's ownleadChannel.selfCoordId(). Passing a peer's coord-id is refused with a reason (coordId "X" is not this daemon's own coord-id...) instead of repeating the original bug's silent wrong-inbox behavior — this directly refuses the confusion that caused the ticket in the first place (a lead passing a peer's id expecting to read that peer's mail, which this system has no route for).The new Authz action
Its switch row in
Authz.permits:The switch has no
default— this is a forcing function: the build does not compile until everyAuthz.Actionhas an explicit case, soCOORD_READcould not be silently left unhandled.The "pending: 0" trap
mailbox.pendingonly counts broker-ready (unacked-but-deliverable) messages — a held, durably-queued message doesn't show there, so a perfectly healthy lead with real mail waiting reads aspending: 0, inviting the wrong conclusion that nothing is there or that it's in-memory-only.coordinatorViewnow also reportsheldCount(the real count fromleadChannel.peek()) andheldDurable: true(AMQP consumption here is manual-ack,LeadMailbox.java:196, so held mail survives a daemon restart) as siblings ofmailbox, not folded into it —mailboxViewis shared withpeerView, where a held-count concept doesn't apply (you can'tpeek()a peer's mailbox).Tests added
AuthzTest.coordReadIsThePrimarysAloneNotAWidenedRead— primary allowed, worker and architect refused.FleetMcpAuthzTest.pollingByCoordIdIsACoordReadNeverAPlainRead— pinspollAction's actual branch selection over both arguments.FleetMcpAuthzTest.aWorkerAndAnArchitectMayNotReadHeldPeerMailOnlyThePrimaryMay— the ticket's most important test: worker refused, architect refused, primary allowed, through the realdeny/handler path.FleetMcpAuthzTest.everyRegisteredToolHasItsHandlerActionPinnedupdated to cover the newfleet_poll{coordId}mapping — no exclusion added.FleetMcpTest.pollWithCoordIdReturnsTheFullBodyWithoutAckingAndLeavesItHeld— reads twice, same full bodies both times, message still inheld[]afterward,channel.acked()stays empty.FleetMcpTest.pollWithCoordIdRefusesAPeersCoordIdInsteadOfReturningTheWrongMailOrNothing— passing a peer's coord-id is refused, and the body is never leaked into the error text.FleetMcpTest.pollWithCoordIdErrorsHonestlyWhenLeadCoordinationIsNotConfigured.FleetMcpTest.listReportsAnHonestHeldCountAndDurabilityNotJustPendingZero.FleetMcpTest.listReportsHeldMessagesWithATruncatedPreviewNeverTheFullBody— strengthened (see mutation proof below).Build — full, unpiped, run myself
(main's baseline before this branch was 1539 — +7 matches the 7 new test methods: 1 in
AuthzTest, 2 inFleetMcpAuthzTest, 4 new + 1 strengthened inFleetMcpTest.)Mutation proof
Each mutation applied one at a time to a clean checkout of this branch, targeted tests run, exact failing test(s) recorded, then reverted from a backup before the next. Restored state confirmed byte-identical to the backup after each revert; final
git diff --statagainst this PR's intended 6 files shows no leftovers.FleetMcp.java:849—pollAction'scoordIdbranch mapped toAuthz.Action.READinstead ofCOORD_READ:Failed exactly:
FleetMcpAuthzTest.everyRegisteredToolHasItsHandlerActionPinned,FleetMcpAuthzTest.aWorkerAndAnArchitectMayNotReadHeldPeerMailOnlyThePrimaryMay,FleetMcpAuthzTest.pollingByCoordIdIsACoordReadNeverAPlainRead. Nothing else in the 111-test authz run moved.FleetMcp.java,pollHeldPeerMail— made the peek ack:Failed exactly:
FleetMcpTest.pollWithCoordIdReturnsTheFullBodyWithoutAckingAndLeavesItHeld:748(expected: <[...one held message...]> but was: <[]>on the second read). 80 otherFleetMcpTesttests stayed green.FleetMcp.java:1445— widened the preview cap:First attempt at this test was too weak to catch it — it used a homogeneous
"x".repeat(200)body, andout.contains("x".repeat(80) + "…")still matched the widened 81-char preview one character later, because every character was'x'(a mutation that applies and passes looks exactly like one that never applied). Fixed by putting a sentinel character ("Y") exactly at index 80 — the first character a widened cap would leak — and asserting it never appears. Re-ran against the mutation: failed exactlyFleetMcpTest.listReportsHeldMessagesWithATruncatedPreviewNeverTheFullBody:701, with the sentinel visible in the actual output. Re-ran against the unmutated code first to confirm the strengthened test is still green there.Authz.java:85— the authz-case-level mutation requested for this report, wideningCOORD_READto include workers:Failed exactly:
AuthzTest.coordReadIsThePrimarysAloneNotAWidenedRead:124(a worker must not read held lead-to-lead mail ==> expected: <false> but was: <true>) andFleetMcpAuthzTest.aWorkerAndAnArchitectMayNotReadHeldPeerMailOnlyThePrimaryMay:306. All other Authz/FleetMcpAuthz tests (28 of 30) stayed green.All four mutations reverted;
git diff --statconfirms the working tree matches the 6 committed files with noMUTATIONmarkers remaining; final fullmvn clean installabove is post-revert.CLAUDE.md / wiki note (for the lead to carry over)
Added a row to the primary's intent→tool table in
CLAUDE.md(worker cannot touchwiki/— it's a submodule):fleet_poll{coordId: <your own coord-id, from fleet_list's coordinator.selfId>}— primary-only; never acks, sofleet_list'sheld[]still shows it after.fleet_list'sheld[]gives only a truncated preview — this is the only way to read the full bodyThis desyncs
CLAUDE.md's canonical block fromwiki/7-Use-Cases.md's template copy (verified by the python snippetCLAUDE.mditself documents). Please carry this row into the wiki template manually.Suggested wiki/11-Features.md-style entry (also not committable from this worktree):
One thing noticed, not investigated (per the brief, left alone)
Authz.Action.READ's case comment ("the roster carries no secrets") is the exact claim this ticket had to carveCOORD_READout from for message bodies — butfleet_list'scoordinatorrow, still gated on plainREAD, already hands every worker cross-host topology (peer coord-ids, mailbox existence/pending state, and now this PR'sheldCount/heldDurable) that is coordination metadata, not roster data. Noting only, per scope — not fixing.Out of scope (noted, not investigated)
None encountered beyond the above.
fleet_list truncated held lead-to-lead messages to an 80-char preview with no way to read the full body, and fleet_poll{target} drained the wrong inbox (a worker's reply queue, not the coordinator mailbox) -- it silently returned []. fleet_ack would have destroyed the message unread. Add a non-destructive read: fleet_poll{coordId} peeks (never acks) this daemon's own held mail via LeadChannel.peek(). The coordId must equal the caller's own selfCoordId -- passing a peer's id is refused with a reason, instead of repeating the original silent-[] confusion. This is authorization-sensitive: mapping it to the existing READ action would let any worker read every peer lead's mail in full. READ's openness rests on "the roster carries no secrets" (Authz.java), which does not hold for lead-to-lead coordination bodies. Added Authz.Action.COORD_READ, primary-only (not even the architect, which holds READ today), and made pollAction's signature depend on both target and coordId so every call site states explicitly what it passes. Also fixes fleet_list's "pending: 0" trap: mailbox.pending only counts broker-ready messages, so a healthy held mailbox reads as empty. Added heldCount/heldDurable beside held[] so the durability fact isn't implied only by reading the code. Mutation-tested: pollAction's COORD_READ->READ mapping, the peek->ack substitution, the 80-char preview cap widened to 81, and the Authz case widened to include caller.isWorker() -- each breaks exactly its matching test and nothing else. The first attempt at the preview-cap test used a homogeneous "x"*200 body, which a widened cap slipped through unnoticed (contains() found a shifted match); replaced with a sentinel character at index 80 to actually pin the boundary. Updates CLAUDE.md's intent->tool table for fleet_poll's new coordId semantics, per this repo's own "prompt is part of the product" rule. wiki/ is a submodule and not committable from a worker's worktree -- wiki-bound content is in the PR body instead.