A charter naming an unregistered tool is refused at startup but accepted on reload #474
Closed
opened 2026-09-10 15:11:17 +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#474
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?
#469 made the daemon refuse to start when a launch charter names an MCP tool the server does not register. That gate covers the startup direction only. The reload direction is open, so a config that cannot boot can still be installed into a running daemon.
The reachable path
fleetd.yamland putsbridge_send— the pre-CB-634 name, removed from the tool surface — intofleet.charters.dev.ConfigWatcherfires,ConfigRef.reload()runsfresh.validateAll(). That passes:validateCharters()only checks that the key is a role wire name and the text is not blank. It never looks at what the text names.devmember gets a charter telling it to call a tool that does not exist.That is exactly the symptom #469 was filed for, arriving through the one door #469 does not cover.
Measured, not assumed
One call site, in
Fleetd.main.ConfigRef.java:362callsfresh.validateAll(), which does not include it.Charters really are hot and really are read per spawn:
HerdrPeerLauncher.java:499—String roleCharter = liveFleet == null ? null : liveFleet.charterFor(role);FleetConfig.java:1221— "the launcher readsfleet.charters()from the live config"ConfigRef.java:549nameschartersas hot, pinned byConfigRefTest.aHotChangeIsAppliedAndReadThroughGetSo the bad charter reaches a member without a restart. Measured on
mainat1477e43.Why this is worth its own ticket
ConfigRef.javaalready states the rule this breaks, in the comment directly above thevalidateAll()call it makes:And the Features page says the same thing about this specific validation: "Validation runs on both the startup path and the reload path. Wiring only one of the two is the whole bug." Right now that sentence is true of
validateCharters()and false of the tool-name check beside it.This is also the shape of #149/#258: a gate added after an incident closes only the direction the incident came from. The incident was a charter found at startup. Nobody asked which other states open the same door.
Scope
Make the reload path run the same check as startup, so both directions refuse the same config.
The design constraint that pushed this check out of
FleetConfigstill applies and is not up for revisiting:CharterToolSurfacelives in themcppackage because the canonical tool set does, and config is loaded before the MCP server exists, soFleetConfigmust not depend onmcp. Whatever the fix is, it must not put that dependency back. The seam to look at is whereConfigRef.reload()runs its gate, notvalidateAll()itself.Acceptance criteria
ConfigRefTestis where the hot/cold reload behaviour is already pinned.FleetConfigstill has no dependency on themcppackage. State how you checked.Not in scope
Changing what counts as a tool-shaped token in charter text, or adding config to turn the check off.
The fix is on
mainas4466ee0.ConfigRef.reload()now runs the same charter tool-surface gateFleetd.mainruns at startup, so a charter naming an unregistered tool can no longer be installed into a running daemon. 1633 tests green on that tree.FleetConfigstill has no dependency onmcp— 0 files underdev/ltms/fleet/config/import it. The design constraint I marked as not-to-revisit was respected:ConfigReftakes the check as aConsumer<FleetConfig>, andFleetdis the seam that already holds both a loaded config and themcppackage.Keeping this ticket open for one thing. My mutation battery found that nothing pins the call site. Reverting
Fleetd.java:154fromto the two-argument
new ConfigRef(configPath, cfg)turns the live gate off and leaves all 1633 tests green. The reason is subtle and worth writing down: both new tests build their ownConfigRefwith the method reference, so they prove the combination works and say nothing about whatmainchose.Four other cells all died — deleting the
accept(fresh)call, making the constructor store a no-op, swapping the config in before validating, and moving the call outsidereload()'s try/catch. So the behaviour is well pinned. Only the daemon's choice to wire it is not.This is the fourth
Fleetd.maincall site to survive a battery (#446 M5 and M7, #466 M1, now this). The pattern is now clear enough to state as a rule: extracting a check into a well-tested helper moves the untested surface up, into the one line that chooses to call it. Every extraction needs a cell on the call site as well as one on the value.The repo already holds the answer in three source-text wiring tests —
FleetdBackendQuarantineWiringTest,FleetdLeadSeatWiringTest,FleetdCompletionResolverWiringTest— each readingFleetd.javaand assertingmainstill contains the exact call. There is a second, weaker sibling pattern that builds the wiring itself; that is the one this ticket's work followed. Delegated as a follow-up with the surviving mutation as its acceptance criterion. I will close this ticket when that test kills M2.Closing. I said on this ticket that I would close it when a test kills M2. That test now exists and kills M2 by name.
What shipped
Two commits, both on
origin/main:4466ee0— the production fix.ConfigReftakes aConsumer<FleetConfig> extraValidation, called insidereload()'s existing try, so a refused reload still returnsOutcome.failedand keeps the running config rather than throwing.Fleetd.mainpassesFleetd::assertChartersNameOnlyRegisteredTools, a package-private adapter that readscfg.fleet().charters(). The two-argument constructor stays and delegates with a no-op consumer.49a404d— the follow-up merge (PR #476), one new test file, no production change.git rev-list --count origin/main..HEAD= 0.origin/main=49a404d.Why the follow-up was needed
My battery on
4466ee0(treefbb9b58) ran five mutations, each a full build against a 1633-test baseline:extraValidation.accept(fresh)fromreload()Fleetd.java:154to the two-argument constructorcurrent.set(fresh)before validatingaccept(fresh)outside the try/catchM2 is the one that mattered.
ConfigRefwas correct, the adapter was correct, and both new tests kept passing, because each builds its ownConfigRefwith the method reference. Only what the daemon wires changed — and with that one line reverted the live reload gate was off and this ticket was undone, with nothing red.That is the fifth
Fleetd.maincall site to survive a battery in this repo (#446 M5 and M7, #466 M1, now this). The shape is settled: extracting a check into a well-tested helper moves the untested surface up, into the one line that chooses to call it. TwoFleetd*WiringTestpatterns look interchangeable and are not — building the same wiring yourself proves the combination works but cannot provemainchose it; only readingFleetd.javaas text and asserting the exact call can.The gap is closed
FleetdConfigRefWiringTest(80 lines, one file) readsFleetd.javaand asserts both halves — the 3-arg text is present, the 2-arg form is absent — behind a vacuity guard on an unrelated anchor. Re-measured on the merge (tree701bf04):Tests run: 1634, Failures: 0, Errors: 0—BUILD SUCCESS, rc=0.FleetdConfigRefWiringTest.mainStillWiresTheThreeArgumentConfigRefConstructor, on the negative assertion.Files.readString, not on the guard's message. So a missing file cannot pass silently, but the guard itself is still unproven against a read that succeeds and returns the wrong content. Recorded on PR #476 rather than left implied.What I am not claiming
The new test proves the text is in
main. It does not prove the call executes at startup, and it does not prove the gate works —ConfigRefTestandFleetdConfigRefCharterToolSurfaceWiringTestcover the behaviour.Nothing here has been exercised on the live daemon. The running
fleetdstarted 2026-09-10 16:34:27 and 48 commits onmainare dated after that, this fix among them. The reload gate is live code only after a redeploy, which is owed and separate.Wiki entry updated:
11-Features.mdnow says a reload is checked too, not only startup, with the reason (config loads beforemcpexists) and the remaining gotcha.Filed separately, from the same review pass: #478 (a directory in
parityOverlayis reported as copied and arrives empty). fleet01's note on this ticket — that a one-call-site check inmainwhilereloadruns onlyvalidateAllis the same shape as the CI job's hardcoded-Dtestlist — is the sentence worth keeping.