diff --git a/src/agents/model-auth-env-vars.ts b/src/agents/model-auth-env-vars.ts index ae751df4a92..dd6620e21ee 100644 --- a/src/agents/model-auth-env-vars.ts +++ b/src/agents/model-auth-env-vars.ts @@ -1,8 +1,12 @@ import { listKnownProviderAuthEnvVarNames, + resolveProviderAuthEvidence, resolveProviderAuthEnvVarCandidates, } from "../secrets/provider-env-vars.js"; -import type { ProviderEnvVarLookupParams } from "../secrets/provider-env-vars.js"; +import type { + ProviderAuthEvidence, + ProviderEnvVarLookupParams, +} from "../secrets/provider-env-vars.js"; export function resolveProviderEnvApiKeyCandidates( params?: ProviderEnvVarLookupParams, @@ -10,6 +14,12 @@ export function resolveProviderEnvApiKeyCandidates( return resolveProviderAuthEnvVarCandidates(params); } +export function resolveProviderEnvAuthEvidence( + params?: ProviderEnvVarLookupParams, +): Record { + return resolveProviderAuthEvidence(params); +} + export const PROVIDER_ENV_API_KEY_CANDIDATES = resolveProviderEnvApiKeyCandidates(); export function listKnownProviderEnvApiKeyNames(): string[] { diff --git a/src/secrets/provider-env-vars.dynamic.test.ts b/src/secrets/provider-env-vars.dynamic.test.ts index 9039c24f9f0..954ea1b5d1a 100644 --- a/src/secrets/provider-env-vars.dynamic.test.ts +++ b/src/secrets/provider-env-vars.dynamic.test.ts @@ -6,6 +6,7 @@ import { listKnownSecretEnvVarNames, PROVIDER_AUTH_ENV_VAR_CANDIDATES, PROVIDER_ENV_VARS, + resolveProviderAuthEvidence, } from "./provider-env-vars.js"; type MockManifestRegistry = { @@ -19,6 +20,15 @@ type MockManifestRegistry = { providers?: Array<{ id: string; envVars?: string[]; + authEvidence?: Array<{ + type: "local-file-with-env"; + fileEnvVar?: string; + fallbackPaths?: string[]; + requiresAnyEnv?: string[]; + requiresAllEnv?: string[]; + credentialMarker: string; + source?: string; + }>; }>; }; }>; @@ -107,6 +117,44 @@ describe("provider env vars dynamic manifest metadata", () => { expect(listKnownSecretEnvVarNames()).toContain("MODEL_STUDIO_API_KEY"); }); + it("includes setup provider auth evidence without loading setup runtime", async () => { + pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ + plugins: [ + { + id: "external-cloud", + origin: "global", + setup: { + providers: [ + { + id: "external-cloud", + authEvidence: [ + { + type: "local-file-with-env", + fileEnvVar: "EXTERNAL_CLOUD_CREDENTIALS", + requiresAllEnv: ["EXTERNAL_CLOUD_PROJECT"], + credentialMarker: "external-cloud-local-credentials", + source: "external cloud credentials", + }, + ], + }, + ], + }, + }, + ], + diagnostics: [], + }); + + expect(resolveProviderAuthEvidence()["external-cloud"]).toEqual([ + { + type: "local-file-with-env", + fileEnvVar: "EXTERNAL_CLOUD_CREDENTIALS", + requiresAllEnv: ["EXTERNAL_CLOUD_PROJECT"], + credentialMarker: "external-cloud-local-credentials", + source: "external cloud credentials", + }, + ]); + }); + it("appends setup provider env vars after explicit provider auth env vars", async () => { pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ plugins: [ diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index 6fd8f27a787..af899cd4bb0 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -29,6 +29,16 @@ export type ProviderEnvVarLookupParams = { includeUntrustedWorkspacePlugins?: boolean; }; +export type ProviderAuthEvidence = { + type: "local-file-with-env"; + fileEnvVar?: string; + fallbackPaths?: readonly string[]; + requiresAnyEnv?: readonly string[]; + requiresAllEnv?: readonly string[]; + credentialMarker: string; + source?: string; +}; + function isWorkspacePluginTrustedForProviderEnvVars( plugin: PluginManifestRecord, config: OpenClawConfig | undefined, @@ -73,6 +83,27 @@ function appendUniqueEnvVarCandidates( } } +function appendUniqueAuthEvidence( + target: Record, + providerId: string, + evidence: readonly ProviderAuthEvidence[], +) { + const normalizedProviderId = providerId.trim(); + if (!normalizedProviderId || evidence.length === 0) { + return; + } + const bucket = (target[normalizedProviderId] ??= []); + const seen = new Set(bucket.map((entry) => JSON.stringify(entry))); + for (const entry of evidence) { + const key = JSON.stringify(entry); + if (seen.has(key)) { + continue; + } + seen.add(key); + bucket.push(entry); + } +} + function resolveManifestProviderAuthEnvVarCandidates( params?: ProviderEnvVarLookupParams, ): Record { @@ -111,6 +142,37 @@ function resolveManifestProviderAuthEnvVarCandidates( return candidates; } +function resolveManifestProviderAuthEvidence( + params?: ProviderEnvVarLookupParams, +): Record { + const registry = loadPluginManifestRegistryForPluginRegistry({ + config: params?.config, + workspaceDir: params?.workspaceDir, + env: params?.env, + preferPersisted: false, + includeDisabled: true, + }); + const evidenceByProvider: Record = {}; + for (const plugin of registry.plugins) { + if (!shouldUsePluginProviderEnvVars(plugin, params)) { + continue; + } + for (const provider of plugin.setup?.providers ?? []) { + appendUniqueAuthEvidence(evidenceByProvider, provider.id, provider.authEvidence ?? []); + } + } + const aliases = resolveProviderAuthAliasMap(params); + for (const [alias, target] of Object.entries(aliases).toSorted(([left], [right]) => + left.localeCompare(right), + )) { + const evidence = evidenceByProvider[target]; + if (evidence) { + appendUniqueAuthEvidence(evidenceByProvider, alias, evidence); + } + } + return evidenceByProvider; +} + export function resolveProviderAuthEnvVarCandidates( params?: ProviderEnvVarLookupParams, ): Record { @@ -120,6 +182,12 @@ export function resolveProviderAuthEnvVarCandidates( }; } +export function resolveProviderAuthEvidence( + params?: ProviderEnvVarLookupParams, +): Record { + return resolveManifestProviderAuthEvidence(params); +} + export function resolveProviderEnvVars( params?: ProviderEnvVarLookupParams, ): Record {