fleetd #185 stage 3: opt-in worktreeGroup for group-shared worktrees #208

Closed
agent wants to merge 0 commits from worker/cb185-worktree-group-fc0c99-1 into main
Member

Implements the git-side piece of #185 stage 3: an opt-in top-level worktreeGroup config key that makes a provisioned worktree's repo group-shared, so a member spawned under a different OS user (via memberHerdrSocket) can write its own worktree, git metadata, and commit objects.

  • FleetConfig.worktreeGroup (last record component, back-compat constructor added, added to KNOWN_TOP_LEVEL_KEYS, documented in fleetd.example.yaml).
  • New seam Worktrees.shareWithGroup(repoRoot, worktreePath); GitWorktrees implementation shells git config core.sharedRepository group then a one-time chgrp/chmod g+rwX fix-up (setgid on directories only) over the worktree dir, .git/objects, refs, logs, worktrees, and packed-refs when present. No-op (zero processes) when no group is configured. Fails loudly (WorktreeException naming the group) when chgrp is refused.
  • SessionManager.acquireWithWorktree calls it AFTER overlayParity, not folded into add() — overlayParity copies more files in after add() returns, so sharing earlier would leave those files operator-owned.
  • Javadoc + example config explicitly say this isolates credentials, not the repository: a member in the group can still write the operator's git objects/refs.

Out of scope (per ticket): making the operator's own single-user worktrees group-shared by default; herdr socket mode / pane-id qualification / hostEnvNames (other #185 stages); testing against a real second OS user (none on this host — these are unit tests against a recording exec seam, not a live-group integration test).

Tests added: FleetConfigTest (worktreeGroup parsing + back-compat-constructor-chain preservation through withDefaults()), GitWorktreesTest (no-op when unconfigured via a recording exec seam, git-config-then-chgrp/chmod/setgid-per-path when configured, packed-refs tolerance, WorktreeException naming a nonexistent group), WorktreeSessionManagerTest (shareWithGroup runs after overlayParity, pinned via FakeWorktrees call-order tags). Every existing Worktrees fake (FakeWorktrees, RecordingWorktrees) updated to implement the new method.

mvn clean install (fleetd dir): BUILD SUCCESS, Tests run: 1057, Failures: 0, Errors: 0, Skipped: 0.

Implements the git-side piece of #185 stage 3: an opt-in top-level `worktreeGroup` config key that makes a provisioned worktree's repo group-shared, so a member spawned under a different OS user (via `memberHerdrSocket`) can write its own worktree, git metadata, and commit objects. - `FleetConfig.worktreeGroup` (last record component, back-compat constructor added, added to `KNOWN_TOP_LEVEL_KEYS`, documented in `fleetd.example.yaml`). - New seam `Worktrees.shareWithGroup(repoRoot, worktreePath)`; `GitWorktrees` implementation shells `git config core.sharedRepository group` then a one-time chgrp/chmod g+rwX fix-up (setgid on directories only) over the worktree dir, `.git/objects`, `refs`, `logs`, `worktrees`, and `packed-refs` when present. No-op (zero processes) when no group is configured. Fails loudly (WorktreeException naming the group) when chgrp is refused. - `SessionManager.acquireWithWorktree` calls it AFTER `overlayParity`, not folded into `add()` — overlayParity copies more files in after add() returns, so sharing earlier would leave those files operator-owned. - Javadoc + example config explicitly say this isolates credentials, not the repository: a member in the group can still write the operator's git objects/refs. Out of scope (per ticket): making the operator's own single-user worktrees group-shared by default; herdr socket mode / pane-id qualification / hostEnvNames (other #185 stages); testing against a real second OS user (none on this host — these are unit tests against a recording exec seam, not a live-group integration test). Tests added: FleetConfigTest (worktreeGroup parsing + back-compat-constructor-chain preservation through withDefaults()), GitWorktreesTest (no-op when unconfigured via a recording exec seam, git-config-then-chgrp/chmod/setgid-per-path when configured, packed-refs tolerance, WorktreeException naming a nonexistent group), WorktreeSessionManagerTest (shareWithGroup runs after overlayParity, pinned via FakeWorktrees call-order tags). Every existing Worktrees fake (FakeWorktrees, RecordingWorktrees) updated to implement the new method. `mvn clean install` (fleetd dir): BUILD SUCCESS, Tests run: 1057, Failures: 0, Errors: 0, Skipped: 0.
agent added 1 commit 2026-08-31 10:53:26 +02:00
fleetd #185: opt-in worktreeGroup config for group-shared worktrees
CI / contract (pull_request) Successful in 1m7s
CI / build (pull_request) Successful in 1m45s
8067ee4ec4
Adds worktreeGroup (top-level FleetConfig key), Worktrees.shareWithGroup
(GitWorktrees impl: git config core.sharedRepository group + one-time
chgrp/chmod g+rwX/setgid fix-up over the worktree, .git/objects, refs,
logs, worktrees, and packed-refs when present), and wires SessionManager
to call it AFTER overlayParity so overlay files are covered too. Off by
default (byte-identical behaviour when unset). Documents the
credentials-not-repository caveat in the javadoc and example config.
ltms added 1 commit 2026-08-31 16:35:04 +02:00
#185: skip absent git paths, and resolve the git dir instead of assuming .git
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m38s
6d82ca95a4
Two defects in the stage-3 share pass, both of which would have failed EVERY
provisioning spawn once worktreeGroup was set, not only the two-user case.

