mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
fix(runner): keep Codex API keys ephemeral
This commit is contained in:
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),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user