mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
Remove Codex CLI auth import
This commit is contained in:
@@ -33,9 +33,10 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard).
|
||||
- **Anthropic API key**: uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use.
|
||||
- **Anthropic API key**: preferred Anthropic assistant choice in onboarding/configure.
|
||||
- **Anthropic setup-token**: still available in onboarding/configure, though OpenClaw now prefers Claude CLI reuse when available.
|
||||
- **OpenAI Code (Codex) subscription (Codex CLI)**: if `~/.codex/auth.json` exists, onboarding can reuse it. Reused Codex CLI credentials stay managed by Codex CLI; on expiry OpenClaw re-reads that source first and, when the provider can refresh it, writes the refreshed credential back to Codex storage instead of taking ownership itself.
|
||||
- **OpenAI Code (Codex) subscription (OAuth)**: browser flow; paste the `code#state`.
|
||||
- Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`.
|
||||
- **OpenAI Code (Codex) subscription (device pairing)**: browser pairing flow with a short-lived device code.
|
||||
- Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`.
|
||||
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then stores it in auth profiles.
|
||||
- Sets `agents.defaults.model` to `openai/gpt-5.4` when model is unset, `openai/*`, or `openai-codex/*`.
|
||||
- **xAI (Grok) API key**: prompts for `XAI_API_KEY` and configures xAI as a model provider.
|
||||
|
||||
@@ -129,18 +129,17 @@ What you set:
|
||||
<Accordion title="Anthropic API key">
|
||||
Uses `ANTHROPIC_API_KEY` if present or prompts for a key, then saves it for daemon use.
|
||||
</Accordion>
|
||||
<Accordion title="OpenAI Code subscription (Codex CLI reuse)">
|
||||
If `~/.codex/auth.json` exists, the wizard can reuse it.
|
||||
Reused Codex CLI credentials stay managed by Codex CLI; on expiry OpenClaw
|
||||
re-reads that source first and, when the provider can refresh it, writes
|
||||
the refreshed credential back to Codex storage instead of taking ownership
|
||||
itself.
|
||||
</Accordion>
|
||||
<Accordion title="OpenAI Code subscription (OAuth)">
|
||||
Browser flow; paste `code#state`.
|
||||
|
||||
Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="OpenAI Code subscription (device pairing)">
|
||||
Browser pairing flow with a short-lived device code.
|
||||
|
||||
Sets `agents.defaults.model` to `openai-codex/gpt-5.4` when model is unset or `openai/*`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="OpenAI API key">
|
||||
Uses `OPENAI_API_KEY` if present or prompts for a key, then stores the credential in auth profiles.
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
CLI_FRESH_WATCHDOG_DEFAULTS,
|
||||
CLI_RESUME_WATCHDOG_DEFAULTS,
|
||||
} from "openclaw/plugin-sdk/cli-backend";
|
||||
import { OPENAI_CODEX_DEFAULT_PROFILE_ID } from "./openai-codex-cli-auth.js";
|
||||
import { prepareOpenAICodexCliExecution } from "./openai-codex-cli-bridge.js";
|
||||
|
||||
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
|
||||
const CODEX_CLI_DEFAULT_MODEL_REF = "codex-cli/gpt-5.4";
|
||||
|
||||
export function buildOpenAICodexCliBackend(): CliBackendPlugin {
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtimeMocks = vi.hoisted(() => ({
|
||||
debug: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
debug: runtimeMocks.debug,
|
||||
}),
|
||||
}));
|
||||
|
||||
import {
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
hasOpenAICodexCliOAuthCredential,
|
||||
readOpenAICodexCliOAuthProfile,
|
||||
} from "./openai-codex-cli-auth.js";
|
||||
|
||||
function buildJwt(payload: Record<string, unknown>) {
|
||||
const encode = (value: Record<string, unknown>) =>
|
||||
Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
return `${encode({ alg: "none", typ: "JWT" })}.${encode(payload)}.sig`;
|
||||
}
|
||||
|
||||
function mockCodexCliChatGptAuth(params?: {
|
||||
email?: string;
|
||||
accountId?: string;
|
||||
accessToken?: string;
|
||||
}) {
|
||||
const accessToken =
|
||||
params?.accessToken ??
|
||||
buildJwt({
|
||||
exp: Math.floor(Date.now() / 1000) + 600,
|
||||
"https://api.openai.com/profile": {
|
||||
email: params?.email ?? "codex@example.com",
|
||||
},
|
||||
});
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: params?.accountId ?? "acct_123",
|
||||
},
|
||||
}),
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reads Codex CLI chatgpt auth into the default OpenAI Codex profile", () => {
|
||||
const accessToken = mockCodexCliChatGptAuth();
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
expect(parsed?.credential.expires).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("detects an existing Codex CLI chatgpt login for setup labeling", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hasOpenAICodexCliOAuthCredential()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not override a locally managed OpenAI Codex profile", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "local-access",
|
||||
refresh: "local-refresh",
|
||||
expires: Date.now() + 10 * 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
it("does not override explicit local non-oauth auth with Codex CLI bootstrap", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "api_key",
|
||||
provider: "openai-codex",
|
||||
key: "sk-local",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
it("refuses Codex CLI bootstrap when an expired local default belongs to a different account", () => {
|
||||
mockCodexCliChatGptAuth({
|
||||
email: "codex-b@example.com",
|
||||
accountId: "acct_b",
|
||||
});
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "near-expiry-local-access",
|
||||
refresh: "near-expiry-local-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct_a",
|
||||
email: "codex-a@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
it("allows cli bootstrap when the stored default profile is expired", () => {
|
||||
const accessToken = mockCodexCliChatGptAuth();
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-local-access",
|
||||
refresh: "expired-local-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct_123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: {
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses cli bootstrap when the stored default profile is expired but identity mismatches", () => {
|
||||
mockCodexCliChatGptAuth();
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-local-access",
|
||||
refresh: "expired-local-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct_local",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
it("allows the runtime-only Codex CLI profile when the stored default already matches", () => {
|
||||
const accessToken = mockCodexCliChatGptAuth();
|
||||
|
||||
const firstParse = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
expect(firstParse).not.toBeNull();
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: firstParse!.credential,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: {
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null without logging when the Codex CLI auth file is missing", () => {
|
||||
const error = Object.assign(new Error("missing"), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
expect(runtimeMocks.debug).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs a sanitized code for invalid auth JSON", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue("{");
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
expect(runtimeMocks.debug).toHaveBeenCalledWith(
|
||||
"Failed to read Codex CLI auth file (code=INVALID_JSON)",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not leak auth file paths in debug logs for filesystem failures", () => {
|
||||
const error = Object.assign(
|
||||
new Error("EACCES: permission denied, open '/Users/alice/.codex/auth.json'"),
|
||||
{
|
||||
code: "EACCES",
|
||||
},
|
||||
);
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
expect(runtimeMocks.debug).toHaveBeenCalledWith(
|
||||
"Failed to read Codex CLI auth file (code=EACCES)",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,187 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
hasUsableOAuthCredential,
|
||||
resolveRequiredHomeDir,
|
||||
type AuthProfileStore,
|
||||
type OAuthCredential,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
resolveCodexAccessTokenExpiry,
|
||||
resolveCodexAuthIdentity,
|
||||
} from "./openai-codex-auth-identity.js";
|
||||
import { trimNonEmptyString } from "./openai-codex-shared.js";
|
||||
|
||||
const PROVIDER_ID = "openai-codex";
|
||||
const log = createSubsystemLogger("openai/codex-cli-auth");
|
||||
|
||||
export const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`;
|
||||
export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`;
|
||||
|
||||
type CodexCliAuthFile = {
|
||||
auth_mode?: unknown;
|
||||
tokens?: {
|
||||
id_token?: unknown;
|
||||
access_token?: unknown;
|
||||
refresh_token?: unknown;
|
||||
account_id?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function resolveCodexCliHome(env: NodeJS.ProcessEnv): string {
|
||||
const configured = trimNonEmptyString(env.CODEX_HOME);
|
||||
if (!configured) {
|
||||
return path.join(resolveRequiredHomeDir(), ".codex");
|
||||
}
|
||||
if (configured === "~") {
|
||||
return resolveRequiredHomeDir();
|
||||
}
|
||||
if (configured.startsWith("~/")) {
|
||||
return path.join(resolveRequiredHomeDir(), configured.slice(2));
|
||||
}
|
||||
return path.resolve(configured);
|
||||
}
|
||||
|
||||
function readCodexCliAuthFile(env: NodeJS.ProcessEnv): CodexCliAuthFile | null {
|
||||
try {
|
||||
const authPath = path.join(resolveCodexCliHome(env), "auth.json");
|
||||
const raw = fs.readFileSync(authPath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === "object" ? (parsed as CodexCliAuthFile) : null;
|
||||
} catch (error) {
|
||||
const code =
|
||||
error instanceof SyntaxError
|
||||
? "INVALID_JSON"
|
||||
: error instanceof Error && "code" in error
|
||||
? (error as NodeJS.ErrnoException).code
|
||||
: undefined;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
log.debug(
|
||||
`Failed to read Codex CLI auth file (code=${typeof code === "string" ? code : "UNKNOWN"})`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasOpenAICodexCliOAuthCredential(params?: { env?: NodeJS.ProcessEnv }): boolean {
|
||||
const authFile = readCodexCliAuthFile(params?.env ?? process.env);
|
||||
if (!authFile || authFile.auth_mode !== "chatgpt") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
trimNonEmptyString(authFile.tokens?.access_token) &&
|
||||
trimNonEmptyString(authFile.tokens?.refresh_token),
|
||||
);
|
||||
}
|
||||
|
||||
function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean {
|
||||
return (
|
||||
a.type === b.type &&
|
||||
a.provider === b.provider &&
|
||||
a.access === b.access &&
|
||||
a.refresh === b.refresh &&
|
||||
a.clientId === b.clientId &&
|
||||
a.email === b.email &&
|
||||
a.displayName === b.displayName &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId &&
|
||||
a.idToken === b.idToken
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAuthIdentityToken(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeAuthEmailToken(value: string | undefined): string | undefined {
|
||||
return normalizeAuthIdentityToken(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
function hasIdentityContinuity(
|
||||
existing: Pick<OAuthCredential, "accountId" | "email"> | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing) {
|
||||
return true;
|
||||
}
|
||||
if (oauthCredentialMatches(existing as OAuthCredential, incoming)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const existingAccountId = normalizeAuthIdentityToken(existing.accountId);
|
||||
const incomingAccountId = normalizeAuthIdentityToken(incoming.accountId);
|
||||
if (existingAccountId !== undefined && incomingAccountId !== undefined) {
|
||||
return existingAccountId === incomingAccountId;
|
||||
}
|
||||
|
||||
const existingEmail = normalizeAuthEmailToken(existing.email);
|
||||
const incomingEmail = normalizeAuthEmailToken(incoming.email);
|
||||
if (existingEmail !== undefined && incomingEmail !== undefined) {
|
||||
return existingEmail === incomingEmail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function readOpenAICodexCliOAuthProfile(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
store: AuthProfileStore;
|
||||
}): { profileId: string; credential: OAuthCredential } | null {
|
||||
const authFile = readCodexCliAuthFile(params.env ?? process.env);
|
||||
if (!authFile || authFile.auth_mode !== "chatgpt") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const access = trimNonEmptyString(authFile.tokens?.access_token);
|
||||
const refresh = trimNonEmptyString(authFile.tokens?.refresh_token);
|
||||
if (!access || !refresh) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountId = trimNonEmptyString(authFile.tokens?.account_id);
|
||||
const idToken = trimNonEmptyString(authFile.tokens?.id_token);
|
||||
const identity = resolveCodexAuthIdentity({ accessToken: access });
|
||||
const credential: OAuthCredential = {
|
||||
type: "oauth",
|
||||
provider: PROVIDER_ID,
|
||||
access,
|
||||
refresh,
|
||||
expires: resolveCodexAccessTokenExpiry(access) ?? 0,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...(idToken ? { idToken } : {}),
|
||||
...(identity.email ? { email: identity.email } : {}),
|
||||
...(identity.profileName ? { displayName: identity.profileName } : {}),
|
||||
};
|
||||
const existing = params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
|
||||
const existingOAuth =
|
||||
existing?.type === "oauth" && existing.provider === PROVIDER_ID ? existing : undefined;
|
||||
if (existing && !existingOAuth) {
|
||||
log.debug("kept explicit local auth over Codex CLI bootstrap", {
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
localType: existing.type,
|
||||
localProvider: existing.provider,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!hasIdentityContinuity(existingOAuth, credential)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
existingOAuth &&
|
||||
hasUsableOAuthCredential(existingOAuth) &&
|
||||
!oauthCredentialMatches(existingOAuth, credential)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential,
|
||||
};
|
||||
}
|
||||
@@ -1,32 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn());
|
||||
const readOpenAICodexCliOAuthProfileMock = vi.hoisted(() => vi.fn());
|
||||
const hasOpenAICodexCliOAuthCredentialMock = vi.hoisted(() => vi.fn());
|
||||
const loginOpenAICodexDeviceCodeMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./openai-codex-provider.runtime.js", () => ({
|
||||
refreshOpenAICodexToken: refreshOpenAICodexTokenMock,
|
||||
}));
|
||||
|
||||
vi.mock("./openai-codex-cli-auth.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./openai-codex-cli-auth.js")>();
|
||||
return {
|
||||
...actual,
|
||||
hasOpenAICodexCliOAuthCredential: hasOpenAICodexCliOAuthCredentialMock,
|
||||
readOpenAICodexCliOAuthProfile: readOpenAICodexCliOAuthProfileMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./openai-codex-device-code.js", () => ({
|
||||
loginOpenAICodexDeviceCode: loginOpenAICodexDeviceCodeMock,
|
||||
}));
|
||||
|
||||
let buildOpenAICodexProviderPlugin: typeof import("./openai-codex-provider.js").buildOpenAICodexProviderPlugin;
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createCodexTemplate(overrides: {
|
||||
id?: string;
|
||||
@@ -67,18 +52,9 @@ describe("openai codex provider", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
refreshOpenAICodexTokenMock.mockReset();
|
||||
readOpenAICodexCliOAuthProfileMock.mockReset();
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReset();
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReturnValue(false);
|
||||
loginOpenAICodexDeviceCodeMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the cached credential when accountId extraction fails", async () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const credential = {
|
||||
@@ -149,14 +125,10 @@ describe("openai codex provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("offers OpenAI menu auth methods for login, import, and device pairing", () => {
|
||||
it("offers OpenAI menu auth methods for browser login and device pairing", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(provider.auth?.map((method) => method.id)).toEqual([
|
||||
"oauth",
|
||||
"device-code",
|
||||
"import-codex-cli",
|
||||
]);
|
||||
expect(provider.auth?.map((method) => method.id)).toEqual(["oauth", "device-code"]);
|
||||
expect(provider.auth?.find((method) => method.id === "oauth")).toMatchObject({
|
||||
label: "OpenAI Codex Browser Login",
|
||||
hint: "Sign in with OpenAI in your browser",
|
||||
@@ -176,65 +148,6 @@ describe("openai codex provider", () => {
|
||||
assistantPriority: -10,
|
||||
},
|
||||
});
|
||||
expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({
|
||||
label: "Import Existing Codex Login",
|
||||
hint: "Import an existing ~/.codex login",
|
||||
kind: "oauth",
|
||||
wizard: {
|
||||
choiceId: "openai-codex-import",
|
||||
choiceLabel: "Import Existing Codex Login",
|
||||
assistantPriority: -20,
|
||||
assistantVisibility: "manual-only",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("annotates the import option when ~/.codex auth is detected", () => {
|
||||
hasOpenAICodexCliOAuthCredentialMock.mockReturnValueOnce(true);
|
||||
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(provider.auth?.find((method) => method.id === "import-codex-cli")).toMatchObject({
|
||||
label: "Import Existing Codex Login (~/.codex detected)",
|
||||
wizard: {
|
||||
choiceLabel: "Import Existing Codex Login (~/.codex detected)",
|
||||
assistantVisibility: "visible",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("soft-fails import when no compatible ~/.codex login exists", async () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli");
|
||||
const note = vi.fn(async () => {});
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce(null);
|
||||
|
||||
const result = await importMethod?.run({
|
||||
config: {},
|
||||
env: process.env,
|
||||
prompter: {
|
||||
note,
|
||||
progress: vi.fn(),
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
oauth: { createVpsAwareHandlers: (() => ({})) as never },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ profiles: [] });
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead.",
|
||||
);
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead.",
|
||||
"Import Existing Codex Login",
|
||||
);
|
||||
});
|
||||
|
||||
it("stores device-code logins as OpenAI Codex oauth profiles", async () => {
|
||||
@@ -343,89 +256,6 @@ describe("openai codex provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes Codex CLI auth as a runtime-only external profile", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const credential = {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
};
|
||||
readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce({
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.resolveExternalAuthProfiles?.({
|
||||
env: { CODEX_HOME: "/sandboxed/codex-home" } as NodeJS.ProcessEnv,
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
persistence: "runtime-only",
|
||||
},
|
||||
]);
|
||||
expect(readOpenAICodexCliOAuthProfileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({ CODEX_HOME: "/sandboxed/codex-home" }),
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the provider auth context env when importing Codex CLI auth", async () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli");
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-openai-codex-provider-"));
|
||||
tempDirs.push(agentDir);
|
||||
readOpenAICodexCliOAuthProfileMock.mockImplementationOnce(({ env }) => {
|
||||
expect(env).toMatchObject({
|
||||
CODEX_HOME: "/sandboxed/codex-home",
|
||||
});
|
||||
return {
|
||||
profileId: "openai-codex:default",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
email: "codex@example.com",
|
||||
displayName: "Codex User",
|
||||
accountId: "acct-123",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await expect(
|
||||
importMethod?.run({
|
||||
config: {},
|
||||
env: { CODEX_HOME: "/sandboxed/codex-home" },
|
||||
agentDir,
|
||||
prompter: {} as never,
|
||||
runtime: {} as never,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
oauth: { createVpsAwareHandlers: (() => ({})) as never },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: expect.objectContaining({
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("owns native reasoning output mode for Codex responses", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
listProfilesForProvider,
|
||||
type OAuthCredential,
|
||||
type ProviderAuthResult,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { loginOpenAICodexOAuth } from "openclaw/plugin-sdk/provider-auth-login";
|
||||
@@ -24,11 +23,6 @@ import { isOpenAIApiBaseUrl, isOpenAICodexBaseUrl } from "./base-url.js";
|
||||
import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js";
|
||||
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
|
||||
import { buildOpenAICodexProvider } from "./openai-codex-catalog.js";
|
||||
import {
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
hasOpenAICodexCliOAuthCredential,
|
||||
readOpenAICodexCliOAuthProfile,
|
||||
} from "./openai-codex-cli-auth.js";
|
||||
import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js";
|
||||
import {
|
||||
buildOpenAIResponsesProviderHooks,
|
||||
@@ -45,14 +39,11 @@ const OPENAI_WIZARD_GROUP = {
|
||||
groupLabel: "OpenAI",
|
||||
groupHint: "API key + Codex auth",
|
||||
} as const;
|
||||
const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`;
|
||||
const OPENAI_CODEX_LOGIN_ASSISTANT_PRIORITY = -30;
|
||||
const OPENAI_CODEX_IMPORT_ASSISTANT_PRIORITY = -20;
|
||||
const OPENAI_CODEX_DEVICE_PAIRING_ASSISTANT_PRIORITY = -10;
|
||||
const OPENAI_CODEX_LOGIN_LABEL = "OpenAI Codex Browser Login";
|
||||
const OPENAI_CODEX_LOGIN_HINT = "Sign in with OpenAI in your browser";
|
||||
const OPENAI_CODEX_IMPORT_LABEL = "Import Existing Codex Login";
|
||||
const OPENAI_CODEX_IMPORT_HINT = "Import an existing ~/.codex login";
|
||||
const OPENAI_CODEX_IMPORT_DETECTED_SUFFIX = "~/.codex detected";
|
||||
const OPENAI_CODEX_DEVICE_PAIRING_LABEL = "OpenAI Codex Device Pairing";
|
||||
const OPENAI_CODEX_DEVICE_PAIRING_HINT = "Pair in browser with a device code";
|
||||
const OPENAI_CODEX_GPT_54_MODEL_ID = "gpt-5.4";
|
||||
@@ -371,53 +362,6 @@ async function runOpenAICodexDeviceCode(ctx: ProviderAuthContext) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) {
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env ?? process.env,
|
||||
store: ensureAuthProfileStoreForLocalUpdate(ctx.agentDir),
|
||||
});
|
||||
if (!profile) {
|
||||
const message =
|
||||
"No compatible ~/.codex ChatGPT login found. Use Browser Login or Device Pairing instead.";
|
||||
ctx.runtime.error(message);
|
||||
await ctx.prompter.note(message, OPENAI_CODEX_IMPORT_LABEL);
|
||||
return { profiles: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
profiles: [{ profileId: profile.profileId, credential: profile.credential }],
|
||||
configPatch: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
[OPENAI_CODEX_DEFAULT_MODEL]: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
notes: ["Imported existing Codex CLI login into OpenClaw canonical auth."],
|
||||
} satisfies ProviderAuthResult;
|
||||
}
|
||||
|
||||
function ensureOpenAICodexCatalogAuthStore(ctx: { agentDir?: string; env?: NodeJS.ProcessEnv }) {
|
||||
const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir);
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env ?? process.env,
|
||||
store,
|
||||
});
|
||||
if (!profile) {
|
||||
return store;
|
||||
}
|
||||
return {
|
||||
...store,
|
||||
profiles: {
|
||||
...store.profiles,
|
||||
[profile.profileId]: profile.credential,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) {
|
||||
if (ctx.profileId !== CODEX_CLI_PROFILE_ID) {
|
||||
return undefined;
|
||||
@@ -425,16 +369,7 @@ function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) {
|
||||
return "Deprecated profile. Run `openclaw models auth login --provider openai-codex` or `openclaw configure`.";
|
||||
}
|
||||
|
||||
function buildOpenAICodexImportWizardLabel(hasCodexCliCredential: boolean) {
|
||||
if (!hasCodexCliCredential) {
|
||||
return OPENAI_CODEX_IMPORT_LABEL;
|
||||
}
|
||||
return `${OPENAI_CODEX_IMPORT_LABEL} (${OPENAI_CODEX_IMPORT_DETECTED_SUFFIX})`;
|
||||
}
|
||||
|
||||
export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
const hasCodexCliCredential = hasOpenAICodexCliOAuthCredential();
|
||||
const importWizardLabel = buildOpenAICodexImportWizardLabel(hasCodexCliCredential);
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: "OpenAI Codex",
|
||||
@@ -474,26 +409,11 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "import-codex-cli",
|
||||
label: importWizardLabel,
|
||||
hint: OPENAI_CODEX_IMPORT_HINT,
|
||||
kind: "oauth",
|
||||
wizard: {
|
||||
choiceId: "openai-codex-import",
|
||||
choiceLabel: importWizardLabel,
|
||||
choiceHint: OPENAI_CODEX_IMPORT_HINT,
|
||||
assistantPriority: OPENAI_CODEX_IMPORT_ASSISTANT_PRIORITY,
|
||||
assistantVisibility: hasCodexCliCredential ? "visible" : "manual-only",
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
run: async (ctx) => await runImportOpenAICodexCliAuth(ctx),
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
order: "profile",
|
||||
run: async (ctx) => {
|
||||
const authStore = ensureOpenAICodexCatalogAuthStore(ctx);
|
||||
const authStore = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir);
|
||||
if (listProfilesForProvider(authStore, PROVIDER_ID).length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -546,13 +466,6 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
fetchUsageSnapshot: async (ctx) =>
|
||||
await fetchCodexUsage(ctx.token, ctx.accountId, ctx.timeoutMs, ctx.fetchFn),
|
||||
refreshOAuth: async (cred) => await refreshOpenAICodexOAuthCredential(cred),
|
||||
resolveExternalAuthProfiles: (ctx) => {
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env,
|
||||
store: ctx.store,
|
||||
});
|
||||
return profile ? [{ ...profile, persistence: "runtime-only" }] : [];
|
||||
},
|
||||
augmentModelCatalog: (ctx) => {
|
||||
const gpt54Template = findCatalogTemplate({
|
||||
entries: ctx.entries,
|
||||
|
||||
@@ -41,20 +41,6 @@ export function createOpenAICodexProvider(): ProviderPlugin {
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "import-codex-cli",
|
||||
kind: "oauth",
|
||||
label: "Import Existing Codex Login",
|
||||
hint: "Import an existing ~/.codex login",
|
||||
run: noopAuth,
|
||||
wizard: {
|
||||
choiceId: "openai-codex-import",
|
||||
choiceLabel: "Import Existing Codex Login",
|
||||
choiceHint: "Import an existing ~/.codex login",
|
||||
assistantPriority: -20,
|
||||
...OPENAI_WIZARD_GROUP,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ describe("buildAuthChoiceOptions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders OpenAI auth methods as api key, login, import, then device pairing", () => {
|
||||
it("orders OpenAI auth methods as api key, browser login, then device pairing", () => {
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
value: "openai-api-key",
|
||||
@@ -463,13 +463,6 @@ describe("buildAuthChoiceOptions", () => {
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -30,
|
||||
},
|
||||
{
|
||||
value: "openai-codex-import",
|
||||
label: "Import Existing Codex Login (~/.codex detected)",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
assistantPriority: -20,
|
||||
},
|
||||
{
|
||||
value: "openai-codex-device-code",
|
||||
label: "OpenAI Codex Device Pairing",
|
||||
@@ -489,7 +482,6 @@ describe("buildAuthChoiceOptions", () => {
|
||||
expect(openAIGroup?.options.map((option) => option.value)).toEqual([
|
||||
"openai-api-key",
|
||||
"openai-codex",
|
||||
"openai-codex-import",
|
||||
"openai-codex-device-code",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user