.git/logs was handed to chgrp unguarded while packed-refs was guarded. It does
not exist with core.logAllRefUpdates=false, or before the first ref update, and
chgrp on a missing path exits non-zero -- surfacing as a WorktreeException that
blames a group which is in fact fine. Every path is now skipped when absent.

repoRoot + "/.git" was hardcoded. That is a FILE, not a directory, when the
checkout is itself a linked worktree -- the very thing this class creates for
every member. It now asks git: rev-parse --git-common-dir, resolved against
repoRoot because git answers relatively for an ordinary checkout.

Both new tests were watched failing with the fix removed before being kept.
Owner

Merged to main manually, integrated with #207 into a55079a. Full suite on main: 1061 tests, 0 failures. Daemon redeployed onto that jar (9ec5fab0f136).

Two defects were fixed on top of this branch before merging — both would have broken every spawn, and the branch's own green suite did not catch either:

  • shareWithGroup assumed <repoRoot>/.git is a directory. For a linked worktree it is a file, so the chgrp -R/chmod -R walk failed. Now resolved through git rev-parse --git-common-dir, which answers relatively for an ordinary checkout and absolutely for a linked worktree.
  • The walk touched objects, refs, logs, worktrees and packed-refs unconditionally. Several of those do not exist in a fresh repository, so the command failed on a missing path. Now each path is skipped when absent, and the log line reports which paths were actually touched.

Also moved the shareWithGroup call in SessionManager.acquireWithWorktree to run after worktrees.overlayParity(...), not inside add(). overlayParity copies more files into the worktree after add() returns; sharing the group any earlier leaves those overlay files operator-owned and unreadable for a different-uid member. There is a comment at the call site saying so.

Two tests were added and watched failing with the guard removed: shareWithGroupSkipsPathsThatDoNotExist and shareWithGroupFollowsAnAbsoluteGitCommonDir.

Closing as merged.

Merged to `main` manually, integrated with #207 into `a55079a`. Full suite on main: 1061 tests, 0 failures. Daemon redeployed onto that jar (`9ec5fab0f136`). Two defects were fixed on top of this branch before merging — both would have broken every spawn, and the branch's own green suite did not catch either: - `shareWithGroup` assumed `<repoRoot>/.git` is a directory. For a linked worktree it is a *file*, so the `chgrp -R`/`chmod -R` walk failed. Now resolved through `git rev-parse --git-common-dir`, which answers relatively for an ordinary checkout and absolutely for a linked worktree. - The walk touched `objects`, `refs`, `logs`, `worktrees` and `packed-refs` unconditionally. Several of those do not exist in a fresh repository, so the command failed on a missing path. Now each path is skipped when absent, and the log line reports which paths were actually touched. Also moved the `shareWithGroup` call in `SessionManager.acquireWithWorktree` to run **after** `worktrees.overlayParity(...)`, not inside `add()`. `overlayParity` copies more files into the worktree after `add()` returns; sharing the group any earlier leaves those overlay files operator-owned and unreadable for a different-uid member. There is a comment at the call site saying so. Two tests were added and watched failing with the guard removed: `shareWithGroupSkipsPathsThatDoNotExist` and `shareWithGroupFollowsAnAbsoluteGitCommonDir`. Closing as merged.
ltms closed this pull request 2026-08-31 16:58:54 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m38s

Pull request closed

Sign in to join this conversation.