From 02a8c135016665f0ef58122cf67bdeade507f645 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 22:22:51 +0100 Subject: [PATCH] fix(codex): stop materializing auth bridges --- CHANGELOG.md | 2 +- extensions/acpx/src/codex-auth-bridge.test.ts | 68 +--- extensions/acpx/src/codex-auth-bridge.ts | 86 +---- .../codex/src/app-server/auth-bridge.test.ts | 311 ++---------------- .../codex/src/app-server/auth-bridge.ts | 25 +- extensions/openai/cli-backend.ts | 5 - .../openai/openai-codex-cli-bridge.test.ts | 146 -------- extensions/openai/openai-codex-cli-bridge.ts | 29 -- ...th-profiles.ensureauthprofilestore.test.ts | 1 - .../oauth-common-mocks.test-support.ts | 1 - ...auth.openai-codex-refresh-fallback.test.ts | 9 - src/agents/cli-backends.test.ts | 9 +- src/agents/cli-credentials.test.ts | 114 ------- src/agents/cli-credentials.ts | 139 -------- src/plugin-sdk/provider-auth-runtime.test.ts | 105 +----- src/plugin-sdk/provider-auth-runtime.ts | 240 -------------- 16 files changed, 41 insertions(+), 1249 deletions(-) delete mode 100644 extensions/openai/openai-codex-cli-bridge.test.ts delete mode 100644 extensions/openai/openai-codex-cli-bridge.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83738b7dd0a..3b017fe7100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ Docs: https://docs.openclaw.ai - Agents/OpenAI: surface selected-model capacity failures with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc. - Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint. - Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc. -- ACPX/Codex: stop the embedded Codex ACP auth bridge from falling back to raw `~/.codex` file copies; ACPX now only uses OpenClaw's canonical Codex OAuth bridge. - Voice-call/realtime: wait for OpenAI session configuration before greeting or forwarding buffered audio, and reject non-allowlisted Twilio callers before stream setup. (#43501) Thanks @forrestblount. +- ACPX/Codex: stop materializing `auth.json` bridge files for Codex ACP, Codex app-server, and Codex CLI runs; Codex-owned runtimes now use their normal `CODEX_HOME`/`~/.codex` auth path directly. - Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui. - OpenAI/image generation: send reference-image edits as guarded multipart uploads instead of JSON data URLs, restoring complex multi-reference `gpt-image-2` edits. Fixes #70642. Thanks @dashhuang. - QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc. diff --git a/extensions/acpx/src/codex-auth-bridge.test.ts b/extensions/acpx/src/codex-auth-bridge.test.ts index 143f40872d3..8b4609a6cc6 100644 --- a/extensions/acpx/src/codex-auth-bridge.test.ts +++ b/extensions/acpx/src/codex-auth-bridge.test.ts @@ -1,7 +1,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { saveAuthProfileStore } from "openclaw/plugin-sdk/agent-runtime"; import { afterEach, describe, expect, it } from "vitest"; import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js"; import { resolveAcpxPluginConfig } from "./config.js"; @@ -28,10 +27,6 @@ function restoreEnv(name: keyof typeof previousEnv): void { } } -function unquoteCommandPath(command: string): string { - return command.replace(/^'|'$/g, "").replace(/'\\''/g, "'"); -} - afterEach(async () => { restoreEnv("CODEX_HOME"); restoreEnv("OPENCLAW_AGENT_DIR"); @@ -42,28 +37,10 @@ afterEach(async () => { }); describe("prepareAcpxCodexAuthConfig", () => { - it("wraps built-in Codex ACP with an isolated CODEX_HOME from canonical OpenClaw OAuth", async () => { + it("does not synthesize a Codex ACP auth home from canonical OpenClaw OAuth", async () => { const root = await makeTempDir(); const agentDir = path.join(root, "agent"); const stateDir = path.join(root, "state"); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - accountId: "acct-123", - idToken: "id-token", - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); process.env.OPENCLAW_AGENT_DIR = agentDir; delete process.env.PI_CODING_AGENT_DIR; @@ -76,44 +53,16 @@ describe("prepareAcpxCodexAuthConfig", () => { stateDir, }); - const wrapperPath = unquoteCommandPath(resolved.agents.codex ?? ""); - expect(wrapperPath).toBe(path.join(stateDir, "acpx", "codex-acp-wrapper.mjs")); - await expect(fs.access(wrapperPath)).resolves.toBeUndefined(); - - const bridgeRoot = path.join(agentDir, "acp-auth", "codex"); - const bridgeDirs = await fs.readdir(bridgeRoot); - expect(bridgeDirs).toHaveLength(1); - const bridgeDir = bridgeDirs[0]; - if (!bridgeDir) { - throw new Error("expected one Codex auth bridge directory"); - } - const isolatedAuthPath = path.join(bridgeRoot, bridgeDir, "auth.json"); - const copiedAuth = JSON.parse(await fs.readFile(isolatedAuthPath, "utf8")) as { - auth_mode?: string; - tokens?: Record; - }; - expect(copiedAuth).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: expect.any(String), - }); - expect((await fs.stat(isolatedAuthPath)).mode & 0o777).toBe(0o600); + expect(resolved.agents.codex).toBeUndefined(); await expect( - fs.access(path.join(agentDir, "acp-auth", "codex-source", "auth.json")), + fs.access(path.join(stateDir, "acpx", "codex-acp-wrapper.mjs")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.access(path.join(agentDir, "acp-auth", "codex", "auth.json")), ).rejects.toMatchObject({ code: "ENOENT" }); - - const wrapper = await fs.readFile(wrapperPath, "utf8"); - expect(wrapper).toContain(`CODEX_HOME: ${JSON.stringify(path.dirname(isolatedAuthPath))}`); - expect(wrapper).toContain("for (const key of [])"); - expect(wrapper).not.toContain("access-token"); }); - it("does not copy source Codex auth when canonical OpenClaw OAuth is unavailable", async () => { + it("does not copy source Codex auth", async () => { const root = await makeTempDir(); const sourceCodexHome = path.join(root, "source-codex"); const agentDir = path.join(root, "agent"); @@ -139,6 +88,9 @@ describe("prepareAcpxCodexAuthConfig", () => { await expect( fs.access(path.join(agentDir, "acp-auth", "codex-source", "auth.json")), ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.access(path.join(agentDir, "acp-auth", "codex", "auth.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("does not override an explicitly configured Codex agent command", async () => { diff --git a/extensions/acpx/src/codex-auth-bridge.ts b/extensions/acpx/src/codex-auth-bridge.ts index 9b3fc221f8f..60d553435f7 100644 --- a/extensions/acpx/src/codex-auth-bridge.ts +++ b/extensions/acpx/src/codex-auth-bridge.ts @@ -1,89 +1,11 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/provider-auth"; -import { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-runtime"; -import type { PluginLogger } from "../runtime-api.js"; import type { ResolvedAcpxPluginConfig } from "./config.js"; -const CODEX_AGENT_ID = "codex"; -const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default"; -// acpx selects ACP auth methods from the OpenClaw process env before the wrapper -// launches. Keep those env vars visible to the child so its auth method matches. -const CODEX_AUTH_ENV_CLEAR_KEYS: string[] = []; - -function shellArg(value: string): string { - return `'${value.replace(/'/g, `'\\''`)}'`; -} - -async function writeCodexAcpWrapper(params: { - wrapperPath: string; - codexHome: string; - clearEnv: string[]; -}): Promise { - await fs.mkdir(path.dirname(params.wrapperPath), { recursive: true, mode: 0o700 }); - const content = `#!/usr/bin/env node -import { spawn } from "node:child_process"; - -const env = { ...process.env, CODEX_HOME: ${JSON.stringify(params.codexHome)} }; -for (const key of ${JSON.stringify(params.clearEnv)}) { - delete env[key]; -} - -const child = spawn("npx", ["@zed-industries/codex-acp@^0.11.1"], { - stdio: "inherit", - env, -}); - -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - return; - } - process.exit(code ?? 1); -}); - -child.on("error", (error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); -}); -`; - await fs.writeFile(params.wrapperPath, content, { mode: 0o700 }); - await fs.chmod(params.wrapperPath, 0o700); - return shellArg(params.wrapperPath); -} - export async function prepareAcpxCodexAuthConfig(params: { pluginConfig: ResolvedAcpxPluginConfig; stateDir: string; - logger?: PluginLogger; + logger?: unknown; }): Promise { - if (params.pluginConfig.agents[CODEX_AGENT_ID]) { - return params.pluginConfig; - } - - const agentDir = resolveOpenClawAgentDir(); - const bridge = await prepareCodexAuthBridge({ - agentDir, - bridgeDir: "acp-auth", - profileId: DEFAULT_CODEX_AUTH_PROFILE_ID, - }); - - if (!bridge) { - params.logger?.debug?.("codex ACP auth bridge skipped: no canonical OpenClaw OAuth found"); - return params.pluginConfig; - } - - const wrapperCommand = await writeCodexAcpWrapper({ - wrapperPath: path.join(params.stateDir, "acpx", "codex-acp-wrapper.mjs"), - codexHome: bridge.codexHome, - clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], - }); - - return { - ...params.pluginConfig, - agents: { - ...params.pluginConfig.agents, - [CODEX_AGENT_ID]: wrapperCommand, - }, - }; + void params.stateDir; + void params.logger; + return params.pluginConfig; } diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 62b8955f6c5..a1051f444d6 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -1,306 +1,33 @@ -import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { saveAuthProfileStore } from "openclaw/plugin-sdk/agent-runtime"; -import { afterEach, beforeAll, describe, expect, it } from "vitest"; - -let bridgeCodexAppServerStartOptions: typeof import("./auth-bridge.js").bridgeCodexAppServerStartOptions; +import { describe, expect, it } from "vitest"; +import { bridgeCodexAppServerStartOptions } from "./auth-bridge.js"; describe("bridgeCodexAppServerStartOptions", () => { - const tempDirs: string[] = []; - const resolveHashedCodexHome = (agentDir: string, profileId: string) => - path.join( - agentDir, - "harness-auth", - "codex", - crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16), - ); - - async function createAgentDirWithDefaultProfile( - profile: Record = {}, - ): Promise { + it("leaves Codex app-server start options unchanged", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); - tempDirs.push(agentDir); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - ...profile, - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - return agentDir; - } - - beforeAll(async () => { - ({ bridgeCodexAppServerStartOptions } = await import("./auth-bridge.js")); - }); - - afterEach(async () => { - await Promise.all( - tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), - ); - }); - - it("bridges canonical OpenClaw oauth into an isolated CODEX_HOME", async () => { - const agentDir = await createAgentDirWithDefaultProfile({ - accountId: "acct-123", - idToken: "id-token", - }); - - const result = await bridgeCodexAppServerStartOptions({ - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server"], - headers: { authorization: "Bearer dev-token" }, - env: { EXISTING: "1" }, - clearEnv: ["FOO"], - }, - agentDir, - }); - - expect(result).toMatchObject({ - env: { - EXISTING: "1", - CODEX_HOME: expect.stringContaining(path.join(agentDir, "harness-auth", "codex")), - }, - clearEnv: expect.arrayContaining(["FOO", "OPENAI_API_KEY"]), - }); - - const authFile = JSON.parse( - await fs.readFile(path.join(result.env?.CODEX_HOME ?? "", "auth.json"), "utf8"), - ); - expect(authFile).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: expect.any(String), - }); - if (process.platform !== "win32") { - const authStat = await fs.stat(path.join(result.env?.CODEX_HOME ?? "", "auth.json")); - expect(authStat.mode & 0o777).toBe(0o600); - } - }); - - it("hydrates Codex-only auth fields from a matching Codex CLI auth file", async () => { - const sourceCodexHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-source-home-")); - tempDirs.push(sourceCodexHome); - await fs.writeFile( - path.join(sourceCodexHome, "auth.json"), - `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }, - null, - 2, - )}\n`, - ); - const agentDir = await createAgentDirWithDefaultProfile({ - accountId: "acct-123", - }); - - const result = await bridgeCodexAppServerStartOptions({ - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server"], - headers: {}, - env: { CODEX_HOME: sourceCodexHome }, - }, - agentDir, - }); - - expect(result.env?.CODEX_HOME).not.toBe(sourceCodexHome); - const authFile = JSON.parse( - await fs.readFile(path.join(result.env?.CODEX_HOME ?? "", "auth.json"), "utf8"), - ); - expect(authFile).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }); - }); - - it("keeps the selected profile tokens when hydrating from a same-account Codex CLI auth file", async () => { - const sourceCodexHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-source-home-")); - tempDirs.push(sourceCodexHome); - await fs.writeFile( - path.join(sourceCodexHome, "auth.json"), - `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "stale-source-access-token", - refresh_token: "stale-source-refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }, - null, - 2, - )}\n`, - ); - const agentDir = await createAgentDirWithDefaultProfile({ - access: "selected-profile-access-token", - refresh: "selected-profile-refresh-token", - accountId: "acct-123", - idToken: "selected-profile-id-token", - }); - - const result = await bridgeCodexAppServerStartOptions({ - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server"], - headers: {}, - env: { CODEX_HOME: sourceCodexHome }, - }, - agentDir, - }); - - expect(result.env?.CODEX_HOME).not.toBe(sourceCodexHome); - const authFile = JSON.parse( - await fs.readFile(path.join(result.env?.CODEX_HOME ?? "", "auth.json"), "utf8"), - ); - expect(authFile).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "selected-profile-id-token", - access_token: "selected-profile-access-token", - refresh_token: "selected-profile-refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }); - }); - - it("hydrates from inherited CODEX_HOME when start options do not override it", async () => { - const sourceCodexHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-source-home-")); - tempDirs.push(sourceCodexHome); - await fs.writeFile( - path.join(sourceCodexHome, "auth.json"), - `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }, - null, - 2, - )}\n`, - ); - const previousCodexHome = process.env.CODEX_HOME; - process.env.CODEX_HOME = sourceCodexHome; - try { - const agentDir = await createAgentDirWithDefaultProfile({ - accountId: "acct-123", - }); - - const result = await bridgeCodexAppServerStartOptions({ - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server"], - headers: {}, - }, - agentDir, - }); - - expect(result.env?.CODEX_HOME).not.toBe(sourceCodexHome); - const authFile = JSON.parse( - await fs.readFile(path.join(result.env?.CODEX_HOME ?? "", "auth.json"), "utf8"), - ); - expect(authFile).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: "2026-04-22T00:00:00.000Z", - }); - } finally { - if (previousCodexHome === undefined) { - delete process.env.CODEX_HOME; - } else { - process.env.CODEX_HOME = previousCodexHome; - } - } - }); - - it("leaves start options unchanged when canonical oauth is unavailable", async () => { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); - tempDirs.push(agentDir); const startOptions = { transport: "stdio" as const, command: "codex", args: ["app-server"], headers: { authorization: "Bearer dev-token" }, + env: { CODEX_HOME: "/tmp/source-codex-home", EXISTING: "1" }, + clearEnv: ["FOO"], }; - saveAuthProfileStore({ version: 1, profiles: {} }, agentDir, { - filterExternalAuthProfiles: false, - }); - - await expect( - bridgeCodexAppServerStartOptions({ - startOptions, - agentDir, - authProfileId: "openai-codex:missing", - }), - ).resolves.toEqual(startOptions); - }); - - it("refuses to overwrite a symlinked auth bridge file", async () => { - const agentDir = await createAgentDirWithDefaultProfile(); - - const codexHome = resolveHashedCodexHome(agentDir, "openai-codex:default"); - await fs.mkdir(codexHome, { recursive: true }); - await fs.symlink(path.join(agentDir, "outside.txt"), path.join(codexHome, "auth.json")); - - await expect( - bridgeCodexAppServerStartOptions({ - startOptions: { - transport: "stdio", - command: "codex", - args: ["app-server"], - headers: {}, - }, - agentDir, - }), - ).rejects.toThrow("must not be a symlink"); + try { + await expect( + bridgeCodexAppServerStartOptions({ + startOptions, + agentDir, + authProfileId: "openai-codex:default", + }), + ).resolves.toBe(startOptions); + await expect(fs.access(path.join(agentDir, "harness-auth"))).rejects.toMatchObject({ + code: "ENOENT", + }); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + } }); }); diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 8dec6cd7252..054a8b53c02 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -1,30 +1,11 @@ -import { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-runtime"; import type { CodexAppServerStartOptions } from "./config.js"; -const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default"; - export async function bridgeCodexAppServerStartOptions(params: { startOptions: CodexAppServerStartOptions; agentDir: string; authProfileId?: string; }): Promise { - const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID; - const bridge = await prepareCodexAuthBridge({ - agentDir: params.agentDir, - bridgeDir: "harness-auth", - profileId, - sourceCodexHome: params.startOptions.env?.CODEX_HOME, - }); - if (!bridge) { - return params.startOptions; - } - - return { - ...params.startOptions, - env: { - ...params.startOptions.env, - CODEX_HOME: bridge.codexHome, - }, - clearEnv: Array.from(new Set([...(params.startOptions.clearEnv ?? []), ...bridge.clearEnv])), - }; + void params.agentDir; + void params.authProfileId; + return params.startOptions; } diff --git a/extensions/openai/cli-backend.ts b/extensions/openai/cli-backend.ts index 591d30db0e4..5416c140902 100644 --- a/extensions/openai/cli-backend.ts +++ b/extensions/openai/cli-backend.ts @@ -3,9 +3,7 @@ import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, } from "openclaw/plugin-sdk/cli-backend"; -import { prepareOpenAICodexCliExecution } from "./openai-codex-cli-bridge.js"; -const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; const CODEX_CLI_DEFAULT_MODEL_REF = "codex-cli/gpt-5.5"; export function buildOpenAICodexCliBackend(): CliBackendPlugin { @@ -22,9 +20,6 @@ export function buildOpenAICodexCliBackend(): CliBackendPlugin { }, bundleMcp: true, bundleMcpMode: "codex-config-overrides", - defaultAuthProfileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - authEpochMode: "profile-only", - prepareExecution: prepareOpenAICodexCliExecution, config: { command: "codex", args: [ diff --git a/extensions/openai/openai-codex-cli-bridge.test.ts b/extensions/openai/openai-codex-cli-bridge.test.ts deleted file mode 100644 index f23197b0449..00000000000 --- a/extensions/openai/openai-codex-cli-bridge.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import crypto from "node:crypto"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { saveAuthProfileStore } from "openclaw/plugin-sdk/agent-runtime"; -import { afterEach, describe, expect, it } from "vitest"; -import { prepareOpenAICodexCliExecution } from "./openai-codex-cli-bridge.js"; - -describe("prepareOpenAICodexCliExecution", () => { - const tempDirs: string[] = []; - const resolveHashedCodexHome = (agentDir: string, profileId: string) => - path.join( - agentDir, - "cli-auth", - "codex", - crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16), - ); - - afterEach(async () => { - await Promise.all( - tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), - ); - }); - - it("writes a private CODEX_HOME bridge from canonical OpenClaw oauth", async () => { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-cli-bridge-")); - tempDirs.push(agentDir); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - accountId: "acct-123", - idToken: "id-token", - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - - const result = await prepareOpenAICodexCliExecution({ - config: undefined, - workspaceDir: agentDir, - agentDir, - provider: "codex-cli", - modelId: "gpt-5.4", - authProfileId: "openai-codex:default", - }); - - expect(result).toMatchObject({ - env: { - CODEX_HOME: expect.stringContaining(path.join(agentDir, "cli-auth", "codex")), - }, - clearEnv: ["OPENAI_API_KEY"], - }); - - const authFile = JSON.parse( - await fs.readFile(path.join(result?.env?.CODEX_HOME ?? "", "auth.json"), "utf8"), - ); - expect(authFile).toEqual({ - auth_mode: "chatgpt", - tokens: { - id_token: "id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - last_refresh: expect.any(String), - }); - if (process.platform !== "win32") { - const authStat = await fs.stat(path.join(result?.env?.CODEX_HOME ?? "", "auth.json")); - expect(authStat.mode & 0o777).toBe(0o600); - } - }); - - it("returns null when there is no bridgeable canonical oauth credential", async () => { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-cli-bridge-")); - tempDirs.push(agentDir); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "api_key", - provider: "openai-codex", - key: "sk-test", - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - - await expect( - prepareOpenAICodexCliExecution({ - config: undefined, - workspaceDir: agentDir, - agentDir, - provider: "codex-cli", - modelId: "gpt-5.4", - authProfileId: "openai-codex:default", - }), - ).resolves.toBeNull(); - }); - - it("refuses to overwrite a symlinked codex cli auth bridge file", async () => { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-cli-bridge-")); - tempDirs.push(agentDir); - const codexHome = resolveHashedCodexHome(agentDir, "openai-codex:default"); - await fs.mkdir(codexHome, { recursive: true }); - await fs.symlink(path.join(agentDir, "outside.txt"), path.join(codexHome, "auth.json")); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - - await expect( - prepareOpenAICodexCliExecution({ - config: undefined, - workspaceDir: agentDir, - agentDir, - provider: "codex-cli", - modelId: "gpt-5.4", - authProfileId: "openai-codex:default", - }), - ).rejects.toThrow("must not be a symlink"); - }); -}); diff --git a/extensions/openai/openai-codex-cli-bridge.ts b/extensions/openai/openai-codex-cli-bridge.ts deleted file mode 100644 index fbfa92cb6eb..00000000000 --- a/extensions/openai/openai-codex-cli-bridge.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - CliBackendPreparedExecution, - CliBackendPrepareExecutionContext, -} from "openclaw/plugin-sdk/cli-backend"; -import { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-runtime"; - -export async function prepareOpenAICodexCliExecution( - ctx: CliBackendPrepareExecutionContext, -): Promise { - if (!ctx.agentDir || !ctx.authProfileId) { - return null; - } - - const bridge = await prepareCodexAuthBridge({ - agentDir: ctx.agentDir, - bridgeDir: "cli-auth", - profileId: ctx.authProfileId, - }); - if (!bridge) { - return null; - } - - return { - env: { - CODEX_HOME: bridge.codexHome, - }, - clearEnv: bridge.clearEnv, - }; -} diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index fae102e77ec..6fdf30b31d2 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -53,7 +53,6 @@ vi.mock("./cli-credentials.js", () => ({ }, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: vi.fn(), - writeCodexCliCredentials: vi.fn(() => false), })); describe("ensureAuthProfileStore", () => { diff --git a/src/agents/auth-profiles/oauth-common-mocks.test-support.ts b/src/agents/auth-profiles/oauth-common-mocks.test-support.ts index 374c7a6e837..e873ee223e0 100644 --- a/src/agents/auth-profiles/oauth-common-mocks.test-support.ts +++ b/src/agents/auth-profiles/oauth-common-mocks.test-support.ts @@ -16,7 +16,6 @@ vi.mock("../cli-credentials.js", () => ({ readCodexCliCredentialsCached: () => null, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: () => undefined, - writeCodexCliCredentials: () => true, })); vi.mock("../../plugins/provider-runtime.runtime.js", () => ({ diff --git a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts index 0243f73020b..b6e36a6b2f4 100644 --- a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts +++ b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts @@ -24,10 +24,6 @@ const { readCodexCliCredentialsCachedMock } = vi.hoisted(() => ({ readCodexCliCredentialsCachedMock: vi.fn<() => OAuthCredential | null>(() => null), })); -const { writeCodexCliCredentialsMock } = vi.hoisted(() => ({ - writeCodexCliCredentialsMock: vi.fn(() => true), -})); - const { refreshProviderOAuthCredentialWithPluginMock, formatProviderAuthProfileApiKeyWithPluginMock, @@ -42,7 +38,6 @@ const { vi.mock("../cli-credentials.js", () => ({ readCodexCliCredentialsCached: readCodexCliCredentialsCachedMock, - writeCodexCliCredentials: writeCodexCliCredentialsMock, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: () => undefined, })); @@ -116,8 +111,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => { }); readCodexCliCredentialsCachedMock.mockReset(); readCodexCliCredentialsCachedMock.mockReturnValue(null); - writeCodexCliCredentialsMock.mockReset(); - writeCodexCliCredentialsMock.mockReturnValue(true); refreshProviderOAuthCredentialWithPluginMock.mockReset(); refreshProviderOAuthCredentialWithPluginMock.mockResolvedValue(undefined); formatProviderAuthProfileApiKeyWithPluginMock.mockReset(); @@ -274,8 +267,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => { provider: "openai-codex", email: undefined, }); - expect(writeCodexCliCredentialsMock).not.toHaveBeenCalled(); - const persisted = await readPersistedStore(agentDir); expect(persisted.profiles[profileId]).toMatchObject({ type: "oauth", diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index 42008a5b65f..0088a61a70b 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -230,9 +230,6 @@ beforeEach(() => { id: "codex-cli", bundleMcp: true, bundleMcpMode: "codex-config-overrides", - defaultAuthProfileId: "openai-codex:default", - authEpochMode: "profile-only", - prepareExecution: async () => null, config: { command: "codex", args: [ @@ -761,9 +758,9 @@ describe("resolveCliBackendConfig google-gemini-cli defaults", () => { expect(resolved).not.toBeNull(); expect(resolved?.bundleMcp).toBe(true); expect(resolved?.bundleMcpMode).toBe("codex-config-overrides"); - expect(resolved?.defaultAuthProfileId).toBe("openai-codex:default"); - expect(resolved?.authEpochMode).toBe("profile-only"); - expect(typeof resolved?.prepareExecution).toBe("function"); + expect(resolved?.defaultAuthProfileId).toBeUndefined(); + expect(resolved?.authEpochMode).toBeUndefined(); + expect(resolved?.prepareExecution).toBeUndefined(); expect(resolved?.config.systemPromptFileConfigArg).toBe("-c"); expect(resolved?.config.systemPromptFileConfigKey).toBe("model_instructions_file"); expect(resolved?.config.systemPromptWhen).toBe("first"); diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index c2291e04be7..e2b37e058cd 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -12,8 +12,6 @@ let resetCliCredentialCachesForTest: typeof import("./cli-credentials.js").reset let writeClaudeCliKeychainCredentials: typeof import("./cli-credentials.js").writeClaudeCliKeychainCredentials; let writeClaudeCliCredentials: typeof import("./cli-credentials.js").writeClaudeCliCredentials; let readCodexCliCredentials: typeof import("./cli-credentials.js").readCodexCliCredentials; -let writeCodexCliCredentials: typeof import("./cli-credentials.js").writeCodexCliCredentials; -let writeCodexCliFileCredentials: typeof import("./cli-credentials.js").writeCodexCliFileCredentials; function mockExistingClaudeKeychainItem() { execFileSyncMock.mockImplementation((file: unknown, args: unknown) => { @@ -76,8 +74,6 @@ describe("cli credentials", () => { writeClaudeCliKeychainCredentials, writeClaudeCliCredentials, readCodexCliCredentials, - writeCodexCliCredentials, - writeCodexCliFileCredentials, } = await import("./cli-credentials.js")); }); @@ -366,114 +362,4 @@ describe("cli credentials", () => { fs.rmSync(tempHome, { recursive: true, force: true }); } }); - - it("updates existing Codex auth.json in place", () => { - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-write-")); - process.env.CODEX_HOME = tempHome; - try { - fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 }); - const authPath = path.join(tempHome, "auth.json"); - fs.writeFileSync( - authPath, - JSON.stringify( - { - auth_mode: "chatgpt", - OPENAI_API_KEY: "sk-existing", - tokens: { - id_token: "id-token", - access_token: "old-access", - refresh_token: "old-refresh", - account_id: "acct-old", - }, - last_refresh: "2026-03-01T00:00:00.000Z", - }, - null, - 2, - ), - "utf8", - ); - - const ok = writeCodexCliFileCredentials({ - access: "new-access", - refresh: "new-refresh", - expires: Date.now() + 60_000, - idToken: "new-id-token", - accountId: "acct-new", - }); - - expect(ok).toBe(true); - const persisted = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record; - expect(persisted).toMatchObject({ - auth_mode: "chatgpt", - OPENAI_API_KEY: "sk-existing", - }); - expect(persisted.tokens).toMatchObject({ - id_token: "new-id-token", - access_token: "new-access", - refresh_token: "new-refresh", - account_id: "acct-new", - }); - expect(typeof persisted.last_refresh).toBe("string"); - } finally { - fs.rmSync(tempHome, { recursive: true, force: true }); - } - }); - - it("prefers the existing Codex keychain entry over auth.json on darwin writes", () => { - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-keychain-write-")); - process.env.CODEX_HOME = tempHome; - try { - const expSeconds = Math.floor(Date.parse("2026-03-26T12:34:56Z") / 1000); - execSyncMock.mockImplementation((command: unknown) => { - const cmd = String(command); - expect(cmd).toContain("Codex Auth"); - return JSON.stringify({ - auth_mode: "chatgpt", - tokens: { - id_token: "id-token", - access_token: createJwtWithExp(expSeconds), - refresh_token: "old-refresh", - account_id: "acct-old", - }, - last_refresh: "2026-03-01T00:00:00.000Z", - }); - }); - - const ok = writeCodexCliCredentials( - { - access: "new-access", - refresh: "new-refresh", - expires: Date.now() + 60_000, - idToken: "new-id-token", - accountId: "acct-new", - }, - { - platform: "darwin", - execSync: execSyncMock, - execFileSync: execFileSyncMock, - }, - ); - - expect(ok).toBe(true); - expect(execFileSyncMock).toHaveBeenCalledTimes(1); - const addCall = getAddGenericPasswordCall(); - expect(addCall?.[0]).toBe("security"); - const payload = (() => { - const args = (addCall?.[1] as string[] | undefined) ?? []; - const valueIndex = args.indexOf("-w"); - return valueIndex >= 0 ? args[valueIndex + 1] : undefined; - })(); - expect(payload).toBeDefined(); - const parsed = JSON.parse(String(payload)) as Record; - expect(parsed.tokens).toMatchObject({ - id_token: "new-id-token", - access_token: "new-access", - refresh_token: "new-refresh", - account_id: "acct-new", - }); - expect(parsed.auth_mode).toBe("chatgpt"); - } finally { - fs.rmSync(tempHome, { recursive: true, force: true }); - } - }); }); diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index 6fb72c1eb9d..f954b7fe386 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -77,26 +77,6 @@ type ClaudeCliWriteOptions = ClaudeCliFileOptions & { writeFile?: (credentials: OAuthCredentials, options?: ClaudeCliFileOptions) => boolean; }; -type CodexCliFileOptions = { - codexHome?: string; -}; - -type CodexCliWriteOptions = CodexCliFileOptions & { - platform?: NodeJS.Platform; - execSync?: ExecSyncFn; - execFileSync?: ExecFileSyncFn; - writeKeychain?: ( - credentials: OAuthCredentials, - options?: { - codexHome?: string; - platform?: NodeJS.Platform; - execSync?: ExecSyncFn; - execFileSync?: ExecFileSyncFn; - }, - ) => boolean; - writeFile?: (credentials: OAuthCredentials, options?: CodexCliFileOptions) => boolean; -}; - type ExecSyncFn = typeof execSync; type ExecFileSyncFn = typeof execFileSync; @@ -533,125 +513,6 @@ export function writeClaudeCliCredentials( return writeFile(newCredentials, { homeDir: options?.homeDir }); } -function buildUpdatedCodexAuthRecord( - existing: Record | null, - newCredentials: OAuthCredentials, -): Record { - const next = existing ? { ...existing } : {}; - const existingTokens = - next.tokens && typeof next.tokens === "object" ? (next.tokens as Record) : {}; - next.auth_mode = next.auth_mode ?? "chatgpt"; - next.tokens = { - ...existingTokens, - access_token: newCredentials.access, - refresh_token: newCredentials.refresh, - ...(typeof newCredentials.idToken === "string" && newCredentials.idToken.trim().length > 0 - ? { id_token: newCredentials.idToken } - : {}), - ...(typeof newCredentials.accountId === "string" && newCredentials.accountId.trim().length > 0 - ? { account_id: newCredentials.accountId } - : {}), - }; - next.last_refresh = new Date().toISOString(); - return next; -} - -export function writeCodexCliKeychainCredentials( - newCredentials: OAuthCredentials, - options?: { - codexHome?: string; - platform?: NodeJS.Platform; - execSync?: ExecSyncFn; - execFileSync?: ExecFileSyncFn; - }, -): boolean { - const { platform, codexHome } = resolveCodexKeychainParams(options); - if (platform !== "darwin") { - return false; - } - const existing = readCodexKeychainAuthRecord(options); - if (!existing) { - return false; - } - - const execFileSyncImpl = options?.execFileSync ?? execFileSync; - const account = computeCodexKeychainAccount(codexHome); - const next = buildUpdatedCodexAuthRecord(existing, newCredentials); - - try { - execFileSyncImpl( - "security", - ["add-generic-password", "-U", "-s", "Codex Auth", "-a", account, "-w", JSON.stringify(next)], - { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }, - ); - codexCliCache = null; - log.info("wrote refreshed credentials to codex cli keychain", { - expires: new Date(newCredentials.expires).toISOString(), - }); - return true; - } catch (error) { - log.warn("failed to write credentials to codex cli keychain", { - error: formatErrorMessage(error), - }); - return false; - } -} - -export function writeCodexCliFileCredentials( - newCredentials: OAuthCredentials, - options?: CodexCliFileOptions, -): boolean { - const codexHome = resolveCodexHomePath(options?.codexHome); - const authPath = path.join(codexHome, CODEX_CLI_AUTH_FILENAME); - if (!fs.existsSync(authPath)) { - return false; - } - - try { - const raw = loadJsonFile(authPath); - if (!raw || typeof raw !== "object") { - return false; - } - const next = buildUpdatedCodexAuthRecord(raw as Record, newCredentials); - saveJsonFile(authPath, next); - codexCliCache = null; - log.info("wrote refreshed credentials to codex cli file", { - expires: new Date(newCredentials.expires).toISOString(), - }); - return true; - } catch (error) { - log.warn("failed to write credentials to codex cli file", { - error: formatErrorMessage(error), - }); - return false; - } -} - -export function writeCodexCliCredentials( - newCredentials: OAuthCredentials, - options?: CodexCliWriteOptions, -): boolean { - const platform = options?.platform ?? process.platform; - const writeKeychain = options?.writeKeychain ?? writeCodexCliKeychainCredentials; - const writeFile = - options?.writeFile ?? - ((credentials, fileOptions) => writeCodexCliFileCredentials(credentials, fileOptions)); - - if ( - platform === "darwin" && - writeKeychain(newCredentials, { - codexHome: options?.codexHome, - platform, - execSync: options?.execSync, - execFileSync: options?.execFileSync, - }) - ) { - return true; - } - - return writeFile(newCredentials, { codexHome: options?.codexHome }); -} - export function readCodexCliCredentials(options?: { codexHome?: string; platform?: NodeJS.Platform; diff --git a/src/plugin-sdk/provider-auth-runtime.test.ts b/src/plugin-sdk/provider-auth-runtime.test.ts index 20f36777a91..0c169c75ff3 100644 --- a/src/plugin-sdk/provider-auth-runtime.test.ts +++ b/src/plugin-sdk/provider-auth-runtime.test.ts @@ -1,117 +1,14 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { saveAuthProfileStore } from "./agent-runtime.js"; +import { describe, expect, it } from "vitest"; import * as providerAuthRuntime from "./provider-auth-runtime.js"; describe("plugin-sdk provider-auth-runtime", () => { - const tempDirs: string[] = []; - - async function makeTempDir(): Promise { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-provider-auth-runtime-")); - tempDirs.push(dir); - return dir; - } - - afterEach(async () => { - await Promise.all( - tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), - ); - }); - it("exports the runtime-ready auth helper", () => { expect(typeof providerAuthRuntime.getRuntimeAuthForModel).toBe("function"); }); - it("exports the Codex auth bridge helper", () => { - expect(typeof providerAuthRuntime.prepareCodexAuthBridge).toBe("function"); - }); - it("exports OAuth callback helpers", () => { expect(typeof providerAuthRuntime.generateOAuthState).toBe("function"); expect(typeof providerAuthRuntime.parseOAuthCallbackInput).toBe("function"); expect(typeof providerAuthRuntime.waitForLocalOAuthCallback).toBe("function"); }); - - it("does not write incomplete Codex ChatGPT auth without an id token", async () => { - const agentDir = await makeTempDir(); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - - const bridge = await providerAuthRuntime.prepareCodexAuthBridge({ - agentDir, - bridgeDir: "harness-auth", - profileId: "openai-codex:default", - }); - - expect(bridge).toBeUndefined(); - }); - - it("hydrates missing Codex id token from a matching source auth file", async () => { - const root = await makeTempDir(); - const agentDir = path.join(root, "agent"); - const sourceCodexHome = path.join(root, "codex-home"); - await fs.mkdir(sourceCodexHome, { recursive: true }); - await fs.writeFile( - path.join(sourceCodexHome, "auth.json"), - `${JSON.stringify( - { - auth_mode: "chatgpt", - tokens: { - id_token: "source-id-token", - access_token: "access-token", - refresh_token: "refresh-token", - account_id: "acct-123", - }, - }, - null, - 2, - )}\n`, - ); - saveAuthProfileStore( - { - version: 1, - profiles: { - "openai-codex:default": { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - accountId: "acct-123", - expires: Date.now() + 60_000, - }, - }, - }, - agentDir, - { filterExternalAuthProfiles: false }, - ); - - const bridge = await providerAuthRuntime.prepareCodexAuthBridge({ - agentDir, - bridgeDir: "harness-auth", - profileId: "openai-codex:default", - sourceCodexHome, - }); - - expect(bridge?.codexHome).toContain(path.join(agentDir, "harness-auth", "codex")); - const authFile = JSON.parse( - await fs.readFile(path.join(bridge?.codexHome ?? "", "auth.json"), "utf8"), - ); - expect(authFile.tokens.id_token).toBe("source-id-token"); - }); }); diff --git a/src/plugin-sdk/provider-auth-runtime.ts b/src/plugin-sdk/provider-auth-runtime.ts index 0b571f223ed..a220e2db125 100644 --- a/src/plugin-sdk/provider-auth-runtime.ts +++ b/src/plugin-sdk/provider-auth-runtime.ts @@ -3,13 +3,9 @@ import crypto from "node:crypto"; import fs from "node:fs"; import { createServer } from "node:http"; -import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { ensureAuthProfileStoreForLocalUpdate } from "../agents/auth-profiles/store.js"; -import type { OAuthCredential } from "../agents/auth-profiles/types.js"; import { resolveApiKeyForProvider as resolveModelApiKeyForProvider } from "../agents/model-auth.js"; -import { writePrivateSecretFileAtomic } from "../infra/secret-file.js"; export { resolveEnvApiKey } from "../agents/model-auth-env.js"; export { @@ -25,15 +21,6 @@ export { export type { ProviderPreparedRuntimeAuth } from "../plugins/types.js"; export type { ResolvedProviderRuntimeAuth } from "../plugins/runtime/model-auth-types.js"; -export const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const; - -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; - -export type PreparedCodexAuthBridge = { - codexHome: string; - clearEnv: string[]; -}; - export type OAuthCallbackResult = { code: string; state: string }; export function generateOAuthState(): string { @@ -182,233 +169,6 @@ function escapeHtmlText(value: string): string { .replace(/'/g, "'"); } -export function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential { - return Boolean( - value && - typeof value === "object" && - value !== null && - "type" in value && - "provider" in value && - "access" in value && - "refresh" in value && - value.type === "oauth" && - value.provider === OPENAI_CODEX_PROVIDER_ID && - typeof value.access === "string" && - value.access.trim().length > 0 && - typeof value.refresh === "string" && - value.refresh.trim().length > 0, - ); -} - -type CodexAuthBridgeRecord = { - auth_mode?: unknown; - tokens?: { - id_token?: unknown; - access_token?: unknown; - refresh_token?: unknown; - account_id?: unknown; - }; - last_refresh?: unknown; - OPENAI_API_KEY?: unknown; -}; - -type CodexAuthBridgeMaterial = { - accountId?: string; - idToken?: string; - lastRefresh?: string | number; - openaiApiKey?: string; -}; - -export function resolveCodexAuthBridgeHome(params: { - agentDir: string; - bridgeDir: string; - profileId: string; -}): string { - const digest = crypto.createHash("sha256").update(params.profileId).digest("hex").slice(0, 16); - return path.join(params.agentDir, params.bridgeDir, "codex", digest); -} - -function assertExistingCodexAuthBridgeFileSafe(codexHome: string): void { - const authFile = path.join(codexHome, "auth.json"); - try { - const stat = fs.lstatSync(authFile); - if (stat.isSymbolicLink()) { - throw new Error(`Private secret file ${authFile} must not be a symlink.`); - } - if (!stat.isFile()) { - throw new Error(`Private secret file ${authFile} must be a regular file.`); - } - } catch (error) { - if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") { - throw error; - } - } -} - -export function buildCodexAuthBridgeFile( - credential: OAuthCredential, - material: Partial = {}, -): string { - const lastRefresh = - normalizeCodexAuthLastRefresh(material.lastRefresh) ?? new Date().toISOString(); - const openaiApiKey = readCodexAuthString(material.openaiApiKey); - const idToken = readCodexAuthString(credential.idToken) ?? readCodexAuthString(material.idToken); - const accountId = - readCodexAuthString(credential.accountId) ?? readCodexAuthString(material.accountId); - return `${JSON.stringify( - { - auth_mode: "chatgpt", - ...(openaiApiKey ? { OPENAI_API_KEY: openaiApiKey } : {}), - tokens: { - ...(idToken ? { id_token: idToken } : {}), - access_token: credential.access, - refresh_token: credential.refresh, - ...(accountId ? { account_id: accountId } : {}), - }, - last_refresh: lastRefresh, - }, - null, - 2, - )}\n`; -} - -export async function prepareCodexAuthBridge(params: { - agentDir: string; - bridgeDir: string; - profileId: string; - sourceCodexHome?: string; - env?: NodeJS.ProcessEnv; -}): Promise { - const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); - const credential = store.profiles[params.profileId]; - if (!isCodexBridgeableOAuthCredential(credential)) { - return undefined; - } - - const codexHome = resolveCodexAuthBridgeHome(params); - assertExistingCodexAuthBridgeFileSafe(codexHome); - const material = resolveCodexAuthBridgeMaterial({ - credential, - sourceCodexHome: params.sourceCodexHome, - env: { ...process.env, ...params.env }, - }); - if (!readCodexAuthString(credential.idToken) && !readCodexAuthString(material.idToken)) { - return undefined; - } - await writePrivateSecretFileAtomic({ - rootDir: params.agentDir, - filePath: path.join(codexHome, "auth.json"), - content: buildCodexAuthBridgeFile(credential, material), - }); - - return { - codexHome, - clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS], - }; -} - -function resolveCodexAuthBridgeMaterial(params: { - credential: OAuthCredential; - sourceCodexHome?: string; - env?: NodeJS.ProcessEnv; -}): Partial { - const source = readCodexAuthBridgeSourceFile({ - codexHome: params.sourceCodexHome, - env: params.env, - }); - if (!source || source.auth_mode !== "chatgpt") { - return {}; - } - - const tokens = source.tokens; - if (!tokens || typeof tokens !== "object") { - return {}; - } - const access = readCodexAuthString(tokens.access_token); - const refresh = readCodexAuthString(tokens.refresh_token); - if (!access || !refresh) { - return {}; - } - - const accountId = readCodexAuthString(tokens.account_id); - if (!codexAuthSourceMatchesCredential(params.credential, { access, refresh, accountId })) { - return {}; - } - const idToken = readCodexAuthString(tokens.id_token); - const lastRefresh = normalizeCodexAuthLastRefresh(source.last_refresh); - const openaiApiKey = readCodexAuthString(source.OPENAI_API_KEY); - - return { - ...(accountId ? { accountId } : {}), - ...(idToken ? { idToken } : {}), - ...(lastRefresh ? { lastRefresh } : {}), - ...(openaiApiKey ? { openaiApiKey } : {}), - }; -} - -function codexAuthSourceMatchesCredential( - credential: OAuthCredential, - source: { access: string; refresh: string; accountId?: string }, -): boolean { - if (credential.access === source.access && credential.refresh === source.refresh) { - return true; - } - const credentialAccountId = credential.accountId?.trim(); - const sourceAccountId = source.accountId?.trim(); - return Boolean(credentialAccountId && sourceAccountId && credentialAccountId === sourceAccountId); -} - -function readCodexAuthBridgeSourceFile(params: { - codexHome?: string; - env?: NodeJS.ProcessEnv; -}): CodexAuthBridgeRecord | undefined { - const codexHome = resolveSourceCodexHome(params); - if (!codexHome) { - return undefined; - } - try { - const parsed = JSON.parse(fs.readFileSync(path.join(codexHome, "auth.json"), "utf8")); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as CodexAuthBridgeRecord) - : undefined; - } catch { - return undefined; - } -} - -function resolveSourceCodexHome(params: { - codexHome?: string; - env?: NodeJS.ProcessEnv; -}): string | undefined { - const configured = params.codexHome?.trim() || params.env?.CODEX_HOME?.trim(); - if (configured) { - return resolveTildePath(configured); - } - const home = os.homedir(); - return home ? path.join(home, ".codex") : undefined; -} - -function resolveTildePath(value: string): string { - if (value === "~") { - return os.homedir(); - } - if (value.startsWith("~/")) { - return path.join(os.homedir(), value.slice(2)); - } - return path.resolve(value); -} - -function readCodexAuthString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value : undefined; -} - -function normalizeCodexAuthLastRefresh(value: unknown): string | number | undefined { - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - return readCodexAuthString(value); -} - type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider; type GetRuntimeAuthForModel = typeof import("../plugins/runtime/runtime-model-auth.runtime.js").getRuntimeAuthForModel;