From fda4d5927dedcbfa332d487befb4c7f25f4df6c9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 27 Aug 2026 09:59:20 -0500 Subject: [PATCH] fix native Codex runner E2E authentication --- .../runtime-context-materializer.test.ts | 19 +++++++++++++++++++ .../drivers/runtime-context-materializer.ts | 15 ++++++++++++++- .../src/live/runnerd-codex-transport.ts | 2 ++ tests/runner-e2e/runner.spec.ts | 8 ++++---- 4 files changed, 39 insertions(+), 5 deletions(-) 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 bee7a96c03f..a39009de562 100644 --- a/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts +++ b/packages/paperclip-runner/src/drivers/runtime-context-materializer.test.ts @@ -106,6 +106,25 @@ describe("runtime context materialization", () => { } }); + it("materializes a configured 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 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); + }); + it("reconciles changed and empty assignments without following unexpected symlinks", async () => { const root = await mkdtemp(join(tmpdir(), "paperclip-runtime-context-reconcile-")); roots.push(root); diff --git a/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts b/packages/paperclip-runner/src/drivers/runtime-context-materializer.ts index 93c98a2cfee..02889d8d339 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 }): Promise { +export async function prepareIsolatedCodexHome(input: { context: NativeRuntimeContextSnapshot | null; codexHome: string; sourceCodexHome?: string | null; nativeMcp?: NativeMcpLaunchBinding | null; apiKey?: string | null }): Promise { await mkdir(input.codexHome, { recursive: true, mode: 0o700 }); await chmod(input.codexHome, 0o700); await materializeNativeRuntimeSkills(input.context, join(input.codexHome, "skills")); @@ -64,6 +64,19 @@ 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 sourceHome = input.sourceCodexHome?.trim(); if (!sourceHome) return; const sourceAuth = join(sourceHome, "auth.json"); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 6a8a7fb28c0..faf1d70ffec 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1376,6 +1376,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { sourceCodexHome: this.options.sourceCodexHome ?? resolveSourceCodexHome(this.options.environment), + apiKey: this.options.environment?.OPENAI_API_KEY, nativeMcp: nativeMcpLaunchBinding(this.options.environment), }); } @@ -1698,6 +1699,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { sourceCodexHome: this.options.sourceCodexHome ?? resolveSourceCodexHome(this.options.environment), + apiKey: this.options.environment?.OPENAI_API_KEY, nativeMcp: nativeMcpLaunchBinding(this.options.environment), }); } diff --git a/tests/runner-e2e/runner.spec.ts b/tests/runner-e2e/runner.spec.ts index 3764ffae61a..de7fae4bb72 100644 --- a/tests/runner-e2e/runner.spec.ts +++ b/tests/runner-e2e/runner.spec.ts @@ -253,7 +253,7 @@ function normalizePlanMarkdown(body: string | null | undefined) { return (body ?? "").replaceAll("\\_", "_"); } -function renderedPlanMarkerPattern(marker: string) { +function renderedMarkerPattern(marker: string) { return new RegExp( marker .split("_") @@ -496,7 +496,7 @@ for (const execution of executions) { await expect( page .getByTestId("task-chat-plan-preview") - .filter({ hasText: renderedPlanMarkerPattern(planMarkers.draft) }) + .filter({ hasText: renderedMarkerPattern(planMarkers.draft) }) .last(), ).toBeVisible({ timeout: 30_000 }); await captureScreenshot( @@ -580,7 +580,7 @@ for (const execution of executions) { page .getByTestId("task-chat-plan-preview") .filter({ - hasText: renderedPlanMarkerPattern(planMarkers.revised), + hasText: renderedMarkerPattern(planMarkers.revised), }) .last(), ).toBeVisible({ timeout: 30_000 }); @@ -854,7 +854,7 @@ for (const execution of executions) { page .getByTestId("task-chat-thread") .getByTestId("task-chat-agent-bubble") - .filter({ hasText: marker }) + .filter({ hasText: renderedMarkerPattern(marker) }) .last(), ).toBeVisible({ timeout: 30_000 }); await expect(