Implements ADR 0014 Amendment 1 (4-layer Solution 1) + ADR 0002 Amendment 9 (Provider ISOLATION contract) + opus 4.8 model.
Fresh-context opus reviewer APPROVE_WITH_MINOR; 2 nit fold-ins applied. 813 unit tests pass.
Known deferred coverage: Suite 44 PI231 E2E tests are placeholders under describe.skip pending Task #9 (PI231 prod-target validation). The load-bearing negative test ('in-sandbox cat ~/.olp/keys.json MUST fail') will be validated when Task #9 runs against the merged code.
PR-B outer-bwrap superseded; archive at phase-7-pr-b-outer-bwrap-snapshot branch.
PR-B PI231 live verification surfaced a hard problem: HTTP-path anthropic
spawns produce no claude stdout when sandbox-wrapped, while manual exec
of the identical wrap script in the same node process DOES produce output.
Root cause not yet isolated — likely interaction between SandboxManager's
in-process proxy sockets (HTTP/SOCKS Unix sockets in /tmp) and OLP's
HTTP request-handler event loop.
Until the root cause is debugged and Suite 44-equivalent E2E tests cover
the full HTTP request → sandbox spawn → response pipeline, this commit
adds an env-var emergency disable: OLP_SANDBOX_DISABLED=1 in the server
environment causes bootstrapSandbox() to short-circuit immediately,
returning { active: false, reason: '...' }. Provider plugins continue
to spawn unsandboxed (matching pre-PR-B behavior).
Default remains sandbox-enabled. Only operators with broken HTTP-path
sandbox should set the env var (which is everyone on PI231 today,
until follow-up debugging completes).
Operational follow-up (PI231 right now):
setsid env ... OLP_SANDBOX_DISABLED=1 node ~/olp/server.mjs ...
→ /health.sandbox.active=false
→ HTTP requests resume working
Future PR-B follow-up issues:
1. Reproduce HTTP-path sandbox spawn failure in unit test
2. Investigate in-process proxy lifecycle vs HTTP handler event loop
3. Possibly: switch to per-spawn SandboxManager.initialize() lifecycle
4. Re-enable sandbox by default after fix + Suite 44 HTTP-path coverage
Sandbox doctor (PR-A) unchanged. ADR 0014 unchanged (the disable is a
runtime gate, not a contract change). Suite 44 negative tests still
pass when enabled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live PI231 verification of d0dcd28 surfaced two real-runtime issues:
1. ~/.claude was denyRead, but claude CLI MUST read its own OAuth
credentials file to authenticate. Result: 100% of anthropic requests
on sandboxed PI231 failed with "Not logged in · Please run /login".
Live transcript:
{"event":"fallback_hop_error", "provider":"anthropic",
"error":"Not logged in · Please run /login"}
The cross-tenant risk for ~/.claude was a false security trade-off:
(a) ~/.claude is the spawn's OWN auth, not cross-tenant material
(all OLP clients share the same Anthropic OAuth — the file is
not multi-tenant secret)
(b) Real protection for the model accidentally exfiltrating creds
via tool_use is Phase 6c's --system-prompt suppressing tool
descriptions; sandbox-runtime can only blanket-deny a path, not
distinguish "claude reads creds" from "model emits Read tool"
Fix: remove ~/.claude from denyRead. Keep ~/.olp, ~/.ssh, ~/.config,
~/.codex (all genuinely cross-tenant).
Suite 44a (cat ~/.olp/keys.json MUST fail) unchanged — still proves
the core acceptance.
Suite 44c added (regression guard): in-sandbox cat ~/.claude/.credentials.json
MUST succeed when the file exists.
2. anthropic.mjs::_spawnAndStream called wrapSpawn() unconditionally,
including when a test had set __setSpawnImpl(mockFn). The wrap
rewrites bin to '/bin/sh -c <wrapped-string>' which breaks every HTTP
integration test that asserts on spawn args. Result on PI231 (sandbox
active): 16 production-shape tests failed because their mocks were
never called with the expected bin/args.
Fix: skip wrapSpawn when spawnImpl !== defaultSpawn (mock active).
Mocks don't exec, so isolation is meaningless there anyway. Real-CLI
spawns (production + Suite 44) still get wrapped.
Tests: 813 → 813 (44c is also PI231-gated, skips on Mac with file
absent; the fold-in does not change Mac test count).
Authority:
- @anthropic-ai/sandbox-runtime v0.0.52 — wrapWithSandbox() shell-string
return shape is the proximate cause of (2)
- Live PI231 2026-05-28 transcript: /v1/chat/completions returning
content=null + "Not logged in" from anthropic; OLP_E2E_SANDBOX=1 npm
test showing 16 prior pass tests now fail
- ADR 0014 § 4.1 PR-B acceptance criteria — the negative test (44a)
continues to pass; the false-positive denyRead (~/.claude) is corrected
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps the claude CLI spawn in @anthropic-ai/sandbox-runtime per ADR 0014
§ PR-B. Achieves multi-tenant filesystem + network isolation: the spawned
claude subprocess can no longer read ~/.olp/keys.json, ~/.claude/.credentials.json,
~/.ssh/, or any other per-client OAuth material. Only api.anthropic.com and
statsig.anthropic.com are reachable; only /tmp/olp-spawn/<uuid>/ is writable
per spawn (ephemeral, UUID-scoped to prevent cross-request contamination).
## Authority citations
- @anthropic-ai/sandbox-runtime v0.0.52
https://github.com/anthropic-experimental/sandbox-runtime
dist/sandbox/sandbox-manager.js — SandboxManager.initialize(), wrapWithSandbox()
dist/sandbox/sandbox-utils.js — getDefaultWritePaths()
- 2026-05-28 spike report at /tmp/sandbox-spike/report.md on PI231:
spike-anthropic.mjs (wrapWithSandbox call signature + NDJSON proof),
spike-deny.mjs (cat ~/.olp/keys.json MUST fail)
- OLP ADR 0014 § 2.1 PR-B, § 4.1 PR-B acceptance criteria
- OLP ADR 0009 Amendment 1 § Caveats #3 (sandbox is cloud prerequisite)
- OLP ALIGNMENT.md Rule 1 — provider plugin authority citation
- cc-mem incident 2026-05-27 § 3 (multi-tenant OAuth token exposure via
prompt injection)
## Files changed
- lib/sandbox/manager.mjs (new): bootstrap + spawn-wrap layer.
Exports: bootstrapSandbox(), isSandboxActive(), wrapSpawn(),
__resetSandboxManagerForTests(). Config-at-boot model (one
SandboxManager.initialize() at server start; per-request wrapWithSandbox()
reads from already-initialized state). Transparent pass-through when inactive.
- lib/providers/anthropic.mjs: spawn site wrapped via wrapSpawn({
bin, args, env, allowedDomains: ['api.anthropic.com','statsig.anthropic.com']
}). ADR 0009 Amendment 1 spawn args unchanged — only execution is wrapped.
- server.mjs: bootstrapSandbox() called before server.listen(); startup banner
logs sandbox.active state. /health.sandbox now includes active:boolean field
(distinguishes "deps present" from "SandboxManager initialized and wrapping").
- test-features.mjs: Suite 43 (8 tests — manager unit: bootstrap state, idempotency,
isSandboxActive, wrapSpawn passthrough, /health.active field) + Suite 44
(2 PI231-gated tests: 44a security negative test + 44b positive echo test,
skipped by default, run with OLP_E2E_SANDBOX=1 npm test).
- CHANGELOG.md, docs/adr/0014-sandbox-runtime-integration.md: PR-B status
updated; ADR table + /health JSON example updated with active field.
## Test count
805 (pre-PR-B) → 813 (+8 Suite 43; Suite 44 skipped on macOS, runs on PI231)
All 813 pass on macOS dev machine. 0 regressions.
## PI231 validation (required before merge — Suite 44)
After apt-get install bubblewrap socat (ripgrep already present) + server restart:
1. curl /health → confirm sandbox.available=true AND sandbox.active=true
2. Real anthropic request → confirm stream-json still works end-to-end
3. OLP_E2E_SANDBOX=1 npm test → confirm Suite 44a (cat ~/.olp/keys.json MUST fail)
and Suite 44b (echo SANDBOX_PROOF succeeds)
Reviewer: must SSH PI231, run Suite 44, and confirm the ADR 0014 § 4.1 criteria.
This commit is ready to push; do NOT push before Suite 44 transcript is captured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>