fleetd: central allow-list of usable models (models: + validateModels()) #398
Closed
agent
wants to merge 0 commits from
worker/models-allowlist-aa9e9b-3 into main
pull from: worker/models-allowlist-aa9e9b-3
merge into: fleet:main
fleet:main
fleet:worker/fleetd-612-unita-87807e-1
fleet:worker/612-b3-mcpwirings-da2b58-3
fleet:worker/612-b2-cb185-176d3a-2
fleet:worker/612-b1-completion-457459-1
fleet:worker/612-agaps-73a926-2
fleet:worker/608-sleeps-3a64ff-3
fleet:worker/621-b4520b-1
fleet:worker/618-b83894-2
fleet:worker/fleetd-615-e05481-5
fleet:worker/lead-autocompact-5f1ab2-3
fleet:worker/fleetd-613-f85deb-3
fleet:worker/fleetd-608-flaky-nudge-test-d0c2d1-3
fleet:worker/lead-context-gauge-ad404f-1
fleet:worker/gauge-wiring-9158c1-4
fleet:worker/redeploy-slowstart-ead0e5-5
fleet:worker/charter-bytes-13668c-6
fleet:worker/rollover-outcome-291483-2
fleet:worker/589-f64303-2
fleet:worker/593-1a8025-5
fleet:worker/589-fcd2aa-1
fleet:worker/568-9fdaa2-3
fleet:worker/571-attempted-outcome-5739f7-2
fleet:worker/581-completionresolver-cas-sites-0542b7-6
fleet:worker/562-loop-health-wiring-test-99611c-5
fleet:worker/562-surface-loop-health-7df5cc-4
fleet:worker/575-waiter-cleanup-sites-62ad80-1
fleet:worker/572-answer-lock-release-46a9ae-5
fleet:worker/567-probe-channel-leak-a38fc5-6
fleet:worker/551-record-before-send-7cbf56-1
fleet:worker/561-listener-fanout-survives-a-throw-61d538-2
fleet:worker/555-redeploy-main-flow-seam-65c2f5-2
fleet:worker/556-injector-owns-registration-e027a5-1
fleet:worker/552-post-restart-mktemp-abort-bc2672-4
fleet:worker/553-onstatus-completion-leak-0da881-2
fleet:worker/550-shasum-linux-196132-1
fleet:worker/538-loop-dies-on-error-4a5eeb-6
fleet:worker/426-health-coverage-ef1fd4-4
fleet:worker/504-failed-reported-clean-3cfd66-3
fleet:worker/537-capturedlog-close-e4c437-2
fleet:worker/459-broken-link-targets-cadc17-5
fleet:worker/535-appender-leak-fe74c1-1
fleet:worker/512-part2-shutdown-detection-434701-9
fleet:worker/529-logger-level-sweep-2a5533-8
fleet:worker/528-drain-gate-call-site-5de83d-7
fleet:charter/forge-mcp-vs-token
fleet:worker/521-swap-guard-unpinned-28e931-5
fleet:worker/519-probe-test-harness-d25ab8-4
fleet:worker/525-logger-level-leak-1b4eb0-6
fleet:worker/518-fleetmcp-resolver-wiring-8ef96c-1
fleet:worker/512-drain-complete-line-7edd71-3
fleet:worker/517-abort-branch-and-jar-id-41b641-2
fleet:worker/500-9e52c9-3
fleet:worker/509-4912f4-2
fleet:worker/511-9a4b23-1
fleet:worker/493-479f45-2
fleet:worker/505-03f8b2-1
fleet:worker/492-followup-detect-unclear
fleet:worker/501-a31fa0-7
fleet:worker/498-451d1c-5
fleet:worker/494-1015ce-2
fleet:worker/492-209647-1
fleet:worker/489-001902-2
fleet:worker/480-relative-handover-path-906323-1
fleet:worker/480-b-handover-skill-45bf1f-5
fleet:worker/474-followup-source-pin-f54a55-17
fleet:worker/474-charter-check-on-reload-f54a55-17
fleet:worker/466-quarantine-repeatcount-report
fleet:worker/393-opencode-skill-seeding-71854b-13
fleet:worker/469-canonical-tool-names-2a472a-16
fleet:worker/466-quarantine-escalation-5ae9c1-15
fleet:worker/446-hot-exhausted-pattern-0af580-6
fleet:worker/464-charter-tool-name-guard-a85635-12
fleet:worker/463-listfleet-default-fails-open-f1c76c-11
fleet:worker/458-invariant-5-by-purpose-862f9a-10
fleet:worker/439-coordinator-row-gate-bc032a-8
fleet:worker/449-herdr-protocol-576015-4
fleet:worker/450-abstract-spawn-599e1c-5
fleet:worker/437-ack-refuses-177d91-1
fleet:worker/444-placement-window-feb56a-2
fleet:worker/440-helddurable-derived-d462d7-13
fleet:worker/425-rework-placement-resolve-c58ba1-9
fleet:worker/421-lead-peek-held-msgs-cdbad2-10
fleet:worker/435-fixed-policy-cap-fe11de-12
fleet:worker/422-gate-state-observability-9e79d6-11
fleet:worker/431-memberregistry-live-readers-cdbad2-10
fleet:worker/424-architect-slot-hot-038b41-7
fleet:worker/422-model-gate-spawn-c29f48-6
fleet:worker/425-default-profile-live-f55534-8
fleet:worker/415-coverage-wording-2cbf9c-5
fleet:worker/416-3ad1da-1
fleet:worker/418-588283-3
fleet:worker/deterministic-stamp-race-409-3cb7b6-10
fleet:worker/armed-reads-live-config-404-ed931f-9
fleet:worker/reply-peer-refusal-391-5a34bd-7
fleet:worker/ttl-stamp-race-399-f1122f-8
fleet:worker/scrub-receipt-400-316b3e-5
fleet:worker/exhaustion-detection-395-105105-6
fleet:worker/scrub-abort-394-316b3e-5
fleet:fix/scrub-uid-abort
fleet:worker/task-scrub-517574-2
fleet:worker/t386-clock-bd5b78-4
fleet:worker/t384-scrub-813790-5
fleet:worker/t381-cc-748314-2
fleet:worker/t373-336973-2
fleet:worker/t365-3920c5-3
fleet:worker/t358-6e989b-1
fleet:worker/t355-8b321c-1
fleet:worker/fleetd-369-hermetic-git-tests-e8b19a-3
fleet:worker/fleetd-368-stale-lead-binding-f5682e-2
fleet:worker/fleetd-360-deploy-units-0d3793-1
fleet:worker/359-dead-lead-tabs-f1253b-4
fleet:worker/362-worktree-skills-c03e51-3
fleet:worker/361-coord-visibility-655144-1
fleet:362-plugin-visibility-and-drift
fleet:worker/errscan-bed2ca-2
fleet:worker/amqp-log-identity-bed2ca-2
fleet:worker/withdefaults-guard-561704
fleet:worker/sleepguard-82076d-1
fleet:worker/fd334-9ee1b6-5
fleet:worker/fd348-f1ab27-4
fleet:worker/fd335-a71c35-1
fleet:worker/fd342-174a17-2
fleet:worker/fd345-490d0f-3
fleet:worker/fleetd-337-5ec7d4-21
fleet:worker/fleetd-341-af5a6b-24
fleet:worker/fleetd-339-5ca0a2-23
fleet:worker/fleetd-338-83a4a1-22
fleet:worker/fleetd-333-281f46-18
fleet:worker/fleetd-329-11bdbb-16
fleet:worker/fleetd-330-2770fb-17
fleet:worker/fix-326-50506e-15
fleet:worker/fix-324-3e9bbf-14
fleet:worker/fix-323-b8287d-13
fleet:worker/fix-316b-bd0860-11
fleet:worker/fix-318-76ca36-9
fleet:worker/fix-317-486aec-8
fleet:worker/fix-315-ce47c5-6
fleet:worker/fix-307-275890-6
fleet:worker/fix-308-b4f664-7
fleet:worker/fix-309-ec3939-8
fleet:worker/fix-310-7a3974-9
fleet:worker/fix-302-52ad0e-9
fleet:worker/fix-298-ce1acb-8
fleet:worker/fix-297-66bd11-7
fleet:worker/fix-296-104622-6
fleet:worker/fix-293-bare-closetab-eb22b5-3
fleet:worker/fix-280-gone-ask-lapse-bca98e-2
fleet:worker/fix-290-reapidle-guard-coverage-9b0dd1-1
fleet:worker/fix-285-trust-seed-8f3565-10
fleet:worker/fix-284-backend-error-seat-85912c-11
fleet:worker/fix-282-chained-ask-e6d0bb-8
fleet:worker/fix-283-teardown-leaks-f40dfa-9
fleet:worker/fix-281-pin-handler-actions-4921ac-7
fleet:worker/audit-rendezvous-lifecycle-d072ae-2
fleet:worker/audit-health-placement-1a2476-6
fleet:worker/audit-teardown-exits-e207a5-3
fleet:worker/audit-launcher-asymmetry-27e370-4
fleet:worker/audit-rest-authz-6ca53c-5
fleet:worker/investigate-275-abandon-asking-fdef52-8
fleet:worker/fix-274-worktree-leak-b0095d-7
fleet:worker/fix-273-exhausted-pattern-9665b5-6
fleet:worker/fleetd-267-model-check-bd8068-1
fleet:worker/fleetd-131-archunit-18b834-7
fleet:worker/fleetd-266-sshagent-rename-a014ff-6
fleet:worker/fleetd-184-uid-claim-8e1f31-4
fleet:worker/fleetd-184-warn-b381ee-10
fleet:worker/fleetd-184-docs-be1d12-9
fleet:worker/fleetd-257-9bf010-7
fleet:worker/fleetd-103-23a113-6
fleet:worker/fleetd-247-342356-5
fleet:worker/fleetd-116-04dea8-4
fleet:worker/fleetd-252-a830e0-3
fleet:worker/fleetd-111-7e8673-9
fleet:worker/fleetd-155c-f8ef4b-8
fleet:worker/fleetd-176-b928ca-3
fleet:worker/fleetd-249-7a7878-2
fleet:worker/cb248-composition-root-b-9acdf7-15
fleet:worker/cb148-envrc-default-fa6c82-12
fleet:worker/cb201-unit5-wiring-6c12e6-8
fleet:worker/cb241-fallback-echo-1175e9-11
fleet:worker/cb149-trust-dialog-2392a5-9
fleet:worker/cb134-148-overlay-visible-c9b986-10
fleet:worker/cb234-session-id-keyed-04e1fc-1
fleet:worker/cb201-unit3-nudge-abdf5c-6
fleet:worker/cb201-unit2-policy-c1102c-5
fleet:worker/cb201-unit4-outcome-a13bfa-7
fleet:worker/cb201-unit1-classifier-91b9b1-4
fleet:worker/cb201-227-refine-831980-3
fleet:worker/cb175-model-readback-0f085f-1
fleet:worker/cb222-charter-tmpdir-17f013-1
fleet:worker/cb226-architect-slot-race-cd3aa8-3
fleet:worker/cb224-worktree-root-group-024523-2
fleet:worker/cb-123-role-demotion-c600f7-2
fleet:worker/cb-219-opencode-roots-1f677e-1
fleet:worker/cb214-claude-session-id-b9eab4-4
fleet:worker/cb213-zdotdir-wrong-process-dd6de4-3
fleet:worker/cb211-exhaustion-classification-9546e0-2
fleet:worker/cb137-ambiguous-task-4df3d8-4
fleet:worker/cb209-agentsessionid-4dfdb6-2
fleet:worker/cb185-hostenvnames-2692b5-3
fleet:worker/cb206-opencode-sqlite-128718-2
fleet:worker/cb185-worktree-group-fc0c99-1
fleet:worker/cb-137-ask-ticket-e7760c-2
fleet:worker/cb-172-broker-uri-d36ae4-4
fleet:worker/cb-175-model-readback-76ead6-3
fleet:worker/cb-161-pane-ancestry-293510-1
fleet:worker/cb-164-rebase-885863-8
fleet:worker/cb-164-empty-scrape-false-success-1a80af-3
fleet:fix/cb-197-ticket-ttl-from-completion
fleet:worker/cb-189-remote-url-coverage-4692f3-1
fleet:worker/cb-185-blockers-027756-4
fleet:worker/cb-192-gap-log-11b631-2
fleet:worker/cb-633-fix-5f4396-3
fleet:worker/cb185-router-d6436d-3
fleet:worker/cb185-router-routing-gaps-9e9d33-3
fleet:worker/cb185-paneids-992586-2
fleet:worker/cb-633-allow-list-union-ed374b-1
fleet:worker/cb-157-credential-in-remote-url-496e44-2
fleet:worker/cb-641-health-herdr-evidence-8f1f54-6
fleet:worker/cb-640-health-msg-evidence-99c9cd-1
fleet:worker/cb-642-fleets-status-skill-bbbc40-5
fleet:cb-634-ide-mcp
fleet:worker/lead-comms-wiring-c014b9-7
fleet:worker/lead-mailbox-c19577-6
fleet:worker/autocompact-window-82bc2f-5
fleet:worker/cb-634-probe-18056f-4
fleet:worker/cb635-broker-urienv
fleet:worker/cb-632-config-retry-8e0efa-7
fleet:lead/cb-622e-claude-md
fleet:lead/cb-622-followup
fleet:worker/cb-622a-165dff-1
fleet:lead/cb-622d-opencode-mount
fleet:worker/cb-622b-717c67-2
fleet:worker/cb-622c-ab7759-3
fleet:worker/cb-617b2-20ca4b-3
fleet:worker/cb-617a-5c2f4a-1
fleet:worker/cb596-4e49ef-3
fleet:worker/cb586-10500c-1
fleet:worker/cb-606-b9343a-25
fleet:worker/cb604-1445f8-24
fleet:worker/cb582-477374-21
fleet:worker/cb584-8c2281-22
fleet:worker/cb600-e6b9a9-20
fleet:worker/cb602-ce257f-19
fleet:worker/cb601-b42837-18
fleet:worker/cb598-6c7ba7-17
fleet:worker/cb599-740fe4-16
fleet:worker/cb597-282224-15
fleet:worker/cb590fix-185e9a-10
fleet:worker/cb528-recovery-race
fleet:worker/cb594-96bead-8
fleet:worker/cb590-916766-2
fleet:worker/cb527-997d99-3
fleet:worker/cb592-env-leak-3cbf9c-1
fleet:worker/cb588-async-ticket-nudge-3218f7-5
fleet:worker/cb578b-9dcb13-6
fleet:worker/cb581-d24826-5
fleet:worker/m2-u5-ef8c42-15
fleet:worker/cb578a-516499-2
fleet:worker/cb576-01a04b-17
fleet:worker/cb579-lead-tab-acba06-20
fleet:worker/cb580-terminal-health-ed6058-21
fleet:worker/cb577-f36fdc-18
fleet:worker/cb573b-3db06f-16
fleet:worker/cb568c-f36fdc-18
fleet:worker/cb568-drop-cause-c3ac1c
fleet:worker/cb575-cancelled-notification-c3ac1c
fleet:worker/m4-sol-a2cbec-3
fleet:worker/cb574-async-ask-c3ac1c
fleet:worker/cb573-health-model-8ca857-14
fleet:worker/cb572-unknown-target-7f2e35-13
fleet:worker/u4-700706-9
fleet:worker/u3-b9fcb6-6
fleet:worker/u2-ef5b68-4
fleet:worker/u1-469dce-1-clean
fleet:worker/u1-469dce-1
fleet:worker/cb-564-health-events-70cf7e-2
fleet:worker/cb-565-recycle-drops-role-98e58f-3
fleet:worker/cb-563-missing-reply-df2866-1
fleet:worker/cb-562-readiness-gate-silent-6c23c9-3
fleet:worker/cb-560-architect-presence-da8155-1
fleet:worker/cb-561-architect-silent-off-a71cab-2
fleet:worker/cb-548-bind-architect-slot-fe1b8c-1
fleet:worker/parity-overlay-settings-5fb711-1
fleet:secrets-central-store
fleet:cb-559-hot-key-correction
fleet:cb-557-fleet-role-pools
fleet:worker/cb-553-maxload-explicit-spawn-305ee3-6
fleet:worker/cb-551-idle-lead-heartbeat-f1633c-1
fleet:worker/cb-544-drain-preserves-worktree-925fad-3
fleet:worker/cb-552-docs-sync-1cb9cf-4
fleet:worker/cb-548-rendezvous-guard-rebased
fleet:worker/cb-548-rendezvous-guard-116b53-10
fleet:worker/cb-548-authz-v2-586df6-8
fleet:worker/cb-548-authz-264363-5
fleet:salvage/cb-528b-codex-home
fleet:salvage/cb-528a-codex-launcher
fleet:CB-518-primary-flow
fleet:feature/peer-launcher-spi
fleet:cb-103-injector
No Reviewers
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#398
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 "worker/models-allowlist-aa9e9b-3"
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?
Unit: a central allow-list of usable models (fleetd)
What changed
models: { allow: [ { model: <id> }, ... ] }onFleetConfig(
FleetConfig.Models/FleetConfig.Models.ModelEntry).FleetConfig.validateModels(): whenmodels.allow:is non-empty, anyprofiles:entry whosemodel:is not in the allow-list fails config load, naming both the model and the profile. Aprofile with no
model:set (e.g. asubscription: trueprofile) is never checked.Fleetd.main()next to the othervalidateXxx()calls, and intoConfigRef.reload()(same reasoning as
validateMembers(): a config that would refuse to boot must not slip inthrough a reload) plus
ConfigRef.DEFERRED_KEYS/changedDeferredKeysso a reload correctlyreports a changed
models:block as "needs a restart" rather than silently doing nothing.models:section infleetd.example.yaml(this file isthe only committed description of the schema, since
fleetd.yamlis gitignored on every host).modelstoKNOWN_TOP_LEVEL_KEYSso it isn't WARNed as an unknown top-level key.Both invariants hold
validateModels()only ever readsprofiles:and checks eachmodel:againstmodels.allow:— it can only refuse, never widen, what aprofiles:editpermits. A dedicated test (
addingAProfileCannotWidenTheAllowListByItself) pins this directly.models.allow:makesvalidateModels()a no-op. Existing gitignored
fleetd.yamlconfigs on both hosts keep working unchanged afterupgrade — no forced migration.
Shape decisions (asked to state explicitly)
ModelEntry(String model)record ratherthan a
List<String>, specifically so a later unit can hang an on/off flag or a load limit offeach entry without changing the YAML shape underneath an operator who already wrote one.
claude-sonnet-5) and an opencodeprovider-prefixed id (
openai/gpt-5.6-terra) are both just opaque strings compared for exactequality —
validateModels()never parses a provider prefix or branches on a profile'skind:.Covered by a live-shape test (
aBareClaudeIdAndAnOpencodeProviderPrefixedIdBothFitOneAllowList)mixing
amazon-bedrock/opencode/openai-backed profiles in one list.Out of scope (per the ticket) — not touched
No spawn-time enforcement, no runtime on/off switch, no
BackendQuarantinechange, no cataloguelookup against a live backend.
validateModels()is config-load (and config-reload) validationonly.
Tests
FleetConfigTest(13 new cases): absent block, emptyallow:, a profile outside the list refused(naming both), a profile inside the list passing, a profile with no
model:passing even with anactive allow-list, the live multi-provider shape (bare + provider-prefixed ids, one allowed one
not), the "editing profiles: alone cannot widen" pin, and
modelspresent inKNOWN_TOP_LEVEL_KEYS. All fixtures use@TempDir— no file touches a real.claude.jsonorfleetd.yaml.Also updated three existing "coverage" tests that assert every
FleetConfigtop-level componentis accounted for somewhere (
ConfigRefTopLevelReportingCoverageTest,FleetConfigWithDefaultsPreservesEveryComponentTest) or triaged into aConfigRefreload class(
ConfigRefTopLevelCoverageTest, satisfied by classifyingmodelsasDEFERRED_KEYSinConfigRef.java— nothing rebuilds off it after startup, so a reload accepts a change but reportsit needs a restart, same shape as
guard:/quarantineCooldownSeconds:).Verification (mvn clean install, unpiped, redirected to a file)
Tests run: 1478, Failures: 0, Errors: 0, Skipped: 0—BUILD SUCCESS.(
FleetConfig.java,Fleetd.java,ConfigRef.java), reverted them withgit checkout -- <files>(kept the new tests), and ranmvn test -Dtest=FleetConfigTest,ConfigRefTopLevelReportingCoverageTest,FleetConfigWithDefaultsPreservesEveryComponentTest,ConfigRefTopLevelCoverageTest:compilation failure —
cannot find symbol: class Models(inConfigRefTopLevelReportingCoverageTest/FleetConfigWithDefaultsPreservesEveryComponentTest)and
cannot find symbol: method validateModels()(inFleetConfigTest, 8 call sites). Restoredthe patch with
git apply, re-ran the full build: green again (Tests run: 1478, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS).@TempDirfixtures rather than reasoning about it,per the ticket's instruction (
fleetd.yamlis gitignored on both hosts, so no test can read thereal one):
aBareClaudeIdAndAnOpencodeProviderPrefixedIdBothFitOneAllowListmixesamazon-bedrock/opencode/openaiprofiles the way the ticket's own measurement described(235 reachable models: 151 amazon-bedrock, 69 opencode, 15 openai; 3 configured).
Shape, not instance — other free-form config strings this ticket did not touch
(Reported per instructions; not fixed here.)
Profile.baseUrl— handed straight to the launcher as the backend endpoint; no scheme/hostvalidation anywhere in
FleetConfig.java.Profile.configDir/Profile.cwd— filesystem paths passed through as-is; no existence check.Profile.tokenEnv/gitTokenEnv/gitHostEnv— env-var names; nothing checks the namedvariable actually resolves at load time (only at spawn, via the launcher).
Profile.workspace/tabLabel— free-form label templates; no charset/length/placeholder check.Coordinator.selfId— declared free-form; nothing checks it's actually unique across peers(
peers:is a bare declared list, never cross-checked against a live daemon).memberLoginShell— checked only for azshsuffix (stringendsWith), not that the path existsor is executable.
Caveats for review
models: { allow: [...] }(wrapped record) over a bare top-levelList<ModelEntry> modelsfor symmetry with
MemberCredentials.allow/Guard's naming convention — a style call, notforced by anything structural.
validateModels()now also runs insideConfigRef.reload(), which is slightly more than theticket literally asked for ("at config load") — I judged this necessary because
Fleetd.reload()already re-runs every other
validateXxx()on a hot reload with an explicit comment saying aconfig that would refuse to boot must not slip in through a reload; leaving
models:out of thatset would have been a silent gap in exactly the shape this repo's own
CLAUDE.mdcalls outelsewhere as a "my ticket instructions are a defect source" hazard. Flagging this explicitly
in case the lead judges it out of scope.
Add an optional top-level `models:` block (Models{allow: List<ModelEntry>}) naming the models any profiles: entry may use. Absent/empty allow: keeps today's behaviour exactly (no check, no warning). When configured, FleetConfig.validateModels() fails config load (and reload, via ConfigRef) naming both the model and the profile, if any profile's model: is outside the list. The check is one-way: editing profiles: alone can never widen what is permitted, only models.allow: can. Wired into Fleetd.main() alongside the other validateXxx() calls, and into ConfigRef.reload()/DEFERRED_KEYS so a bad edit can't slip in through a reload either. Each ModelEntry is its own record (not a bare string) so a later unit can add per-model on/off or load-limit state without changing the YAML shape. One flat string namespace covers both a bare Claude id and an opencode provider-prefixed id.Scope note: there is a seventh unpinned startup call, not six
Measured just now while reviewing PR #401 (fleetd #395), which has since been merged as
7180b1a.#395 added a new startup reporter,
Fleetd.reportExhaustedPatternGap(FleetConfig), called fromFleetd.java:140. I deleted that call and ran the full suite:So the startup call is not pinned. The report's own behaviour is well tested — the worker's
two new test classes catch every mutation I asked for inside the method. What no test catches is
whether anybody still calls it.
This is the same shape as the six
FleetConfig.validateXxx()calls this ticket already covers.I found that gap the same way on PR #398: deleting both
validateModels()call sites left1478 tests green.
What this changes about this ticket
Please treat the acceptance criteria as covering the startup sequence, not a list of six
method names. Concretely:
cfg.validateXxx()calls fails a test.reportExhaustedPatternGap(cfg)fails a test.validateXxx()onFleetConfigthat nobody wires into startup alsofails a test.
Criterion 3 is the one that matters most, because it is the only one that survives the next person
adding an eighth call. A test that hardcodes seven names is a list that goes stale the day #402
lands — exactly how this gap appeared in the first place.
Note the two kinds are not identical and a check should not pretend they are: a
validateXxx()throws and refuses startup, while
reportExhaustedPatternGaponly logs. Whatever mechanism proves"it is called" has to work for a method whose only effect is a log line.
Recorded in the #395 merge commit and in the Features wiki entry as a known gap owned by this
ticket, so it is not lost if this comment is missed.
Lead review — the follow-up work was recovered from a dead worker, and it is good
First, the recovery. The worker's agent died mid-turn (
herdr error [agent_not_found]) with all of this work uncommitted in its worktree, and with the startup call left disabled from its own mutation run:So the tree as found had no startup validation at all. I restored the call, built, and committed the work myself as
b540a17, pushed to this branch. Nothing was lost. Worth recording thatgit -C <worktree> statusis what saved it — the ticket looked failed and the work was complete.What the follow-up does
FleetConfig.validateAll()sweeps this class's own public, no-arg,voidvalidateXxx()methods by reflection and invokes each in alphabetical order. Both real call sites —Fleetd.mainandConfigRef.reload()— call that one method instead of six.FleetdStartupValidationTestthen calls the realFleetd.mainwith six configs, each failing exactly one validator.Calling the real
mainis the right call and it is safe for a reason the test states correctly:validateAll()runs afterSubscriptionGuard.assertPrimaryCleanand beforemainopens the herdr socket or binds Javalin, so an intentionally invalid config always throws first.Verified myself
Build after I restored the call:
Tests run: 1491, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS, 0 compile errors.Mutation A — the reload call site (the caller the worker did not mutate). Replaced
fresh.validateAll();inConfigRef.reload()with a no-op:Caught. Both real call sites are now pinned.
Mutation B — NOT caught, and it makes a claim in the tests false
I reverted
validateAll()to a hardcoded list of today's six calls, behaviour-identical:So the reflective sweep is not pinned to
validateAll().theSweepMechanismIsGenericNotHardcodedToFleetConfigsSixNamesprovesinvokeAllValidatorsis generic (on an unrelated class), andvalidateAllReachesEveryOneOfTodaysSixValidatorsproves today's six run — and a hardcoded list satisfies both. Nothing connects the two.That made this sentence in
FleetConfigValidateAllTest's javadoc false:First half true. Second half measured green.
The interaction is the actual finding. The denominator test
fleetConfigDeclaresExactlyTheseSixValidatorsToday()is a genuine tripwire — an equality assertion, so declaring a seventh validator does fail it. But its failure message told the author:Put those two facts together: if the sweep is ever replaced by a name list, the one assertion that fires tells whoever added the seventh validator that it is safe to just bump the number. The tripwire would hand back a false all-clear at the exact moment it fired. This is the same shape as #334 — a guard well pinned by tests whose excusing comment was false.
I fixed both strings myself rather than respawning a worker for two sentences: the javadoc now states what I measured, and the assertion message now says "confirm
validateAll()still delegates toinvokeAllValidators(this)first, then update the expected set" — turning a false assurance into the instruction it should have been.Design verdict: the reflection is a convenience, and that is fine. The guarantee against a forgotten seventh validator is the denominator test failing. That is now what the code says.
Still not covered, and I am not asking for it here
validateAll()only reachesvalidateXxx()methods onFleetConfig. It does not reach the five log-only reporters inmain—reportRequiredSecrets,reportGitHostShape,reportMemberTrustModel,reportMemberCredentialsGap, andreportExhaustedPatternGap(added by #395 on currentmain, so not on this branch). None of those call sites is pinned, and deleting any one still ships a green build.So my earlier comment on this PR was narrower than the truth: I said "a seventh unpinned startup call". It is a seventh plus five reporters.
FleetdStartupValidationTestis the right place to close them, because they need a caller-level test and it already boots the realmain. They differ from validators in one way that matters: a validator throws, while a reporter only logs, so pinning a reporter means asserting on a log appender rather than on an exception.Not blocking this merge. Filing it separately so the number is right.
Merged into
mainased99c20("Merge #398: a central allow-list of usable models, with the startup validators pinned"). Verified withgit merge-base --is-ancestor origin/worker/models-allowlist-aa9e9b-3 origin/main— the branch tipaf4c88dis contained inmain.The merge was done locally and pushed, so this PR stayed open. Closing it now. Spawn-time enforcement of the on/off state is the follow-up, tracked in #422 (PR #429).
Pull request closed