Summary
A command that needs ~/Library (build/SPM caches, DerivedData) or a system XPC service — e.g. xcodebuild … build — has no working path to run outside the sandbox, even when the agent sets expects_sandbox_block: true. It runs under the workspace seatbelt, which denies exactly the paths and services the build needs, and the up-front escalation never fires.
Root cause
expects_sandbox_block: true only pulls the escalation prompt forward when the command's scope verdict is 'ambiguous':
// src/main/services/security/permission-policy.ts:309 (shellExpectedBlockEscalation)
if (analysis.verdict !== 'ambiguous') return { eligible: false, reasons: [] }
A command with no network/outside-path markers in its text is classified 'sandbox' (contained):
// src/main/services/security/shell-scope.ts:390
return { verdict: 'sandbox', reasons: ['no network or outside-path signals detected'] }
xcodebuild -workspace … build reads/writes ~/Library/Caches/org.swift.swiftpm, ~/Library/Developer/Xcode/DerivedData, the system $TMPDIR (/var/folders/.../T), and talks to com.apple.CoreSimulator.CoreSimulatorService — but none of that is visible in the command string, so it's classified 'sandbox', the expects_sandbox_block hint is silently ignored (shell-tool.ts:288), and it runs under seatbelt. The seatbelt overlay allows only the workspace + workspace $TMPDIR, so the build fails with permissionDenied on the SPM cache, DerivedData, and the result bundle, plus CoreSimulatorService connection invalid.
The only built-in escape for such a command is the reactive maybeRetryUnsandboxed offer (shell-tool.ts:324-340), which requires a non-zero exit + recorded sandbox violations. In practice that path is fragile here (see the sibling | tail issue) and the agent never gets an unsandboxed run.
Not a hardware/OS limitation
The observing agent concluded CoreSimulator was "unreachable even outside the sandbox." That's wrong: Copse ships with hardenedRuntime only, not the macOS App Sandbox (build/entitlements.mac.plist has no com.apple.security.app-sandbox), so a genuinely unsandboxed child can reach CoreSimulator. In the one run where DerivedData was redirected into the workspace, xcodebuild got past the cache denials and successfully enumerated installed simulators — proving the block is the seatbelt, not the OS.
Suggested fixes (options)
- Recognize known build drivers (
xcodebuild, gradle, swift build, cargo) as 'ambiguous' so expects_sandbox_block / the escalation prompt can fire for them, and/or
- Add the common build-cache paths (
~/Library/Caches/org.swift.swiftpm, ~/Library/Developer/Xcode/DerivedData) and the CoreSimulator service to the seatbelt overlay so contained builds work without escaping, and/or
- Let
expects_sandbox_block: true escalate a 'sandbox'-verdict command too (with a clear prompt), since today it's a no-op for the exact commands most likely to need it.
Context
Surfaced while investigating a failed DuckDuckGo iOS build. Sibling issues: the 300s timeout_ms cap, and | tail masking the exit code (which defeats the reactive retry offer above).
Related prior art: #623 (ACP unsandboxed retry), #481 (tmp dir sandbox escape), #104 (spoofable sandbox-failure — why the reactive path is runner-signal-only).
Summary
A command that needs
~/Library(build/SPM caches, DerivedData) or a system XPC service — e.g.xcodebuild … build— has no working path to run outside the sandbox, even when the agent setsexpects_sandbox_block: true. It runs under the workspace seatbelt, which denies exactly the paths and services the build needs, and the up-front escalation never fires.Root cause
expects_sandbox_block: trueonly pulls the escalation prompt forward when the command's scope verdict is'ambiguous':A command with no network/outside-path markers in its text is classified
'sandbox'(contained):xcodebuild -workspace … buildreads/writes~/Library/Caches/org.swift.swiftpm,~/Library/Developer/Xcode/DerivedData, the system$TMPDIR(/var/folders/.../T), and talks tocom.apple.CoreSimulator.CoreSimulatorService— but none of that is visible in the command string, so it's classified'sandbox', theexpects_sandbox_blockhint is silently ignored (shell-tool.ts:288), and it runs under seatbelt. The seatbelt overlay allows only the workspace + workspace$TMPDIR, so the build fails withpermissionDeniedon the SPM cache, DerivedData, and the result bundle, plusCoreSimulatorService connection invalid.The only built-in escape for such a command is the reactive
maybeRetryUnsandboxedoffer (shell-tool.ts:324-340), which requires a non-zero exit + recorded sandbox violations. In practice that path is fragile here (see the sibling| tailissue) and the agent never gets an unsandboxed run.Not a hardware/OS limitation
The observing agent concluded CoreSimulator was "unreachable even outside the sandbox." That's wrong: Copse ships with
hardenedRuntimeonly, not the macOS App Sandbox (build/entitlements.mac.plisthas nocom.apple.security.app-sandbox), so a genuinely unsandboxed child can reach CoreSimulator. In the one run where DerivedData was redirected into the workspace, xcodebuild got past the cache denials and successfully enumerated installed simulators — proving the block is the seatbelt, not the OS.Suggested fixes (options)
xcodebuild,gradle,swift build,cargo) as'ambiguous'soexpects_sandbox_block/ the escalation prompt can fire for them, and/or~/Library/Caches/org.swift.swiftpm,~/Library/Developer/Xcode/DerivedData) and the CoreSimulator service to the seatbelt overlay so contained builds work without escaping, and/orexpects_sandbox_block: trueescalate a'sandbox'-verdict command too (with a clear prompt), since today it's a no-op for the exact commands most likely to need it.Context
Surfaced while investigating a failed DuckDuckGo iOS build. Sibling issues: the 300s
timeout_mscap, and| tailmasking the exit code (which defeats the reactive retry offer above).Related prior art: #623 (ACP unsandboxed retry), #481 (tmp dir sandbox escape), #104 (spoofable sandbox-failure — why the reactive path is runner-signal-only).