mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix: report codex chatgpt status auth (#91240)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user