fleetd#326: classify primary and configReload as deferred top-level keys #328
Closed
agent
wants to merge 0 commits from
worker/fix-326-50506e-15 into main
pull from: worker/fix-326-50506e-15
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/models-allowlist-aa9e9b-3
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-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#328
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/fix-326-50506e-15"
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?
What changed
Two top-level
FleetConfigkeys were missing fromConfigRef.changedDeferredKeys, so a reload thatchanged only one of them reported a bare
config reloadedwhile the daemon kept the old value:primary—Fleetd.java:506, 519, 520readcfg.primary()only off the startup snapshot tobuild
PrimaryRegistry(pinned terminal) and sizeReplyPushLoop's reminder cap/backoff. Neitheris rebuilt on reload.
configReload—Fleetd.java:679-680read it only at startup to decide whether to build aConfigWatcherat all, and with what interval. The watcher that would apply a later change isitself built once.
Both are now added to
changedDeferredKeysand to the class doc's deferred list, each proven with afailing-first test in
ConfigRefTest(changingPrimaryIsReportedAsDeferred,changingConfigReloadIsReportedAsDeferred) and a revert/restore mutation check.healthandcoordinatorare deliberately left unclassified — see the options writeup below.ConfigRefProfileCoverageTest's exclusion-set assertion (Set.of("weight", "maxLoad", "credentialId")) was not touched.configReload: deferred, not cold — reasoningThe class doc defines cold as a key whose new value would leave the daemon inconsistent with an
already-open resource (
bind,herdrSocket,broker,auth— a bound socket, an open connection,an already-listening port).
configReloadhas no such resource.Fleetd.java:679-680only decides,once, whether to construct a
ConfigWatcherand what interval to give it:If a reload changes
enabledorintervalSeconds, nothing goes inconsistent — a running watcher (ifone exists) just keeps polling at its original interval and ignores the new
enabledflag, and adaemon with no watcher stays without one. That is exactly the deferred shape already used for
lifecycle,guard, etc.: "accepted into the new snapshot, but the wiring built at startup keepsthe old value until a restart." So
configReload→ deferred.(The "obvious joke" the issue names — turning reload off through a reload — is specifically why this
needed a decision rather than a one-line copy: refusing the reload as cold would be wrong, since
nothing breaks; reporting nothing at all is the bug being fixed.)
health/coordinator: options, not a fix (per the issue — I decide nothing here)Both are read twice, off two different things, and no single bucket is correct for either.
healthFleetd.java:556-563— startup snapshot. Decides whetherFleetHealthMonitoris built at all andits
intervalOrDefault()/workingSuspectAfterOrDefault()— frozen at startup.Fleetd.java:648-650(FleetMcp.HealthCoverageSource) — live, viaconfig.get().health(), onevery
fleet_profilescall. Feeds the reported coverage string (detection-onlyvs the fullstring), including whether
notificationsis configured.coordinatorFleetd.java:502(openLeadMailbox) — startup snapshot. Resolvescoordinator.effectiveUri()(honoring
uriEnvoveruri) and opens theLeadMailboxonce with that URI, plusselfIdandprefetch. All four sub-fields (uri,uriEnv,selfId,prefetch) are consumed only here forthis purpose — frozen.
MemberEnvAllowList.java:165, reached fromHerdrPeerLauncher.java:1529-1530— live, viaconfig.get()on every spawn. Readscoordinator.uriEnv()only (noturi/selfId/prefetch) to build the exclusion set that keeps the broker URI env var name out of a member'sallow-list/scrub.
So
coordinator.uriEnvspecifically is the split field (likehealth.enabled);coordinator.uri,selfId,prefetchare cleanly deferred-only (only the startup snapshot ever reads them).Options, with costs
Report the whole key as deferred whenever anything under it changes. Cheapest: one
Objects.equals(old.health(), fresh.health())/...coordinator()...check, same shape aslifecycle/guard. Cost: honest for the frozen half, wrong for the live half — changing onlyhealth.notifications.mode(read live at :648-650) or onlycoordinator.prefetchwould tell theoperator a restart is needed when nothing needs to restart. This is exactly the kind of
over-claim the ticket is trying to get away from, just in the safer direction.
Split by sub-field.
health.enabled/intervalSeconds/workingSuspectAfterSeconds→deferred (frozen into the monitor);
health.notifications→ also touches the live coveragestring, so it's arguably hot-for-reporting-purposes but its value is still read live either way
— no restart needed.
coordinator.uri/selfId/prefetch→ deferred;coordinator.uriEnv→split (deferred for the mailbox's actual connection, hot for the spawn-time exclusion list).
Cost: more machinery —
changedDeferredKeyscurrently compares whole nested records with oneObjects.equals; sub-field comparison means unpacking each nested record's fields by hand (orreflection) and picking a bucket per field, including one field (
coordinator.uriEnv) that iscorrectly in both buckets. The issue's own text names the open question: what happens when both
the deferred half and the hot half of the same key change in one reload? Does
Outcome.deferred()name
"coordinator.uri"while staying silent on"coordinator.uriEnv"even thoughuriEnvchanging genuinely needs a restart for the mailbox to move? That needs an explicit rule, and I
don't think there's a "safe default" here — it has to be decided, not defaulted.
Make the frozen reader live. For
health: reconstruct/reconfigureFleetHealthMonitor(interval, enabled) on a running daemon — means starting/stopping its own scheduler and thread
without racing the ordered shutdown hook or an in-flight health check. For
coordinator: rebuilda running
LeadMailbox— close the existing AMQP consumer/connection and open a new one with thenew URI/selfId/prefetch on a live daemon. This is the one I want to flag as possibly unsafe, not
just expensive:
selfIdnames this daemon's own inbox queue (lead.<selfId>.inbox) — a peerlead that already learned the old coord-id would not automatically discover the new one, so
changing
coordinator.selfIdlive is a distributed-identity change, not just a reconnect. I didnot find anything in
LeadMailbox/LeadCoordLoopthat handles a self-id changing under a runningdaemon (nor should this ticket add it — invariant 1 forbids making anything take effect live
here). Biggest change, and for
coordinatorspecifically I'd want that identity question answeredbefore calling it safe.
I'm not picking one — that's the point of this section.
The
coordinator.uriEnvquestion — what I actually foundQuestion: if
coordinator.uriEnvchanges in a reload, can the broker URI variable end up visibleto a member spawned after that reload?
What I read:
MemberEnvAllowList.brokerUriEnvNames(FleetConfig)(MemberEnvAllowList.java:159- 167) readsconfig.coordinator()(andconfig.broker()) directly off whateverFleetConfigit'shanded. Both call sites hand it the live config:
HerdrPeerLauncher.brokerUriEnvNames()(HerdrPeerLauncher.java:1529-1530) callsMemberEnvAllowList.brokerUriEnvNames(config.get())—configis theSupplier<FleetConfig>(
ConfigRef), so this re-reads on every spawn.memberCredentials.policy:allow-list(derivedAllowedNames,HerdrPeerLauncher.java:1517-1527): the set issubtracted from the derived allow-list twice — once inside
MemberEnvAllowList.derive(..., excludedNames)(removes it from the profile/allow:-derived union) and again explicitly(
allowed.removeAll(brokerUriEnvNames), line 1525) afterlaunch.env()'s own keys are unionedin. The scrub then blanks anything not in this final
allowedset, running after thepane's login shell has sourced everything (
EnvAllowListScrub, post-shell).deny-by-default(overlayBlockedCredentials,HerdrPeerLauncher.java:1317-1325): the setis unioned into
blockedand a sentinel value is written over each blocked name in thepane-creation env map only — a pre-shell overlay, per
MemberEnvAllowList's own class doc(lines 149-157): "Under the deny-list policy there is no scrub: the name is only removed from
the pre-shell env map, and a login shell that sources the operator's secret store re-exports it.
... deny-list deployments do NOT get this guarantee."
So: after a reload changes
coordinator.uriEnvfrom (say)OLD_NAMEtoNEW_NAME, the very nextspawn's exclusion set is
{NEW_NAME, ...}—OLD_NAMEdrops out of it immediately, while thealready-open
LeadMailboxkeeps using whatever host variableOLD_NAMEnamed (it was neverrebuilt). Whether that makes the value visible to a newly-spawned member depends on the policy:
deny-by-default: the protection this exclusion buys was already documented as weakregardless of reload — a login shell that re-sources the operator's secret store (which is where
OLD_NAME's value would live in the first place, per "Central secret store" / "Login shell beatslauncher env") overwrites the pre-shell sentinel outright. So under this policy the reload-vs-
mailbox desync doesn't introduce a new hole on top of the one the class doc already names — the
exclusion's protection for either name was never guaranteed to survive a login shell under this
policy.
allow-list: the post-shell scrub is the real control, and it defaults to denyeverything not explicitly derived.
OLD_NAMEwas never added to the allow set by the exclusionmechanism — the exclusion only ever removes it from an allow set some other source put it in
(a profile's
env:map, or the operator's ownmemberCredentials.allow:list — seeMemberEnvAllowList's class doc, "even when the operator lists them underallow:, they areexcluded here"). So in the default case (
OLD_NAMEis not independently allow-listed elsewhere),losing the exclusion changes nothing:
OLD_NAMEstill isn't in the derived allow set, and thescrub still blanks it.
The narrow case where it does matter: if
OLD_NAMEis also present in a profile'senv:map (frozen at startup, from
HerdrPeerLauncher's ownMap.copyOf(profiles)) or in the operator'smemberCredentials.allow:list (read live, so this can even be added in the same reload) — i.e.exactly the coincidence the exclusion mechanism exists to guard against — then after the reload,
OLD_NAMEre-enters the derived allow set with nothing left to strip it back out, the scrub keepsit, and if the operator's shell also still exports
OLD_NAMEwith the mailbox's real (still-live)AMQP URI, a member spawned after the reload keeps that value in its environment.
So: yes, it can, but only in that narrow, config-dependent case — an operator who has (or adds,
in the same reload)
OLD_NAMEtomemberCredentials.allow:or a profile'senv:map. In thedefault configuration (nothing else references that variable name), no — the general deny-by-default
allow-set logic already excludes it independent of the coordinator-specific safety net. I read
MemberEnvAllowList.javain full and the two call sites named above; I did not write or run anythingthat exercises the exposure path, per the issue's instruction, and I didn't touch
MemberEnvAllowList— invariant 2.Is a top-level reflection coverage test (the #323 mechanism) worth building here?
No, not now — and not a blind copy.
ConfigRefProfileCoverageTestworks becauseProfileis aflat record of scalars: it mutates one component at a time via reflection and asserts a single
boolean method (
sameLaunchSettings) notices, then pins the exclusion set with a reason. Two thingsbreak that shape at the top level:
FleetConfig's components are nested records with their own defaulting, several deeply(
Fleetalone holdsleaders: Map<String, Leader>,charters,developers/reviewers/architects,tabLabel). Building valid "base" and "alt" instances for every nested record byreflection (as
baseValues()/altValues()do forProfile) is real authoring work, not amechanical extension —
FleetandMemberCredentialsare not "one more scalar row" the wayProfile's fields are.Profile, "is this component compared or excluded" is a closed question the method itselfanswers — reflection can observe it directly. For
FleetConfig, "is this key genuinely hot" means"is it read through
config.get()at every point of use inFleetd.java/HerdrPeerLauncher.java/etc.", which is a fact about other files, invisible to reflection over the config record. A
checker built this way could only assert "this key is explicitly named somewhere (cold, deferred,
or a hot-exclusion set with a citation)" — it cannot verify the citation is true, the same gap
LAUNCH_SETTINGS_EXCLUDEDalready has (a human still has to read the wiring to trust the excuse).health/coordinatordon't fit a three-way partition at all. A reflection mutate-and-assertloop needs each component to land in exactly one bucket (cold / deferred / hot-excluded). These
two need a fourth "split — see doc" bucket the #323 mechanism never needed, and building that
bucket well is exactly the open design question in the options section above, not something a
generic coverage test can settle.
What such a checker WOULD still catch, if built: a new top-level
FleetConfigcomponent addedlater with no explicit triage anywhere — the literal shape of both #323 and this issue. That's real,
recurring value (two real instances so far). What it would NOT catch: a wrongly-justified
exclusion (an "it's read live" comment that isn't true), or a wrong verdict on a split key — both
still require a human reading the wiring, same as today.
Given the authoring cost (especially
Fleet) and that the two genuinely dangerous gaps here(
health,coordinator) can't be resolved by this mechanism until their split classification isdecided, I'd hold off building it now. If/when
health/coordinatorget a final classification, anarrower version — enumerate top-level components, require each to be named in
COLD_KEYS, anexplicit deferred-key name list (
changedDeferredKeysdoesn't currently have one — it's all inlineifs), or a documented hot/split exclusion set — becomes a much smaller, well-defined change. Bothanswers were explicitly fine per the issue; this is mine, and I'm open to being told to build it
anyway.
Build
Full run:
Tests run: 1341, Failures: 0, Errors: 0, Skipped: 0—BUILD SUCCESS(45.575 s).ConfigRefTestalone:Tests run: 19, Failures: 0, Errors: 0, Skipped: 0.Mutation proof
primary— removed theprimarycheck fromchangedDeferredKeys, ran only the new test:Restored, re-ran full
ConfigRefTestgreen.configReload— removed theconfigReloadcheck, ran only the new test:Restored, re-ran full
ConfigRefTestgreen (19/19), then fullmvn clean install(1341/1341) asquoted above.
Caveats for review
healthandcoordinatorare untouched by design — this PR does not resolve them, per the issue.fleetd/fleetd.yamlis gitignored and absent from my worktree; nothing here depends on or reportsits contents.
deny-by-default's pre-shell-only protection forcoordinator/brokerURI env names (already documented as bypassable by a login shell) should bestrengthened — that's the existing, separately-documented deny-list weakness, not something this
ticket's reload-classification scope covers.
Pull request closed