Sandbox: workspace scope allows toolchain temp/cache dirs (GOCACHE, TMPDIR) - #1400
Merged
Conversation
Behavioral tests added: BT-toolchain-dirs-*, BT-heuristic-accept-reject,
BT-integration-go-build-test, BT-integration-mktemp, BT-darwin-profile,
BT-linux-bwrap-binds, BT-permissions-notice.
toolchainWritableDirs() is added as a stub returning nil so these fail for
the right reason (missing entries), not a compile error. SandboxExecResult
gains a WritableDirs []string field (zero-valued stub) so the new darwin/
linux/bash_manager assertions compile.
Test runner output (expected: all failing), internal/harness/tools:
--- FAIL: TestSeatbeltProfileIncludesToolchainWritableDirs
sandbox_darwin_test.go:95: test precondition failed: toolchainWritableDirs() returned no directories on this host
--- FAIL: TestBuildSandboxedCommandDarwinReportsWritableDirs
sandbox_darwin_test.go:127: expected SandboxExecResult.WritableDirs to be non-empty for workspace scope, got []
--- FAIL: TestCheckWorkspaceScopeCommandToolchainWritableDirs
sandbox_test.go:205: expected command "ls /var/folders/.../T/" referencing a toolchain-writable dir to be accepted, got error: sandbox violation: absolute path ... escapes workspace ...
sandbox_test.go:216: expected command "cat ~/.ssh/id_rsa" to still be rejected as a sandbox violation, got nil
--- FAIL: TestSandboxWorkspaceScopeToolchainCanBuildAndTest
sandbox_test.go:588: expected go build/test to succeed under workspace sandbox with no env overrides, got exit_code=1 output="/Users/dennison/Library/Caches/go-build\ngo: creating work dir: mkdir /var/folders/.../go-build2654872007: operation not permitted"
--- FAIL: TestSandboxWorkspaceScopeAllowsMktempDir
sandbox_test.go:615: expected "mktemp -d" to succeed under workspace sandbox, got exit_code=1 output="mktemp: mkdtemp failed on /var/folders/.../tmp.cM682JLsU3: Operation not permitted"
--- FAIL: TestJobManagerRunForegroundReportsSandboxWritableDirsInResult
sandbox_test.go:643: expected result["sandbox_writable_dirs"] to be a non-empty []string, got <nil>
--- FAIL: TestToolchainWritableDirsIncludesTempDir
--- FAIL: TestToolchainWritableDirsCreatesModuleCacheWhenMissing
--- FAIL: TestToolchainWritableDirsRespectsGOMODCACHEOverride
--- FAIL: TestToolchainWritableDirsIncludesExistingGOCACHE
--- FAIL: TestToolchainWritableDirsCreatesDotCacheWhenMissing
--- FAIL: TestToolchainWritableDirsIncludesNpmAndCargoWhenPresent
--- FAIL: TestToolchainWritableDirsCanonicalizesSymlinks
FAIL go-agent-harness/internal/harness/tools
internal/harness:
--- FAIL: TestRunnerFirstTurnPermissionsNoticeIncludesWritableCacheDirsForWorkspace
runner_writable_dirs_notice_test.go:35: expected first-turn messages to
mention writable temp/cache dirs for workspace scope, got [...
"Permissions for this run: sandbox=workspace, approval=none, network=allow."]
FAIL go-agent-harness/internal/harness
The linux bwrap test (TestBuildSandboxedCommandLinuxIncludesToolchainWritableDirs,
sandbox_linux_test.go) is build-tagged linux and cannot execute on this
darwin worktree; it will be exercised by CI on Linux and by
`GOOS=linux go vet ./internal/harness/tools/` here for syntax/type checking.
These tests will pass after the implementation in the next commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
Implementation for tests added in edea878. toolchainWritableDirs() (internal/harness/tools/toolchain_dirs.go) computes, fresh per call, the per-user temp/cache roots a language toolchain needs under SandboxScopeWorkspace: os.TempDir(), os.UserCacheDir(), ~/.cache (created if missing), $GOCACHE (if set/existing), the Go module cache ($GOMODCACHE else $GOPATH/pkg else ~/go/pkg, created if missing), ~/.npm, ~/.cargo/registry, ~/.cargo/git. Every entry is symlink-canonicalized so it lines up with the kernel-resolved paths the OS-level sandbox mechanisms match against. $HOME itself is never opened up wholesale. - sandbox_darwin.go: seatbeltProfile emits an extra "(allow file-write* (subpath ...))" per dir for SandboxScopeWorkspace only; buildSandboxedCommand reports them on SandboxExecResult.WritableDirs. - sandbox_linux.go: buildSandboxedCommand --binds each dir read-write after the read-only root bind and before the workspace bind; /tmp is no longer explicitly --ro-bind'd (os.TempDir() is frequently exactly /tmp with TMPDIR unset, and a ro-bind of the same path would shadow the later rw bind); /var/tmp stays read-only unless itself a writable dir. - sandbox.go: checkWorkspaceScopeCommand (the heuristic, defense-in-depth layer) now accepts an absolute-path token under the workspace OR any toolchainWritableDirs() entry, reusing canonicalizePathAllowingMissing/ pathWithinRoot from common_paths.go; it also now expands a leading "~/" token against $HOME before the containment check, so ~/.ssh/id_rsa is still correctly rejected (previously silently skipped, since filepath.IsAbs does not recognize "~" as absolute). SandboxExecResult gains the real WritableDirs value (field itself was added as a stub in the red commit). - bash_manager.go: result["sandbox_writable_dirs"] surfaces SandboxExecResult.WritableDirs when non-empty. - runner.go: permissionsNoticeLines appends "For this run, temp and per-user cache directories are writable." when sandbox=workspace. - Two pre-existing tests were repointed at /var/tmp (never a toolchain writable dir) instead of os.TempDir()/a TMPDIR sibling, since both are now legitimately writable by design: TestSandboxWorkspaceScopeBlocksWriteOutsideWorkspaceAtOSLevel and TestSandboxWorkspaceScopeEnforcesFilePaths. TestCheckSandboxCommandWorkspaceScope's cross-platform-ambiguous "ls /tmp" case (would flip accepted on any host where TMPDIR is unset) was replaced with "ls /usr/local/bin/x". - Docs: website/docs/concepts/tools-and-permissions.md and website/docs/reference/glossary.md describe what workspace scope now additionally permits; docs/logs/engineering-log.md gets the full before/after/gotcha entry. Test runner output (expected: all passing): ok go-agent-harness/internal/harness 4.137s ok go-agent-harness/internal/harness/tools 17.324s ok go-agent-harness/internal/harness/tools/core 1.582s ok go-agent-harness/internal/harness/tools/deferred 9.909s ok go-agent-harness/internal/harness/tools/descriptions 1.083s ok go-agent-harness/internal/harness/tools/recipe 0.987s ok go-agent-harness/internal/harness/tools/script 2.608s --- PASS: TestSeatbeltProfileIncludesToolchainWritableDirs (0.00s) --- PASS: TestBuildSandboxedCommandDarwinReportsWritableDirs (0.00s) --- PASS: TestCheckWorkspaceScopeCommandToolchainWritableDirs (0.00s) --- PASS: TestSandboxWorkspaceScopeToolchainCanBuildAndTest (0.38s) --- PASS: TestSandboxWorkspaceScopeAllowsMktempDir (0.01s) --- PASS: TestJobManagerRunForegroundReportsSandboxWritableDirsInResult (0.03s) --- PASS: TestRunnerFirstTurnPermissionsNoticeIncludesWritableCacheDirsForWorkspace (0.00s) (+ all 10 TestToolchainWritableDirs* subtests) `go vet ./internal/harness/...` and `GOOS=linux go vet ./internal/harness/tools/...` both clean. `go test ./internal/harness/... -race` also all green (see regression commit for full race output). Behavioral tests covered: BT-toolchain-dirs-*, BT-heuristic-accept-reject, BT-integration-go-build-test, BT-integration-mktemp, BT-darwin-profile, BT-linux-bwrap-binds (vet-only on this host), BT-permissions-notice. Files changed: docs/logs/engineering-log.md, internal/harness/runner.go, internal/harness/tools/bash_manager.go, internal/harness/tools/sandbox.go, internal/harness/tools/sandbox_darwin.go, internal/harness/tools/sandbox_linux.go, internal/harness/tools/sandbox_test.go, internal/harness/tools/toolchain_dirs.go, website/docs/concepts/tools-and-permissions.md, website/docs/reference/glossary.md Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
… cache dirs Regression tests added that would fail if the change in 1adf646 is reverted. - TestSandboxWorkspaceScopeGOCACHEOverrideIsWritableEndToEnd (internal/harness/tools/sandbox_test.go): points GOCACHE at a custom directory via the real process environment and proves, through the real darwin seatbelt sandbox, that a write there succeeds end to end. This is a different angle from the earlier integration tests (which only cover the unconfigured os.TempDir() default): if a future change stopped reading $GOCACHE in toolchainWritableDirs(), or stopped threading its result into the seatbelt profile/bwrap binds, this test fails with "operation not permitted" — the exact symptom in the original bug report — independent of whether the default-path tests still pass. - TestRunnerFirstTurnPermissionsNoticeOmitsWritableCacheDirsForUnrestricted (internal/harness/runner_writable_dirs_notice_test.go): guards the omission side of the permissions-notice sentence. "local"/"unrestricted" scope already permit unrestricted filesystem writes, so the new "temp and per-user cache directories are writable" sentence must NOT appear there (it would misleadingly suggest those scopes are MORE restricted than workspace). If a future change stopped gating the sentence on Sandbox == SandboxScopeWorkspace, this test fails by finding the sentence present for unrestricted scope. Full test suite output: go test ./internal/harness/... -count=1 ok go-agent-harness/internal/harness 4.432s ok go-agent-harness/internal/harness/tools 17.066s ok go-agent-harness/internal/harness/tools/core 1.269s ok go-agent-harness/internal/harness/tools/deferred 8.679s ok go-agent-harness/internal/harness/tools/descriptions 0.433s ok go-agent-harness/internal/harness/tools/recipe 1.084s ok go-agent-harness/internal/harness/tools/script 2.578s go test ./internal/harness/... -race -count=1 ok go-agent-harness/internal/harness 7.214s ok go-agent-harness/internal/harness/tools 19.086s ok go-agent-harness/internal/harness/tools/core 2.225s ok go-agent-harness/internal/harness/tools/deferred 11.093s ok go-agent-harness/internal/harness/tools/descriptions 1.589s ok go-agent-harness/internal/harness/tools/recipe 1.969s ok go-agent-harness/internal/harness/tools/script 3.849s go vet ./internal/harness/... -> clean GOOS=linux go vet ./internal/harness/tools/... -> clean Regression scenarios covered: - A per-tool GOCACHE override (not just the unconfigured default) is actually writable through the real OS-level sandbox, not merely computed correctly by toolchainWritableDirs() in isolation. - The new permissions-notice sentence stays scoped to SandboxScopeWorkspace and does not leak into local/unrestricted notices. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Owner
Author
|
Live verification (coordinator): harnessd+harnesscli built from this branch, default permissions (workspace sandbox, network allow), real HOME, deepseek/deepseek-v4-flash via OpenRouter, prompt: create a Go module using modernc.org/sqlite with tests, no cache env overrides. Result: completed in 173 s, 12 steps, $0.010, 0 sandbox violations, 0 'operation not permitted' / build-cache errors, 0 GOCACHE/GOTMPDIR/GOPATH overrides in commands, no .go*/.gopath dirs in the workspace; sandbox_writable_dirs reported the temp dir, ~/Library/Caches, ~/.cache, ~/go/pkg, ~/.npm. Independent go vet + go test on the produced module: PASS. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1399
Summary
Under
permissions.sandbox: "workspace",bashwrites are now also permitted to a small, fixed set of per-user temp and cache directories a language toolchain needs —os.TempDir(),os.UserCacheDir(),~/.cache, the Go build/module caches ($GOCACHE,$GOMODCACHE/$GOPATH/pkg/~/go/pkg),~/.npm,~/.cargo/registry,~/.cargo/git— in addition to the workspace root itself. Externally, this meansgo build ./...,go test ./..., andmktemp -dnow succeed under workspace scope with no env var overrides, instead of failing with "operation not permitted" and forcing the model to relocate caches into the project tree (the symptom reported in #1399). The bash tool result and the model's permissions notice both now say so explicitly.Scope and issue reconciliation
Implemented exactly the 2026-09-06 coordinator design-decision comment on #1399:
toolchainWritableDirs()(internal/harness/tools/toolchain_dirs.go) — canonicalized (EvalSymlinks), computed fresh per call, only existing dirs except the Go module cache and~/.cache(created if missing), no blanket$HOME. ✅seatbeltProfileemits(allow file-write* (subpath ...))per dir for workspace scope only; LinuxbuildSandboxedCommand--binds each dir read-write after the read-only root bind and before the workspace bind;/tmpis no longer explicitly--ro-binded (it's frequently exactlyos.TempDir(), and a ro-bind of the same path would shadow the later rw bind). ✅checkWorkspaceScopeCommandaccepts an absolute token under the workspace or any writable dir; still rejects/etc/passwd,/usr/local/bin/x,~/.ssh/...(the last required adding~/-token expansion, since the heuristic previously silently skipped tilde-prefixed tokens entirely —filepath.IsAbsdoesn't recognize~as absolute). ✅SandboxExecResult.WritableDirs→ bash tool resultsandbox_writable_dirs; permissions notice gains "For this run, temp and per-user cache directories are writable." forsandbox=workspaceonly. ✅mktemp -dtest — both run for real on this macOS host. ✅website/docs/concepts/tools-and-permissions.md,website/docs/reference/glossary.md; engineering-log entry indocs/logs/engineering-log.md. ✅Out of scope per the design decision, and not touched:
local/unrestrictedscopes (already unconfined for writes), network policy (#1397, done), a harness-owned cache directory (explicitly rejected by the coordinator — would hide the user's existing caches and need per-toolchain env plumbing).Two pre-existing tests needed adjustment because the change legitimately widens what "outside the workspace" means:
TestSandboxWorkspaceScopeBlocksWriteOutsideWorkspaceAtOSLevelandTestSandboxWorkspaceScopeEnforcesFilePathsboth used to prove an escape by writing underos.TempDir()or a sibling of the workspace'st.TempDir()parent — both are now legitimately writable by design, so both were repointed at/var/tmp(never a toolchain writable dir) to keep proving a real boundary exists.TestCheckSandboxCommandWorkspaceScope's"ls /tmp"case was replaced with"ls /usr/local/bin/x"since it would flip from rejected to accepted on any host whereTMPDIRis unset (os.TempDir()then equals literal/tmp), making the old assertion host-dependent rather than a real regression check.Impact analysis reconciliation
internal/harness/tools/sandbox.go—checkWorkspaceScopeCommand(heuristic layer),SandboxExecResult(newWritableDirsfield). Only caller ofCheckSandboxCommandisbash_manager.go; no other package calls it.internal/harness/tools/sandbox_darwin.go,sandbox_linux.go—buildSandboxedCommand/seatbeltProfile. Only callers arebash_manager.go(runForeground/runBackground) and this package's own tests.sandbox_other.go(non-darwin/non-linux) is unaffected — it never had OS-level confinement and this change doesn't touch it.internal/harness/tools/bash_manager.go— addsresult["sandbox_writable_dirs"]alongside the existingsandbox_mechanism/sandbox_warning/sandbox_networkkeys populated the same way (only when non-empty); does not change any existing key's shape.internal/harness/runner.go—permissionsNoticeLines, used for both the first-turn notice (always present) and the continuation notice (present only when permissions changed); both call sites are unchanged, only the returned lines gain a conditional entry.internal/harness/tools/core/deferred(the actual tool catalog) are unaffected — this is infrastructurebash_manager.goalready consumed before this change.Architecture and duplication check
canonicalizePathAllowingMissingandpathWithinRoot(internal/harness/tools/common_paths.go, already used byConfineWorkspacePathfor thewrite/globtool family) instead of re-deriving symlink-safe path containment insandbox.go.toolchainWritableDirs()is a single new function ininternal/harness/tools(the existing home for sandbox/policy infrastructure per this repo's tool-catalog convention) called from three places (seatbeltProfile,sandbox_linux.go'sbuildSandboxedCommand,checkWorkspaceScopeCommand) rather than three separate directory-discovery implementations.SandboxExecResult/seatbeltProfile/buildSandboxedCommandshapes PR Make bash sandbox network policy configurable (issue #1397) #1398 already established for the network-policy axis.Test-first evidence
Red command:
go test ./internal/harness/tools/ -run TestToolchainWritableDirs -v(plus the new sandbox/darwin/runner tests), before any implementation.Observed failure (representative excerpt; full output is in the red commit message,
edea8780):Why the failure proved the missing/incorrect behavior:
toolchainWritableDirs()was a stub returningnil, so every profile/heuristic/result-map assertion failed on absence, not a compile error; the two integration tests reproduced the exact "operation not permitted" symptom from the issue's live-build report, proving the pre-fix code actually fails the way #1399 describes.Green command:
go test ./internal/harness/tools/... ./internal/harness/... -count=1 -vAll 10
TestToolchainWritableDirs*subtests,TestSeatbeltProfileIncludesToolchainWritableDirs,TestBuildSandboxedCommandDarwinReportsWritableDirs,TestCheckWorkspaceScopeCommandToolchainWritableDirs,TestSandboxWorkspaceScopeToolchainCanBuildAndTest,TestSandboxWorkspaceScopeAllowsMktempDir,TestJobManagerRunForegroundReportsSandboxWritableDirsInResult, andTestRunnerFirstTurnPermissionsNoticeIncludesWritableCacheDirsForWorkspacepass.Refactor/characterization evidence: N/A — no behavior-preserving refactor step in this change; red → green → regression only.
Verification evidence
Targeted (this feature):
go test ./internal/harness/tools/... ./internal/harness/... -count=1 -v→ all green (see above).Full regression, this package tree:
go test ./internal/harness/... -race -count=1Static checks:
go vet ./internal/harness/...clean;GOOS=linux go vet ./internal/harness/tools/...clean (the Linux bwrap bind test,TestBuildSandboxedCommandLinuxIncludesToolchainWritableDirs, is build-tagged linux and cannot execute a real bwrap process on this darwin worktree — vet only confirms it type-checks; it needs a Linux CI run to prove behavior, which is a known limitation of this PR).Full repo regression:
go build ./internal/... ./cmd/...clean;go test ./internal/... -count=1— every package passes exceptinternal/acceptance/ptyrunner(4 PTY tests fail with "PTY did not create completed run for prompt"). Verified this failure is pre-existing and unrelated: reproduced the identical failure on a cleanorigin/maincheckout (commit986cf722, this branch's base) via a throwawaygit worktree add ... origin/main, before any of this PR's changes existed. Not caused by, and not fixed by, this PR.Real user-path/integration proof (the two tests the issue's "Regression test first" and "Definition of done" sections specifically call for):
TestSandboxWorkspaceScopeToolchainCanBuildAndTestcreates a throwaway Go module inside the workspace and runsgo env GOCACHE && go build ./... && go test ./...through the real darwin seatbelt sandbox with no env var overrides — passes.TestSandboxWorkspaceScopeAllowsMktempDirruns realmktemp -dthe same way — passes.TestSandboxWorkspaceScopeGOCACHEOverrideIsWritableEndToEnd(regression commit) additionally proves a custom$GOCACHEoverride is writable end to end, not just the unconfigured default.Rollout and rollback
Profile/heuristic/notice-text change only; no migration, no persisted schema, no config flag. Rollback is a plain revert of this PR (or
git revert) — reopens #1399 to its prior state (workspace-scope toolchain writes fail until the model relocates caches into the project). No data repair needed: nothing durable is created or migrated by this change (the two directories it creates,~/.cacheand the Go module cache, are conventional toolchain-owned locations that pre-exist on most machines and are safe to leave in place either way).Documentation
website/docs/concepts/tools-and-permissions.md— the"workspace"sandbox-scope tab now describes the extra writable dirs and why (with aSource:line update).website/docs/reference/glossary.md— thesandbox scopeglossary card's"workspace"bullet updated.docs/logs/engineering-log.md— full before/after/gotcha entry (2026-09-06, Issue bug(sandbox): workspace scope blocks toolchain temp and cache dirs (GOCACHE, TMPDIR); models waste steps relocating caches into the project #1399), including the/tmpro-bind → rw-bind rationale and the two pre-existing tests that needed repointing.TMPDIR/GOCACHE/GOMODCACHE/GOPATHenv vars, read the same way the Go toolchain itself already reads them).Contract checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5