fix: report codex chatgpt status auth (#91240)

This commit is contained in:
Yzx
2026-07-01 16:23:21 +08:00
committed by GitHub
parent ab754ceb38
commit 7b8f50d229
6 changed files with 188 additions and 0 deletions
+51
View File
@@ -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;
+11
View File
@@ -140,6 +140,14 @@ function resolveCodexHomePath(codexHome?: string) {
}
}
function codexAuthJsonUsesChatGptTokens(data: Record<string, unknown>): 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<string, unknown>;
if (!codexAuthJsonUsesChatGptTokens(data)) {
return null;
}
const tokens = data.tokens as Record<string, unknown> | undefined;
if (!tokens || typeof tokens !== "object") {
return null;
+33
View File
@@ -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,
+13
View File
@@ -33,6 +33,7 @@ export function resolveModelAuthLabel(params: {
sessionEntry?: Partial<Pick<SessionEntry, "authProfileOverride">>;
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,
@@ -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();
+17
View File
@@ -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<st
sessionKey,
sessionEntry,
}));
const codexCliCredentialsHome = resolveStatusCodexCliCredentialsHome({
agentDir: statusAgentDir,
effectiveHarness,
});
const selectedStatusProvider = resolveStatusRuntimeProvider({
provider: selectedLookupProvider,
effectiveHarness,
@@ -398,6 +413,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
sessionEntry,
agentDir: statusAgentDir,
workspaceDir: statusWorkspaceDir,
codexCliCredentialsHome,
includeExternalProfiles: false,
});
const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride")
@@ -410,6 +426,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
sessionEntry,
agentDir: statusAgentDir,
workspaceDir: statusWorkspaceDir,
codexCliCredentialsHome,
includeExternalProfiles: false,
})
: selectedModelAuth;