diff --git a/src/agents/pi-auth-discovery-core.ts b/src/agents/pi-auth-discovery-core.ts new file mode 100644 index 00000000000..4b5499a45a7 --- /dev/null +++ b/src/agents/pi-auth-discovery-core.ts @@ -0,0 +1,72 @@ +import fs from "node:fs"; +import { isRecord } from "../utils.js"; +import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js"; +import { resolveEnvApiKey } from "./model-auth-env.js"; +import type { PiCredentialMap } from "./pi-auth-credentials.js"; + +export function addEnvBackedPiCredentials( + credentials: PiCredentialMap, + env: NodeJS.ProcessEnv = process.env, +): PiCredentialMap { + const next = { ...credentials }; + // pi-coding-agent hides providers from its registry when auth storage lacks + // a matching credential entry. Mirror env-backed provider auth here so + // live/model discovery sees the same providers runtime auth can use. + for (const provider of Object.keys(resolveProviderEnvApiKeyCandidates({ env }))) { + if (next[provider]) { + continue; + } + const resolved = resolveEnvApiKey(provider, env); + if (!resolved?.apiKey) { + continue; + } + next[provider] = { + type: "api_key", + key: resolved.apiKey, + }; + } + return next; +} + +export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string): void { + if (process.env.OPENCLAW_AUTH_STORE_READONLY === "1") { + return; + } + if (!fs.existsSync(pathname)) { + return; + } + + let parsed: unknown; + try { + parsed = JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown; + } catch { + return; + } + if (!isRecord(parsed)) { + return; + } + + let changed = false; + for (const [provider, value] of Object.entries(parsed)) { + if (!isRecord(value)) { + continue; + } + if (value.type !== "api_key") { + continue; + } + delete parsed[provider]; + changed = true; + } + + if (!changed) { + return; + } + + if (Object.keys(parsed).length === 0) { + fs.rmSync(pathname, { force: true }); + return; + } + + fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8"); + fs.chmodSync(pathname, 0o600); +} diff --git a/src/agents/pi-auth-discovery.ts b/src/agents/pi-auth-discovery.ts new file mode 100644 index 00000000000..a2643cb696d --- /dev/null +++ b/src/agents/pi-auth-discovery.ts @@ -0,0 +1,50 @@ +import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js"; +import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js"; +import { + ensureAuthProfileStore, + loadAuthProfileStoreForSecretsRuntime, +} from "./auth-profiles/store.js"; +import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; +import { addEnvBackedPiCredentials } from "./pi-auth-discovery-core.js"; + +export type DiscoverAuthStorageOptions = { + readOnly?: boolean; +}; + +export function resolvePiCredentialsForDiscovery( + agentDir: string, + options?: DiscoverAuthStorageOptions, +): PiCredentialMap { + const store = + options?.readOnly === true + ? loadAuthProfileStoreForSecretsRuntime(agentDir) + : ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); + const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store)); + for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) { + if (credentials[provider]) { + continue; + } + const resolved = resolveProviderSyntheticAuthWithPlugin({ + provider, + context: { + config: undefined, + provider, + providerConfig: undefined, + }, + }); + const apiKey = resolved?.apiKey?.trim(); + if (!apiKey) { + continue; + } + credentials[provider] = { + type: "api_key", + key: apiKey, + }; + } + return credentials; +} + +export { + addEnvBackedPiCredentials, + scrubLegacyStaticAuthJsonEntriesForDiscovery, +} from "./pi-auth-discovery-core.js"; diff --git a/src/agents/pi-model-discovery.auth.test.ts b/src/agents/pi-model-discovery.auth.test.ts index dd187ffff61..7e656167601 100644 --- a/src/agents/pi-model-discovery.auth.test.ts +++ b/src/agents/pi-model-discovery.auth.test.ts @@ -6,7 +6,7 @@ import { resolvePiCredentialMapFromStore } from "./pi-auth-credentials.js"; import { addEnvBackedPiCredentials, scrubLegacyStaticAuthJsonEntriesForDiscovery, -} from "./pi-model-discovery.js"; +} from "./pi-auth-discovery-core.js"; vi.mock("./model-auth-env-vars.js", () => ({ resolveProviderEnvApiKeyCandidates: () => ({ diff --git a/src/agents/pi-model-discovery.synthetic-auth.test.ts b/src/agents/pi-model-discovery.synthetic-auth.test.ts index 207ba6b7d19..b5b987b9e87 100644 --- a/src/agents/pi-model-discovery.synthetic-auth.test.ts +++ b/src/agents/pi-model-discovery.synthetic-auth.test.ts @@ -2,7 +2,6 @@ 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 { saveAuthProfileStore } from "./auth-profiles/store.js"; const resolveRuntimeSyntheticAuthProviderRefs = vi.hoisted(() => vi.fn(() => ["claude-cli"])); @@ -30,7 +29,7 @@ vi.mock("../plugins/provider-runtime.js", () => ({ resolveExternalAuthProfilesWithPlugins: () => [], })); -let discoverAuthStorage: typeof import("./pi-model-discovery.js").discoverAuthStorage; +let resolvePiCredentialsForDiscovery: typeof import("./pi-auth-discovery.js").resolvePiCredentialsForDiscovery; async function withAgentDir(run: (agentDir: string) => Promise): Promise { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-synthetic-auth-")); @@ -43,7 +42,7 @@ async function withAgentDir(run: (agentDir: string) => Promise): Promise { beforeAll(async () => { - ({ discoverAuthStorage } = await import("./pi-model-discovery.js")); + ({ resolvePiCredentialsForDiscovery } = await import("./pi-auth-discovery.js")); }); beforeEach(() => { @@ -57,17 +56,9 @@ describe("pi model discovery synthetic auth", () => { vi.unstubAllEnvs(); }); - it("mirrors plugin-owned synthetic cli auth into pi auth storage", async () => { + it("mirrors plugin-owned synthetic cli auth into pi credential discovery", async () => { await withAgentDir(async (agentDir) => { - saveAuthProfileStore( - { - version: 1, - profiles: {}, - }, - agentDir, - ); - - const authStorage = discoverAuthStorage(agentDir); + const credentials = resolvePiCredentialsForDiscovery(agentDir, { readOnly: true }); expect(resolveRuntimeSyntheticAuthProviderRefs).toHaveBeenCalled(); expect(resolveProviderSyntheticAuthWithPlugin).toHaveBeenCalledWith({ @@ -78,8 +69,10 @@ describe("pi model discovery synthetic auth", () => { providerConfig: undefined, }, }); - expect(authStorage.hasAuth("claude-cli")).toBe(true); - await expect(authStorage.getApiKey("claude-cli")).resolves.toBe("claude-cli-access-token"); + expect(credentials["claude-cli"]).toEqual({ + type: "api_key", + key: "claude-cli-access-token", + }); }); }); }); diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index 04b53a4137f..7c6dbdec4cc 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -1,4 +1,3 @@ -import fs from "node:fs"; import path from "node:path"; import type { Api, Model } from "@mariozechner/pi-ai"; import * as PiCodingAgent from "@mariozechner/pi-coding-agent"; @@ -11,17 +10,14 @@ import { applyProviderResolvedModelCompatWithPlugins, applyProviderResolvedTransportWithPlugin, normalizeProviderResolvedModelWithPlugin, - resolveProviderSyntheticAuthWithPlugin, } from "../plugins/provider-runtime.js"; -import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js"; import { isRecord } from "../utils.js"; +import type { PiCredentialMap } from "./pi-auth-credentials.js"; import { - ensureAuthProfileStore, - loadAuthProfileStoreForSecretsRuntime, -} from "./auth-profiles/store.js"; -import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js"; -import { resolveEnvApiKey } from "./model-auth-env.js"; -import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; + resolvePiCredentialsForDiscovery, + scrubLegacyStaticAuthJsonEntriesForDiscovery, + type DiscoverAuthStorageOptions, +} from "./pi-auth-discovery.js"; import { normalizeProviderId } from "./provider-id.js"; const PiAuthStorageClass = PiCodingAgent.AuthStorage; @@ -168,49 +164,6 @@ function createOpenClawModelRegistry( return registry; } -export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string): void { - if (process.env.OPENCLAW_AUTH_STORE_READONLY === "1") { - return; - } - if (!fs.existsSync(pathname)) { - return; - } - - let parsed: unknown; - try { - parsed = JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown; - } catch { - return; - } - if (!isRecord(parsed)) { - return; - } - - let changed = false; - for (const [provider, value] of Object.entries(parsed)) { - if (!isRecord(value)) { - continue; - } - if (value.type !== "api_key") { - continue; - } - delete parsed[provider]; - changed = true; - } - - if (!changed) { - return; - } - - if (Object.keys(parsed).length === 0) { - fs.rmSync(pathname, { force: true }); - return; - } - - fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8"); - fs.chmodSync(pathname, 0o600); -} - function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCredentialMap) { const withInMemory = AuthStorageLike as { inMemory?: (data?: unknown) => unknown }; if (typeof withInMemory.inMemory === "function") { @@ -256,67 +209,6 @@ function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCred return withRuntimeOverride; } -export function addEnvBackedPiCredentials( - credentials: PiCredentialMap, - env: NodeJS.ProcessEnv = process.env, -): PiCredentialMap { - const next = { ...credentials }; - // pi-coding-agent hides providers from its registry when auth storage lacks - // a matching credential entry. Mirror env-backed provider auth here so - // live/model discovery sees the same providers runtime auth can use. - for (const provider of Object.keys(resolveProviderEnvApiKeyCandidates({ env }))) { - if (next[provider]) { - continue; - } - const resolved = resolveEnvApiKey(provider, env); - if (!resolved?.apiKey) { - continue; - } - next[provider] = { - type: "api_key", - key: resolved.apiKey, - }; - } - return next; -} - -type DiscoverAuthStorageOptions = { - readOnly?: boolean; -}; - -export function resolvePiCredentialsForDiscovery( - agentDir: string, - options?: DiscoverAuthStorageOptions, -): PiCredentialMap { - const store = - options?.readOnly === true - ? loadAuthProfileStoreForSecretsRuntime(agentDir) - : ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); - const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store)); - for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) { - if (credentials[provider]) { - continue; - } - const resolved = resolveProviderSyntheticAuthWithPlugin({ - provider, - context: { - config: undefined, - provider, - providerConfig: undefined, - }, - }); - const apiKey = resolved?.apiKey?.trim(); - if (!apiKey) { - continue; - } - credentials[provider] = { - type: "api_key", - key: apiKey, - }; - } - return credentials; -} - // Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed). export function discoverAuthStorage( agentDir: string, @@ -342,3 +234,10 @@ export function discoverModels( options, ); } + +export { + addEnvBackedPiCredentials, + resolvePiCredentialsForDiscovery, + scrubLegacyStaticAuthJsonEntriesForDiscovery, + type DiscoverAuthStorageOptions, +} from "./pi-auth-discovery.js";