From 7b8f50d229fbcf9f977cf0488df403ce27a5b17d Mon Sep 17 00:00:00 2001 From: Yzx <53250620+849261680@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:23:21 +0800 Subject: [PATCH] fix: report codex chatgpt status auth (#91240) --- src/agents/cli-credentials.test.ts | 51 ++++++++++++++++ src/agents/cli-credentials.ts | 11 ++++ src/agents/model-auth-label.test.ts | 33 ++++++++++ src/agents/model-auth-label.ts | 13 ++++ src/auto-reply/reply/commands-status.test.ts | 63 ++++++++++++++++++++ src/status/status-text.ts | 17 ++++++ 6 files changed, 188 insertions(+) diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index d1da5e925c4f..777a7285cf3d 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -305,6 +305,57 @@ describe("cli credentials", () => { }); }); + it("does not read stale Codex tokens when auth.json resolves to API-key mode", () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-api-key-mode-")); + process.env.CODEX_HOME = tempHome; + const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000); + execSyncMock.mockImplementation(() => { + throw new Error("not found"); + }); + + const authPath = path.join(tempHome, "auth.json"); + fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 }); + fs.writeFileSync( + authPath, + JSON.stringify({ + auth_mode: "apikey", + OPENAI_API_KEY: "sk-codex-api-key", + tokens: { + access_token: createJwtWithExp(expSeconds), + refresh_token: "stale-file-refresh", + }, + }), + "utf8", + ); + + expect(readCodexCliCredentials({ platform: "linux", execSync: execSyncMock })).toBeNull(); + }); + + it("treats an empty Codex auth.json API-key field as API-key mode", () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-empty-api-key-mode-")); + process.env.CODEX_HOME = tempHome; + const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000); + execSyncMock.mockImplementation(() => { + throw new Error("not found"); + }); + + const authPath = path.join(tempHome, "auth.json"); + fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 }); + fs.writeFileSync( + authPath, + JSON.stringify({ + OPENAI_API_KEY: "", + tokens: { + access_token: createJwtWithExp(expSeconds), + refresh_token: "stale-file-refresh", + }, + }), + "utf8", + ); + + expect(readCodexCliCredentials({ platform: "linux", execSync: execSyncMock })).toBeNull(); + }); + it("rejects Codex auth.json fallback expiry when stat and process clock are invalid", () => { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-invalid-clock-")); process.env.CODEX_HOME = tempHome; diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index e81b6b8ff660..dc31c3813a89 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -140,6 +140,14 @@ function resolveCodexHomePath(codexHome?: string) { } } +function codexAuthJsonUsesChatGptTokens(data: Record): boolean { + const authMode = typeof data.auth_mode === "string" ? data.auth_mode.toLowerCase() : undefined; + if (authMode) { + return authMode === "chatgpt" || authMode === "chatgptauthtokens"; + } + return typeof data.OPENAI_API_KEY !== "string"; +} + function resolveMiniMaxCliCredentialsPath(homeDir?: string) { const baseDir = homeDir ?? resolveUserPath("~"); return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH); @@ -514,6 +522,9 @@ export function readCodexCliCredentials(options?: { } const data = raw as Record; + if (!codexAuthJsonUsesChatGptTokens(data)) { + return null; + } const tokens = data.tokens as Record | undefined; if (!tokens || typeof tokens !== "object") { return null; diff --git a/src/agents/model-auth-label.test.ts b/src/agents/model-auth-label.test.ts index 3f560742d708..28c3344638a8 100644 --- a/src/agents/model-auth-label.test.ts +++ b/src/agents/model-auth-label.test.ts @@ -192,6 +192,39 @@ describe("resolveModelAuthLabel", () => { }); }); + it("uses Codex CLI auth for Codex-backed OpenAI before env fallback", () => { + mocks.ensureAuthProfileStore.mockReturnValue({ + version: 1, + profiles: {}, + } as never); + mocks.resolveAuthProfileOrder.mockReturnValue([]); + mocks.readCodexCliCredentialsCached.mockReturnValue({ + type: "oauth", + provider: "openai", + access: "token", + refresh: "refresh", + expires: Date.now() + 60_000, + }); + mocks.resolveEnvApiKey.mockReturnValue({ + apiKey: "env-key-placeholder", + source: "env: OPENAI_API_KEY", + }); + + const label = resolveModelAuthLabel({ + provider: "openai", + cfg: {}, + codexCliCredentialsHome: "/tmp/openclaw-agent/codex-home", + }); + + expect(label).toBe("oauth (codex-cli)"); + expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith({ + codexHome: "/tmp/openclaw-agent/codex-home", + ttlMs: 5_000, + allowKeychainPrompt: false, + }); + expect(mocks.resolveEnvApiKey).not.toHaveBeenCalled(); + }); + it("shows claude cli auth for claude-cli provider without auth profiles", () => { mocks.ensureAuthProfileStore.mockReturnValue({ version: 1, diff --git a/src/agents/model-auth-label.ts b/src/agents/model-auth-label.ts index 90088c7e0b30..ea0ad87179bb 100644 --- a/src/agents/model-auth-label.ts +++ b/src/agents/model-auth-label.ts @@ -33,6 +33,7 @@ export function resolveModelAuthLabel(params: { sessionEntry?: Partial>; agentDir?: string; workspaceDir?: string; + codexCliCredentialsHome?: string; includeExternalProfiles?: boolean; acceptedProviderIds?: readonly string[]; }): string | undefined { @@ -118,6 +119,18 @@ export function resolveModelAuthLabel(params: { return "unknown"; } + if ( + params.codexCliCredentialsHome && + (providerKey === "openai" || providerKey === "codex") && + readCodexCliCredentialsCached({ + codexHome: params.codexCliCredentialsHome, + ttlMs: 5_000, + allowKeychainPrompt: false, + }) + ) { + return "oauth (codex-cli)"; + } + const envKey = resolveEnvApiKey(providerKey, process.env, { config: params.cfg, workspaceDir: params.workspaceDir, diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index 12be69723757..77593d1eddcd 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -963,6 +963,69 @@ describe("buildStatusReply subagent summary", () => { ); }); + it("uses the Codex app-server account before OpenAI env labels on Codex harness status", async () => { + registerStatusCodexHarness(); + + await withTempHome( + async (dir) => { + const agentDir = path.join(dir, ".openclaw", "agents", "main", "agent"); + const codexHome = path.join(agentDir, "codex-home"); + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync( + path.join(codexHome, "auth.json"), + JSON.stringify({ + auth_mode: "chatgpt", + tokens: { + access_token: "codex-access-token", + refresh_token: "codex-refresh-token", + }, + }), + "utf-8", + ); + + const text = await buildStatusText({ + cfg: { + ...baseCfg, + agents: { + defaults: { + agentRuntime: { id: "codex" }, + }, + }, + }, + sessionEntry: { + sessionId: "sess-status-codex-home-oauth", + updatedAt: 0, + }, + sessionKey: "agent:main:main", + parentSessionKey: "agent:main:main", + sessionScope: "per-sender", + statusChannel: "mobilechat", + provider: "openai", + model: "gpt-5.5", + contextTokens: 32_000, + resolvedFastMode: false, + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolveDefaultThinkingLevel: async () => undefined, + isGroup: false, + defaultGroupActivation: () => "mention", + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Model: openai/gpt-5.5"); + expect(normalized).toContain("Runtime: OpenAI Codex"); + expect(normalized).toContain("oauth (codex-cli)"); + expect(normalized).not.toContain("api-key (env: OPENAI_API_KEY)"); + }, + { + env: { + OPENAI_API_KEY: "status-env-key-placeholder", + OPENAI_OAUTH_TOKEN: undefined, + }, + }, + ); + }); + it("uses Codex usage for bare codex models running on the Codex harness", async () => { registerStatusCodexHarness(); diff --git a/src/status/status-text.ts b/src/status/status-text.ts index 1d3e69df2f7d..59779e08f2c1 100644 --- a/src/status/status-text.ts +++ b/src/status/status-text.ts @@ -1,5 +1,6 @@ // Status text helpers render runtime status summaries for CLI output. import os from "node:os"; +import path from "node:path"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { resolveAgentConfig, @@ -64,6 +65,7 @@ const USAGE_OAUTH_ONLY_PROVIDERS = new Set([ "google-gemini-cli", "openai", ]); +const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home"; function resolveStatusChannelFeatureLine(params: { cfg: OpenClawConfig; @@ -289,6 +291,15 @@ function resolveStatusRuntimeProvider(params: { return params.provider; } +function resolveStatusCodexCliCredentialsHome(params: { + agentDir: string; + effectiveHarness?: string; +}): string | undefined { + return normalizeOptionalLowercaseString(params.effectiveHarness) === "codex" + ? path.join(params.agentDir, CODEX_APP_SERVER_HOME_DIRNAME) + : undefined; +} + function formatAgentTaskCountsLine(agentId: string): string | undefined { const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId)); if (snapshot.totalCount === 0) { @@ -370,6 +381,10 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise