From a412603bad538c930d5634a1dd8d5eb78665eb0d Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 27 Apr 2026 19:16:39 -0400 Subject: [PATCH] fix(codex): honor effective stdio env for fallback auth --- .../codex/src/app-server/auth-bridge.test.ts | 31 +++++++++++++- .../codex/src/app-server/auth-bridge.ts | 4 +- .../src/app-server/transport-stdio.test.ts | 41 +++++++++++++++++-- .../codex/src/app-server/transport-stdio.ts | 21 ++++++++-- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 1895f4874f6..3cc56562f17 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -389,13 +389,15 @@ describe("bridgeCodexAppServerStartOptions", () => { await applyCodexAppServerAuthProfile({ client: { request } as never, agentDir, - startOptions: createStartOptions(), + startOptions: createStartOptions({ + env: { CODEX_API_KEY: "configured-codex-api-key" }, + }), }); expect(request).toHaveBeenNthCalledWith(1, "account/read", { refreshToken: false }); expect(request).toHaveBeenNthCalledWith(2, "account/login/start", { type: "apiKey", - apiKey: "codex-env-api-key", + apiKey: "configured-codex-api-key", }); } finally { await fs.rm(agentDir, { recursive: true, force: true }); @@ -478,6 +480,31 @@ describe("bridgeCodexAppServerStartOptions", () => { } }); + it("honors clearEnv before env API-key fallback", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const request = vi.fn(async (method: string) => { + if (method === "account/read") { + return { account: null, requiresOpenaiAuth: true }; + } + return { type: "apiKey" }; + }); + vi.stubEnv("CODEX_API_KEY", "codex-env-api-key"); + vi.stubEnv("OPENAI_API_KEY", "openai-env-api-key"); + try { + await applyCodexAppServerAuthProfile({ + client: { request } as never, + agentDir, + startOptions: createStartOptions({ + clearEnv: ["CODEX_API_KEY", "OPENAI_API_KEY"], + }), + }); + + expect(request).not.toHaveBeenCalled(); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + } + }); + it("does not send env API-key fallback to websocket app-server connections", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const request = vi.fn(async (method: string) => { diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index dcd5462bb46..42c5b103546 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -12,6 +12,7 @@ import type { CodexAppServerStartOptions } from "./config.js"; import type { ChatgptAuthTokensRefreshResponse } from "./protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.js"; import type { GetAccountResponse } from "./protocol-generated/typescript/v2/GetAccountResponse.js"; import type { LoginAccountParams } from "./protocol-generated/typescript/v2/LoginAccountParams.js"; +import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js"; const CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex"; const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; @@ -51,9 +52,10 @@ export async function applyCodexAppServerAuthProfile(params: { if (params.startOptions?.transport !== "stdio") { return; } + const env = resolveCodexAppServerSpawnEnv(params.startOptions, process.env); const fallbackLoginParams = await resolveCodexAppServerEnvApiKeyLoginParams({ client: params.client, - env: process.env, + env, }); if (fallbackLoginParams) { await params.client.request("account/login/start", fallbackLoginParams); diff --git a/extensions/codex/src/app-server/transport-stdio.test.ts b/extensions/codex/src/app-server/transport-stdio.test.ts index d1f7ba5d162..9e44f452344 100644 --- a/extensions/codex/src/app-server/transport-stdio.test.ts +++ b/extensions/codex/src/app-server/transport-stdio.test.ts @@ -92,8 +92,8 @@ describe("resolveCodexAppServerSpawnInvocation", () => { describe("resolveCodexAppServerSpawnEnv", () => { it("applies configured env overrides before clearing denied env vars", () => { - expect( - resolveCodexAppServerSpawnEnv( + expect({ + ...resolveCodexAppServerSpawnEnv( { env: { OPENAI_API_KEY: "configured-openai-key", @@ -107,8 +107,43 @@ describe("resolveCodexAppServerSpawnEnv", () => { KEEP: "parent", }, ), - ).toEqual({ + }).toEqual({ KEEP: "override", }); }); + + it("uses a null-prototype env map and ignores prototype-polluting keys", () => { + const overrides = Object.create(null) as Record; + Object.defineProperty(overrides, "__proto__", { + value: "polluted", + enumerable: true, + }); + Object.defineProperty(overrides, "constructor", { + value: "polluted", + enumerable: true, + }); + Object.defineProperty(overrides, "prototype", { + value: "polluted", + enumerable: true, + }); + overrides.SAFE = "1"; + + const env = resolveCodexAppServerSpawnEnv( + { + env: overrides as Record, + }, + { + BASE: "1", + }, + ); + + expect(Object.getPrototypeOf(env)).toBeNull(); + expect({ ...env }).toEqual({ + BASE: "1", + SAFE: "1", + }); + expect(Object.hasOwn(env, "__proto__")).toBe(false); + expect(Object.hasOwn(env, "constructor")).toBe(false); + expect(Object.hasOwn(env, "prototype")).toBe(false); + }); }); diff --git a/extensions/codex/src/app-server/transport-stdio.ts b/extensions/codex/src/app-server/transport-stdio.ts index 16a7b2a8cf4..5efb7f217a5 100644 --- a/extensions/codex/src/app-server/transport-stdio.ts +++ b/extensions/codex/src/app-server/transport-stdio.ts @@ -6,6 +6,8 @@ import { import type { CodexAppServerStartOptions } from "./config.js"; import type { CodexAppServerTransport } from "./transport.js"; +const UNSAFE_ENVIRONMENT_KEYS = new Set(["__proto__", "constructor", "prototype"]); + type CodexAppServerSpawnRuntime = { platform: NodeJS.Platform; env: NodeJS.ProcessEnv; @@ -45,16 +47,27 @@ export function resolveCodexAppServerSpawnEnv( options: Pick, baseEnv: NodeJS.ProcessEnv = process.env, ): NodeJS.ProcessEnv { - const env = { - ...baseEnv, - ...options.env, - }; + const env = Object.create(null) as NodeJS.ProcessEnv; + copySafeEnvironmentEntries(env, baseEnv); + copySafeEnvironmentEntries(env, options.env ?? {}); for (const key of options.clearEnv ?? []) { delete env[key]; } return env; } +function copySafeEnvironmentEntries( + target: NodeJS.ProcessEnv, + source: NodeJS.ProcessEnv | Record, +): void { + for (const [key, value] of Object.entries(source)) { + if (UNSAFE_ENVIRONMENT_KEYS.has(key)) { + continue; + } + target[key] = value; + } +} + export function createStdioTransport(options: CodexAppServerStartOptions): CodexAppServerTransport { const env = resolveCodexAppServerSpawnEnv(options); const invocation = resolveCodexAppServerSpawnInvocation(options, {