fix(runner): keep Codex API keys ephemeral

This commit is contained in:
Dotta committed 2026-08-27 10:30:39 -05:00
1 parent 0c9d931e09
commit 11bab5dc20
4 files changed
+49 -27

No files matched your search

@@ -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 () => {
@@ -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<void> {
export async function prepareIsolatedCodexHome(input: { context: NativeRuntimeContextSnapshot | null; codexHome: string; sourceCodexHome?: string | null; nativeMcp?: NativeMcpLaunchBinding | null }): Promise<void> {
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);
}
@@ -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) => {
@@ -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),
});
}