From bae7b54a85b17d180e4bfc28973a7e4fd1646a93 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 06:47:21 +0100 Subject: [PATCH] fix(agents): detect codex cli auth in status Fixes #70688. Co-authored-by: Jon Brown <801241+jb510@users.noreply.github.com> --- CHANGELOG.md | 1 + src/agents/model-auth-label.test.ts | 29 +++++++++++++++++++++++++++++ src/agents/model-auth-label.ts | 5 +++++ src/agents/model-auth.test.ts | 20 ++++++++++++++++++++ src/agents/model-auth.ts | 8 ++++++++ 5 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46cd5908e7..2d08b95b248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions. +- Codex status: report Codex CLI OAuth as `oauth (codex-cli)` for native `codex/*` sessions instead of showing unknown auth. Fixes #70688. Thanks @jb510. - Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman. - WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932. - Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921. diff --git a/src/agents/model-auth-label.test.ts b/src/agents/model-auth-label.test.ts index 6df2115725d..78b785fe514 100644 --- a/src/agents/model-auth-label.test.ts +++ b/src/agents/model-auth-label.test.ts @@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ resolveAuthProfileDisplayLabel: vi.fn(), resolveUsableCustomProviderApiKey: vi.fn(() => null), resolveEnvApiKey: vi.fn(() => null), + readCodexCliCredentialsCached: vi.fn<() => unknown>(() => null), })); vi.mock("./auth-profiles.js", () => ({ @@ -21,6 +22,10 @@ vi.mock("./model-auth.js", () => ({ resolveEnvApiKey: mocks.resolveEnvApiKey, })); +vi.mock("./cli-credentials.js", () => ({ + readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached, +})); + let resolveModelAuthLabel: typeof import("./model-auth-label.js").resolveModelAuthLabel; describe("resolveModelAuthLabel", () => { @@ -36,6 +41,8 @@ describe("resolveModelAuthLabel", () => { mocks.resolveUsableCustomProviderApiKey.mockReturnValue(null); mocks.resolveEnvApiKey.mockReset(); mocks.resolveEnvApiKey.mockReturnValue(null); + mocks.readCodexCliCredentialsCached.mockReset(); + mocks.readCodexCliCredentialsCached.mockReturnValue(null); }); it("does not include token value in label for token profiles", () => { @@ -112,6 +119,28 @@ describe("resolveModelAuthLabel", () => { expect(label).toBe("oauth (anthropic:oauth)"); }); + it("shows codex cli auth for codex provider without auth profiles", () => { + mocks.ensureAuthProfileStore.mockReturnValue({ + version: 1, + profiles: {}, + } as never); + mocks.resolveAuthProfileOrder.mockReturnValue([]); + mocks.readCodexCliCredentialsCached.mockReturnValue({ + type: "oauth", + provider: "openai-codex", + access: "token", + refresh: "refresh", + expires: Date.now() + 60_000, + }); + + const label = resolveModelAuthLabel({ + provider: "codex", + cfg: {}, + }); + + expect(label).toBe("oauth (codex-cli)"); + }); + it("can skip external auth profile overlays for status labels", () => { mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({ version: 1, diff --git a/src/agents/model-auth-label.ts b/src/agents/model-auth-label.ts index e8c9fa2a737..95502ddc928 100644 --- a/src/agents/model-auth-label.ts +++ b/src/agents/model-auth-label.ts @@ -6,6 +6,7 @@ import { resolveAuthProfileDisplayLabel, resolveAuthProfileOrder, } from "./auth-profiles.js"; +import { readCodexCliCredentialsCached } from "./cli-credentials.js"; import { resolveEnvApiKey, resolveUsableCustomProviderApiKey } from "./model-auth.js"; import { normalizeProviderId } from "./model-selection.js"; @@ -64,6 +65,10 @@ export function resolveModelAuthLabel(params: { return `api-key (${envKey.source})`; } + if (providerKey === "codex" && readCodexCliCredentialsCached({ ttlMs: 5_000 })) { + return "oauth (codex-cli)"; + } + const customKey = resolveUsableCustomProviderApiKey({ cfg: params.cfg, provider: providerKey, diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 9686c514a0f..beb7002ed5f 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -92,12 +92,14 @@ let resolveApiKeyForProvider: typeof import("./model-auth.js").resolveApiKeyForP let resolveAwsSdkEnvVarName: typeof import("./model-auth.js").resolveAwsSdkEnvVarName; let resolveModelAuthMode: typeof import("./model-auth.js").resolveModelAuthMode; let resolveUsableCustomProviderApiKey: typeof import("./model-auth.js").resolveUsableCustomProviderApiKey; +let cliCredentials: typeof import("./cli-credentials.js"); let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; let setRuntimeConfigSnapshot: typeof import("../config/config.js").setRuntimeConfigSnapshot; beforeAll(async () => { vi.resetModules(); ({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = await import("../config/config.js")); + cliCredentials = await import("./cli-credentials.js"); ({ applyAuthHeaderOverride, applyLocalNoAuthHeaderOverride, @@ -259,6 +261,24 @@ describe("resolveModelAuthMode", () => { "aws-sdk", ); }); + + it("returns oauth for codex when Codex CLI auth is available", () => { + const readCodexCliCredentialsCached = vi + .spyOn(cliCredentials, "readCodexCliCredentialsCached") + .mockReturnValue({ + type: "oauth", + provider: "openai-codex", + access: "token", + refresh: "refresh", + expires: Date.now() + 60_000, + }); + + try { + expect(resolveModelAuthMode("codex", undefined, { version: 1, profiles: {} })).toBe("oauth"); + } finally { + readCodexCliCredentialsCached.mockRestore(); + } + }); }); describe("requireApiKey", () => { diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 97dbcbb0d3e..934ff099fdf 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -27,6 +27,7 @@ import { resolveAuthProfileOrder, resolveAuthStorePathForDisplay, } from "./auth-profiles.js"; +import * as cliCredentials from "./cli-credentials.js"; import { resolveEnvApiKey, type EnvApiKeyResult } from "./model-auth-env.js"; import { CUSTOM_LOCAL_AUTH_MARKER, @@ -654,6 +655,13 @@ export function resolveModelAuthMode( return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; } + if ( + normalizeProviderId(resolved) === "codex" && + cliCredentials.readCodexCliCredentialsCached({ ttlMs: 5_000 }) + ) { + return "oauth"; + } + if (hasUsableCustomProviderApiKey(cfg, resolved)) { return "api-key"; }