Skip to content

Commit 8c50340

Browse files
committed
fix: report codex chatgpt status auth
1 parent 797aab5 commit 8c50340

6 files changed

Lines changed: 188 additions & 0 deletions

File tree

src/agents/cli-credentials.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,57 @@ describe("cli credentials", () => {
448448
});
449449
});
450450

451+
it("does not read stale Codex tokens when auth.json resolves to API-key mode", () => {
452+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-api-key-mode-"));
453+
process.env.CODEX_HOME = tempHome;
454+
const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000);
455+
execSyncMock.mockImplementation(() => {
456+
throw new Error("not found");
457+
});
458+
459+
const authPath = path.join(tempHome, "auth.json");
460+
fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
461+
fs.writeFileSync(
462+
authPath,
463+
JSON.stringify({
464+
auth_mode: "apikey",
465+
OPENAI_API_KEY: "sk-codex-api-key",
466+
tokens: {
467+
access_token: createJwtWithExp(expSeconds),
468+
refresh_token: "stale-file-refresh",
469+
},
470+
}),
471+
"utf8",
472+
);
473+
474+
expect(readCodexCliCredentials({ platform: "linux", execSync: execSyncMock })).toBeNull();
475+
});
476+
477+
it("treats an empty Codex auth.json API-key field as API-key mode", () => {
478+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-empty-api-key-mode-"));
479+
process.env.CODEX_HOME = tempHome;
480+
const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000);
481+
execSyncMock.mockImplementation(() => {
482+
throw new Error("not found");
483+
});
484+
485+
const authPath = path.join(tempHome, "auth.json");
486+
fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
487+
fs.writeFileSync(
488+
authPath,
489+
JSON.stringify({
490+
OPENAI_API_KEY: "",
491+
tokens: {
492+
access_token: createJwtWithExp(expSeconds),
493+
refresh_token: "stale-file-refresh",
494+
},
495+
}),
496+
"utf8",
497+
);
498+
499+
expect(readCodexCliCredentials({ platform: "linux", execSync: execSyncMock })).toBeNull();
500+
});
501+
451502
it("rejects Codex auth.json fallback expiry when stat and process clock are invalid", () => {
452503
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-invalid-clock-"));
453504
process.env.CODEX_HOME = tempHome;

src/agents/cli-credentials.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ function resolveCodexHomePath(codexHome?: string) {
154154
}
155155
}
156156

157+
function codexAuthJsonUsesChatGptTokens(data: Record<string, unknown>): boolean {
158+
const authMode = typeof data.auth_mode === "string" ? data.auth_mode.toLowerCase() : undefined;
159+
if (authMode) {
160+
return authMode === "chatgpt" || authMode === "chatgptauthtokens";
161+
}
162+
return typeof data.OPENAI_API_KEY !== "string";
163+
}
164+
157165
function resolveMiniMaxCliCredentialsPath(homeDir?: string) {
158166
const baseDir = homeDir ?? resolveUserPath("~");
159167
return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH);
@@ -649,6 +657,9 @@ export function readCodexCliCredentials(options?: {
649657
}
650658

651659
const data = raw as Record<string, unknown>;
660+
if (!codexAuthJsonUsesChatGptTokens(data)) {
661+
return null;
662+
}
652663
const tokens = data.tokens as Record<string, unknown> | undefined;
653664
if (!tokens || typeof tokens !== "object") {
654665
return null;

src/agents/model-auth-label.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,39 @@ describe("resolveModelAuthLabel", () => {
192192
});
193193
});
194194

195+
it("uses Codex CLI auth for Codex-backed OpenAI before env fallback", () => {
196+
mocks.ensureAuthProfileStore.mockReturnValue({
197+
version: 1,
198+
profiles: {},
199+
} as never);
200+
mocks.resolveAuthProfileOrder.mockReturnValue([]);
201+
mocks.readCodexCliCredentialsCached.mockReturnValue({
202+
type: "oauth",
203+
provider: "openai",
204+
access: "token",
205+
refresh: "refresh",
206+
expires: Date.now() + 60_000,
207+
});
208+
mocks.resolveEnvApiKey.mockReturnValue({
209+
apiKey: "env-key-placeholder",
210+
source: "env: OPENAI_API_KEY",
211+
});
212+
213+
const label = resolveModelAuthLabel({
214+
provider: "openai",
215+
cfg: {},
216+
codexCliCredentialsHome: "/tmp/openclaw-agent/codex-home",
217+
});
218+
219+
expect(label).toBe("oauth (codex-cli)");
220+
expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith({
221+
codexHome: "/tmp/openclaw-agent/codex-home",
222+
ttlMs: 5_000,
223+
allowKeychainPrompt: false,
224+
});
225+
expect(mocks.resolveEnvApiKey).not.toHaveBeenCalled();
226+
});
227+
195228
it("shows claude cli auth for claude-cli provider without auth profiles", () => {
196229
mocks.ensureAuthProfileStore.mockReturnValue({
197230
version: 1,

src/agents/model-auth-label.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function resolveModelAuthLabel(params: {
3333
sessionEntry?: Partial<Pick<SessionEntry, "authProfileOverride">>;
3434
agentDir?: string;
3535
workspaceDir?: string;
36+
codexCliCredentialsHome?: string;
3637
includeExternalProfiles?: boolean;
3738
acceptedProviderIds?: readonly string[];
3839
}): string | undefined {
@@ -118,6 +119,18 @@ export function resolveModelAuthLabel(params: {
118119
return "unknown";
119120
}
120121

122+
if (
123+
params.codexCliCredentialsHome &&
124+
(providerKey === "openai" || providerKey === "codex") &&
125+
readCodexCliCredentialsCached({
126+
codexHome: params.codexCliCredentialsHome,
127+
ttlMs: 5_000,
128+
allowKeychainPrompt: false,
129+
})
130+
) {
131+
return "oauth (codex-cli)";
132+
}
133+
121134
const envKey = resolveEnvApiKey(providerKey, process.env, {
122135
config: params.cfg,
123136
workspaceDir: params.workspaceDir,

src/auto-reply/reply/commands-status.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,69 @@ describe("buildStatusReply subagent summary", () => {
791791
);
792792
});
793793

794+
it("uses the Codex app-server account before OpenAI env labels on Codex harness status", async () => {
795+
registerStatusCodexHarness();
796+
797+
await withTempHome(
798+
async (dir) => {
799+
const agentDir = path.join(dir, ".openclaw", "agents", "main", "agent");
800+
const codexHome = path.join(agentDir, "codex-home");
801+
fs.mkdirSync(codexHome, { recursive: true });
802+
fs.writeFileSync(
803+
path.join(codexHome, "auth.json"),
804+
JSON.stringify({
805+
auth_mode: "chatgpt",
806+
tokens: {
807+
access_token: "codex-access-token",
808+
refresh_token: "codex-refresh-token",
809+
},
810+
}),
811+
"utf-8",
812+
);
813+
814+
const text = await buildStatusText({
815+
cfg: {
816+
...baseCfg,
817+
agents: {
818+
defaults: {
819+
agentRuntime: { id: "codex" },
820+
},
821+
},
822+
},
823+
sessionEntry: {
824+
sessionId: "sess-status-codex-home-oauth",
825+
updatedAt: 0,
826+
},
827+
sessionKey: "agent:main:main",
828+
parentSessionKey: "agent:main:main",
829+
sessionScope: "per-sender",
830+
statusChannel: "mobilechat",
831+
provider: "openai",
832+
model: "gpt-5.5",
833+
contextTokens: 32_000,
834+
resolvedFastMode: false,
835+
resolvedVerboseLevel: "off",
836+
resolvedReasoningLevel: "off",
837+
resolveDefaultThinkingLevel: async () => undefined,
838+
isGroup: false,
839+
defaultGroupActivation: () => "mention",
840+
});
841+
842+
const normalized = normalizeTestText(text);
843+
expect(normalized).toContain("Model: openai/gpt-5.5");
844+
expect(normalized).toContain("Runtime: OpenAI Codex");
845+
expect(normalized).toContain("oauth (codex-cli)");
846+
expect(normalized).not.toContain("api-key (env: OPENAI_API_KEY)");
847+
},
848+
{
849+
env: {
850+
OPENAI_API_KEY: "status-env-key-placeholder",
851+
OPENAI_OAUTH_TOKEN: undefined,
852+
},
853+
},
854+
);
855+
});
856+
794857
it("uses Codex usage for bare codex models running on the Codex harness", async () => {
795858
registerStatusCodexHarness();
796859

src/status/status-text.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Status text helpers render runtime status summaries for CLI output.
22
import os from "node:os";
3+
import path from "node:path";
34
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
45
import {
56
resolveAgentConfig,
@@ -57,6 +58,7 @@ const USAGE_OAUTH_ONLY_PROVIDERS = new Set([
5758
"google-gemini-cli",
5859
"openai",
5960
]);
61+
const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home";
6062

6163
let statusMessageRuntimePromise: Promise<typeof import("../auto-reply/status.runtime.js")> | null =
6264
null;
@@ -249,6 +251,15 @@ function resolveStatusRuntimeProvider(params: {
249251
return params.provider;
250252
}
251253

254+
function resolveStatusCodexCliCredentialsHome(params: {
255+
agentDir: string;
256+
effectiveHarness?: string;
257+
}): string | undefined {
258+
return normalizeOptionalLowercaseString(params.effectiveHarness) === "codex"
259+
? path.join(params.agentDir, CODEX_APP_SERVER_HOME_DIRNAME)
260+
: undefined;
261+
}
262+
252263
function formatAgentTaskCountsLine(agentId: string): string | undefined {
253264
const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId));
254265
if (snapshot.totalCount === 0) {
@@ -313,6 +324,10 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
313324
sessionKey,
314325
sessionEntry,
315326
}));
327+
const codexCliCredentialsHome = resolveStatusCodexCliCredentialsHome({
328+
agentDir: statusAgentDir,
329+
effectiveHarness,
330+
});
316331
const selectedStatusProvider = resolveStatusRuntimeProvider({
317332
provider,
318333
effectiveHarness,
@@ -341,6 +356,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
341356
sessionEntry,
342357
agentDir: statusAgentDir,
343358
workspaceDir: statusWorkspaceDir,
359+
codexCliCredentialsHome,
344360
includeExternalProfiles: false,
345361
});
346362
const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride")
@@ -353,6 +369,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
353369
sessionEntry,
354370
agentDir: statusAgentDir,
355371
workspaceDir: statusWorkspaceDir,
372+
codexCliCredentialsHome,
356373
includeExternalProfiles: false,
357374
})
358375
: selectedModelAuth;

0 commit comments

Comments
 (0)