fleetd#222: put the claude-code charter file where the member can read it #229
Reference in New Issue
Block a user
Delete Branch "worker/cb222-charter-tmpdir-17f013-1"
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?
Fixes fleetd#222.
The defect
ClaudeCodeLauncher#writeCharterFilecalledFiles.createTempFile("fleetd-role-charter-", ".md")with no directory argument, which resolves against fleetd's own
java.io.tmpdir(on macOS: theper-user
$TMPDIRunder/var/folders/..., mode0700, and the file itself0600). UndermemberHerdrSocket:the member pane runs as a DIFFERENT OS user and cannot read that directory —and since #220 the charter file is the ONLY delivery path for
--append-system-prompt-file, always,not merely the fallback it used to be. So every claude-code member spawned under
memberHerdrSocket:would have been handed a charter path it could not read.The fix
Reused the exact mechanism #219 built for
OpenCodeLauncher#configParentDir()— no new abstraction:HerdrPeerLauncher#memberHerdrSocketConfigured()gates the branch.memberScrubParentDir()/memberGroup()resolveworktreeRoot:/worktreeGroup:.EnvAllowListScrub.shareWithGroup(dir, group)(the #213/#219 mechanism) shares a fresh per-spawndirectory read-only with the member's group.
writeCharterFilenow branches:memberHerdrSocketabsent (today's only live mode): byte-identical to before —Files.createTempFile("fleetd-role-charter-", ".md")with no directory argument, still resolvedagainst
java.io.tmpdir.memberHerdrSocketpresent: a fresh per-spawn directory is created underworktreeRoot(neverjava.io.tmpdir), the charter file goes inside it, then the whole directory is shared withworktreeGroupviashareWithGroup.Followed #219's REFUSAL decision, not #213's degrade decision: a charter is not a credential
control that can degrade gracefully, it is the member's turn contract (the rule that ends every turn
with
fleet_reply). So whenmemberHerdrSocketis configured butworktreeRootand/orworktreeGroupis missing, the spawn is refused with anIllegalStateExceptionnaming the missingkey — never a member launched without a readable charter.
Criterion 3 — how I convinced myself the "absent" path is byte-identical
The absent branch is the literal original code, unchanged:
Files.createTempFile("fleetd-role-charter-", ".md")with no directory argument. I did not introduce a stand-in directory or reroute it through anewly-computed "default" path — I kept the exact original call in its own
ifbranch so there is nobehavioral difference to introduce. I also added a regression test
(
memberHerdrSocketAbsentStaysUnderJavaIoTmpdirEvenWithALiveConfigSupplier) that spawns with alive, non-null
configsupplier (not justconfig == null, which every pre-existing test alreadyexercises) and asserts the resulting charter file still lands directly under
java.io.tmpdirwiththe same
fleetd-role-charter-*naming (no wrapping directory).Tests added (all watched RED before the fix, GREEN after)
In
ClaudeCodeLauncherTest:memberHerdrSocketWithWorktreeRootAndGroupPutsCharterUnderWorktreeRootAndSharesIt— charterdirectory lands under
worktreeRoot, permissions arerwxr-x---/rw-r-----.memberHerdrSocketWithoutWorktreeRootOrGroupRefusesTheSpawn— refuses, namesworktreeRoot,nothing started (
agent.startnever called).memberHerdrSocketWithWorktreeRootButNoGroupRefusesTheSpawn—worktreeRootalone insufficient,names
worktreeGroup.memberHerdrSocketAbsentStaysUnderJavaIoTmpdirEvenWithALiveConfigSupplier— criterion 3 above.I proved this by stashing the production change, running these 4 tests against the pre-fix
ClaudeCodeLauncher.java, and confirming 3 of 4 failed (test 4, the "absent" case, correctly passedeven pre-fix, since that behavior was never meant to change). Then I restored the fix and reran —
all 4 green.
Measured: what claude-code does with an unreadable
--append-system-prompt-fileRan the real binary (
claude 2.1.258) directly, in-p/print mode with a bogusANTHROPIC_API_KEYso any success would require it to get past this check first:
chmod 000, still owner-unreadable — a reasonable stand-in for"different OS user"):
Error reading append system prompt file: EACCES: permission denied, open '<path>', exit code 1, immediately — no network call, no hang.Error: Append system prompt file not found: <path>, exit code 1,immediately.
Conclusion: it is the LOUD failure, not the silent one. claude-code checks the file before
touching auth or network, so the pre-fix bug would make the herdr pane exit immediately — which the
CB-306 spawn-readiness gate would surface as "did not reach injectable state" (the same
unexplained-timeout shape fleetd #220 already describes), not a member that silently sits idle and
never calls
fleet_reply. Still a real defect (every claude-code member undermemberHerdrSocket:would fail to spawn), just not the worse, undetectable failure mode. Documented this in the
writeCharterFilejavadoc.Review follow-up:
currentUserGroup()was itself a copy of fleetd#225My first version of
ClaudeCodeLauncherTest#currentUserGroup()was copied fromOpenCodeLauncherTest, which turned out to be a THIRD copy of an open bug (fleetd#225): it read thegroup that owns the current working directory (wherever Maven was started from —
staffin ahome checkout,
wheelunder/private/tmpon macOS), not the process's real primary group. Thoseonly coincide by accident, and the
assumeTrueguard only detected "no POSIX groups at all," never"a resolvable group I'm not a member of" — so a test run from
/private/tmphit a realchgrpfailure instead of a clean skip.
Fixed to resolve the real primary group via
id -gn, matching the shape that landed on PR #230 forthe other two copies (
OpenCodeLauncherTest,HerdrPeerLauncherAllowListWiringTest) — sameapproach, same skip wording.
Verified from both locations, per review request:
Home checkout (
/Users/dai.ha/LTMS/.bridged-worktrees/b86c6b-1/fleetd) —id -gnresolvesto
staff, which the operator is a member of:cp -Runder/private/tmp(owning groupwheel, which the operator is NOT a member of —id -gnstill correctly resolves tostaff):ClaudeCodeLauncherTestalone —This is the case that would have failed with the old (copied) helper —
chgrp ... wheelrefusedfor an operator not in
wheel, exactly the failure mode the review reported.The full
mvn clean installfrom/private/tmpis NOT fully green on this branch:OpenCodeLauncherTestandHerdrPeerLauncherAllowListWiringTeststill carry the OLD,directory-group-reading
currentUserGroup()and fail there withcannot share generated directory ... with group 'wheel'. That is fleetd#225's other two copies, already fixed on PR#230 (not yet merged to
main, so absent from this branch) — out of this PR's scope, and notsomething I touched.
ClaudeCodeLauncherTestitself, and every other test class, is unaffectedand green in both locations.
Build
mvn clean install, full unpiped output, fromfleetd/in the worktree (home checkout):Scope
Touched only
ClaudeCodeLauncher.javaandClaudeCodeLauncherTest.java, reusing existingHerdrPeerLauncher/EnvAllowListScrubhelpers from #213/#219 — no changes toGitWorktrees(#224,in progress by another worker),
OpenCodeLauncher, or the other twocurrentUserGroup()copies(#230's scope).
Caveat for review
writeCharterFilehad to become an instance method (wasstatic) since it now callsmemberHerdrSocketConfigured()/memberScrubParentDir()/memberGroup()onthis— its onlycaller (
argvWithFleet) was already an instance method, so this is mechanical, not a design change.