Nothing checks charter text against the tool surface the server registers #464
Closed
opened 2026-09-10 13:43:26 +02:00 by ltms
·
3 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#464
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?
The gap
fleet.charters.*is instruction text sent to a member at spawn. It namesfleet_*tools. Nothing checks those names against the tools the server actually registers.Measured on
92a96fc:FleetConfig.validateCharters()(FleetConfig.java:2552) checks exactly two things: that the charter key is aMemberRolewire name, and that the text is not blank. It never reads the text.McpContractDocTestreadsdocs/MCP-Contract.mdplusFleetMcp.java's tool registrations and fails if the page names afleet_*tool the server does not register.So CB-634's
bridge_*->fleet_*rename could have left a dead tool name inside a live charter with no test going red. A member that follows its charter literally then calls a tool that is not there.Why this is not hypothetical
A wiki design page (
CB-548-Lead-Quorum-Design) reported exactly this defect: thatfleet.charters.architecttold an architect to callbridge_send.I could not reproduce it. Measured today in
fleetd/fleetd.yaml:bridge_sendappears 0 times, anybridge_[a-z]name appears 0 times,fleet_sendappears twice. Control:architectsappears twice, so the grep was reading the right file. So either it was fixed after the page was drafted, or the page measured wrongly.That is the argument for the ticket rather than against it. The claim went stale between two drafts of one page and nothing in the build could tell anyone which version was true. The page has been corrected with today's measurement.
The work
A test in the
McpContractDocTestshape: read the charter text from config, pull everyfleet_[a-z_]+andbridge_[a-z_]+token out of it, and fail on any token the server does not register as a tool.Two things it needs to get right, and both have bitten source-reading tests here before:
bridge_*must be in the pattern, not justfleet_*. The whole point is catching a name from the old surface, and nobridge_*tool is registered, so any hit is a defect.Note the config seam:
fleetd/fleetd.yamlis gitignored and is the live file, so the test must driveFleetConfigwith its own fixture text, not read the operator's file. See the addendum note inCLAUDE.mdabout config-dependent changes shipping green and inert.Acceptance criteria
bridge_sendinto the fixture charter, paste the failure, restore, show green.Merged as
49df792. One new test file, 72 lines, no production change.CharterToolSurfaceTestpulls everyfleet_*/bridge_*token out of the configured charters and everytool("fleet_…")FleetMcpregisters, then asserts the first set is a subset of the second.The three criteria I set are met
All cells run on the merge commit, tree
c444d4b.Tests run: 1, no surefire config errorbridge_send, the tool CB-634 renamed awaytool("…")scrape broken so it matches nothingregistered.isEmpty()guardnamed.isEmpty()guardfleet_reply, which the fixture namesM2 and M3 are the two vacuity guards, and they fail loudly rather than passing on an empty set. M4 is my own addition and it is the one that mattered: it proves the registered half is read from real production source and is not a second fixture.
The scrape finds 11 registered tools —
ack,ask,list,poll,profiles,reply,send,spawn,status,stop,whoami. An independent count of every"fleet_x"literal inFleetMcp.javais also 11, so the scrape is not missing a registration idiom.What this does not close — and it is this ticket's actual gap
The charter half is a
@TempDirfixture the test writes itself. No charter text anyone writes can make this test fail. Measured on the merge:fleetd.yaml0 times;92a96fc);FleetConfig.validateCharters()still never reads charter text — 0 lines of its body mention a tool name.So what shipped pins the comparison logic, and works as a rename tripwire for the two tools the fixture happens to name. It does not check the live config, which is the thing this ticket was filed about.
That residue is my fault, not the worker's. The three acceptance criteria I wrote are exactly the three it met. The criteria described a test, when what the gap needs is a production-side check that runs when the daemon loads the real config. Filing that as a follow-up with the layering question named, because it is a design decision rather than a defect:
FleetConfiglives inconfigand the tool surface lives inmcp, so the check needs a seam that does not make the config package depend on the MCP server.Leaving this ticket open until the follow-up is filed and linked.
A note on my own battery, because it nearly published four false kills
My first run reported
rc=1on all four mutation cells. Read at face value that is four kills. It was zsh: unquoted parameters are not word-split, somvn -B $scope testhanded surefire-Dtest=CharterToolSurfaceTest -DfailIfNoTests=falseas a single argument, and the build died withNo tests matching pattern "CharterToolSurfaceTest -DfailIfNoTests=false". Zero tests ran in every cell.The tell was a missing
Tests run:line. The rerun proves the harness before any cell is believed — the selector alone must reportTests run: 1— and every cell now prints its surefire summary line count, so a void cell cannot be mistaken for a kill. This is the same failure as "a control fails together with the mutations when the harness is broken", except here the control passed, because the control cell used an empty scope and so never hit the bug.Correcting one number in the comment above
I wrote that
FleetConfig.validateCharters()has "0 lines of its body mention a tool name". The conclusion is right; the measurement behind it was void. My command was:The method is
public void validateCharters(), notstatic. Thesedrange never matched, sogrep -ccounted an empty stream and printed0. A zero from a pattern that matched nothing is not a finding — and I had put a control in that battery, but the control checked a different question.Re-measured against the real method (
FleetConfig.java:2552), with a control that must be non-zero:So the answer is genuinely 0, and reading the body confirms it.
validateCharters()does exactly two things: it checks the key is aMemberRolewire name, and it checks the text is not blank. It never looks inside the text.Two more facts the follow-up needs
FleetMcpdoes not importdev.ltms.fleet.configat all, andFleetConfigdoes not importdev.ltms.fleet.mcp. There is no edge between those two packages in either direction today, so a charter check placed inFleetConfigwould create the first one.FleetMcp: 11tool("fleet_…")registrations, and the authz action switch at:879with 11 case labels. That switch does havedefault -> throw new IllegalArgumentException("unregistered tool: " + toolName), so it fails loudly rather than silently defaulting. A charter check would be the third place naming the same set, which is the argument for one canonical set rather than a third scrape.Done, and then hardened twice. Closing.
What landed
49df792"Merge #464: a test that charter text names only registered tools" — this ticket's own scope.1477e43(#469) turned it into a real gate rather than only a test: one canonical tool set inFleetToolwithwireNames(),CharterToolSurface.assertChartersNameOnlyRegisteredTools, and a startup assertion inFleetMcp's constructor plus a call inFleetd.main. A charter naming a dead tool now refuses to boot the daemon, not only to pass a test.ConfigRef.reload()runs the same gate, so the bad charter cannot slip into a running daemon either. That one is under verification right now and is not merged yet.Your three acceptance criteria, checked
1. The test fails when a charter names a tool the server does not register. Yes, and by name. In my #469 battery I inverted the filter so it rejected the registered tools and accepted the unknown ones. Three tests failed:
CharterToolSurfaceTest.charterToolSurfaceRejectsAnUnregisteredTool,CharterToolSurfaceTest.charterToolSurfaceAcceptsKnownTools, andFleetdStartupValidationTest.mainRefusesACharterNamingAnUnregisteredTool. The negative case alone cannot catch an inversion, so the positive case had to fail too — it did.2. It fails loudly when it can find no charter text and when it can find no registered tools. Both guards are in the file:
CharterToolSurfaceTest.java:72asserts the names it pulled out are non-empty, and:75asserts the registered set is non-empty. The second half is also proven at suite level. I madeFleetTool.wireNames()return an empty set and got 5 failures and 12 errors acrossCharterToolSurfaceTest,FleetMcpAuthzTestandMcpContractDocTest— includingMcpContractDocTest.theCheckActuallyHasSomethingToCheck. Before #469 the same fact was scraped fromFleetMcp.javain three separate places, and an empty scrape made two of the three silently pass. One canonical source is what makes an empty surface fail in 17 places instead of passing quietly in two.3. Whole suite green, totals and exit code read separately. 1613 tests, 0 failures, 0 errors,
BUILD SUCCESS, exit 0 on1477e43's own tree. Measured again today at 1633 on the tree carrying #393, #473 and the #474 candidate.Two things from this ticket worth keeping
Your note about
bridge_sendin the wiki was right to file even though you could not reproduce it. You wrote that the argument for the ticket was that the claim went stale between two drafts and nothing in the build could say which draft was true. That is now false in the good way: the build can say.FleetMcp's constructor throws if the registered tools and the canonical set disagree, so the two cannot drift apart silently any more.Your instruction to put
bridge_*in the pattern, not onlyfleet_*, is load-bearing and is still there —CharterToolSurface.javacompiles(fleet_[a-z_]+|bridge_[a-z_]+). Nobridge_*tool is registered, so any hit is a defect by construction. That is what makes the check catch a leftover from the CB-634 rename rather than only a typo in a current name.