diff --git a/src/agents/model-auth-env-vars.ts b/src/agents/model-auth-env-vars.ts index dd6620e21ee..1f401ccc250 100644 --- a/src/agents/model-auth-env-vars.ts +++ b/src/agents/model-auth-env-vars.ts @@ -20,6 +20,14 @@ export function resolveProviderEnvAuthEvidence( return resolveProviderAuthEvidence(params); } +export function resolveProviderEnvAuthLookupKeys(params?: ProviderEnvVarLookupParams): string[] { + const envCandidateMap = resolveProviderEnvApiKeyCandidates(params); + const authEvidenceMap = resolveProviderEnvAuthEvidence(params); + return Array.from( + new Set([...Object.keys(envCandidateMap), ...Object.keys(authEvidenceMap)]), + ).toSorted((a, b) => a.localeCompare(b)); +} + export const PROVIDER_ENV_API_KEY_CANDIDATES = resolveProviderEnvApiKeyCandidates(); export function listKnownProviderEnvApiKeyNames(): string[] { diff --git a/src/agents/pi-auth-discovery-core.ts b/src/agents/pi-auth-discovery-core.ts index 4b5499a45a7..ff274deb1de 100644 --- a/src/agents/pi-auth-discovery-core.ts +++ b/src/agents/pi-auth-discovery-core.ts @@ -1,22 +1,46 @@ import fs from "node:fs"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isRecord } from "../utils.js"; -import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js"; +import { + resolveProviderEnvApiKeyCandidates, + resolveProviderEnvAuthEvidence, + resolveProviderEnvAuthLookupKeys, +} from "./model-auth-env-vars.js"; import { resolveEnvApiKey } from "./model-auth-env.js"; import type { PiCredentialMap } from "./pi-auth-credentials.js"; +export type PiDiscoveryAuthLookupOptions = { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}; + export function addEnvBackedPiCredentials( credentials: PiCredentialMap, - env: NodeJS.ProcessEnv = process.env, + options: PiDiscoveryAuthLookupOptions = {}, ): PiCredentialMap { + const env = options.env ?? process.env; + const lookupParams = { + config: options.config, + workspaceDir: options.workspaceDir, + env, + }; + const candidateMap = resolveProviderEnvApiKeyCandidates(lookupParams); + const authEvidenceMap = resolveProviderEnvAuthEvidence(lookupParams); 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 }))) { + for (const provider of resolveProviderEnvAuthLookupKeys(lookupParams)) { if (next[provider]) { continue; } - const resolved = resolveEnvApiKey(provider, env); + const resolved = resolveEnvApiKey(provider, env, { + config: options.config, + workspaceDir: options.workspaceDir, + candidateMap, + authEvidenceMap, + }); if (!resolved?.apiKey) { continue; } diff --git a/src/agents/pi-auth-discovery.ts b/src/agents/pi-auth-discovery.ts index fe567c027ed..a5993123db7 100644 --- a/src/agents/pi-auth-discovery.ts +++ b/src/agents/pi-auth-discovery.ts @@ -5,12 +5,15 @@ import { loadAuthProfileStoreForSecretsRuntime, } from "./auth-profiles/store.js"; import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; -import { addEnvBackedPiCredentials } from "./pi-auth-discovery-core.js"; +import { + addEnvBackedPiCredentials, + type PiDiscoveryAuthLookupOptions, +} from "./pi-auth-discovery-core.js"; export type DiscoverAuthStorageOptions = { readOnly?: boolean; skipCredentials?: boolean; -}; +} & PiDiscoveryAuthLookupOptions; export function resolvePiCredentialsForDiscovery( agentDir: string, @@ -20,7 +23,11 @@ export function resolvePiCredentialsForDiscovery( options?.readOnly === true ? loadAuthProfileStoreForSecretsRuntime(agentDir) : ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); - const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store)); + const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store), { + config: options?.config, + workspaceDir: options?.workspaceDir, + env: options?.env, + }); for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) { if (credentials[provider]) { continue; diff --git a/src/agents/pi-model-discovery.auth.test.ts b/src/agents/pi-model-discovery.auth.test.ts index 7e656167601..43b1ca68545 100644 --- a/src/agents/pi-model-discovery.auth.test.ts +++ b/src/agents/pi-model-discovery.auth.test.ts @@ -12,14 +12,34 @@ vi.mock("./model-auth-env-vars.js", () => ({ resolveProviderEnvApiKeyCandidates: () => ({ mistral: ["MISTRAL_API_KEY"], }), + resolveProviderEnvAuthEvidence: () => ({ + "workspace-cloud": [ + { + type: "local-file-with-env", + credentialMarker: "workspace-cloud-local-credentials", + source: "workspace cloud credentials", + }, + ], + }), + resolveProviderEnvAuthLookupKeys: () => ["mistral", "workspace-cloud"], })); vi.mock("./model-auth-env.js", () => ({ - resolveEnvApiKey: (provider: string, env: NodeJS.ProcessEnv) => { - if (provider !== "mistral" || !env.MISTRAL_API_KEY?.trim()) { - return null; + resolveEnvApiKey: ( + provider: string, + env: NodeJS.ProcessEnv, + options?: { workspaceDir?: string }, + ) => { + if (provider === "mistral" && env.MISTRAL_API_KEY?.trim()) { + return { apiKey: env.MISTRAL_API_KEY, source: "env: MISTRAL_API_KEY" }; } - return { apiKey: env.MISTRAL_API_KEY, source: "env: MISTRAL_API_KEY" }; + if (provider === "workspace-cloud" && options?.workspaceDir === "/tmp/workspace") { + return { + apiKey: "workspace-cloud-local-credentials", + source: "workspace cloud credentials", + }; + } + return null; }, })); @@ -144,7 +164,7 @@ describe("discoverAuthStorage", () => { delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS; try { - const credentials = addEnvBackedPiCredentials({}, process.env); + const credentials = addEnvBackedPiCredentials({}, { env: process.env }); expect(credentials.mistral).toEqual({ type: "api_key", @@ -168,4 +188,19 @@ describe("discoverAuthStorage", () => { } } }); + + it("includes workspace-scoped auth evidence in pi discovery credentials", () => { + const credentials = addEnvBackedPiCredentials( + {}, + { + env: {}, + workspaceDir: "/tmp/workspace", + }, + ); + + expect(credentials["workspace-cloud"]).toEqual({ + type: "api_key", + key: "workspace-cloud-local-credentials", + }); + }); });