Skip to content

test(sandbox): add worktree edge-case and Linux integration tests#44

Open
nhuelstng wants to merge 7 commits into
mainfrom
pr31-review-tests
Open

test(sandbox): add worktree edge-case and Linux integration tests#44
nhuelstng wants to merge 7 commits into
mainfrom
pr31-review-tests

Conversation

@nhuelstng

Copy link
Copy Markdown
Contributor

Supplements PR #31 with tests for edge cases and the Linux backend identified during review.

Depends on #31. These tests exercise code added in #31 (gitWorktreeGrants, resolveWorktreeCommonDir, pathWithinRoot). Merge #31 first, then rebase this PR onto main. The tests will fail to compile without #31's changes.

What

Adds 7 tests covering gaps found in the PR #31 review:

Unit tests (grants_test.go)

Test Covers
TestResolveGrantsSubmoduleNoWorktreeGrants Submodule admin dir has no commondir → no worktree grants leak into parent common dir. Pins the PR body's submodule exclusion claim.
TestResolveGrantsWorktreeAccessWrite AccessWrite (write-only) broadens worktree subdirs to read+write (git must read to commit); config/info/packed-refs/hooks stay read-only.
TestResolveGrantsBareRepoWorktree Worktree from a bare repo resolves through the git-invariant check (common dir is the bare repo dir itself).
TestResolveGrantsConcurrentWorktreesIsolation Two worktrees sharing one common dir each get grants for their OWN admin dir only — never the sibling's.

Linux integration tests (integration_worktree_linux_test.go, new file)

Test Covers
TestIntegrationWorktreeHooksRunButNotWritable Linux bwrap counterpart of the darwin test: sandboxed commit succeeds, prepare-commit-msg hook runs, hooks dir not writable.
TestIntegrationWorktreeSymlinkEscape Planted objects -> secret symlink blocked under real bwrap.
TestIntegrationWorktreeKnownLimitations PR body's known-limitation claims: branch -d fails cleanly (ref preserved), gc no-ops, commit succeeds despite packed-refs.lock EPERM, fsck clean.
TestIntegrationWorktreeLandlockCombined Combined bwrap + Landlock path: worktree commit succeeds (fs binds) AND network stays blocked (Landlock holds) in the same sandbox.

Why

The PR shipped a darwin-only integration test and unit tests covering AccessReadWrite/AccessRead/AccessNone. The review found:

  1. No Linux integration test — the bwrap backend (bind mounts) is a different code path from SBPL; the unit test checks the argv but doesn't run bwrap.
  2. Submodule exclusion untested — claimed in the PR body but not pinned by a test.
  3. AccessWrite untested — the worktree path broadens it to read+write, but no test covered this.
  4. Bare-repo worktrees untested — common CI/git-server layout.
  5. Concurrent worktrees untested — sibling admin dir isolation not pinned.
  6. Landlock + worktree combined path untested — the two enforcement layers (bwrap binds + Landlock net) had no combined test.

Verification

All tests pass on Linux (bwrap 0.11.0, git 2.53.0, Landlock ABI 6):

go test ./internal/sandboxrun/... -count=1
ok  github.com/tngtech/oh-my-agentic-coder/internal/sandboxrun  2.4s

Build and vet clean. No regressions in the existing suite.

Review findings from #31 still outstanding

Notes for the PR #31 author, not fixed here (out of scope for a test-only PR):

  • PR description mismatch: body says deny: hooks/ but code makes hooks read-only (readable, not writable, not hard-denied). The code is better than described — update the description.
  • Dead plumbing: denyAdds/worktreeDeny in gitWorktreeGrants is always nil; the third return value and the protected = append(protected, worktreeDeny...) line can be removed.

Sajjad Ahmad and others added 7 commits July 1, 2026 15:46
`omac sandbox run` grants the workdir read+write. For a plain clone that
covers `.git` (it lives inside the workdir), but a linked worktree splits
repo state: the workdir is `<repo>/.worktrees/<name>/` while the git-admin
dir is `<repo>/.git/worktrees/<name>/` — a sibling outside the workdir. So
`git add`/`commit` hit EPERM writing the object store, refs, reflogs and
per-worktree index/HEAD, and the agent can't commit from any worktree.

Detect a linked worktree (`.git` is a file), resolve the shared common dir
via the admin dir's `commondir`, and restore plain-clone parity by granting
the commit-relevant subdirs at the workdir's access level:

  - allow (rw): objects, refs, logs, packed-refs, worktrees/<name>
  - read-only:  config, info  (blocks core.hooksPath / credential.helper)
  - deny:       hooks/         (a writable hooks dir would run un-sandboxed
                                as the host user on the next host-side commit)

