From 704619870c13a77c267c7c5e7e5ccf8a9681ee72 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 12 Jun 2026 11:21:31 -0700 Subject: [PATCH] feat: agent-sdk-runner spawns hermetic children via complete Options.env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The historical 'env: breaks SDK auth' failure was partial-env replacement: Options.env replaces the child's entire environment, so objects lacking ANTHROPIC_API_KEY killed auth. Passing the complete hermetic env (key + PATH + redirected CLAUDE_CONFIG_DIR/GSTACK_HOME) works — validated live via query() with a Bash tool call (success, real cost, Conductor vars scrubbed). Per-test opts.env merges last; ambient key mutation still works because the builder reads process.env at call time. Co-Authored-By: Claude Fable 5 --- test/agent-sdk-runner.test.ts | 8 +++++++- test/helpers/agent-sdk-runner.ts | 20 +++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/test/agent-sdk-runner.test.ts b/test/agent-sdk-runner.test.ts index 39c5db812..b760d6833 100644 --- a/test/agent-sdk-runner.test.ts +++ b/test/agent-sdk-runner.test.ts @@ -347,7 +347,13 @@ describe('runAgentSdkTest — options propagation', () => { expect(opts.permissionMode).toBe('bypassPermissions'); expect(opts.allowDangerouslySkipPermissions).toBe(true); expect(opts.settingSources).toEqual([]); - expect(opts.env).toEqual({ ANTHROPIC_API_KEY: 'fake' }); + // env is the COMPLETE hermetic env with the per-test override merged + // last — partial pass-through was the documented SDK auth-breaker + // (Options.env replaces the child's entire environment). + expect(opts.env?.ANTHROPIC_API_KEY).toBe('fake'); + expect(opts.env?.PATH).toBeTruthy(); + expect(opts.env?.CLAUDE_CONFIG_DIR).toMatch(/\/\.claude$/); + expect(opts.env?.GSTACK_HOME).toContain('gstack-home'); expect(opts.pathToClaudeCodeExecutable).toBe('/fake/path/claude'); }); diff --git a/test/helpers/agent-sdk-runner.ts b/test/helpers/agent-sdk-runner.ts index fc50f7884..9bd5eb8f8 100644 --- a/test/helpers/agent-sdk-runner.ts +++ b/test/helpers/agent-sdk-runner.ts @@ -36,6 +36,7 @@ import { import * as fs from 'fs'; import * as path from 'path'; import { resolveClaudeBinary as resolveClaudeBinaryShared } from '../../browse/src/claude-bin'; +import { hermeticChildEnv } from './hermetic-env'; import type { SkillTestResult } from './session-runner'; // --------------------------------------------------------------------------- @@ -300,12 +301,17 @@ export async function runAgentSdkTest( const queryImpl: QueryProvider = opts.queryProvider ?? query; const model = opts.model ?? 'claude-opus-4-7'; - // NOTE on GSTACK_HEADLESS: the SDK child inherits process.env, so headless - // classification for eval/E2E runs is set by the `test:gate` / `test:evals` - // package.json scripts (scoped to that invocation), NOT mutated here. We must not - // pass sdkOpts.env (it breaks the SDK auth pipeline — see CLAUDE.md) and must not - // mutate process.env ambiently (it would leak headless into later interactive-path - // tests in the same Bun process — Codex review finding). + // NOTE on env: the SDK child gets the COMPLETE hermetic env (allowlist + // scrub + ANTHROPIC_API_KEY + hermetic CLAUDE_CONFIG_DIR/GSTACK_HOME), with + // per-test opts.env merging last. The historical "passing env: breaks SDK + // auth" failure (old CLAUDE.md warning) was partial-env replacement — + // Options.env REPLACES the child's entire environment, so an object without + // the key killed auth. A complete env is safe (validated 2026-06-12 via + // query() with hermeticChildEnv(): success, real cost, Bash tool working). + // Do not mutate process.env ambiently here (it would leak into later + // interactive-path tests in the same Bun process — Codex review finding); + // ambient ANTHROPIC_API_KEY mutation by tests still works because the + // builder reads process.env at call time. let attempt = 0; let lastErr: unknown = null; @@ -356,7 +362,7 @@ export async function runAgentSdkTest( permissionMode: resolvedPermissionMode, allowDangerouslySkipPermissions: resolvedPermissionMode === 'bypassPermissions', settingSources: opts.settingSources ?? [], - env: opts.env, + env: hermeticChildEnv(opts.env), pathToClaudeCodeExecutable: opts.pathToClaudeCodeExecutable, ...(hasCanUseTool ? { canUseTool: opts.canUseTool } : {}), };