mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
feat(sandbox): Phase 7 Solution 1 implementation + opus 4.8 (#68)
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.
This commit is contained in:
+164
-40
@@ -77,11 +77,12 @@ import { homedir } from 'node:os';
|
||||
import * as https from 'node:https';
|
||||
import * as http from 'node:http';
|
||||
import { ProviderError } from './base.mjs';
|
||||
// Phase 7 PR-B (ADR 0014 § PR-B): sandbox spawn wrap.
|
||||
// wrapSpawn() is transparent (returns inputs unchanged) when sandbox is inactive.
|
||||
// Authority: @anthropic-ai/sandbox-runtime v0.0.52, ADR 0014 § PR-B,
|
||||
// ADR 0009 Amendment 1 § unchanged spawn args — only the spawn execution is wrapped.
|
||||
import { wrapSpawn } from '../sandbox/manager.mjs';
|
||||
// Phase 7 Solution 1 (ADR 0014 Amendment 1): wrapSpawn() removed from manager.mjs.
|
||||
// Isolation is composed by server.mjs via prepareIsolatedEnvironment() before
|
||||
// provider.spawn() is called (Task #8). The anthropic ISOLATION block (Task #6)
|
||||
// declares per-provider primitives; _spawnAndStream() applies isolationCtx
|
||||
// (envOverrides, hardenedArgs, wrapForLayer3) on top of its own env-cleanup + args.
|
||||
// No sandbox/manager.mjs import needed in this plugin.
|
||||
|
||||
// ── Binary resolution ─────────────────────────────────────────────────────
|
||||
// OLP_CLAUDE_BIN env takes priority, then falls back to 'claude' from PATH.
|
||||
@@ -868,7 +869,7 @@ function buildSpawnEnv() {
|
||||
// stop chunk; proc.on('close') is the safety net if `result` is never emitted.
|
||||
//
|
||||
// OCP server.mjs:542: const proc = spawn(CLAUDE, cliArgs, { env, stdio: [...] });
|
||||
async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
async function* _spawnAndStream(irRequest, authContext, spawnImpl, isolationCtx) {
|
||||
const auth = authContext ?? readAuthArtifact();
|
||||
if (!auth?.accessToken) {
|
||||
throw new ProviderError(
|
||||
@@ -915,44 +916,54 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
// ADR 0009 Amendment 1: system prompt extracted from IR messages,
|
||||
// prepended with OLP_SYSTEM_PROMPT_WRAPPER, passed via --system-prompt.
|
||||
const systemPrompt = extractSystemPrompt(irRequest);
|
||||
const args = buildCliArgs(irRequest.model, systemPrompt);
|
||||
const baseArgs = buildCliArgs(irRequest.model, systemPrompt);
|
||||
|
||||
// stdin: serialized user/assistant/tool messages (system skipped — goes via --system-prompt)
|
||||
const prompt = irToAnthropic(irRequest);
|
||||
|
||||
// Phase 7 PR-B (ADR 0014 § PR-B): wrap spawn in sandbox-runtime if active.
|
||||
// wrapSpawn() is transparent when sandbox is inactive (returns inputs unchanged).
|
||||
// Per-spawn ephemeral cwd (UUID) is created inside wrapSpawn to prevent cross-
|
||||
// request contamination. Allowed domains are the Anthropic API domains only.
|
||||
//
|
||||
// ADR 0009 Amendment 1 § unchanged spawn args: only the spawn execution is
|
||||
// wrapped — bin/args/env/NDJSON parsing are all unchanged from pre-PR-B.
|
||||
//
|
||||
// 2026-05-28 PR-B fold-in: skip sandbox wrap when a custom spawnImpl is in
|
||||
// use (test mode — __setSpawnImpl was called). Test mocks do not actually
|
||||
// exec a binary, so sandbox isolation provides no protection there; the
|
||||
// wrap only obscures the original bin/args from the mock's assertions and
|
||||
// breaks every HTTP integration test that uses __setSpawnImpl + asserts
|
||||
// on spawn args. wrapSpawn is for real-CLI spawns; Suite 44 exercises that
|
||||
// path directly without going through this provider.
|
||||
//
|
||||
// Authority: @anthropic-ai/sandbox-runtime v0.0.52 wrapWithSandbox() API,
|
||||
// ADR 0014 § PR-B, spike-anthropic.mjs (PI231 2026-05-28).
|
||||
const usingMockSpawn = spawnImpl !== defaultSpawn;
|
||||
const wrapped = usingMockSpawn
|
||||
? { bin, args, env, cwd: undefined, sandboxed: false }
|
||||
: await wrapSpawn({
|
||||
bin,
|
||||
args,
|
||||
env,
|
||||
cwd: undefined, // let manager assign ephemeral cwd
|
||||
allowedDomains: ['api.anthropic.com', 'statsig.anthropic.com'],
|
||||
});
|
||||
// Task #8 — Phase 7 Solution 1: apply isolation context from orchestrator.
|
||||
// isolationCtx is provided by server.mjs (prepareIsolatedEnvironment) when
|
||||
// present. Three layers compose here:
|
||||
// Layer 1 (env): envOverrides have final precedence over buildSpawnEnv output.
|
||||
// Layer 4 (args): hardenedArgs transforms the final args array.
|
||||
// Layer 3 (wrap): wrapForLayer3 optionally wraps the command string via
|
||||
// sandbox-runtime (identity when inactive or hasInnerSandbox=true).
|
||||
// When isolationCtx is absent (legacy callers / tests), behavior is unchanged.
|
||||
// Authority: ADR 0014 Amendment 1 § A1.2 + ADR 0002 Amendment 9 § Backward compat.
|
||||
const envOverrides = isolationCtx?.envOverrides ?? {};
|
||||
const finalEnv = Object.keys(envOverrides).length > 0 ? { ...env, ...envOverrides } : env;
|
||||
|
||||
const hardenedArgs = isolationCtx?.hardenedArgs ?? ((a) => a);
|
||||
const args = hardenedArgs(baseArgs);
|
||||
|
||||
// Layer 3: wrapForLayer3 is async; returns the command string to spawn.
|
||||
// When sandbox-runtime is active and hasInnerSandbox=false for this provider,
|
||||
// the result is a wrapped shell invocation (/bin/sh -c <bwrap-args...> <cmd>).
|
||||
// When inactive (or hasInnerSandbox=true), it is an identity: returns bin unchanged.
|
||||
const wrapForLayer3 = isolationCtx?.wrapForLayer3 ?? (async (c) => c);
|
||||
const wrappedBin = await wrapForLayer3(bin);
|
||||
// If Layer 3 wrapping changed the bin (returns a '/bin/sh -c ...' style string),
|
||||
// pass the entire wrapped command as a shell-execute string; otherwise use bin/args
|
||||
// directly to avoid an unnecessary shell layer.
|
||||
let finalBin, finalArgs;
|
||||
if (wrappedBin !== bin) {
|
||||
// Layer 3 active: wrappedBin is the full shell command string. Invoke via sh -c.
|
||||
finalBin = '/bin/sh';
|
||||
finalArgs = ['-c', wrappedBin];
|
||||
} else {
|
||||
// Layer 3 inactive (identity): use bin + args directly.
|
||||
finalBin = bin;
|
||||
finalArgs = args;
|
||||
}
|
||||
|
||||
// ADR 0009 Amendment 1 § unchanged spawn args: NDJSON parsing unchanged.
|
||||
// Authority: ADR 0014 Amendment 1 § A1.2.3 (Layer 3 is orchestrator responsibility,
|
||||
// not provider responsibility); ADR 0002 Amendment 9 § Backward compatibility
|
||||
// (spawn() method is not changed; orchestrator composes above it).
|
||||
|
||||
// OCP server.mjs:542: spawn(CLAUDE, cliArgs, { env, stdio: ["pipe", "pipe", "pipe"] })
|
||||
const proc = spawnImpl(wrapped.bin, wrapped.args, {
|
||||
env: wrapped.env,
|
||||
...(wrapped.cwd ? { cwd: wrapped.cwd } : {}),
|
||||
const proc = spawnImpl(finalBin, finalArgs, {
|
||||
env: finalEnv,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
@@ -1144,8 +1155,14 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
// Tests set `anthropic._spawnImpl = mockSpawn` before calling `anthropic.spawn()`.
|
||||
let _spawnImpl = defaultSpawn;
|
||||
|
||||
export async function* spawn(irRequest, authContext) {
|
||||
yield* _spawnAndStream(irRequest, authContext, _spawnImpl);
|
||||
// Task #8 — Phase 7 Solution 1: isolationCtx is an optional third argument.
|
||||
// When present (from server.mjs prepareIsolatedEnvironment call), it carries
|
||||
// { envOverrides, hardenedArgs, wrapForLayer3, cleanup } — the orchestrator
|
||||
// composes these on top of the provider's own env-cleanup + args composition.
|
||||
// When absent (legacy callers, tests that don't pass it), behavior is identical
|
||||
// to the pre-Task-#8 path. Authority: ADR 0014 Amendment 1 § A1.2.
|
||||
export async function* spawn(irRequest, authContext, isolationCtx) {
|
||||
yield* _spawnAndStream(irRequest, authContext, _spawnImpl, isolationCtx);
|
||||
}
|
||||
|
||||
// Test hook: allows tests to inject a mock spawn without importing child_process.
|
||||
@@ -1603,3 +1620,110 @@ const anthropic = {
|
||||
};
|
||||
|
||||
export default anthropic;
|
||||
|
||||
// ── Provider ISOLATION contract ───────────────────────────────────────────
|
||||
// ADR 0002 Amendment 9 (2026-05-29) — Provider ISOLATION Contract for
|
||||
// Multi-Tenant Spawn Isolation. Specifies the isolation primitives the
|
||||
// lib/sandbox/manager.mjs orchestrator composes on every uncached spawn
|
||||
// of this provider.
|
||||
//
|
||||
// Authority citations (ALIGNMENT.md Rule 1 — Cite First):
|
||||
// @anthropic-ai/claude-code v2.1.152
|
||||
// § --system-prompt — full system-prompt replacement suppresses env-block
|
||||
// injection, tool descriptions (Bash, Read, Write, Edit), and all other
|
||||
// tool surfaces that Claude Code injects by default. Verified live on
|
||||
// PI231 (arm64 Debian Bookworm) at docs/spikes/2026-05-29-ephemeral-home.md.
|
||||
// § HOME env override — claude CLI v2.1.152 honours HOME completely; all
|
||||
// state writes ($HOME/.claude.json, $HOME/.claude/*) redirect to the
|
||||
// ephemeral root. Auth reads from $HOME/.claude/.credentials.json.
|
||||
// Verified at docs/spikes/2026-05-29-ephemeral-home.md (✅ PASS).
|
||||
// ADR 0009 Amendment 1 (Phase 6c) — the --system-prompt flag that achieves
|
||||
// tool suppression is injected by the spawn() method; it is the enforcement
|
||||
// mechanism crossTenantReadProtection='tool-suppression' cites.
|
||||
// ADR 0014 Amendment 1 (2026-05-29) — supersedes outer-bwrap PR-B with the
|
||||
// per-spawn ephemeral-home + per-provider ISOLATION architecture that this
|
||||
// block participates in. §A1.2 defines the four-layer model; §A1.3 names
|
||||
// this contract surface.
|
||||
// ADR 0002 Amendment 9 (2026-05-29) — specifies the ISOLATION contract shape,
|
||||
// field-level semantics, validation rules, and the anthropic concrete
|
||||
// instance this block implements.
|
||||
// cc-mem incident memory:
|
||||
// ~/.cc-rules/memory/projects/olp/incident_2026_05_27_spawn_cli_security.md
|
||||
// § 6.1 — empirical evidence that --system-prompt suppression is effective:
|
||||
// the model in a stream-json spawn without --tools cannot emit tool_use
|
||||
// blocks because the default Claude Code tool descriptions are absent.
|
||||
// This is the primary empirical basis for crossTenantReadProtection:
|
||||
// 'tool-suppression' in the absence of OS-level bwrap isolation.
|
||||
//
|
||||
// isolation rationale: Anthropic Claude reaches OLP via stream-json transport
|
||||
// without a tool surface (ADR 0009 Amendment 1's --system-prompt injection
|
||||
// suppresses env-block, file tools, Bash, and Read/Write/Edit). The model has
|
||||
// no documented mechanism to read files during the spawn. Cross-tenant read
|
||||
// protection is achieved at the prompt-engineering / CLI-flag layer. The OS-
|
||||
// level isolation primitives (HOME redirect + ephemeral credential mount) add
|
||||
// defense in depth against future CLI changes that might re-introduce a tool
|
||||
// surface. (cf. ADR 0014 Amendment 1 § A1.2.4 — Layer 4 tool hardening)
|
||||
|
||||
export const ISOLATION = {
|
||||
// Returns the env-var overrides that steer claude CLI to use the per-spawn
|
||||
// ephemeral home rather than the server process's real $HOME.
|
||||
// HOME is the POSIX-conventional lookup root; claude v2.1.152 reads
|
||||
// $HOME/.claude/.credentials.json for OAuth and writes session state to
|
||||
// $HOME/.claude.json and $HOME/.claude/*. Redirecting HOME is the
|
||||
// documented and verified mechanism (docs/spikes/2026-05-29-ephemeral-home.md).
|
||||
// CLAUDE_CONFIG_DIR is NOT honored as of v2.1.152 — do not use it.
|
||||
// keyId / reqId are received for signature consistency but unused here.
|
||||
ephemeralEnvOverrides: ({ ephemeralRoot, keyId: _keyId, reqId: _reqId }) => ({
|
||||
HOME: ephemeralRoot,
|
||||
}),
|
||||
|
||||
// Credential files to symlink from the operator's real home into the
|
||||
// ephemeral home so that claude CLI can authenticate without being given
|
||||
// access to the full ~/.claude/ directory.
|
||||
// srcAbsPath MUST be absolute (ADR 0002 Amendment 9 § 2 validation rule).
|
||||
// Authority: anthropic.auth.path above — ~/.claude/.credentials.json is
|
||||
// the documented OAuth artifact for @anthropic-ai/claude-code v2.1.152.
|
||||
credentialMounts: [
|
||||
[join(homedir(), '.claude', '.credentials.json'), '.claude/.credentials.json'],
|
||||
],
|
||||
|
||||
// Directories that must be pre-created (mkdir -p) under ephemeralRoot before
|
||||
// credentialMounts are processed. The CLI expects $HOME/.claude/ to exist;
|
||||
// absent the directory the auth-file symlink's parent would be missing.
|
||||
requiredHomePaths: [
|
||||
'.claude',
|
||||
// No additional mandatory pre-existing subdirs observed as of v2.1.152.
|
||||
// If future CLI versions add a mandatory subdir (e.g. .claude/logs),
|
||||
// add it here with an observed-behavior comment per ADR 0002 Amendment 9
|
||||
// § 3 ("speculative directories are a Rule 2 violation").
|
||||
],
|
||||
|
||||
// claude CLI (stream-json transport) does NOT spawn its own bwrap or
|
||||
// sandbox-exec boundary during normal OLP use. The Layer 3 outer
|
||||
// sandbox-runtime wrap (ADR 0014 Amendment 1 § A1.2.3) is therefore
|
||||
// applicable for this provider and must NOT be skipped.
|
||||
// Authority: @anthropic-ai/claude-code v2.1.152 stream-json path verified
|
||||
// at docs/spikes/2026-05-29-ephemeral-home.md — no nested sandbox observed.
|
||||
hasInnerSandbox: false,
|
||||
|
||||
// ADR 0009 Amendment 1's --system-prompt injection (Phase 6c) replaces the
|
||||
// entire system prompt and eliminates the default tool surface (Bash, Read,
|
||||
// Write, Edit, computer-use blocks) that claude would otherwise expose.
|
||||
// Empirical evidence: incident memory § 6.1 confirms suppression is effective
|
||||
// in stream-json mode. OS-level isolation (Layers 1-3) adds defense in depth.
|
||||
crossTenantReadProtection: 'tool-suppression',
|
||||
|
||||
// With tool-suppression active and no inner sandbox, the model cannot read
|
||||
// arbitrary files; the ephemeral-home + credential-mount isolation (Layers
|
||||
// 1-2) provides per-request HOME isolation. This combination is rated
|
||||
// suitable for a shared-OS-user deployment (all OLP keys on one OS user).
|
||||
// Authority: ADR 0014 Amendment 1 § A1.2 four-layer model + ADR 0006
|
||||
// risk-tier framework.
|
||||
recommendedDeploymentTier: 'shared-os-user',
|
||||
|
||||
// toolHardeningArgs omitted — the existing spawn() method's args already
|
||||
// encode the --system-prompt tool-suppression mechanism (ADR 0009 Amendment
|
||||
// 1). No additional CLI flags are needed at the orchestrator level.
|
||||
// Per ADR 0002 Amendment 9 § 7: absence means the orchestrator passes args
|
||||
// through unchanged from spawn().
|
||||
};
|
||||
|
||||
+166
-5
@@ -458,7 +458,7 @@ function buildSpawnEnv() {
|
||||
//
|
||||
// Authority: Codex CLI reference § "codex exec [flags] PROMPT"
|
||||
// § "--json": NDJSON event stream on stdout
|
||||
async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
async function* _spawnAndStream(irRequest, authContext, spawnImpl, isolationCtx) {
|
||||
const auth = authContext ?? readAuthArtifact();
|
||||
if (!auth?.accessToken) {
|
||||
throw new ProviderError(
|
||||
@@ -468,7 +468,7 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
}
|
||||
|
||||
const bin = resolveCodexBin();
|
||||
const { args, prompt, useStdin } = irToCodex(irRequest);
|
||||
const { args: baseArgs, prompt, useStdin } = irToCodex(irRequest);
|
||||
const env = buildSpawnEnv();
|
||||
|
||||
// Authority: Codex CLI reference § "Authentication"
|
||||
@@ -476,7 +476,34 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
// No explicit token injection: Codex CLI reads its own auth.json
|
||||
// (contrast with Anthropic plugin which injects CLAUDE_CODE_OAUTH_TOKEN).
|
||||
|
||||
const proc = spawnImpl(bin, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
// Task #8 — Phase 7 Solution 1: apply isolation context from orchestrator.
|
||||
// isolationCtx is provided by server.mjs (prepareIsolatedEnvironment) when
|
||||
// present. Three layers compose here:
|
||||
// Layer 1 (env): envOverrides (HOME, CODEX_HOME) have final precedence.
|
||||
// Layer 4 (args): hardenedArgs injects --sandbox read-only + -c approval_policy.
|
||||
// Layer 3 (wrap): wrapForLayer3 is identity for codex (hasInnerSandbox=true).
|
||||
// When isolationCtx is absent (legacy callers / tests), behavior is unchanged.
|
||||
// Authority: ADR 0014 Amendment 1 § A1.2 + ADR 0002 Amendment 9 § Backward compat.
|
||||
const envOverrides = isolationCtx?.envOverrides ?? {};
|
||||
const finalEnv = Object.keys(envOverrides).length > 0 ? { ...env, ...envOverrides } : env;
|
||||
|
||||
const hardenedArgs = isolationCtx?.hardenedArgs ?? ((a) => a);
|
||||
const args = hardenedArgs(baseArgs);
|
||||
|
||||
// Layer 3: wrapForLayer3 for codex is always identity (hasInnerSandbox=true);
|
||||
// included here for API symmetry with the anthropic path and future-proofing.
|
||||
const wrapForLayer3 = isolationCtx?.wrapForLayer3 ?? (async (c) => c);
|
||||
const wrappedBin = await wrapForLayer3(bin);
|
||||
let finalBin, finalArgs;
|
||||
if (wrappedBin !== bin) {
|
||||
finalBin = '/bin/sh';
|
||||
finalArgs = ['-c', wrappedBin];
|
||||
} else {
|
||||
finalBin = bin;
|
||||
finalArgs = args;
|
||||
}
|
||||
|
||||
const proc = spawnImpl(finalBin, finalArgs, { env: finalEnv, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
// Write prompt via stdin for multi-line prompts (D6 assumption A1)
|
||||
if (useStdin) {
|
||||
@@ -659,8 +686,14 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
// spawn: async (irRequest, authContext) => AsyncIterator<ResponseChunk>
|
||||
let _spawnImpl = defaultSpawn;
|
||||
|
||||
export async function* spawn(irRequest, authContext) {
|
||||
yield* _spawnAndStream(irRequest, authContext, _spawnImpl);
|
||||
// Task #8 — Phase 7 Solution 1: isolationCtx is an optional third argument.
|
||||
// When present (from server.mjs prepareIsolatedEnvironment call), it carries
|
||||
// { envOverrides, hardenedArgs, wrapForLayer3, cleanup } — the orchestrator
|
||||
// composes these on top of the provider's own env-cleanup + args composition.
|
||||
// When absent (legacy callers, tests that don't pass it), behavior is unchanged.
|
||||
// Authority: ADR 0014 Amendment 1 § A1.2.
|
||||
export async function* spawn(irRequest, authContext, isolationCtx) {
|
||||
yield* _spawnAndStream(irRequest, authContext, _spawnImpl, isolationCtx);
|
||||
}
|
||||
|
||||
// Test hook: inject mock spawn without importing child_process.
|
||||
@@ -795,6 +828,134 @@ export function doctorChecks({ _binaryExistsFn, _authReadFn } = {}) {
|
||||
];
|
||||
}
|
||||
|
||||
// ── ISOLATION export ─────────────────────────────────────────────────────
|
||||
// Declares per-provider isolation primitives consumed by lib/sandbox/manager.mjs
|
||||
// (per ADR 0014 Amendment 1 + ADR 0002 Amendment 9).
|
||||
//
|
||||
// Authority citations (all required per ALIGNMENT.md Rule 1):
|
||||
// codex CLI v0.133.0 — current PI231 prod version (verified 2026-05-29 spike)
|
||||
// https://developers.openai.com/codex/config-reference — CODEX_HOME env var
|
||||
// (2 occurrences verified: "$CODEX_HOME/profile-name.config.toml" and
|
||||
// "$CODEX_HOME/log" path templates)
|
||||
// https://developers.openai.com/codex/auth/ — ~/.codex/auth.json path
|
||||
// (2 occurrences verified: "auth.json under CODEX_HOME" credential-storage
|
||||
// section)
|
||||
// https://developers.openai.com/codex/concepts/sandboxing — --sandbox flag +
|
||||
// read-only default (codex inner bubblewrap sandbox)
|
||||
// openai/codex#16018 — inner bwrap behavior documented (failure under
|
||||
// restricted env, establishing hasInnerSandbox: true)
|
||||
// ADR 0014 Amendment 1 — orchestrator composition architecture
|
||||
// ADR 0002 Amendment 9 — ISOLATION contract spec (field semantics)
|
||||
// docs/spikes/2026-05-29-ephemeral-home.md § 5.3 — flag-drift caveat
|
||||
// (--ask-for-approval removed in codex v0.133.0; use -c approval_policy=)
|
||||
//
|
||||
// isolation rationale: OpenAI Codex's `codex exec` exposes a shell tool that
|
||||
// actually executes commands during the spawn (cc-mem incident memory § 3.2).
|
||||
// The CLI provides its own inner bubblewrap sandbox (`--sandbox read-only` by
|
||||
// default per https://developers.openai.com/codex/concepts/sandboxing) that
|
||||
// confines shell tool reads/writes. The orchestrator's outer isolation composes
|
||||
// with the inner sandbox: credential-dir redirect via CODEX_HOME
|
||||
// (https://developers.openai.com/codex/config-reference) + HOME redirect for
|
||||
// the inner bwrap's HOME lookup + per-spawn ephemeral credential mount.
|
||||
// hasInnerSandbox: true so the outer profile is relaxed to permit the inner
|
||||
// bwrap's user-namespace clone (openai/codex#16018).
|
||||
|
||||
export const ISOLATION = {
|
||||
// ephemeralEnvOverrides: pure function, no side effects, no fs access.
|
||||
// CODEX_HOME redirects the entire codex config/credential base directory.
|
||||
// HOME is also redirected because the codex inner sandbox inherits the parent
|
||||
// process's HOME for its own home lookup unless overridden.
|
||||
// Authority: CODEX_HOME → https://developers.openai.com/codex/config-reference
|
||||
// HOME → POSIX convention (both verified by PI231 spike § 4.3-4.4).
|
||||
ephemeralEnvOverrides: ({ ephemeralRoot, keyId: _keyId, reqId: _reqId }) => ({
|
||||
HOME: ephemeralRoot,
|
||||
CODEX_HOME: `${ephemeralRoot}/.codex`,
|
||||
}),
|
||||
|
||||
// credentialMounts: static list of [srcAbsPath, dstRelativeToEphemeralRoot].
|
||||
// srcAbsPath uses os.homedir() (imported as `homedir` at top of file) per
|
||||
// ADR 0002 Amendment 9 § Field 2 validation rules: absolute paths only, no
|
||||
// `~/` prefixes (shell-expansion semantics differ from Node.js behavior).
|
||||
// Authority: ~/.codex/auth.json → https://developers.openai.com/codex/auth/
|
||||
// "Codex caches login details locally in a plaintext file at ~/.codex/auth.json"
|
||||
// (matches existing codex.mjs `auth.path` field declaration above).
|
||||
credentialMounts: [
|
||||
[join(homedir(), '.codex', 'auth.json'), '.codex/auth.json'],
|
||||
],
|
||||
|
||||
// requiredHomePaths: directories to mkdir-p under ephemeralRoot before mounts.
|
||||
// .codex is required because CODEX_HOME points there and codex startup may
|
||||
// attempt to read from it before any auto-create logic runs (observed in
|
||||
// PI231 spike § 4.3 post-state: .codex/ created at spawn time).
|
||||
requiredHomePaths: [
|
||||
'.codex',
|
||||
],
|
||||
|
||||
// hasInnerSandbox: true — codex exec spawns its own bubblewrap sandbox
|
||||
// internally. Declaring true tells the outer isolation orchestrator to relax
|
||||
// the outer profile to permit clone(CLONE_NEWUSER) so the inner bwrap can
|
||||
// create user namespaces. Without this flag the inner bwrap fails with
|
||||
// EPERM. Authority: openai/codex#16018 + https://developers.openai.com/codex/concepts/sandboxing
|
||||
hasInnerSandbox: true,
|
||||
|
||||
// crossTenantReadProtection: 'inner-sandbox' — codex's shell tool runs real
|
||||
// commands but the inner bubblewrap sandbox (read-only by default) confines
|
||||
// reads/writes to the inner namespace. The toolHardeningArgs below makes this
|
||||
// default explicit at the spawn-args level. Authority: openai/codex#16018 +
|
||||
// https://developers.openai.com/codex/concepts/sandboxing.
|
||||
crossTenantReadProtection: 'inner-sandbox',
|
||||
|
||||
// recommendedDeploymentTier: 'per-os-user' — the inner bwrap sandbox protects
|
||||
// against accidental cross-tenant leakage from the model's shell tool, but a
|
||||
// sandbox-escape CVE (e.g. in bubblewrap) would expose the OS-user filesystem.
|
||||
// Per-OS-user isolation adds defense in depth. See ADR 0002 Amendment 9
|
||||
// § Field 6 for the full rationale per recommendedDeploymentTier semantics.
|
||||
recommendedDeploymentTier: 'per-os-user',
|
||||
|
||||
// toolHardeningArgs: injects --sandbox read-only if not already present, and
|
||||
// -c approval_policy="never" to suppress interactive approval prompts.
|
||||
//
|
||||
// Flag-drift caveat (docs/spikes/2026-05-29-ephemeral-home.md § 5.3):
|
||||
// ADR 0002 Amendment 9 § codex example uses `--ask-for-approval never`.
|
||||
// PI231 spike (2026-05-29) confirmed this flag was REMOVED in codex
|
||||
// v0.133.0. The codex v0.133.0 `--help` output shows the replacement is
|
||||
// the generic config-override flag: `-c approval_policy="never"`.
|
||||
// We use `-c approval_policy="never"` here. This deviates from the ADR
|
||||
// 0002 Amendment 9 code example (not the field spec — the spec only
|
||||
// requires an injected flag corresponding to a documented CLI flag).
|
||||
// The config-override form is documented at https://developers.openai.com/codex/config-reference
|
||||
// as the mechanism for overriding any config key at spawn time, including
|
||||
// approval_policy. The deviation is intentional, flag-drift-driven, and
|
||||
// takes precedence over the (now-incorrect) Amendment 9 code example per
|
||||
// ALIGNMENT.md Rule 2 (provider CLI is the authority, not the ADR text).
|
||||
//
|
||||
// --sandbox read-only: Authority: https://developers.openai.com/codex/concepts/sandboxing
|
||||
// § "Sandboxing modes" — the default posture is `read-only`; injecting it
|
||||
// explicitly prevents a future codex default change from silently weakening
|
||||
// isolation (same rationale as the existing irToCodex --skip-git-repo-check).
|
||||
toolHardeningArgs: (existingArgs) => {
|
||||
let result = [...existingArgs];
|
||||
|
||||
// Inject --sandbox read-only if the caller has not already specified --sandbox.
|
||||
if (!result.some(arg => arg === '--sandbox' || arg.startsWith('--sandbox='))) {
|
||||
result = [...result, '--sandbox', 'read-only'];
|
||||
}
|
||||
|
||||
// Inject -c approval_policy="never" if not already present.
|
||||
// Checks for the exact -c flag form used by codex v0.133.0 config overrides.
|
||||
// Flag-drift note: --ask-for-approval (pre-v0.133.0) is NOT injected — it
|
||||
// was removed; see header comment above.
|
||||
const approvalAlreadySet = result.some(
|
||||
(arg, i) => arg === '-c' && typeof result[i + 1] === 'string' && result[i + 1].startsWith('approval_policy'),
|
||||
);
|
||||
if (!approvalAlreadySet) {
|
||||
result = [...result, '-c', 'approval_policy="never"'];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Provider export ───────────────────────────────────────────────────────
|
||||
// Conforms to ADR 0002 § "Provider contract (v1.0 interface)" + contractVersion.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user