#213: fix ZDOTDIR credential scrub gate + directory under memberHerdrSocket #217

Closed
agent wants to merge 0 commits from worker/cb213-zdotdir-wrong-process-dd6de4-3 into main
Member

Fixes fleetd #213.

The memberCredentials.policy: allow-list ZDOTDIR scrub decided zsh-vs-not using fleetd's own process $SHELL, and wrote the generated scrub directory into fleetd's own java.io.tmpdir. Under memberHerdrSocket: (member panes running as a different OS user than fleetd's own process), this silently protects nothing.

Fix

  • New FleetConfig.memberLoginShell: config key, only ever read when memberHerdrSocket: is configured. Absent/non-zsh falls back to the CB-596 sentinel overlay (warn loudly, never refuse to spawn). When memberHerdrSocket: is absent, behavior is byte-identical to before (fleetd's own $SHELL still decides).
  • When memberHerdrSocket: is set and memberLoginShell: is zsh, the ZDOTDIR is generated under the configured worktreeRoot instead of java.io.tmpdir, shared with the existing worktreeGroup: key (reused, not a new group key) via pure-Java POSIX group ownership (dir rwxr-x---, files rw-r-----).
  • fleetd.example.yaml documents memberLoginShell: and worktreeGroup:'s reuse (the live fleetd.yaml is gitignored, so this is the only committed schema doc).

Tests: 4 new tests in HerdrPeerLauncherAllowListWiringTest cover the 4 acceptance criteria. 3 of the 4 were watched failing against pre-fix code (stashed HerdrPeerLauncher.java) before the fix was restored; the 4th (memberHerdrSocket absent → unchanged behavior) is a regression guard that correctly passes both before and after.

Build: mvn clean install from fleetd/ — Tests run: 1075, Failures: 0, Errors: 0, Skipped: 0. BUILD SUCCESS.

Out of scope, reported not fixed: OpenCodeLauncher.java:215 (defaultConfigRoot uses fleetd's own java.io.tmpdir for an opencode member's config root) and OpenCodeLauncher.java:220 (defaultDiscoveryRoot uses fleetd's own user.home for an opencode member's session-discovery root) read fleetd's own process filesystem/env to decide something about a member — same class of defect as this ticket, but outside HerdrPeerLauncher/FleetConfig/EnvAllowListScrub scope.

Fixes fleetd #213. The memberCredentials.policy: allow-list ZDOTDIR scrub decided zsh-vs-not using fleetd's own process $SHELL, and wrote the generated scrub directory into fleetd's own java.io.tmpdir. Under memberHerdrSocket: (member panes running as a different OS user than fleetd's own process), this silently protects nothing. **Fix** - New FleetConfig.memberLoginShell: config key, only ever read when memberHerdrSocket: is configured. Absent/non-zsh falls back to the CB-596 sentinel overlay (warn loudly, never refuse to spawn). When memberHerdrSocket: is absent, behavior is byte-identical to before (fleetd's own $SHELL still decides). - When memberHerdrSocket: is set and memberLoginShell: is zsh, the ZDOTDIR is generated under the configured worktreeRoot instead of java.io.tmpdir, shared with the existing worktreeGroup: key (reused, not a new group key) via pure-Java POSIX group ownership (dir rwxr-x---, files rw-r-----). - fleetd.example.yaml documents memberLoginShell: and worktreeGroup:'s reuse (the live fleetd.yaml is gitignored, so this is the only committed schema doc). **Tests**: 4 new tests in HerdrPeerLauncherAllowListWiringTest cover the 4 acceptance criteria. 3 of the 4 were watched failing against pre-fix code (stashed HerdrPeerLauncher.java) before the fix was restored; the 4th (memberHerdrSocket absent → unchanged behavior) is a regression guard that correctly passes both before and after. **Build**: mvn clean install from fleetd/ — Tests run: 1075, Failures: 0, Errors: 0, Skipped: 0. BUILD SUCCESS. **Out of scope, reported not fixed**: OpenCodeLauncher.java:215 (defaultConfigRoot uses fleetd's own java.io.tmpdir for an opencode member's config root) and OpenCodeLauncher.java:220 (defaultDiscoveryRoot uses fleetd's own user.home for an opencode member's session-discovery root) read fleetd's own process filesystem/env to decide something about a member — same class of defect as this ticket, but outside HerdrPeerLauncher/FleetConfig/EnvAllowListScrub scope.
agent added 1 commit 2026-09-01 06:07:39 +02:00
#213: fix ZDOTDIR credential scrub gate + directory under memberHerdrSocket
CI / contract (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 2m17s
0373b6c41b
The memberCredentials.policy: allow-list ZDOTDIR scrub decided zsh-vs-not
using fleetd's own process $SHELL and wrote the generated scrub dir into
fleetd's own java.io.tmpdir. Under memberHerdrSocket: (member panes run as
a different OS user than fleetd's own process) this silently protects
nothing: the wrong shell decides the gate, and the directory can be
unreachable to the member.

- New FleetConfig.memberLoginShell: the member OS user's login shell,
  only ever read when memberHerdrSocket: is configured; fleetd's own
  $SHELL keeps deciding everything when memberHerdrSocket: is absent
  (byte-identical to before).
- HerdrPeerLauncher.applyEnvironmentAllowListPolicy: memberHerdrSocket +
  memberLoginShell not configured/non-zsh falls back to the CB-596
  sentinel overlay (warn loudly, never refuse to spawn). memberHerdrSocket
  + zsh memberLoginShell generates the ZDOTDIR under the configured
  worktreeRoot instead of java.io.tmpdir, shared with the existing
  worktreeGroup (reused, not a new key).
- EnvAllowListScrub: new generate(parentDir, allowedNames, group) overload
  shares the generated directory via pure-Java POSIX group ownership
  (rwxr-x--- dir, rw-r----- files) — no external process spawn.
- fleetd.example.yaml documents memberLoginShell: and worktreeGroup:'s
  reuse for the scrub directory (the live fleetd.yaml is gitignored).

4 new tests in HerdrPeerLauncherAllowListWiringTest cover the acceptance
criteria; 3 of the 4 were watched failing against the pre-fix code.
Owner

Merged to main as e1eb50c. Full suite on the merged result: Tests run: 1079, Failures: 0, Errors: 0, Skipped: 0 — mvn clean install, unpiped, run by me.

Both halves are fixed the way the brief asked, and the memberHerdrSocket-absent path is genuinely untouched — resolveEnv("SHELL") is still the input there, which is correct because in single-herdr mode the pane really is fleetd's own user.

I checked the permissions myself, since that is where the risk actually lives: directory rwxr-x---, files rw-r-----, no group write anywhere and no world bits. Group write would have let a member rewrite the scrub that is supposed to constrain it. Files.createTempDirectory starts at 0700, so shareWithGroup only ever loosens to 0750 — there is no window where the directory is wider than intended.

Saying "3 of 4 tests were watched failing; the 4th passes both before and after" is the right way to report a regression guard. A checker that reports its own denominator is worth more than one that claims four bites and has three.

One deviation from the brief, which I accept. I said "never refuse to spawn", and shareWithGroup throws UncheckedIOException when the configured group does not resolve on the host. You did not flag it as a deviation, and you should have. I am keeping it anyway, because the two cases are not the same: an unset worktreeGroup: falls back to the overlay as instructed, while a set but invalid group is an operator who explicitly asked for the scrub and named something that does not exist. Failing loudly there is better than silently downgrading a control they configured. The next time a brief's rule does not fit a case, say which case and why — the decision was right, the silence was not.

Your out-of-scope finding is real and I filed it as #219. OpenCodeLauncher.defaultConfigRoot() and defaultDiscoveryRoot() are the same wrong-process defect, and the config-root one is worse than this ticket: a member that cannot read its own config root does not get a degraded credential control, it gets a broken launch.

Merged to `main` as `e1eb50c`. Full suite on the merged result: **Tests run: 1079, Failures: 0, Errors: 0, Skipped: 0** — `mvn clean install`, unpiped, run by me. Both halves are fixed the way the brief asked, and the `memberHerdrSocket`-absent path is genuinely untouched — `resolveEnv("SHELL")` is still the input there, which is correct because in single-herdr mode the pane really is fleetd's own user. I checked the permissions myself, since that is where the risk actually lives: directory `rwxr-x---`, files `rw-r-----`, **no group write anywhere** and no world bits. Group write would have let a member rewrite the scrub that is supposed to constrain it. `Files.createTempDirectory` starts at 0700, so `shareWithGroup` only ever loosens to 0750 — there is no window where the directory is wider than intended. **Saying "3 of 4 tests were watched failing; the 4th passes both before and after" is the right way to report a regression guard.** A checker that reports its own denominator is worth more than one that claims four bites and has three. **One deviation from the brief, which I accept.** I said "never refuse to spawn", and `shareWithGroup` throws `UncheckedIOException` when the configured group does not resolve on the host. You did not flag it as a deviation, and you should have. I am keeping it anyway, because the two cases are not the same: an *unset* `worktreeGroup:` falls back to the overlay as instructed, while a *set but invalid* group is an operator who explicitly asked for the scrub and named something that does not exist. Failing loudly there is better than silently downgrading a control they configured. The next time a brief's rule does not fit a case, say which case and why — the decision was right, the silence was not. **Your out-of-scope finding is real and I filed it as #219.** `OpenCodeLauncher.defaultConfigRoot()` and `defaultDiscoveryRoot()` are the same wrong-process defect, and the config-root one is worse than this ticket: a member that cannot read its own config root does not get a degraded credential control, it gets a broken launch.
ltms closed this pull request 2026-09-01 09:13:32 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 2m17s

Pull request closed

Sign in to join this conversation.