diff --git a/CHANGELOG.md b/CHANGELOG.md index a0da6d6e8cc..031a35d6264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ Docs: https://docs.openclaw.ai - Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement when `gateway.auth.mode=none` so Control UI connections behind reverse proxies no longer get stuck on `pairing required` (code 1008) despite auth being explicitly disabled. (#42931) - Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057) - Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy. +- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar. ## 2026.3.12 diff --git a/src/agents/auth-profiles.external-cli-sync.test.ts b/src/agents/auth-profiles.external-cli-sync.test.ts new file mode 100644 index 00000000000..303b85b72d2 --- /dev/null +++ b/src/agents/auth-profiles.external-cli-sync.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it, vi } from "vitest"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; + +const mocks = vi.hoisted(() => ({ + readCodexCliCredentialsCached: vi.fn(), + readQwenCliCredentialsCached: vi.fn(() => null), + readMiniMaxCliCredentialsCached: vi.fn(() => null), +})); + +vi.mock("./cli-credentials.js", () => ({ + readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached, + readQwenCliCredentialsCached: mocks.readQwenCliCredentialsCached, + readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached, +})); + +const { syncExternalCliCredentials } = await import("./auth-profiles/external-cli-sync.js"); +const { CODEX_CLI_PROFILE_ID } = await import("./auth-profiles/constants.js"); + +const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; + +describe("syncExternalCliCredentials", () => { + it("syncs Codex CLI credentials into the supported default auth profile", () => { + const expires = Date.now() + 60_000; + mocks.readCodexCliCredentialsCached.mockReturnValue({ + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires, + accountId: "acct_123", + }); + + const store: AuthProfileStore = { + version: 1, + profiles: {}, + }; + + const mutated = syncExternalCliCredentials(store); + + expect(mutated).toBe(true); + expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith( + expect.objectContaining({ ttlMs: expect.any(Number) }), + ); + expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({ + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires, + accountId: "acct_123", + }); + expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined(); + }); +}); diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 56ca400cf16..2627845ed40 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,4 +1,5 @@ import { + readCodexCliCredentialsCached, readQwenCliCredentialsCached, readMiniMaxCliCredentialsCached, } from "../cli-credentials.js"; @@ -11,6 +12,8 @@ import { } from "./constants.js"; import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js"; +const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; + function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { if (!a) { return false; @@ -37,7 +40,11 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu if (cred.type !== "oauth" && cred.type !== "token") { return false; } - if (cred.provider !== "qwen-portal" && cred.provider !== "minimax-portal") { + if ( + cred.provider !== "qwen-portal" && + cred.provider !== "minimax-portal" && + cred.provider !== "openai-codex" + ) { return false; } if (typeof cred.expires !== "number") { @@ -82,7 +89,8 @@ function syncExternalCliCredentialsForProvider( } /** - * Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI) into the store. + * Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI, Codex CLI) + * into the store. * * Returns true if any credentials were updated. */ @@ -130,6 +138,17 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean { ) { mutated = true; } + if ( + syncExternalCliCredentialsForProvider( + store, + OPENAI_CODEX_DEFAULT_PROFILE_ID, + "openai-codex", + () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }), + now, + ) + ) { + mutated = true; + } return mutated; } diff --git a/src/commands/doctor-auth.deprecated-cli-profiles.test.ts b/src/commands/doctor-auth.deprecated-cli-profiles.test.ts index d6436d7027a..ec7e824cd9e 100644 --- a/src/commands/doctor-auth.deprecated-cli-profiles.test.ts +++ b/src/commands/doctor-auth.deprecated-cli-profiles.test.ts @@ -63,6 +63,13 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => { refresh: "token-r2", expires: Date.now() + 60_000, }, + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + access: "token-c", + refresh: "token-r3", + expires: Date.now() + 60_000, + }, }, }, null, @@ -76,10 +83,11 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => { profiles: { "anthropic:claude-cli": { provider: "anthropic", mode: "oauth" }, "openai-codex:codex-cli": { provider: "openai-codex", mode: "oauth" }, + "openai-codex:default": { provider: "openai-codex", mode: "oauth" }, }, order: { anthropic: ["anthropic:claude-cli"], - "openai-codex": ["openai-codex:codex-cli"], + "openai-codex": ["openai-codex:codex-cli", "openai-codex:default"], }, }, } as const; @@ -94,10 +102,12 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => { }; expect(raw.profiles?.["anthropic:claude-cli"]).toBeUndefined(); expect(raw.profiles?.["openai-codex:codex-cli"]).toBeUndefined(); + expect(raw.profiles?.["openai-codex:default"]).toBeDefined(); expect(next.auth?.profiles?.["anthropic:claude-cli"]).toBeUndefined(); expect(next.auth?.profiles?.["openai-codex:codex-cli"]).toBeUndefined(); + expect(next.auth?.profiles?.["openai-codex:default"]).toBeDefined(); expect(next.auth?.order?.anthropic).toBeUndefined(); - expect(next.auth?.order?.["openai-codex"]).toBeUndefined(); + expect(next.auth?.order?.["openai-codex"]).toEqual(["openai-codex:default"]); }); });