Nothing tests that FleetMcp uses the CallerResolver — forcing the legacy identity path leaves the whole suite green at 1696/0 #518
Closed
opened 2026-09-12 06:07:00 +02:00 by ltms
·
2 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#518
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
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?
Found by mutating a line #515 did not touch, while adjudicating it (merged, fixes #509).
The mutation, and what survived it
FleetMcp.java:326is the single line that decides who every MCP caller is:I replaced it with an unconditional call to the legacy path:
Proven applied:
grep -n 'Principal p = legacyPrincipal'→ line 326;grep -n 'Principal p = callers != null'→ no match; plus a re-read of lines 323-330.mvn -f fleetd/pom.xml clean install→ exit 0,Tests run: 1696, Failures: 0, Errors: 0, Skipped: 0. No test failed.Restored byte-identical (
517279e6897a577dc123114c064ea98379519869d17e24bf78f3920c6eb95390), control run green at the same 1696/0.Harness proof, on this same tree, minutes earlier: I re-applied MUTANTC to
PaneLocator.java:117andPaneLocatorTestwent red — 16 run, 1 failure,anEarlierClientsErrorSurvivesALaterClientsCleanNegative ... expected: <false> but was: <true>. So the suite can fail. It does not fail for this.What the mutant actually does
It routes every MCP caller through the legacy identity path, skipping
CallerResolverentirely. That discards, in one line:isLoopback(remoteAddr)— the check that a caller is local at allc.scanComplete()— the guard #505 added, and the one #509 just pinned on its input sidereq.getHeader("Authorization")is not even read on the legacy pathNot equivalent by a wide margin. Post-#515 the legacy path returns
worker(...)oranonymous()and neverprimary, so on a live daemon this mutant would deny every orchestration call and the fleet would stop working within one message. The live system is the only thing that would catch it.Why nothing catches it
CallerResolveris well tested.legacyPrincipalis now tested, because #515 made it package-private for exactly that. What is untested is the wiring — thatFleetMcpcalls the resolver rather than the legacy path.The
contextExtractorclosure only runs on a real MCP request over the transport. No test drives one. So every test of who-a-caller-is stops at the seam, and the decision above the seam is unexamined.This is the shape already recorded against #393 and #415: a test on the seam does not prove the caller, and extraction moves the untested surface up. Coverage rises while a new uncovered decision appears, and every signal says the code got safer. Here the uncovered decision is the highest-stakes one in the MCP surface.
It is worth saying plainly what makes this different from an ordinary coverage gap: the thing nobody tests is which authorization system is in use. Every test below it assumes the answer.
The fix
Two parts. The first is the one that matters.
1. Apply #415's antidote — make the choice unrepresentable, not merely tested
The reason a defaulted decision survives a green suite is that writing nothing is a legal way to get the wrong answer.
callers != null ? ... : ...is exactly that: the fallback is reached by omission.So do what #415 did. Options, in my order of preference:
callersis null" with a named value the caller must choose — a second constructor, a factory (FleetMcp.withoutAuthorization(...)), or an enum parameter with no default. The test that wants legacy mode names it; production cannot reach it by leaving a field unset. Then the ternary disappears and there is no decision left to get wrong.test_recovery_patterns_match_sourceandtest_swap_ordered_after_wait_and_before_startalready do inscripts/test-redeploy-fleetd.sh. Cheaper, and strictly worse: see #517 — a source-text assertion pins what the text says and never whether it is reached.Prefer (a). A compile error beats a test, because a test can be deleted by the same change that breaks the thing it guards.
2. Drive the contextExtractor at least once
Independently of (1), there should be one test that goes through the real transport path and asserts the resulting
Principalcame from theCallerResolver. One test, not a suite — the point is to make the closure reachable at all, so a future change to it is not invisible.Acceptance
mvn -f fleetd/pom.xml clean installpasses. Report theTests run / Failures / Errors / Skippedline verbatim, and do not pipe the command — a pipe hides a failure behind a zero exit.shasum -a 256, and run a green control. Prove the mutation applied with two greps using different search strings and agrep -nre-read of the line — a mis-quoted search string silently reports zero and looks exactly like a mutation that did not apply.FleetMcpAuthzTest.theLegacyConstructorLeavesTheGateOpenmust keep working, in whatever form legacy mode takes. It is deliberate, not dead code.Related
Severity: deleting a config key silently downgrades authorization
Raised by the fleet01 lead, and it sharpens the severity line above. I had written this up as "there
is an untested branch". That understates it.
The fallback is reached by omission.
callers != nullmeans writing nothing selects the legacypath. So:
That is a different and worse property than an untested branch. An untested branch needs someone to
write wrong code. This needs someone to write no code — or to delete a line from
fleetd.yamlduring an unrelated cleanup. The suite stays at 1696/0 either way, and so does the build, and so does
the startup log.
It is the "a detector whose failure mode grants authority" class again, arriving by a third route:
not a wrong fold (#509), not a conflated sentinel (#505), but an absent configuration.
This is the argument for option (a) over option (b) in the fix above, and I want it stated plainly: a
test cannot fix this, because the thing that goes wrong is not code that a test could execute. Only
making the input required removes the failure.
Why the axis framing matters here
The peer's generalisation, and it is the third instance of a rule we already have:
Coverage counts tests, not axes. Fifteen
PaneLocatortests held the two-client axis fixed(#509). Here the entire authorization suite holds the which-resolver axis fixed. All 1696 tests
run with the seam pinned at the value my mutation changed, so the mutated line executes constantly
and nothing varies the one thing that matters.
The suite is one data point on the only axis this ticket is about. That is why 1696/0 is not
reassuring and why the number itself is the thing doing the concealing.
Caution on the
legacyPrincipaltest#515 made
legacyPrincipalpackage-private so it could be tested. That was right, and it has a costworth naming in the code rather than discovering later:
Making an unsafe path more testable increases the chance a test pins its behaviour as correct, which
makes the path harder to delete. A future session that wants to remove legacy mode will find a
passing test asserting how it behaves, and will reasonably read that as a supported contract.
So: keep testing it, and name it in the test as the legacy path being retired, not as a supported
behaviour.
FleetMcpAuthzTest.theLegacyConstructorLeavesTheGateOpenalready reads that way; whateverform legacy mode takes after option (a), keep that framing in the test name and in a comment saying
why it exists.
Closed by #524 (merged). Verified by my own build and my own mutations, not read off the PR body.
The fix section
FleetMcp.AuthorizationMode { ENFORCED, UNENFORCED }as a required parameter;callersnow required and non-null; the ternary is gone;legacyPrincipaldeleted rather than left testableFleetMcpContextExtractorTest— one test, realHttpServletStreamableServerTransportProvideron a real Jetty server (port 0, loopback,FakeHerdr), driven by a real MCP client over HTTPThe implementer took the option this ticket preferred, and deleted the second heuristic instead of keeping it testable — which is better than what was asked. My own note from #515 was that making an unsafe path more testable raises the chance a test later pins it as correct; deleting it removes that risk entirely.
Acceptance, ticked
mvn clean installpasses, the line quoted verbatim, command not piped — I redirected to a file and captured the exit status directly, so no pipe hid anything:1697 rather than 1696+1 because this branch predates #522's two new tests; no file overlap with anything main changed since its branch point, so not a stale-branch merge.
"show that the mutant no longer compiles and quote the compiler error" — done. I wrote this ticket's mutant back verbatim:
mvn clean compileexit 1. The mutation this ticket is about is now unwritable, which is the whole point of option (a): a compile error cannot be deleted by the change that breaks the thing it guards.A second mutation, because "unwritable" only covers the old spelling. I reinstated the deleted heuristic's behaviour — resolve from the connection only, never reading the
Authorizationheader — which compiles fine.FleetMcpContextExtractorTestfails by name:Then the measurement that actually settles part 2's value: with that mutation still applied I ran the whole suite.
Tests run: 1697, Failures: 1— and the single failure is the new test class. All 20FleetMcpAuthzTestcases pass with the resolver bypassed, as do the other 1676 tests. So this ticket's claim was right and is now measured: nothing in the pre-existing suite could see this, because none of it goes through the transport.two greps with different strings, plus a re-read — mutant marker present = 1, original
callers.resolve(...)call absent = 0, with a control showing it present = 1 in a saved copy.restored byte-identical, green control — hash back to
21a92ff97f706f65a66be337edef4bb54a891424df389ae65d9e556afc8fb1a4, tree clean, control build exit 0.FleetMcpAuthzTest.theLegacyConstructorLeavesTheGateOpenstill works — still present atFleetMcpAuthzTest.java:191, now driven byAuthorizationMode.UNENFORCEDinstead of a missing resolver, with its assertion message updated to say so.legacyPrincipalfully gone — 0 declarations, 0 calls. The four remaining mentions are prose that correctly describes it as deleted.One limit I am recording rather than claiming as covered
callersbeing required stops the fallback being reached by omission, which was the defect. An explicit literalnullis still something a caller could write, and theObjects.requireNonNull(callers, "callers")that catches it has no test of its own. That is the intended bar for #415's antidote, not a gap worth its own ticket.Process note
The #518 worker's pane and worktree were taken by the idle reaper before I finished verifying. Nothing was lost — the branch was pushed and the tree clean — but the worktree goes with the pane, so everything above was built and mutated in a worktree I created myself from the pushed head. Worth knowing: the 1800s idle TTL removes the worktree too, not just the ticket and the pane.