From 6f6fa5c90b0dc296c3458fb916d91c24ed67f826 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Wed, 22 Apr 2026 16:34:53 -0700 Subject: [PATCH] Remove Codex CLI auth import --- docs/reference/wizard.md | 3 +- docs/start/wizard-cli-reference.md | 13 +- extensions/openai/cli-backend.ts | 2 +- .../openai/openai-codex-cli-auth.test.ts | 311 ------------------ extensions/openai/openai-codex-cli-auth.ts | 187 ----------- .../openai/openai-codex-provider.test.ts | 176 +--------- extensions/openai/openai-codex-provider.ts | 91 +---- extensions/openai/provider-contract-api.ts | 14 - src/commands/auth-choice-options.test.ts | 10 +- 9 files changed, 15 insertions(+), 792 deletions(-) delete mode 100644 extensions/openai/openai-codex-cli-auth.test.ts delete mode 100644 extensions/openai/openai-codex-cli-auth.ts diff --git a/docs/reference/wizard.md b/docs/reference/wizard.md index 76d5bd61b5b..d90c0473ca1 100644 --- a/docs/reference/wizard.md +++ b/docs/reference/wizard.md @@ -33,9 +33,10 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard). - **Anthropic API key**: uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use. - **Anthropic API key**: preferred Anthropic assistant choice in onboarding/configure. - **Anthropic setup-token**: still available in onboarding/configure, though OpenClaw now prefers Claude CLI reuse when available. - - **OpenAI Code (Codex) subscription (Codex CLI)**: if `~/.codex/auth.json` exists, onboarding can reuse it. Reused Codex CLI credentials stay managed by Codex CLI; on expiry OpenClaw re-reads that source first and, when the provider can refresh it, writes the refreshed credential back to Codex storage instead of taking ownership itself. - **OpenAI Code (Codex) subscription (OAuth)**: browser flow; paste the `code#state`. - Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`. + - **OpenAI Code (Codex) subscription (device pairing)**: browser pairing flow with a short-lived device code. + - Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`. - **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then stores it in auth profiles. - Sets `agents.defaults.model` to `openai/gpt-5.4` when model is unset, `openai/*`, or `openai-codex/*`. - **xAI (Grok) API key**: prompts for `XAI_API_KEY` and configures xAI as a model provider. diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index 14a8d20f4b4..336a4d0208b 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -129,18 +129,17 @@ What you set: Uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use. - - If `~/.codex/auth.json` exists, the wizard can reuse it. - Reused Codex CLI credentials stay managed by Codex CLI; on expiry OpenClaw - re-reads that source first and, when the provider can refresh it, writes - the refreshed credential back to Codex storage instead of taking ownership - itself. - Browser flow; paste `code#state`. Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`. + + + Browser pairing flow with a short-lived device code. + + Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`. + Uses `OPENAI_API_KEY` if present or prompts for a key, then stores the credential in auth profiles. diff --git a/extensions/openai/cli-backend.ts b/extensions/openai/cli-backend.ts index 3aa597b7d1d..d4771ecf75e 100644 --- a/extensions/openai/cli-backend.ts +++ b/extensions/openai/cli-backend.ts @@ -3,9 +3,9 @@ import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, } from "openclaw/plugin-sdk/cli-backend"; -import { OPENAI_CODEX_DEFAULT_PROFILE_ID } from "./openai-codex-cli-auth.js"; 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.4"; export function buildOpenAICodexCliBackend(): CliBackendPlugin { diff --git a/extensions/openai/openai-codex-cli-auth.test.ts b/extensions/openai/openai-codex-cli-auth.test.ts deleted file mode 100644 index 9954221a77f..00000000000 --- a/extensions/openai/openai-codex-cli-auth.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import fs from "node:fs"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const runtimeMocks = vi.hoisted(() => ({ - debug: vi.fn(), -})); - -vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ - createSubsystemLogger: () => ({ - debug: runtimeMocks.debug, - }), -})); - -import { - OPENAI_CODEX_DEFAULT_PROFILE_ID, - hasOpenAICodexCliOAuthCredential, - readOpenAICodexCliOAuthProfile, -} from "./openai-codex-cli-auth.js"; - -function buildJwt(payload: Record) { - const encode = (value: Record) => - Buffer.from(JSON.stringify(value)).toString("base64url"); - return `${encode({ alg: "none", typ: "JWT" })}.${encode(payload)}.sig`; -} - -function mockCodexCliChatGptAuth(params?: { - email?: string; - accountId?: string; - accessToken?: string; -}) { - const accessToken = - params?.accessToken ?? - buildJwt({ - exp: Math.floor(Date.now() / 1000) + 600, - "https://api.openai.com/profile": { - email: params?.email ?? "codex@example.com", - }, - }); - vi.spyOn(fs, "readFileSync").mockReturnValue( - JSON.stringify({ - auth_mode: "chatgpt", - tokens: { - id_token: "id-token", - access_token: accessToken, - refresh_token: "refresh-token", - account_id: params?.accountId ?? "acct_123", - }, - }), - ); - return accessToken; -} - -describe("readOpenAICodexCliOAuthProfile", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("reads Codex CLI chatgpt auth into the default OpenAI Codex profile", () => { - const accessToken = mockCodexCliChatGptAuth(); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { version: 1, profiles: {} }, - }); - - expect(parsed).toMatchObject({ - profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - credential: { - type: "oauth", - provider: "openai-codex", - access: accessToken, - refresh: "refresh-token", - accountId: "acct_123", - idToken: "id-token", - email: "codex@example.com", - }, - }); - expect(parsed?.credential.expires).toBeGreaterThan(Date.now()); - }); - - it("detects an existing Codex CLI chatgpt login for setup labeling", () => { - vi.spyOn(fs, "readFileSync").mockReturnValue( - JSON.stringify({ - auth_mode: "chatgpt", - tokens: { - access_token: "access-token", - refresh_token: "refresh-token", - }, - }), - ); - - expect(hasOpenAICodexCliOAuthCredential()).toBe(true); - }); - - it("does not override a locally managed OpenAI Codex profile", () => { - vi.spyOn(fs, "readFileSync").mockReturnValue( - JSON.stringify({ - auth_mode: "chatgpt", - tokens: { - access_token: "access-token", - refresh_token: "refresh-token", - }, - }), - ); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "local-access", - refresh: "local-refresh", - expires: Date.now() + 10 * 60_000, - }, - }, - }, - }); - - expect(parsed).toBeNull(); - }); - - it("does not override explicit local non-oauth auth with Codex CLI bootstrap", () => { - vi.spyOn(fs, "readFileSync").mockReturnValue( - JSON.stringify({ - auth_mode: "chatgpt", - tokens: { - access_token: "access-token", - refresh_token: "refresh-token", - }, - }), - ); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: { - type: "api_key", - provider: "openai-codex", - key: "sk-local", - }, - }, - }, - }); - - expect(parsed).toBeNull(); - }); - - it("refuses Codex CLI bootstrap when an expired local default belongs to a different account", () => { - mockCodexCliChatGptAuth({ - email: "codex-b@example.com", - accountId: "acct_b", - }); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "near-expiry-local-access", - refresh: "near-expiry-local-refresh", - expires: Date.now() + 60_000, - accountId: "acct_a", - email: "codex-a@example.com", - }, - }, - }, - }); - - expect(parsed).toBeNull(); - }); - - it("allows cli bootstrap when the stored default profile is expired", () => { - const accessToken = mockCodexCliChatGptAuth(); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "expired-local-access", - refresh: "expired-local-refresh", - expires: Date.now() - 60_000, - accountId: "acct_123", - }, - }, - }, - }); - - expect(parsed).toMatchObject({ - profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - credential: { - access: accessToken, - refresh: "refresh-token", - accountId: "acct_123", - idToken: "id-token", - email: "codex@example.com", - }, - }); - }); - - it("refuses cli bootstrap when the stored default profile is expired but identity mismatches", () => { - mockCodexCliChatGptAuth(); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "expired-local-access", - refresh: "expired-local-refresh", - expires: Date.now() - 60_000, - accountId: "acct_local", - }, - }, - }, - }); - - expect(parsed).toBeNull(); - }); - - it("allows the runtime-only Codex CLI profile when the stored default already matches", () => { - const accessToken = mockCodexCliChatGptAuth(); - - const firstParse = readOpenAICodexCliOAuthProfile({ - store: { version: 1, profiles: {} }, - }); - expect(firstParse).not.toBeNull(); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { - version: 1, - profiles: { - [OPENAI_CODEX_DEFAULT_PROFILE_ID]: firstParse!.credential, - }, - }, - }); - - expect(parsed).toMatchObject({ - profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - credential: { - access: accessToken, - refresh: "refresh-token", - accountId: "acct_123", - idToken: "id-token", - email: "codex@example.com", - }, - }); - }); - - it("returns null without logging when the Codex CLI auth file is missing", () => { - const error = Object.assign(new Error("missing"), { - code: "ENOENT", - }); - vi.spyOn(fs, "readFileSync").mockImplementation(() => { - throw error; - }); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { version: 1, profiles: {} }, - }); - - expect(parsed).toBeNull(); - expect(runtimeMocks.debug).not.toHaveBeenCalled(); - }); - - it("logs a sanitized code for invalid auth JSON", () => { - vi.spyOn(fs, "readFileSync").mockReturnValue("{"); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { version: 1, profiles: {} }, - }); - - expect(parsed).toBeNull(); - expect(runtimeMocks.debug).toHaveBeenCalledWith( - "Failed to read Codex CLI auth file (code=INVALID_JSON)", - ); - }); - - it("does not leak auth file paths in debug logs for filesystem failures", () => { - const error = Object.assign( - new Error("EACCES: permission denied, open '/Users/alice/.codex/auth.json'"), - { - code: "EACCES", - }, - ); - vi.spyOn(fs, "readFileSync").mockImplementation(() => { - throw error; - }); - - const parsed = readOpenAICodexCliOAuthProfile({ - store: { version: 1, profiles: {} }, - }); - - expect(parsed).toBeNull(); - expect(runtimeMocks.debug).toHaveBeenCalledWith( - "Failed to read Codex CLI auth file (code=EACCES)", - ); - }); -}); diff --git a/extensions/openai/openai-codex-cli-auth.ts b/extensions/openai/openai-codex-cli-auth.ts deleted file mode 100644 index 859bc565b42..00000000000 --- a/extensions/openai/openai-codex-cli-auth.ts +++ /dev/null @@ -1,187 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { - hasUsableOAuthCredential, - resolveRequiredHomeDir, - type AuthProfileStore, - type OAuthCredential, -} from "openclaw/plugin-sdk/provider-auth"; -import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; -import { - resolveCodexAccessTokenExpiry, - resolveCodexAuthIdentity, -} from "./openai-codex-auth-identity.js"; -import { trimNonEmptyString } from "./openai-codex-shared.js"; - -const PROVIDER_ID = "openai-codex"; -const log = createSubsystemLogger("openai/codex-cli-auth"); - -export const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`; -export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`; - -type CodexCliAuthFile = { - auth_mode?: unknown; - tokens?: { - id_token?: unknown; - access_token?: unknown; - refresh_token?: unknown; - account_id?: unknown; - }; -}; - -function resolveCodexCliHome(env: NodeJS.ProcessEnv): string { - const configured = trimNonEmptyString(env.CODEX_HOME); - if (!configured) { - return path.join(resolveRequiredHomeDir(), ".codex"); - } - if (configured === "~") { - return resolveRequiredHomeDir(); - } - if (configured.startsWith("~/")) { - return path.join(resolveRequiredHomeDir(), configured.slice(2)); - } - return path.resolve(configured); -} - -function readCodexCliAuthFile(env: NodeJS.ProcessEnv): CodexCliAuthFile | null { - try { - const authPath = path.join(resolveCodexCliHome(env), "auth.json"); - const raw = fs.readFileSync(authPath, "utf8"); - const parsed = JSON.parse(raw); - return parsed && typeof parsed === "object" ? (parsed as CodexCliAuthFile) : null; - } catch (error) { - const code = - error instanceof SyntaxError - ? "INVALID_JSON" - : error instanceof Error && "code" in error - ? (error as NodeJS.ErrnoException).code - : undefined; - if (code === "ENOENT") { - return null; - } - log.debug( - `Failed to read Codex CLI auth file (code=${typeof code === "string" ? code : "UNKNOWN"})`, - ); - return null; - } -} - -export function hasOpenAICodexCliOAuthCredential(params?: { env?: NodeJS.ProcessEnv }): boolean { - const authFile = readCodexCliAuthFile(params?.env ?? process.env); - if (!authFile || authFile.auth_mode !== "chatgpt") { - return false; - } - - return Boolean( - trimNonEmptyString(authFile.tokens?.access_token) && - trimNonEmptyString(authFile.tokens?.refresh_token), - ); -} - -function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean { - return ( - a.type === b.type && - a.provider === b.provider && - a.access === b.access && - a.refresh === b.refresh && - a.clientId === b.clientId && - a.email === b.email && - a.displayName === b.displayName && - a.enterpriseUrl === b.enterpriseUrl && - a.projectId === b.projectId && - a.accountId === b.accountId && - a.idToken === b.idToken - ); -} - -function normalizeAuthIdentityToken(value: string | undefined): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} - -function normalizeAuthEmailToken(value: string | undefined): string | undefined { - return normalizeAuthIdentityToken(value)?.toLowerCase(); -} - -function hasIdentityContinuity( - existing: Pick | undefined, - incoming: OAuthCredential, -): boolean { - if (!existing) { - return true; - } - if (oauthCredentialMatches(existing as OAuthCredential, incoming)) { - return true; - } - - const existingAccountId = normalizeAuthIdentityToken(existing.accountId); - const incomingAccountId = normalizeAuthIdentityToken(incoming.accountId); - if (existingAccountId !== undefined && incomingAccountId !== undefined) { - return existingAccountId === incomingAccountId; - } - - const existingEmail = normalizeAuthEmailToken(existing.email); - const incomingEmail = normalizeAuthEmailToken(incoming.email); - if (existingEmail !== undefined && incomingEmail !== undefined) { - return existingEmail === incomingEmail; - } - - return false; -} - -export function readOpenAICodexCliOAuthProfile(params: { - env?: NodeJS.ProcessEnv; - store: AuthProfileStore; -}): { profileId: string; credential: OAuthCredential } | null { - const authFile = readCodexCliAuthFile(params.env ?? process.env); - if (!authFile || authFile.auth_mode !== "chatgpt") { - return null; - } - - const access = trimNonEmptyString(authFile.tokens?.access_token); - const refresh = trimNonEmptyString(authFile.tokens?.refresh_token); - if (!access || !refresh) { - return null; - } - - const accountId = trimNonEmptyString(authFile.tokens?.account_id); - const idToken = trimNonEmptyString(authFile.tokens?.id_token); - const identity = resolveCodexAuthIdentity({ accessToken: access }); - const credential: OAuthCredential = { - type: "oauth", - provider: PROVIDER_ID, - access, - refresh, - expires: resolveCodexAccessTokenExpiry(access) ?? 0, - ...(accountId ? { accountId } : {}), - ...(idToken ? { idToken } : {}), - ...(identity.email ? { email: identity.email } : {}), - ...(identity.profileName ? { displayName: identity.profileName } : {}), - }; - const existing = params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]; - const existingOAuth = - existing?.type === "oauth" && existing.provider === PROVIDER_ID ? existing : undefined; - if (existing && !existingOAuth) { - log.debug("kept explicit local auth over Codex CLI bootstrap", { - profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - localType: existing.type, - localProvider: existing.provider, - }); - return null; - } - if (!hasIdentityContinuity(existingOAuth, credential)) { - return null; - } - if ( - existingOAuth && - hasUsableOAuthCredential(existingOAuth) && - !oauthCredentialMatches(existingOAuth, credential) - ) { - return null; - } - - return { - profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - credential, - }; -} diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index a31f7375e9d..2f7955b086e 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -1,32 +1,17 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn()); -const readOpenAICodexCliOAuthProfileMock = vi.hoisted(() => vi.fn()); -const hasOpenAICodexCliOAuthCredentialMock = vi.hoisted(() => vi.fn()); const loginOpenAICodexDeviceCodeMock = vi.hoisted(() => vi.fn()); vi.mock("./openai-codex-provider.runtime.js", () => ({ refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); -vi.mock("./openai-codex-cli-auth.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - hasOpenAICodexCliOAuthCredential: hasOpenAICodexCliOAuthCredentialMock, - readOpenAICodexCliOAuthProfile: readOpenAICodexCliOAuthProfileMock, - }; -}); - vi.mock("./openai-codex-device-code.js", () => ({ loginOpenAICodexDeviceCode: loginOpenAICodexDeviceCodeMock, })); let buildOpenAICodexProviderPlugin: typeof import("./openai-codex-provider.js").buildOpenAICodexProviderPlugin; -const tempDirs: string[] = []; function createCodexTemplate(overrides: { id?: string; @@ -67,18 +52,9 @@ describe("openai codex provider", () => { beforeEach(() => { refreshOpenAICodexTokenMock.mockReset(); - readOpenAICodexCliOAuthProfileMock.mockReset(); - hasOpenAICodexCliOAuthCredentialMock.mockReset(); - hasOpenAICodexCliOAuthCredentialMock.mockReturnValue(false); loginOpenAICodexDeviceCodeMock.mockReset(); }); - afterEach(async () => { - await Promise.all( - tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), - ); - }); - it("falls back to the cached credential when accountId extraction fails", async () => { const provider = buildOpenAICodexProviderPlugin(); const credential = { @@ -149,14 +125,10 @@ describe("openai codex provider", () => { ); }); - it("offers OpenAI menu auth methods for login, import, and device pairing", () => { + it("offers OpenAI menu auth methods for browser login and device pairing", () => { const provider = buildOpenAICodexProviderPlugin(); - expect(provider.auth?.map((method) => method.id)).toEqual([ - "oauth", - "device-code", - "import-codex-cli", - ]); + expect(provider.auth?.map((method) => method.id)).toEqual(["oauth", "device-code"]); expect(provider.auth?.find((method) => method.id === "oauth")).toMatchObject({ label: "OpenAI Codex Browser Login", hint: "Sign in with OpenAI in your browser", @@ -176,65 +148,6 @@ describe("openai codex provider", () => { assistantPriority: -10, }, }); - expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({ - label: "Import Existing Codex Login", - hint: "Import an existing ~/.codex login", - kind: "oauth", - wizard: { - choiceId: "openai-codex-import", - choiceLabel: "Import Existing Codex Login", - assistantPriority: -20, - assistantVisibility: "manual-only", - }, - }); - }); - - it("annotates the import option when ~/.codex auth is detected", () => { - hasOpenAICodexCliOAuthCredentialMock.mockReturnValueOnce(true); - - const provider = buildOpenAICodexProviderPlugin(); - - expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({ - label: "Import Existing Codex Login (~/.codex detected)", - wizard: { - choiceLabel: "Import Existing Codex Login (~/.codex detected)", - assistantVisibility: "visible", - }, - }); - }); - - it("soft-fails import when no compatible ~/.codex login exists", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli"); - const note = vi.fn(async () => {}); - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce(null); - - const result = await importMethod?.run({ - config: {}, - env: process.env, - prompter: { - note, - progress: vi.fn(), - } as never, - runtime: runtime as never, - isRemote: false, - openUrl: async () => {}, - oauth: { createVpsAwareHandlers: (() => ({})) as never }, - }); - - expect(result).toEqual({ profiles: [] }); - expect(runtime.error).toHaveBeenCalledWith( - "No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead.", - ); - expect(note).toHaveBeenCalledWith( - "No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead.", - "Import Existing Codex Login", - ); }); it("stores device-code logins as OpenAI Codex oauth profiles", async () => { @@ -343,89 +256,6 @@ describe("openai codex provider", () => { ); }); - it("exposes Codex CLI auth as a runtime-only external profile", () => { - const provider = buildOpenAICodexProviderPlugin(); - const credential = { - type: "oauth" as const, - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - accountId: "acct-123", - }; - readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce({ - profileId: "openai-codex:default", - credential, - }); - - expect( - provider.resolveExternalAuthProfiles?.({ - env: { CODEX_HOME: "/sandboxed/codex-home" } as NodeJS.ProcessEnv, - store: { version: 1, profiles: {} }, - }), - ).toEqual([ - { - profileId: "openai-codex:default", - credential, - persistence: "runtime-only", - }, - ]); - expect(readOpenAICodexCliOAuthProfileMock).toHaveBeenCalledWith( - expect.objectContaining({ - env: expect.objectContaining({ CODEX_HOME: "/sandboxed/codex-home" }), - store: { version: 1, profiles: {} }, - }), - ); - }); - - it("uses the provider auth context env when importing Codex CLI auth", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli"); - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-openai-codex-provider-")); - tempDirs.push(agentDir); - readOpenAICodexCliOAuthProfileMock.mockImplementationOnce(({ env }) => { - expect(env).toMatchObject({ - CODEX_HOME: "/sandboxed/codex-home", - }); - return { - profileId: "openai-codex:default", - credential: { - type: "oauth", - provider: "openai-codex", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - email: "codex@example.com", - displayName: "Codex User", - accountId: "acct-123", - }, - }; - }); - - await expect( - importMethod?.run({ - config: {}, - env: { CODEX_HOME: "/sandboxed/codex-home" }, - agentDir, - prompter: {} as never, - runtime: {} as never, - isRemote: false, - openUrl: async () => {}, - oauth: { createVpsAwareHandlers: (() => ({})) as never }, - }), - ).resolves.toMatchObject({ - profiles: [ - { - profileId: "openai-codex:default", - credential: expect.objectContaining({ - provider: "openai-codex", - access: "access-token", - }), - }, - ], - }); - }); - it("owns native reasoning output mode for Codex responses", () => { const provider = buildOpenAICodexProviderPlugin(); diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 49cf0f4ff45..246593e3a6d 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -8,7 +8,6 @@ import { ensureAuthProfileStoreForLocalUpdate, listProfilesForProvider, type OAuthCredential, - type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; import { loginOpenAICodexOAuth } from "openclaw/plugin-sdk/provider-auth-login"; @@ -24,11 +23,6 @@ import { isOpenAIApiBaseUrl, isOpenAICodexBaseUrl } from "./base-url.js"; import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js"; import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js"; import { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; -import { - CODEX_CLI_PROFILE_ID, - hasOpenAICodexCliOAuthCredential, - readOpenAICodexCliOAuthProfile, -} from "./openai-codex-cli-auth.js"; import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js"; import { buildOpenAIResponsesProviderHooks, @@ -45,14 +39,11 @@ const OPENAI_WIZARD_GROUP = { groupLabel: "OpenAI", groupHint: "API key + Codex auth", } as const; +const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`; const OPENAI_CODEX_LOGIN_ASSISTANT_PRIORITY = -30; -const OPENAI_CODEX_IMPORT_ASSISTANT_PRIORITY = -20; const OPENAI_CODEX_DEVICE_PAIRING_ASSISTANT_PRIORITY = -10; const OPENAI_CODEX_LOGIN_LABEL = "OpenAI Codex Browser Login"; const OPENAI_CODEX_LOGIN_HINT = "Sign in with OpenAI in your browser"; -const OPENAI_CODEX_IMPORT_LABEL = "Import Existing Codex Login"; -const OPENAI_CODEX_IMPORT_HINT = "Import an existing ~/.codex login"; -const OPENAI_CODEX_IMPORT_DETECTED_SUFFIX = "~/.codex detected"; const OPENAI_CODEX_DEVICE_PAIRING_LABEL = "OpenAI Codex Device Pairing"; const OPENAI_CODEX_DEVICE_PAIRING_HINT = "Pair in browser with a device code"; const OPENAI_CODEX_GPT_54_MODEL_ID = "gpt-5.4"; @@ -371,53 +362,6 @@ async function runOpenAICodexDeviceCode(ctx: ProviderAuthContext) { } } -async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) { - const profile = readOpenAICodexCliOAuthProfile({ - env: ctx.env ?? process.env, - store: ensureAuthProfileStoreForLocalUpdate(ctx.agentDir), - }); - if (!profile) { - const message = - "No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead."; - ctx.runtime.error(message); - await ctx.prompter.note(message, OPENAI_CODEX_IMPORT_LABEL); - return { profiles: [] }; - } - - return { - profiles: [{ profileId: profile.profileId, credential: profile.credential }], - configPatch: { - agents: { - defaults: { - models: { - [OPENAI_CODEX_DEFAULT_MODEL]: {}, - }, - }, - }, - }, - defaultModel: OPENAI_CODEX_DEFAULT_MODEL, - notes: ["Imported existing Codex CLI login into OpenClaw canonical auth."], - } satisfies ProviderAuthResult; -} - -function ensureOpenAICodexCatalogAuthStore(ctx: { agentDir?: string; env?: NodeJS.ProcessEnv }) { - const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); - const profile = readOpenAICodexCliOAuthProfile({ - env: ctx.env ?? process.env, - store, - }); - if (!profile) { - return store; - } - return { - ...store, - profiles: { - ...store.profiles, - [profile.profileId]: profile.credential, - }, - }; -} - function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) { if (ctx.profileId !== CODEX_CLI_PROFILE_ID) { return undefined; @@ -425,16 +369,7 @@ function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) { return "Deprecated profile. Run `openclaw models auth login --provider openai-codex` or `openclaw configure`."; } -function buildOpenAICodexImportWizardLabel(hasCodexCliCredential: boolean) { - if (!hasCodexCliCredential) { - return OPENAI_CODEX_IMPORT_LABEL; - } - return `${OPENAI_CODEX_IMPORT_LABEL} (${OPENAI_CODEX_IMPORT_DETECTED_SUFFIX})`; -} - export function buildOpenAICodexProviderPlugin(): ProviderPlugin { - const hasCodexCliCredential = hasOpenAICodexCliOAuthCredential(); - const importWizardLabel = buildOpenAICodexImportWizardLabel(hasCodexCliCredential); return { id: PROVIDER_ID, label: "OpenAI Codex", @@ -474,26 +409,11 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { } }, }, - { - id: "import-codex-cli", - label: importWizardLabel, - hint: OPENAI_CODEX_IMPORT_HINT, - kind: "oauth", - wizard: { - choiceId: "openai-codex-import", - choiceLabel: importWizardLabel, - choiceHint: OPENAI_CODEX_IMPORT_HINT, - assistantPriority: OPENAI_CODEX_IMPORT_ASSISTANT_PRIORITY, - assistantVisibility: hasCodexCliCredential ? "visible" : "manual-only", - ...OPENAI_WIZARD_GROUP, - }, - run: async (ctx) => await runImportOpenAICodexCliAuth(ctx), - }, ], catalog: { order: "profile", run: async (ctx) => { - const authStore = ensureOpenAICodexCatalogAuthStore(ctx); + const authStore = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); if (listProfilesForProvider(authStore, PROVIDER_ID).length === 0) { return null; } @@ -546,13 +466,6 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { fetchUsageSnapshot: async (ctx) => await fetchCodexUsage(ctx.token, ctx.accountId, ctx.timeoutMs, ctx.fetchFn), refreshOAuth: async (cred) => await refreshOpenAICodexOAuthCredential(cred), - resolveExternalAuthProfiles: (ctx) => { - const profile = readOpenAICodexCliOAuthProfile({ - env: ctx.env, - store: ctx.store, - }); - return profile ? [{ ...profile, persistence: "runtime-only" }] : []; - }, augmentModelCatalog: (ctx) => { const gpt54Template = findCatalogTemplate({ entries: ctx.entries, diff --git a/extensions/openai/provider-contract-api.ts b/extensions/openai/provider-contract-api.ts index ec492daf5af..3abdc46c186 100644 --- a/extensions/openai/provider-contract-api.ts +++ b/extensions/openai/provider-contract-api.ts @@ -41,20 +41,6 @@ export function createOpenAICodexProvider(): ProviderPlugin { ...OPENAI_WIZARD_GROUP, }, }, - { - id: "import-codex-cli", - kind: "oauth", - label: "Import Existing Codex Login", - hint: "Import an existing ~/.codex login", - run: noopAuth, - wizard: { - choiceId: "openai-codex-import", - choiceLabel: "Import Existing Codex Login", - choiceHint: "Import an existing ~/.codex login", - assistantPriority: -20, - ...OPENAI_WIZARD_GROUP, - }, - }, ], }; } diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 88734f3e9b4..d86c065574f 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -447,7 +447,7 @@ describe("buildAuthChoiceOptions", () => { ]); }); - it("orders OpenAI auth methods as api key, login, import, then device pairing", () => { + it("orders OpenAI auth methods as api key, browser login, then device pairing", () => { resolveProviderWizardOptions.mockReturnValue([ { value: "openai-api-key", @@ -463,13 +463,6 @@ describe("buildAuthChoiceOptions", () => { groupLabel: "OpenAI", assistantPriority: -30, }, - { - value: "openai-codex-import", - label: "Import Existing Codex Login (~/.codex detected)", - groupId: "openai", - groupLabel: "OpenAI", - assistantPriority: -20, - }, { value: "openai-codex-device-code", label: "OpenAI Codex Device Pairing", @@ -489,7 +482,6 @@ describe("buildAuthChoiceOptions", () => { expect(openAIGroup?.options.map((option) => option.value)).toEqual([ "openai-api-key", "openai-codex", - "openai-codex-import", "openai-codex-device-code", ]); });