Files
openclaw/src/secrets/runtime-web-tools.shared.ts
2026-04-11 02:46:41 +01:00

496 lines
16 KiB
TypeScript

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<RuntimeWebDiagnosticCode, SecretResolverWarningCode>;
export type SecretResolutionResult<TSource extends string> = {
value?: string;
source: TSource;
secretRefConfigured: boolean;
unresolvedRefReason?: string;
fallbackEnvVar?: string;
fallbackUsedAfterRefFailure: boolean;
};
export type RuntimeWebProviderMetadataBase<TSource extends string> = {
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<string, unknown> | undefined,
TSource extends string,
TMetadata extends RuntimeWebProviderMetadataBase<TSource>,
> = {
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;
resolveSecretInput: (params: {
value: unknown;
path: string;
envVars: string[];
}) => Promise<SecretResolutionResult<TSource>>;
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<TSource>;
}) => Promise<void>;
};
export function ensureObject(
target: Record<string, unknown>,
key: string,
): Record<string, unknown> {
const current = target[key];
if (isRecord(current)) {
return current;
}
const next: Record<string, unknown> = {};
target[key] = next;
return next;
}
export function normalizeKnownProvider<TProvider extends { id: string }>(
value: unknown,
providers: TProvider[],
): 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<TProvider extends { id: string }> = {
providers: TProvider[];
configuredProvider?: string;
enabled: boolean;
hasConfiguredSurface: boolean;
};
export type ResolveRuntimeWebProviderSurfaceParams<
TProvider extends {
id: string;
requiresCredential?: boolean;
},
TToolConfig extends Record<string, unknown> | 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<TProvider[]>;
sortProviders: (providers: TProvider[]) => TProvider[];
readConfiguredCredential: (params: {
provider: TProvider;
config: OpenClawConfig;
toolConfig: TToolConfig;
}) => unknown;
ignoreKeylessProvidersForConfiguredSurface?: boolean;
emptyProvidersWhenSurfaceMissing?: boolean;
normalizeConfiguredProviderAgainstActiveProviders?: boolean;
};
export async function resolveRuntimeWebProviderSurface<
TProvider extends {
id: string;
requiresCredential?: boolean;
},
TToolConfig extends Record<string, unknown> | undefined,
>(
params: ResolveRuntimeWebProviderSurfaceParams<TProvider, TToolConfig>,
): Promise<RuntimeWebProviderSurface<TProvider>> {
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) {
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
);
});
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<string, unknown> | undefined,
TSource extends string,
TMetadata extends RuntimeWebProviderMetadataBase<TSource>,
>(
params: RuntimeWebProviderSelectionParams<TProvider, TToolConfig, TSource, TMetadata>,
): Promise<void> {
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<TSource> | 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 : [],
});
if (resolution.secretRefConfigured && resolution.fallbackUsedAfterRefFailure) {
const diagnostic: RuntimeWebDiagnostic = {
code: params.fallbackUsedCode,
message:
`${path} SecretRef could not be resolved; using ${resolution.fallbackEnvVar ?? "env fallback"}. ` +
(resolution.unresolvedRefReason ?? "").trim(),
path,
};
params.diagnostics.push(diagnostic);
params.metadata.diagnostics.push(diagnostic);
pushWarning(params.context, {
code: params.fallbackUsedCode,
path,
message: diagnostic.message,
});
}
if (resolution.secretRefConfigured && !resolution.value && resolution.unresolvedRefReason) {
unresolvedWithoutFallback.push({
provider: provider.id,
path,
reason: resolution.unresolvedRefReason,
});
}
if (params.configuredProvider) {
selectedProvider = provider.id;
selectedResolution = resolution;
if (resolution.value) {
params.setResolvedCredential({
resolvedConfig: params.resolvedConfig,
provider,
value: resolution.value,
});
}
break;
}
if (resolution.value) {
selectedProvider = provider.id;
selectedResolution = resolution;
params.setResolvedCredential({
resolvedConfig: params.resolvedConfig,
provider,
value: resolution.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) {
for (const provider of params.providers) {
if (provider.id === params.metadata.selectedProvider) {
continue;
}
const value = params.readConfiguredCredential({
provider,
config: params.sourceConfig,
toolConfig: params.toolConfig,
});
if (!params.hasConfiguredSecretRef(value, params.defaults)) {
continue;
}
for (const path of params.inactivePathsForProvider(provider)) {
pushInactiveSurfaceWarning({
context: params.context,
path,
details: `${params.scopePath} auto-detected provider is "${params.metadata.selectedProvider}".`,
});
}
}
} else if (params.toolConfig && !params.enabled) {
for (const provider of params.providers) {
const value = params.readConfiguredCredential({
provider,
config: params.sourceConfig,
toolConfig: params.toolConfig,
});
if (!params.hasConfiguredSecretRef(value, params.defaults)) {
continue;
}
for (const path of params.inactivePathsForProvider(provider)) {
pushInactiveSurfaceWarning({
context: params.context,
path,
details: `${params.scopePath} is disabled.`,
});
}
}
}
if (params.enabled && params.toolConfig && params.configuredProvider) {
for (const provider of params.providers) {
if (provider.id === params.configuredProvider) {
continue;
}
const value = params.readConfiguredCredential({
provider,
config: params.sourceConfig,
toolConfig: params.toolConfig,
});
if (!params.hasConfiguredSecretRef(value, params.defaults)) {
continue;
}
for (const path of params.inactivePathsForProvider(provider)) {
pushInactiveSurfaceWarning({
context: params.context,
path,
details: `${params.scopePath}.provider is "${params.configuredProvider}".`,
});
}
}
}
}