From 07faab6ac32762519e324a65389b6e2c4b2a709b Mon Sep 17 00:00:00 2001 From: loiie45e Date: Fri, 13 Feb 2026 19:39:37 +0800 Subject: [PATCH] openai-codex: bridge OAuth profiles into pi auth.json for model discovery (#15184) --- src/agents/model-catalog.ts | 3 + src/agents/pi-auth-json.test.ts | 42 +++++++++++ src/agents/pi-auth-json.ts | 100 +++++++++++++++++++++++++++ src/commands/models/list.registry.ts | 2 + 4 files changed, 147 insertions(+) create mode 100644 src/agents/pi-auth-json.test.ts create mode 100644 src/agents/pi-auth-json.ts diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index 15862015564..c1c12db555d 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -91,6 +91,9 @@ export async function loadModelCatalog(params?: { try { const cfg = params?.config ?? loadConfig(); await ensureOpenClawModelsJson(cfg); + await ( + await import("./pi-auth-json.js") + ).ensurePiAuthJsonFromAuthProfiles(resolveOpenClawAgentDir()); // IMPORTANT: keep the dynamic import *inside* the try/catch. // If this fails once (e.g. during a pnpm install that temporarily swaps node_modules), // we must not poison the cache with a rejected promise (otherwise all channel handlers diff --git a/src/agents/pi-auth-json.test.ts b/src/agents/pi-auth-json.test.ts new file mode 100644 index 00000000000..e07a2840dc6 --- /dev/null +++ b/src/agents/pi-auth-json.test.ts @@ -0,0 +1,42 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { saveAuthProfileStore } from "./auth-profiles.js"; +import { ensurePiAuthJsonFromAuthProfiles } from "./pi-auth-json.js"; + +describe("ensurePiAuthJsonFromAuthProfiles", () => { + it("writes openai-codex oauth credentials into auth.json for pi-coding-agent discovery", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); + + saveAuthProfileStore( + { + version: 1, + profiles: { + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + }, + }, + }, + agentDir, + ); + + const first = await ensurePiAuthJsonFromAuthProfiles(agentDir); + expect(first.wrote).toBe(true); + + const authPath = path.join(agentDir, "auth.json"); + const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record; + expect(auth["openai-codex"]).toMatchObject({ + type: "oauth", + access: "access-token", + refresh: "refresh-token", + }); + + const second = await ensurePiAuthJsonFromAuthProfiles(agentDir); + expect(second.wrote).toBe(false); + }); +}); diff --git a/src/agents/pi-auth-json.ts b/src/agents/pi-auth-json.ts new file mode 100644 index 00000000000..c32abff1863 --- /dev/null +++ b/src/agents/pi-auth-json.ts @@ -0,0 +1,100 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; + +type AuthJsonCredential = + | { + type: "api_key"; + key: string; + } + | { + type: "oauth"; + access: string; + refresh: string; + expires: number; + [key: string]: unknown; + }; + +type AuthJsonShape = Record; + +async function readAuthJson(filePath: string): Promise { + try { + const raw = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(raw) as unknown; + if (!parsed || typeof parsed !== "object") { + return {}; + } + return parsed as AuthJsonShape; + } catch { + return {}; + } +} + +/** + * pi-coding-agent's ModelRegistry/AuthStorage expects OAuth credentials in auth.json. + * + * OpenClaw stores OAuth credentials in auth-profiles.json instead. This helper + * bridges a subset of credentials into agentDir/auth.json so pi-coding-agent can + * (a) consider the provider authenticated and (b) include built-in models in its + * registry/catalog output. + * + * Currently used for openai-codex. + */ +export async function ensurePiAuthJsonFromAuthProfiles(agentDir: string): Promise<{ + wrote: boolean; + authPath: string; +}> { + const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); + const codexProfiles = listProfilesForProvider(store, "openai-codex"); + if (codexProfiles.length === 0) { + return { wrote: false, authPath: path.join(agentDir, "auth.json") }; + } + + const profileId = codexProfiles[0]; + const cred = profileId ? store.profiles[profileId] : undefined; + if (!cred || cred.type !== "oauth") { + return { wrote: false, authPath: path.join(agentDir, "auth.json") }; + } + + const accessRaw = (cred as { access?: unknown }).access; + const refreshRaw = (cred as { refresh?: unknown }).refresh; + const expiresRaw = (cred as { expires?: unknown }).expires; + + const access = typeof accessRaw === "string" ? accessRaw.trim() : ""; + const refresh = typeof refreshRaw === "string" ? refreshRaw.trim() : ""; + const expires = typeof expiresRaw === "number" ? expiresRaw : Number.NaN; + + if (!access || !refresh || !Number.isFinite(expires) || expires <= 0) { + return { wrote: false, authPath: path.join(agentDir, "auth.json") }; + } + + const authPath = path.join(agentDir, "auth.json"); + const next = await readAuthJson(authPath); + + const existing = next["openai-codex"]; + const desired: AuthJsonCredential = { + type: "oauth", + access, + refresh, + expires, + }; + + const isSame = + existing && + typeof existing === "object" && + (existing as { type?: unknown }).type === "oauth" && + (existing as { access?: unknown }).access === access && + (existing as { refresh?: unknown }).refresh === refresh && + (existing as { expires?: unknown }).expires === expires; + + if (isSame) { + return { wrote: false, authPath }; + } + + next["openai-codex"] = desired; + + await fs.mkdir(agentDir, { recursive: true, mode: 0o700 }); + await fs.writeFile(authPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 }); + + return { wrote: true, authPath }; +} diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 00f14643c4d..a8ff5ded52a 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -10,6 +10,7 @@ import { resolveEnvApiKey, } from "../../agents/model-auth.js"; import { ensureOpenClawModelsJson } from "../../agents/models-config.js"; +import { ensurePiAuthJsonFromAuthProfiles } from "../../agents/pi-auth-json.js"; import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js"; import { modelKey } from "./shared.js"; @@ -48,6 +49,7 @@ const hasAuthForProvider = (provider: string, cfg: OpenClawConfig, authStore: Au export async function loadModelRegistry(cfg: OpenClawConfig) { await ensureOpenClawModelsJson(cfg); const agentDir = resolveOpenClawAgentDir(); + await ensurePiAuthJsonFromAuthProfiles(agentDir); const authStorage = discoverAuthStorage(agentDir); const registry = discoverModels(authStorage, agentDir); const models = registry.getAll();