import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveSecretInputRef } from "../config/types.secrets.js"; import { createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import type { ResolverContext, SecretDefaults, SecretResolverWarningCode, } from "./runtime-shared.js"; import { pushInactiveSurfaceWarning, pushWarning } from "./runtime-shared.js"; import type { RuntimeWebDiagnostic, RuntimeWebDiagnosticCode } from "./runtime-web-tools.types.js"; export { isRecord } from "./shared.js"; import { isRecord } from "./shared.js"; const loadResolveManifestContractOwnerPluginId = createLazyRuntimeNamedExport( () => import("./runtime-web-tools-manifest.runtime.js"), "resolveManifestContractOwnerPluginId", ); type RuntimeWebWarningCode = Extract; export type SecretResolutionResult = { value?: string; source: TSource; secretRefConfigured: boolean; unresolvedRefReason?: string; fallbackEnvVar?: string; fallbackUsedAfterRefFailure: boolean; }; export type RuntimeWebProviderMetadataBase = { providerConfigured?: string; providerSource: "configured" | "auto-detect" | "none"; selectedProvider?: string; selectedProviderKeySource?: TSource; diagnostics: RuntimeWebDiagnostic[]; }; export type RuntimeWebProviderSelectionParams< TProvider extends { id: string; requiresCredential?: boolean; }, TToolConfig extends Record | undefined, TSource extends string, TMetadata extends RuntimeWebProviderMetadataBase, > = { scopePath: string; toolConfig: TToolConfig; enabled: boolean; providers: TProvider[]; configuredProvider?: string; metadata: TMetadata; diagnostics: RuntimeWebDiagnostic[]; sourceConfig: OpenClawConfig; resolvedConfig: OpenClawConfig; context: ResolverContext; defaults: SecretDefaults | undefined; deferKeylessFallback: boolean; fallbackUsedCode: RuntimeWebWarningCode; noFallbackCode: RuntimeWebWarningCode; autoDetectSelectedCode: RuntimeWebWarningCode; readConfiguredCredential: (params: { provider: TProvider; config: OpenClawConfig; toolConfig: TToolConfig; }) => unknown; readConfiguredCredentialFallback?: (params: { provider: TProvider; config: OpenClawConfig; toolConfig: TToolConfig; }) => { path: string; value: unknown } | undefined; resolveSecretInput: (params: { value: unknown; path: string; envVars: string[]; }) => Promise>; setResolvedCredential: (params: { resolvedConfig: OpenClawConfig; provider: TProvider; value: string; }) => void; inactivePathsForProvider: (provider: TProvider) => string[]; hasConfiguredSecretRef: (value: unknown, defaults: SecretDefaults | undefined) => boolean; mergeRuntimeMetadata?: (params: { provider: TProvider; metadata: TMetadata; toolConfig: TToolConfig; selectedResolution?: SecretResolutionResult; }) => Promise; }; function pushInactiveProviderCredentialWarnings< TProvider extends { id: string; requiresCredential?: boolean }, TToolConfig extends Record | undefined, TSource extends string, TMetadata extends RuntimeWebProviderMetadataBase, >(params: { selection: RuntimeWebProviderSelectionParams; skipProviderId?: string; details: string; }): void { for (const provider of params.selection.providers) { if (provider.id === params.skipProviderId) { continue; } const value = params.selection.readConfiguredCredential({ provider, config: params.selection.sourceConfig, toolConfig: params.selection.toolConfig, }); if (!params.selection.hasConfiguredSecretRef(value, params.selection.defaults)) { continue; } for (const path of params.selection.inactivePathsForProvider(provider)) { pushInactiveSurfaceWarning({ context: params.selection.context, path, details: params.details, }); } } } export function ensureObject( target: Record, key: string, ): Record { const current = target[key]; if (isRecord(current)) { return current; } const next: Record = {}; target[key] = next; return next; } function normalizeKnownProvider( value: unknown, providers: Array<{ id: string }>, ): string | undefined { const normalized = normalizeOptionalLowercaseString(value); if (!normalized) { return undefined; } if (providers.some((provider) => provider.id === normalized)) { return normalized; } return undefined; } export function hasConfiguredSecretRef( value: unknown, defaults: SecretDefaults | undefined, ): boolean { return Boolean( resolveSecretInputRef({ value, defaults, }).ref, ); } export type RuntimeWebProviderSurface = { providers: TProvider[]; configuredProvider?: string; enabled: boolean; hasConfiguredSurface: boolean; }; export type ResolveRuntimeWebProviderSurfaceParams< TProvider extends { id: string; requiresCredential?: boolean; }, TToolConfig extends Record | undefined, > = { contract: "webSearchProviders" | "webFetchProviders"; rawProvider: string; providerPath: string; toolConfig: TToolConfig; diagnostics: RuntimeWebDiagnostic[]; metadataDiagnostics: RuntimeWebDiagnostic[]; invalidAutoDetectCode: RuntimeWebWarningCode; sourceConfig: OpenClawConfig; context: ResolverContext; configuredBundledPluginIdHint?: string; resolveProviders: (params: { configuredBundledPluginId?: string }) => Promise; sortProviders: (providers: TProvider[]) => TProvider[]; readConfiguredCredential: (params: { provider: TProvider; config: OpenClawConfig; toolConfig: TToolConfig; }) => unknown; readConfiguredCredentialFallback?: (params: { provider: TProvider; config: OpenClawConfig; toolConfig: TToolConfig; }) => { path: string; value: unknown } | undefined; ignoreKeylessProvidersForConfiguredSurface?: boolean; emptyProvidersWhenSurfaceMissing?: boolean; normalizeConfiguredProviderAgainstActiveProviders?: boolean; }; export async function resolveRuntimeWebProviderSurface< TProvider extends { id: string; requiresCredential?: boolean; }, TToolConfig extends Record | undefined, >( params: ResolveRuntimeWebProviderSurfaceParams, ): Promise> { let configuredBundledPluginId = params.configuredBundledPluginIdHint; if (!configuredBundledPluginId && params.rawProvider) { const resolveManifestContractOwnerPluginId = await loadResolveManifestContractOwnerPluginId(); configuredBundledPluginId = resolveManifestContractOwnerPluginId({ contract: params.contract, value: params.rawProvider, origin: "bundled", config: params.sourceConfig, env: { ...process.env, ...params.context.env }, }); } let allProviders = params.sortProviders( await params.resolveProviders({ configuredBundledPluginId, }), ); if ( params.rawProvider && params.configuredBundledPluginIdHint && configuredBundledPluginId && !allProviders.some((provider) => provider.id === params.rawProvider) ) { configuredBundledPluginId = undefined; } if ( params.rawProvider && !configuredBundledPluginId && !allProviders.some((provider) => provider.id === params.rawProvider) ) { const resolveManifestContractOwnerPluginId = await loadResolveManifestContractOwnerPluginId(); configuredBundledPluginId = resolveManifestContractOwnerPluginId({ contract: params.contract, value: params.rawProvider, origin: "bundled", config: params.sourceConfig, env: { ...process.env, ...params.context.env }, }); allProviders = params.sortProviders( await params.resolveProviders({ configuredBundledPluginId, }), ); } const hasConfiguredSurface = Boolean(params.toolConfig) || allProviders.some((provider) => { if ( params.ignoreKeylessProvidersForConfiguredSurface && provider.requiresCredential === false ) { return false; } return ( params.readConfiguredCredential({ provider, config: params.sourceConfig, toolConfig: params.toolConfig, }) !== undefined || params.readConfiguredCredentialFallback?.({ provider, config: params.sourceConfig, toolConfig: params.toolConfig, })?.value !== undefined ); }); const providers = hasConfiguredSurface || !params.emptyProvidersWhenSurfaceMissing ? allProviders : []; const configuredProvider = normalizeKnownProvider( params.rawProvider, params.normalizeConfiguredProviderAgainstActiveProviders ? providers : allProviders, ); if (params.rawProvider && !configuredProvider) { const diagnostic: RuntimeWebDiagnostic = { code: params.invalidAutoDetectCode, message: `${params.providerPath} is "${params.rawProvider}". Falling back to auto-detect precedence.`, path: params.providerPath, }; params.diagnostics.push(diagnostic); params.metadataDiagnostics.push(diagnostic); pushWarning(params.context, { code: params.invalidAutoDetectCode, path: params.providerPath, message: diagnostic.message, }); } return { providers, configuredProvider, enabled: hasConfiguredSurface && (!isRecord(params.toolConfig) || params.toolConfig.enabled !== false), hasConfiguredSurface, }; } export async function resolveRuntimeWebProviderSelection< TProvider extends { id: string; requiresCredential?: boolean; }, TToolConfig extends Record | undefined, TSource extends string, TMetadata extends RuntimeWebProviderMetadataBase, >( params: RuntimeWebProviderSelectionParams, ): Promise { if (params.configuredProvider) { params.metadata.providerConfigured = params.configuredProvider; params.metadata.providerSource = "configured"; } if (params.enabled) { const candidates = params.configuredProvider ? params.providers.filter((provider) => provider.id === params.configuredProvider) : params.providers; const unresolvedWithoutFallback: Array<{ provider: string; path: string; reason: string }> = []; let selectedProvider: string | undefined; let selectedResolution: SecretResolutionResult | undefined; let keylessFallbackProvider: TProvider | undefined; for (const provider of candidates) { if (provider.requiresCredential === false) { if (params.deferKeylessFallback && !params.configuredProvider) { keylessFallbackProvider ||= provider; continue; } selectedProvider = provider.id; selectedResolution = { source: "missing" as TSource, secretRefConfigured: false, fallbackUsedAfterRefFailure: false, }; break; } const path = params.inactivePathsForProvider(provider)[0] ?? ""; const value = params.readConfiguredCredential({ provider, config: params.sourceConfig, toolConfig: params.toolConfig, }); const resolution = await params.resolveSecretInput({ value, path, envVars: "envVars" in provider && Array.isArray(provider.envVars) ? provider.envVars : [], }); let selectedCandidatePath = path; let selectedCandidateResolution = resolution; if (!resolution.value && !resolution.secretRefConfigured) { const fallback = params.readConfiguredCredentialFallback?.({ provider, config: params.sourceConfig, toolConfig: params.toolConfig, }); if (fallback?.value !== undefined) { selectedCandidatePath = fallback.path; selectedCandidateResolution = await params.resolveSecretInput({ value: fallback.value, path: fallback.path, envVars: [], }); } } if ( selectedCandidateResolution.secretRefConfigured && selectedCandidateResolution.fallbackUsedAfterRefFailure ) { const diagnostic: RuntimeWebDiagnostic = { code: params.fallbackUsedCode, message: `${selectedCandidatePath} SecretRef could not be resolved; using ${selectedCandidateResolution.fallbackEnvVar ?? "env fallback"}. ` + (selectedCandidateResolution.unresolvedRefReason ?? "").trim(), path: selectedCandidatePath, }; params.diagnostics.push(diagnostic); params.metadata.diagnostics.push(diagnostic); pushWarning(params.context, { code: params.fallbackUsedCode, path: selectedCandidatePath, message: diagnostic.message, }); } if ( selectedCandidateResolution.secretRefConfigured && !selectedCandidateResolution.value && selectedCandidateResolution.unresolvedRefReason ) { unresolvedWithoutFallback.push({ provider: provider.id, path: selectedCandidatePath, reason: selectedCandidateResolution.unresolvedRefReason, }); } if (params.configuredProvider) { selectedProvider = provider.id; selectedResolution = selectedCandidateResolution; if (selectedCandidateResolution.value) { params.setResolvedCredential({ resolvedConfig: params.resolvedConfig, provider, value: selectedCandidateResolution.value, }); } break; } if (selectedCandidateResolution.value) { selectedProvider = provider.id; selectedResolution = selectedCandidateResolution; params.setResolvedCredential({ resolvedConfig: params.resolvedConfig, provider, value: selectedCandidateResolution.value, }); break; } } if (!selectedProvider && keylessFallbackProvider) { selectedProvider = keylessFallbackProvider.id; selectedResolution = { source: "missing" as TSource, secretRefConfigured: false, fallbackUsedAfterRefFailure: false, }; } const failUnresolvedNoFallback = (unresolved: { path: string; reason: string }) => { const diagnostic: RuntimeWebDiagnostic = { code: params.noFallbackCode, message: unresolved.reason, path: unresolved.path, }; params.diagnostics.push(diagnostic); params.metadata.diagnostics.push(diagnostic); pushWarning(params.context, { code: params.noFallbackCode, path: unresolved.path, message: unresolved.reason, }); throw new Error(`[${params.noFallbackCode}] ${unresolved.reason}`); }; if (params.configuredProvider) { const unresolved = unresolvedWithoutFallback[0]; if (unresolved) { failUnresolvedNoFallback(unresolved); } } else { if (!selectedProvider && unresolvedWithoutFallback.length > 0) { failUnresolvedNoFallback(unresolvedWithoutFallback[0]); } if (selectedProvider) { const selectedProviderEntry = params.providers.find( (entry) => entry.id === selectedProvider, ); const selectedDetails = selectedProviderEntry?.requiresCredential === false ? `${params.scopePath} auto-detected keyless provider "${selectedProvider}" as the default fallback.` : `${params.scopePath} auto-detected provider "${selectedProvider}" from available credentials.`; const diagnostic: RuntimeWebDiagnostic = { code: params.autoDetectSelectedCode, message: selectedDetails, path: `${params.scopePath}.provider`, }; params.diagnostics.push(diagnostic); params.metadata.diagnostics.push(diagnostic); } } if (selectedProvider) { params.metadata.selectedProvider = selectedProvider; params.metadata.selectedProviderKeySource = selectedResolution?.source; if (!params.configuredProvider) { params.metadata.providerSource = "auto-detect"; } const provider = params.providers.find((entry) => entry.id === selectedProvider); if (provider && params.mergeRuntimeMetadata) { await params.mergeRuntimeMetadata({ provider, metadata: params.metadata, toolConfig: params.toolConfig, selectedResolution, }); } } } if (params.enabled && !params.configuredProvider && params.metadata.selectedProvider) { pushInactiveProviderCredentialWarnings({ selection: params, skipProviderId: params.metadata.selectedProvider, details: `${params.scopePath} auto-detected provider is "${params.metadata.selectedProvider}".`, }); } else if (params.toolConfig && !params.enabled) { pushInactiveProviderCredentialWarnings({ selection: params, details: `${params.scopePath} is disabled.`, }); } if (params.enabled && params.toolConfig && params.configuredProvider) { pushInactiveProviderCredentialWarnings({ selection: params, skipProviderId: params.configuredProvider, details: `${params.scopePath}.provider is "${params.configuredProvider}".`, }); } }