No-op for a plain clone or a submodule (whose admin dir has no `commondir`).

Verified below the process boundary: a real-`git worktree` pipeline test
drives ResolveGrants -> SBPL and bwrap argv generation and asserts the
object store is bound read+write, config read-only, and hooks masked.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
The worktree grant paths are derived from in-workdir file content
(`.git`, `<admin>/commondir`) that a prior sandboxed session could have
tampered with, and both backends symlink-canonicalize every grant into a
kernel rule. A malicious agent with rw workdir could plant a crafted
`.git` + admin/`commondir` chain whose resolved common dir it controls,
with `objects` a symlink to a sensitive dir (e.g. ~/.ssh). Because SBPL
emits write-allows AFTER protected-path denies (last match wins), the
symlink-resolved write grant would punch through the ~/.ssh denial —
read+write escape on the next `omac sandbox run` in that workdir.

Confine the grant:
- Enforce git's structural invariant: admin must be <common>/worktrees/<name>,
  rejecting a commondir aimed at an unrelated location.
- Admit only subdirs whose symlink-resolved path physically lives inside
  the resolved common dir; drop any entry that escapes (pathWithinRoot).

Adds regression tests proving a symlinked `objects`/`refs` never widens a
grant to the target, and a spoofed commondir yields no worktree grants.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
End-to-end testing under real Seatbelt showed the earlier "make
packed-refs writable" change was both ineffective and unnecessary:

- git 2.x's ref transaction tries to create <common>/packed-refs.LOCK in
  the common-dir root on every ref update. That root is deliberately not
  granted (it holds config and hooks), so the lock EPERMs — but git falls
  back to the loose-ref path and the commit succeeds (verified: exit 0,
  fsck clean, refs correct). Granting the packed-refs *file* rw cannot
  silence this: the blocker is the lock sibling in the ungranted root.
- Granting the common root rw to allow the lock would, on macOS, let a
  config write slip through (SBPL emits write-allows last, after the
  protected denies), defeating the core.hooksPath guard — the exact vector
  #30 closes. So the harmless lock noise is the accepted cost.

A normal add/commit only ever writes loose refs under refs/; packed-refs
is only rewritten by gc/pack-refs, which need that ungranted lock anyway.
So packed-refs is correctly read-only (git still reads it for status/log
and for committing onto a packed ref). Documented the limitation on
gitWorktreeGrants.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Flip the linked-worktree shared hooks dir from a hard deny to read-only:
readable+runnable so host-authored commit hooks (prepare-commit-msg,
commit-msg, pre-commit) execute inside the sandbox, but never writable so
the agent still cannot plant a hook that would run un-sandboxed on the
host's next commit (the #30 persistence vector). The read grant stays inside
the contained() symlink-containment check, since unlike a deny it would
otherwise follow a planted hooks->secret symlink to the target.

Add a real-Seatbelt e2e (rooted under $HOME, not $TMPDIR, so the baseline
temp-write grant doesn't mask the read-only hooks grant) proving the hook
runs during a sandboxed commit while the hooks dir stays unwritable.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Adds 7 tests covering gaps identified during PR #31 review:

Unit tests (grants_test.go):
- Submodule exclusion: submodule admin dir has no commondir, so no
  worktree grants leak into the parent common dir.
- AccessWrite worktree: write-only workdir broadens worktree subdirs to
  read+write (git must read objects/refs to commit); config/info/
  packed-refs/hooks stay read-only.
- Bare-repo worktree: worktree from a bare repo resolves through the
  git-invariant check (common dir is the bare repo dir itself).
- Concurrent worktrees isolation: two worktrees sharing one common dir
  each get grants for their OWN admin dir only, never the sibling's.

Linux integration tests (integration_worktree_linux_test.go, new file):
- Worktree hooks run but hooks dir not writable (Linux counterpart of
  the darwin test).
- Symlink escape blocked under real bwrap (objects -> secret).
- Known limitations: branch -d fails cleanly (ref preserved), gc
  no-ops, commit succeeds despite packed-refs.lock EPERM, fsck clean.
- Landlock + worktree combined path: commit succeeds (fs binds) AND
  network stays blocked (Landlock holds) in the same sandbox.

Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
staticcheck SA4006: out never used after branch -d call.
Replace out, code with _, code.

Fixes lint failure in #44.

Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant