CB-592: every member inherits herdr's whole environment, so workers carry the admin GITEA_ACCESS_TOKEN #77
Closed
opened 2026-08-15 18:20:34 +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#77
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?
Confirmed live on 2026-08-15 by a probe, not inferred. Operator's rule, stated the same day: "only
leader/arch allow to use GITEA_ACCESS_TOKEN. others use WORKER_GITEA_TOKEN." We do not enforce that.
What was measured
A
localmember was spawned and asked to report which credential names are set — never values:The primary's own pane reports 99 variables and
ANTHROPIC_*allunset.Why it happens
HerdrPeerLauncher.baseEnvbuilds a freshLinkedHashMapholding onlyPATHand the profile'senv:. Reading our code alone, a member looks like it gets a small, explicit environment.It does not. That map is an overlay, not the environment. herdr spawns the pane from its own
process environment and layers our map on top. herdr was started from a login shell, so it carries
everything
${SHARED_ENV}/tools/secrets.shexports — including the adminGITEA_ACCESS_TOKEN. The99-vs-108 gap is exactly the bridge's explicit additions on top of an inherited base.
Nothing in the launcher accounts for what sits underneath, because from inside the launcher there is
nothing to see.
Impact
and reach every repo the token reaches. That defeats
CLAUDE.md's "the lead never delegates themerge" and the member contract's "Never merge" — those are currently honour-system, not enforced.
GITEA_TOKENviagitTokenEnvso a worker gets the least privilege it needs. The admin token is simply sittingbeside it.
opencode.jsonturns it into a tool. That file is tracked (H, so it ships in everyworktree) and mounts the gitea MCP with
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}". Sincethe variable resolves, an opencode member gets admin forge tools, not just a string it would have
to think to use.
Contrast:
.mcp.jsonisS(skip-worktree, the primary's local copy) and referencesGITEA_ACCESS_TOKEN— which is correct, because the primary is the lead.Not affected
The subscription boundary holds. The primary's pane has no
ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,so herdr's environment carries none, so a
subscription: truemember cannot inherit one and be pushedoff-subscription.
SubscriptionGuardand theBridgedConfighard-strip are not defeated by this.Acceptance criteria
GITEA_ACCESS_TOKEN. Proven by the sameprobe: spawn, report names only,
GITEA_ACCESS_TOKENmust not be usable.bridged.yaml. A per-profileenv:entry is exactly the silent-default shape this repo has shipped nine times: add a sixthprofile, forget the line, and the hole is back with nothing failing.
GITEA_TOKEN(the CB-302 repo-scoped grant) still reaches members and still works — the point isleast privilege, not no privilege. A worker must still be able to open its own PR.
opencode.jsonnamesWORKER_GITEA_TOKEN, notGITEA_ACCESS_TOKEN. A shared file thatships in every worktree must grant the least privilege; anything needing more overrides locally,
the way
.mcp.jsonalready does by being skip-worktree.CONTEXT7_TOKENandAI_GATEWAY_TOKENare also currently readable by every member. They are far less dangerous than anadmin forge token, but "we chose to allow it" and "we never noticed" must not look the same.
baseEnvcannot quietly reopen it.CLAUDE.md's claim that a member mounts "only the bridge MCP" is corrected — it is already falsefor opencode members, which get context7 and gitea from the tracked
opencode.json.Note for whoever takes this
Verify how herdr merges the map before assuming a fix works. Setting a variable to the empty string in
the overlay is the obvious approach, but "does an empty value override an inherited one, or is it
skipped as blank?" is exactly the kind of thing that silently does nothing. Prove it with the probe
above, on a real spawn — not with a unit test alone.
Live verification: the shadow did NOT hold. Root cause found.
I redeployed onto
831a918(pid 31615, jar7e981ec19df4), spawned a reallocalmember andprobed its pane.
GITEA_ACCESS_TOKENinside the member was still the real admin token, not thesentinel.
Why — measured, not guessed
The overlay works. It is the step after it that undoes the work.
~/.zprofileline 41 sources${SHARED_ENV}/tools/secrets.sh.export GITEA_ACCESS_TOKEN=....Confirmed directly:
So the real token is put back over our sentinel before the member process starts.
The overlay itself is fine.
GITEA_TOKENis injected the same way, is exported by no shell file,and was observed set inside the live pane. The rule is narrow and clear:
Today that exception is exactly
GITEA_ACCESS_TOKENandWORKER_GITEA_TOKEN.The 403 was my bad test, not a regression
I had the probe call
/api/v1/user. A minimalwrite:repositorytoken cannot read that endpoint,so 403 is the correct answer. On a repo endpoint the same token is fine:
CB-302 is intact. I withdraw that half of the report.
What shipped now —
0331ecdAdded
BRIDGED_MEMBER=1to every member's env. It is a namesecrets.shnever exports, so nothingoverwrites it — the same mechanism
GITEA_TOKENalready proves. Tests 805 → 807; both new testswere shown to fail when the marker is reverted.
The sentinel stays. It is correct for any peer kind whose pane does not start a login shell, and it
keeps the intent visible at the one place every adapter passes through. The javadoc and test javadoc
no longer claim a protection that was measured not to hold.
What is left — one line, and it is not ours to write
${SHARED_ENV}/tools/secrets.shis the operator's file. The whole remaining fix is:Waiting on the operator. Until then, the admin token is still readable by any member from a shell,
and the enforced boundary is the MCP mount only (
6939e0c,opencode.json→WORKER_GITEA_TOKEN).Wider lesson
This kills a whole class of fix. "Set the variable at spawn" is not a security control on this host
for any name the login shell re-exports. Anything that must not reach a member has to be either
absent from
secrets.sh, guarded by the marker, or enforced somewhere other than the environment.VERIFIED ON A LIVE MEMBER PANE — the leak is closed
Operator added the guard to
${SHARED_ENV}/tools/secrets.sh:33:Daemon redeployed onto
0331ecd— pid 75467, jar4cb34bc1348d, 807 tests,/healthz200,herdr protocol 19, fresh
bridged listeningline, no ERROR lines,bridge_whoamistillprimary.Then spawned a real
localmember and probed its pane. Raw output:What each line proves
BRIDGED_MEMBER=PRESENTADMIN=SENTINELThe operator's rule — "only leader/arch use
GITEA_ACCESS_TOKEN, others useWORKER_GITEA_TOKEN" — is now enforced at the environment, not only at the MCP mount.Why it needed both halves
Neither half works alone:
secrets.sh(previous comment).secrets.shguard alone would leaveGITEA_ACCESS_TOKENsimply unset, and an unsetvariable is inherited straight through from herdr's own process environment — which carries the
real admin token.
Together the launcher writes a dead value and the guard stops the shell replacing it. That is why
ADMIN=SENTINELrather thanADMINbeing empty.Standing constraint for anyone touching this
secrets.shis outside this repo and is not covered by our tests. A future edit that drops theguard re-opens the leak silently, and
mvnwill stay green. The pinned tests(
everySpawnShadowsTheAdminGiteaAccessToken,everySpawnMarksThePaneAsAMember,aProfileEnvEntryCannotClearTheMemberMarker) protect only the launcher's half.Closing.