diff --git a/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts b/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts index a39009de562..28f43f7b211 100644 --- a/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts +++ b/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts @@ -106,23 +106,23 @@ describe("runtime context materialization", () => { } }); - it("materializes a configured API key into the isolated Codex home", async () => { + it("does not materialize an API key into the isolated Codex home", async () => { const root = await mkdtemp(join(tmpdir(), "paperclip-runtime-context-api-key-")); roots.push(root); - const sourceCodexHome = join(root, "source-codex-home"); const codexHome = join(root, "codex-home"); - await mkdir(sourceCodexHome, { recursive: true }); - await writeFile(join(sourceCodexHome, "auth.json"), JSON.stringify({ OPENAI_API_KEY: "host-key-must-not-win" }), { mode: 0o600 }); + await mkdir(codexHome, { recursive: true }); + await writeFile( + join(codexHome, "auth.json"), + JSON.stringify({ OPENAI_API_KEY: "stale-persisted-key" }), + { mode: 0o600 }, + ); await prepareIsolatedCodexHome({ context: null, codexHome, - sourceCodexHome, - apiKey: "configured-agent-key", }); - await expect(readFile(join(codexHome, "auth.json"), "utf8")).resolves.toBe(JSON.stringify({ OPENAI_API_KEY: "configured-agent-key" })); - expect((await stat(join(codexHome, "auth.json"))).mode & 0o777).toBe(0o600); + await expect(stat(join(codexHome, "auth.json"))).rejects.toThrow(); }); it("reconciles changed and empty assignments without following unexpected symlinks", async () => { diff --git a/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts b/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts index 02889d8d339..8e8cb53be94 100644 --- a/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts +++ b/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts @@ -51,7 +51,7 @@ export async function materializeNativeRuntimeSkills(context: NativeRuntimeConte } } -export async function prepareIsolatedCodexHome(input: { context: NativeRuntimeContextSnapshot | null; codexHome: string; sourceCodexHome?: string | null; nativeMcp?: NativeMcpLaunchBinding | null; apiKey?: string | null }): Promise { +export async function prepareIsolatedCodexHome(input: { context: NativeRuntimeContextSnapshot | null; codexHome: string; sourceCodexHome?: string | null; nativeMcp?: NativeMcpLaunchBinding | null }): Promise { await mkdir(input.codexHome, { recursive: true, mode: 0o700 }); await chmod(input.codexHome, 0o700); await materializeNativeRuntimeSkills(input.context, join(input.codexHome, "skills")); @@ -64,24 +64,13 @@ export async function prepareIsolatedCodexHome(input: { context: NativeRuntimeCo "", ].join("\n"), { mode: 0o600 }); } - const apiKey = input.apiKey; - if (apiKey?.trim()) { - // Codex app-server intentionally receives a small allowlisted process - // environment and does not inherit OPENAI_API_KEY. Materialize an - // API-key-only credential in the per-session Codex home instead. That - // home is excluded from durable backup credentials and removed with the - // isolated runner state. - const target = join(input.codexHome, "auth.json"); - await rm(target, { force: true }); - await writeFile(target, JSON.stringify({ OPENAI_API_KEY: apiKey }), { mode: 0o600 }); - await chmod(target, 0o600); - return; - } + const targetAuth = join(input.codexHome, "auth.json"); + await rm(targetAuth, { force: true }); const sourceHome = input.sourceCodexHome?.trim(); if (!sourceHome) return; const sourceAuth = join(sourceHome, "auth.json"); const stat = await lstat(sourceAuth).catch(() => null); if (!stat?.isFile() || stat.isSymbolicLink()) return; - await copyFile(sourceAuth, join(input.codexHome, "auth.json")); - await chmod(join(input.codexHome, "auth.json"), 0o600); + await copyFile(sourceAuth, targetAuth); + await chmod(targetAuth, 0o600); } diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index 70d6a728ebb..f7ac3bace5d 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -88,6 +88,38 @@ it("preserves OpenCode runtime bindings when a durable runner is respawned", () }); }); +it("passes the configured Codex API key only through the provider process environment", () => { + const environment = createCapabilityRunnerdProviderEnvironment({ + provider: "codex", + options: { + provider: "codex", + environment: { + PATH: "/bin", + OPENAI_API_KEY: "configured-provider-key", + PAPERCLIP_API_KEY: "must-not-reach-provider", + }, + }, + identity: { + runnerInstanceId: "runner-1", + environmentLeaseId: "lease-1", + runId: "run-1", + normalizedSessionId: "session-1", + turnId: "turn-1", + itemId: "item-1", + }, + codexHome: "/isolated/codex-home", + runtimeContextPath: "/isolated/runtime-context.json", + hasRuntimeContext: false, + }); + expect(environment).toMatchObject({ + PATH: "/bin", + HOME: "/isolated/codex-home", + CODEX_HOME: "/isolated/codex-home", + OPENAI_API_KEY: "configured-provider-key", + }); + expect(environment.PAPERCLIP_API_KEY).toBeUndefined(); +}); + it.each(["opencode", "acpx"] as const)( "advertises runner-managed planning through the %s provider boundary", async (provider) => { diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index faf1d70ffec..c423871a9ba 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -846,11 +846,14 @@ export function createCapabilityRunnerdProviderEnvironment(input: { if (input.provider === "aws_agentcore") { return createSanitizedAwsAgentCoreEnvironment(input.options.environment); } - return createSanitizedCodexEnvironment({ + const environment = createSanitizedCodexEnvironment({ ...input.options.environment, HOME: input.codexHome, CODEX_HOME: input.codexHome, }); + const apiKey = input.options.environment?.OPENAI_API_KEY; + if (apiKey?.trim()) environment.OPENAI_API_KEY = apiKey; + return environment; } export function resolveSourceCodexHome( @@ -1376,7 +1379,6 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { sourceCodexHome: this.options.sourceCodexHome ?? resolveSourceCodexHome(this.options.environment), - apiKey: this.options.environment?.OPENAI_API_KEY, nativeMcp: nativeMcpLaunchBinding(this.options.environment), }); } @@ -1699,7 +1701,6 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { sourceCodexHome: this.options.sourceCodexHome ?? resolveSourceCodexHome(this.options.environment), - apiKey: this.options.environment?.OPENAI_API_KEY, nativeMcp: nativeMcpLaunchBinding(this.options.environment), }); }