mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
refactor: share web provider runtime helpers
This commit is contained in:
@@ -1,86 +1,31 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
type PluginManifestRecord,
|
||||
} from "./manifest-registry.js";
|
||||
import { getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
||||
import { type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
import {
|
||||
resolveBundledWebFetchResolutionConfig,
|
||||
sortWebFetchProviders,
|
||||
} from "./web-fetch-providers.shared.js";
|
||||
import {
|
||||
mapRegistryProviders,
|
||||
resolveManifestDeclaredWebProviderCandidatePluginIds,
|
||||
} from "./web-provider-resolution-shared.js";
|
||||
import {
|
||||
createWebProviderSnapshotCache,
|
||||
resolvePluginWebProviders,
|
||||
resolveRuntimeWebProviders,
|
||||
} from "./web-provider-runtime-shared.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
type WebFetchProviderSnapshotCacheEntry = {
|
||||
expiresAt: number;
|
||||
providers: PluginWebFetchProviderEntry[];
|
||||
};
|
||||
let webFetchProviderSnapshotCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebFetchProviderSnapshotCacheEntry>>
|
||||
>();
|
||||
let webFetchProviderSnapshotCache = createWebProviderSnapshotCache<PluginWebFetchProviderEntry>();
|
||||
|
||||
function resetWebFetchProviderSnapshotCacheForTests() {
|
||||
webFetchProviderSnapshotCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebFetchProviderSnapshotCacheEntry>>
|
||||
>();
|
||||
webFetchProviderSnapshotCache = createWebProviderSnapshotCache<PluginWebFetchProviderEntry>();
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetWebFetchProviderSnapshotCacheForTests,
|
||||
} as const;
|
||||
|
||||
function buildWebFetchSnapshotCacheKey(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat === true,
|
||||
origin: params.origin ?? "",
|
||||
onlyPluginIds: [...new Set(params.onlyPluginIds ?? [])].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
env: buildPluginSnapshotCacheEnvKey(params.env),
|
||||
});
|
||||
}
|
||||
|
||||
function pluginManifestDeclaresWebFetch(record: PluginManifestRecord): boolean {
|
||||
if ((record.contracts?.webFetchProviders?.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
const configUiHintKeys = Object.keys(record.configUiHints ?? {});
|
||||
if (configUiHintKeys.some((key) => key === "webFetch" || key.startsWith("webFetch."))) {
|
||||
return true;
|
||||
}
|
||||
if (!isRecord(record.configSchema)) {
|
||||
return false;
|
||||
}
|
||||
const properties = record.configSchema.properties;
|
||||
return isRecord(properties) && "webFetch" in properties;
|
||||
}
|
||||
|
||||
function resolveWebFetchCandidatePluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
@@ -88,86 +33,26 @@ function resolveWebFetchCandidatePluginIds(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): string[] | undefined {
|
||||
const contractIds = new Set(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: "webFetchProviders",
|
||||
origin: params.origin,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
}),
|
||||
);
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = loadPluginManifestRegistry({
|
||||
return resolveManifestDeclaredWebProviderCandidatePluginIds({
|
||||
contract: "webFetchProviders",
|
||||
configKey: "webFetch",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(contractIds.has(plugin.id) || pluginManifestDeclaresWebFetch(plugin)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return ids.length > 0 ? ids : undefined;
|
||||
}
|
||||
|
||||
function resolveWebFetchLoadOptions(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
resolveBundledWebFetchResolutionConfig({
|
||||
...params,
|
||||
workspaceDir,
|
||||
env,
|
||||
});
|
||||
const onlyPluginIds = resolveWebFetchCandidatePluginIds({
|
||||
config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
});
|
||||
return {
|
||||
env,
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
...(onlyPluginIds ? { onlyPluginIds } : {}),
|
||||
logger: createPluginLoaderLogger(log),
|
||||
} satisfies PluginLoadOptions;
|
||||
}
|
||||
|
||||
function mapRegistryWebFetchProviders(params: {
|
||||
registry: ReturnType<typeof loadOpenClawPlugins>;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return sortWebFetchProviders(
|
||||
params.registry.webFetchProviders
|
||||
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
|
||||
.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
return mapRegistryProviders({
|
||||
entries: params.registry.webFetchProviders,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
sortProviders: sortWebFetchProviders,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginWebFetchProviders(params: {
|
||||
@@ -181,83 +66,12 @@ export function resolvePluginWebFetchProviders(params: {
|
||||
mode?: "runtime" | "setup";
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
resolveWebFetchCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds,
|
||||
}),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: pluginIds,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return mapRegistryWebFetchProviders({ registry, onlyPluginIds: pluginIds });
|
||||
}
|
||||
const cacheOwnerConfig = params.config;
|
||||
const shouldMemoizeSnapshot =
|
||||
params.activate !== true && params.cache !== true && shouldUsePluginSnapshotCache(env);
|
||||
const cacheKey = buildWebFetchSnapshotCacheKey({
|
||||
config: cacheOwnerConfig,
|
||||
workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
env,
|
||||
return resolvePluginWebProviders(params, {
|
||||
snapshotCache: webFetchProviderSnapshotCache,
|
||||
resolveBundledResolutionConfig: resolveBundledWebFetchResolutionConfig,
|
||||
resolveCandidatePluginIds: resolveWebFetchCandidatePluginIds,
|
||||
mapRegistryProviders: mapRegistryWebFetchProviders,
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const configCache = webFetchProviderSnapshotCache.get(cacheOwnerConfig);
|
||||
const envCache = configCache?.get(env);
|
||||
const cached = envCache?.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.providers;
|
||||
}
|
||||
}
|
||||
const loadOptions = resolveWebFetchLoadOptions({ ...params, workspaceDir });
|
||||
// Keep repeated runtime reads on the already-compatible active registry when
|
||||
// possible, then fall back to a fresh snapshot load only when necessary.
|
||||
const resolved = mapRegistryWebFetchProviders({
|
||||
registry:
|
||||
resolveCompatibleRuntimePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions),
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
let configCache = webFetchProviderSnapshotCache.get(cacheOwnerConfig);
|
||||
if (!configCache) {
|
||||
configCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, WebFetchProviderSnapshotCacheEntry>
|
||||
>();
|
||||
webFetchProviderSnapshotCache.set(cacheOwnerConfig, configCache);
|
||||
}
|
||||
let envCache = configCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, WebFetchProviderSnapshotCacheEntry>();
|
||||
configCache.set(env, envCache);
|
||||
}
|
||||
envCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
providers: resolved,
|
||||
});
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function resolveRuntimeWebFetchProviders(params: {
|
||||
@@ -268,19 +82,10 @@ export function resolveRuntimeWebFetchProviders(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(
|
||||
params.config === undefined
|
||||
? undefined
|
||||
: resolveWebFetchLoadOptions({
|
||||
...params,
|
||||
workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(),
|
||||
}),
|
||||
);
|
||||
if (runtimeRegistry) {
|
||||
return mapRegistryWebFetchProviders({
|
||||
registry: runtimeRegistry,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
}
|
||||
return resolvePluginWebFetchProviders(params);
|
||||
return resolveRuntimeWebProviders(params, {
|
||||
snapshotCache: webFetchProviderSnapshotCache,
|
||||
resolveBundledResolutionConfig: resolveBundledWebFetchResolutionConfig,
|
||||
resolveCandidatePluginIds: resolveWebFetchCandidatePluginIds,
|
||||
mapRegistryProviders: mapRegistryWebFetchProviders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { type NormalizedPluginsConfig } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { resolveManifestContractPluginIds } from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
|
||||
function resolveBundledWebFetchCompatPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveManifestContractPluginIds({
|
||||
contract: "webFetchProviders",
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
function compareWebFetchProvidersAlphabetically(
|
||||
left: Pick<PluginWebFetchProviderEntry, "id" | "pluginId">,
|
||||
right: Pick<PluginWebFetchProviderEntry, "id" | "pluginId">,
|
||||
): number {
|
||||
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
|
||||
}
|
||||
import {
|
||||
resolveBundledWebProviderResolutionConfig,
|
||||
sortPluginProviders,
|
||||
sortPluginProvidersForAutoDetect,
|
||||
} from "./web-provider-resolution-shared.js";
|
||||
|
||||
export function sortWebFetchProviders(
|
||||
providers: PluginWebFetchProviderEntry[],
|
||||
): PluginWebFetchProviderEntry[] {
|
||||
return providers.toSorted(compareWebFetchProvidersAlphabetically);
|
||||
return sortPluginProviders(providers);
|
||||
}
|
||||
|
||||
export function sortWebFetchProvidersForAutoDetect(
|
||||
providers: PluginWebFetchProviderEntry[],
|
||||
): PluginWebFetchProviderEntry[] {
|
||||
return providers.toSorted((left, right) => {
|
||||
const leftOrder = left.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightOrder = right.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
return compareWebFetchProvidersAlphabetically(left, right);
|
||||
});
|
||||
return sortPluginProvidersForAutoDetect(providers);
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchResolutionConfig(params: {
|
||||
@@ -55,23 +30,11 @@ export function resolveBundledWebFetchResolutionConfig(params: {
|
||||
activationSourceConfig?: PluginLoadOptions["config"];
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
} {
|
||||
const activation = resolveBundledPluginCompatibleActivationInputs({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
return resolveBundledWebProviderResolutionConfig({
|
||||
contract: "webFetchProviders",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
applyAutoEnable: true,
|
||||
compatMode: {
|
||||
allowlist: params.bundledAllowlistCompat,
|
||||
enablement: "always",
|
||||
vitest: true,
|
||||
},
|
||||
resolveCompatPluginIds: resolveBundledWebFetchCompatPluginIds,
|
||||
env: params.env,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
});
|
||||
|
||||
return {
|
||||
config: activation.config,
|
||||
normalized: activation.normalized,
|
||||
activationSourceConfig: activation.activationSourceConfig,
|
||||
autoEnabledReasons: activation.autoEnabledReasons,
|
||||
};
|
||||
}
|
||||
|
||||
190
src/plugins/web-provider-resolution-shared.ts
Normal file
190
src/plugins/web-provider-resolution-shared.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import type { NormalizedPluginsConfig } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
type PluginManifestRecord,
|
||||
} from "./manifest-registry.js";
|
||||
|
||||
export type WebProviderContract = "webSearchProviders" | "webFetchProviders";
|
||||
export type WebProviderConfigKey = "webSearch" | "webFetch";
|
||||
|
||||
type WebProviderSortEntry = {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
autoDetectOrder?: number;
|
||||
};
|
||||
|
||||
function comparePluginProvidersAlphabetically(
|
||||
left: Pick<WebProviderSortEntry, "id" | "pluginId">,
|
||||
right: Pick<WebProviderSortEntry, "id" | "pluginId">,
|
||||
): number {
|
||||
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
|
||||
}
|
||||
|
||||
export function sortPluginProviders<T extends Pick<WebProviderSortEntry, "id" | "pluginId">>(
|
||||
providers: T[],
|
||||
): T[] {
|
||||
return providers.toSorted(comparePluginProvidersAlphabetically);
|
||||
}
|
||||
|
||||
export function sortPluginProvidersForAutoDetect<T extends WebProviderSortEntry>(
|
||||
providers: T[],
|
||||
): T[] {
|
||||
return providers.toSorted((left, right) => {
|
||||
const leftOrder = left.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightOrder = right.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
return comparePluginProvidersAlphabetically(left, right);
|
||||
});
|
||||
}
|
||||
|
||||
function pluginManifestDeclaresProviderConfig(
|
||||
record: PluginManifestRecord,
|
||||
configKey: WebProviderConfigKey,
|
||||
contract: WebProviderContract,
|
||||
): boolean {
|
||||
if ((record.contracts?.[contract]?.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
const configUiHintKeys = Object.keys(record.configUiHints ?? {});
|
||||
if (configUiHintKeys.some((key) => key === configKey || key.startsWith(`${configKey}.`))) {
|
||||
return true;
|
||||
}
|
||||
const properties = record.configSchema?.properties;
|
||||
return typeof properties === "object" && properties !== null && configKey in properties;
|
||||
}
|
||||
|
||||
export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: {
|
||||
contract: WebProviderContract;
|
||||
configKey: WebProviderConfigKey;
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): string[] | undefined {
|
||||
const contractIds = new Set(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: params.contract,
|
||||
origin: params.origin,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
}),
|
||||
);
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(contractIds.has(plugin.id) ||
|
||||
pluginManifestDeclaresProviderConfig(plugin, params.configKey, params.contract)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return ids.length > 0 ? ids : undefined;
|
||||
}
|
||||
|
||||
function resolveBundledWebProviderCompatPluginIds(params: {
|
||||
contract: WebProviderContract;
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveManifestContractPluginIds({
|
||||
contract: params.contract,
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveBundledWebProviderResolutionConfig(params: {
|
||||
contract: WebProviderContract;
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): {
|
||||
config: PluginLoadOptions["config"];
|
||||
normalized: NormalizedPluginsConfig;
|
||||
activationSourceConfig?: PluginLoadOptions["config"];
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
} {
|
||||
const activation = resolveBundledPluginCompatibleActivationInputs({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
applyAutoEnable: true,
|
||||
compatMode: {
|
||||
allowlist: params.bundledAllowlistCompat,
|
||||
enablement: "always",
|
||||
vitest: true,
|
||||
},
|
||||
resolveCompatPluginIds: (compatParams) =>
|
||||
resolveBundledWebProviderCompatPluginIds({
|
||||
contract: params.contract,
|
||||
...compatParams,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
config: activation.config,
|
||||
normalized: activation.normalized,
|
||||
activationSourceConfig: activation.activationSourceConfig,
|
||||
autoEnabledReasons: activation.autoEnabledReasons,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWebProviderSnapshotCacheKey(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
envKey: string;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat === true,
|
||||
origin: params.origin ?? "",
|
||||
onlyPluginIds: [...new Set(params.onlyPluginIds ?? [])].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
env: params.envKey,
|
||||
});
|
||||
}
|
||||
|
||||
export function mapRegistryProviders<
|
||||
TProvider extends { id: string },
|
||||
TEntry extends { pluginId: string; provider: TProvider },
|
||||
>(params: {
|
||||
entries: readonly TEntry[];
|
||||
onlyPluginIds?: readonly string[];
|
||||
sortProviders: (
|
||||
providers: Array<TProvider & { pluginId: string }>,
|
||||
) => Array<TProvider & { pluginId: string }>;
|
||||
}): Array<TProvider & { pluginId: string }> {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return params.sortProviders(
|
||||
params.entries
|
||||
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
|
||||
.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
219
src/plugins/web-provider-runtime-shared.ts
Normal file
219
src/plugins/web-provider-runtime-shared.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import { getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptionsFromValues,
|
||||
createPluginRuntimeLoaderLogger,
|
||||
} from "./runtime/load-context.js";
|
||||
import { buildWebProviderSnapshotCacheKey } from "./web-provider-resolution-shared.js";
|
||||
|
||||
type WebProviderSnapshotCacheEntry<TEntry> = {
|
||||
expiresAt: number;
|
||||
providers: TEntry[];
|
||||
};
|
||||
|
||||
export type WebProviderSnapshotCache<TEntry> = WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebProviderSnapshotCacheEntry<TEntry>>>
|
||||
>;
|
||||
|
||||
export type ResolvePluginWebProvidersParams = {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
mode?: "runtime" | "setup";
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
};
|
||||
|
||||
type ResolveWebProviderRuntimeDeps<TEntry> = {
|
||||
snapshotCache: WebProviderSnapshotCache<TEntry>;
|
||||
resolveBundledResolutionConfig: (params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}) => {
|
||||
config: PluginLoadOptions["config"];
|
||||
activationSourceConfig?: PluginLoadOptions["config"];
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
};
|
||||
resolveCandidatePluginIds: (params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}) => string[] | undefined;
|
||||
mapRegistryProviders: (params: {
|
||||
registry: PluginRegistry;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}) => TEntry[];
|
||||
};
|
||||
|
||||
export function createWebProviderSnapshotCache<TEntry>(): WebProviderSnapshotCache<TEntry> {
|
||||
return new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebProviderSnapshotCacheEntry<TEntry>>>
|
||||
>();
|
||||
}
|
||||
|
||||
function resolveWebProviderLoadOptions<TEntry>(
|
||||
params: ResolvePluginWebProvidersParams,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
) {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
deps.resolveBundledResolutionConfig({
|
||||
...params,
|
||||
workspaceDir,
|
||||
env,
|
||||
});
|
||||
const onlyPluginIds = deps.resolveCandidatePluginIds({
|
||||
config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
});
|
||||
return buildPluginRuntimeLoadOptionsFromValues(
|
||||
{
|
||||
env,
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir,
|
||||
logger: createPluginRuntimeLoaderLogger(),
|
||||
},
|
||||
{
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
...(onlyPluginIds ? { onlyPluginIds } : {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePluginWebProviders<TEntry>(
|
||||
params: ResolvePluginWebProvidersParams,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
): TEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
deps.resolveCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins(
|
||||
buildPluginRuntimeLoadOptionsFromValues(
|
||||
{
|
||||
config: withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds,
|
||||
}),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir,
|
||||
env,
|
||||
logger: createPluginRuntimeLoaderLogger(),
|
||||
},
|
||||
{
|
||||
onlyPluginIds: pluginIds,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
},
|
||||
),
|
||||
);
|
||||
return deps.mapRegistryProviders({ registry, onlyPluginIds: pluginIds });
|
||||
}
|
||||
|
||||
const cacheOwnerConfig = params.config;
|
||||
const shouldMemoizeSnapshot =
|
||||
params.activate !== true && params.cache !== true && shouldUsePluginSnapshotCache(env);
|
||||
const cacheKey = buildWebProviderSnapshotCacheKey({
|
||||
config: cacheOwnerConfig,
|
||||
workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
envKey: buildPluginSnapshotCacheEnvKey(env),
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const configCache = deps.snapshotCache.get(cacheOwnerConfig);
|
||||
const envCache = configCache?.get(env);
|
||||
const cached = envCache?.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.providers;
|
||||
}
|
||||
}
|
||||
|
||||
const loadOptions = resolveWebProviderLoadOptions(params, deps);
|
||||
const resolved = deps.mapRegistryProviders({
|
||||
registry:
|
||||
resolveCompatibleRuntimePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions),
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
let configCache = deps.snapshotCache.get(cacheOwnerConfig);
|
||||
if (!configCache) {
|
||||
configCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, WebProviderSnapshotCacheEntry<TEntry>>
|
||||
>();
|
||||
deps.snapshotCache.set(cacheOwnerConfig, configCache);
|
||||
}
|
||||
let envCache = configCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, WebProviderSnapshotCacheEntry<TEntry>>();
|
||||
configCache.set(env, envCache);
|
||||
}
|
||||
envCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
providers: resolved,
|
||||
});
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function resolveRuntimeWebProviders<TEntry>(
|
||||
params: Omit<ResolvePluginWebProvidersParams, "activate" | "cache" | "mode">,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
): TEntry[] {
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(
|
||||
params.config === undefined ? undefined : resolveWebProviderLoadOptions(params, deps),
|
||||
);
|
||||
if (runtimeRegistry) {
|
||||
return deps.mapRegistryProviders({
|
||||
registry: runtimeRegistry,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
}
|
||||
return resolvePluginWebProviders(params, deps);
|
||||
}
|
||||
@@ -546,27 +546,30 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
expectLoaderCallCount(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "invalidates the snapshot cache when config contents change in place",
|
||||
mutate: (config: { plugins?: Record<string, unknown> }, _env: NodeJS.ProcessEnv) => {
|
||||
config.plugins = { allow: ["perplexity"] };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalidates the snapshot cache when env contents change in place",
|
||||
mutate: (_config: { plugins?: Record<string, unknown> }, env: NodeJS.ProcessEnv) => {
|
||||
env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
|
||||
},
|
||||
},
|
||||
] as const)("$name", ({ mutate }) => {
|
||||
it("retains the snapshot cache when config contents change in place", () => {
|
||||
const config = createBraveAllowConfig();
|
||||
const env = createWebSearchEnv({ OPENCLAW_HOME: "/tmp/openclaw-home-a" });
|
||||
|
||||
expectSnapshotLoaderCalls({
|
||||
config,
|
||||
env,
|
||||
mutate: () => mutate(config, env),
|
||||
mutate: () => {
|
||||
config.plugins = { allow: ["perplexity"] };
|
||||
},
|
||||
expectedLoaderCalls: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates the snapshot cache when env contents change in place", () => {
|
||||
const config = createBraveAllowConfig();
|
||||
const env = createWebSearchEnv({ OPENCLAW_HOME: "/tmp/openclaw-home-a" });
|
||||
|
||||
expectSnapshotLoaderCalls({
|
||||
config,
|
||||
env,
|
||||
mutate: () => {
|
||||
env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
|
||||
},
|
||||
expectedLoaderCalls: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,84 +1,30 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
type PluginManifestRecord,
|
||||
} from "./manifest-registry.js";
|
||||
import { getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
||||
import { type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
import {
|
||||
mapRegistryProviders,
|
||||
resolveManifestDeclaredWebProviderCandidatePluginIds,
|
||||
} from "./web-provider-resolution-shared.js";
|
||||
import {
|
||||
createWebProviderSnapshotCache,
|
||||
resolvePluginWebProviders,
|
||||
resolveRuntimeWebProviders,
|
||||
} from "./web-provider-runtime-shared.js";
|
||||
import {
|
||||
resolveBundledWebSearchResolutionConfig,
|
||||
sortWebSearchProviders,
|
||||
} from "./web-search-providers.shared.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
type WebSearchProviderSnapshotCacheEntry = {
|
||||
expiresAt: number;
|
||||
providers: PluginWebSearchProviderEntry[];
|
||||
};
|
||||
let webSearchProviderSnapshotCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebSearchProviderSnapshotCacheEntry>>
|
||||
>();
|
||||
let webSearchProviderSnapshotCache = createWebProviderSnapshotCache<PluginWebSearchProviderEntry>();
|
||||
|
||||
function resetWebSearchProviderSnapshotCacheForTests() {
|
||||
webSearchProviderSnapshotCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, WebSearchProviderSnapshotCacheEntry>>
|
||||
>();
|
||||
webSearchProviderSnapshotCache = createWebProviderSnapshotCache<PluginWebSearchProviderEntry>();
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetWebSearchProviderSnapshotCacheForTests,
|
||||
} as const;
|
||||
function buildWebSearchSnapshotCacheKey(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat === true,
|
||||
origin: params.origin ?? "",
|
||||
onlyPluginIds: [...new Set(params.onlyPluginIds ?? [])].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
env: buildPluginSnapshotCacheEnvKey(params.env),
|
||||
});
|
||||
}
|
||||
|
||||
function pluginManifestDeclaresWebSearch(record: PluginManifestRecord): boolean {
|
||||
if ((record.contracts?.webSearchProviders?.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
const configUiHintKeys = Object.keys(record.configUiHints ?? {});
|
||||
if (configUiHintKeys.some((key) => key === "webSearch" || key.startsWith("webSearch."))) {
|
||||
return true;
|
||||
}
|
||||
if (!isRecord(record.configSchema)) {
|
||||
return false;
|
||||
}
|
||||
const properties = record.configSchema.properties;
|
||||
return isRecord(properties) && "webSearch" in properties;
|
||||
}
|
||||
|
||||
function resolveWebSearchCandidatePluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
@@ -87,86 +33,26 @@ function resolveWebSearchCandidatePluginIds(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): string[] | undefined {
|
||||
const contractIds = new Set(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: params.origin,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
}),
|
||||
);
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = loadPluginManifestRegistry({
|
||||
return resolveManifestDeclaredWebProviderCandidatePluginIds({
|
||||
contract: "webSearchProviders",
|
||||
configKey: "webSearch",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
(contractIds.has(plugin.id) || pluginManifestDeclaresWebSearch(plugin)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return ids.length > 0 ? ids : undefined;
|
||||
}
|
||||
|
||||
function resolveWebSearchLoadOptions(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
resolveBundledWebSearchResolutionConfig({
|
||||
...params,
|
||||
workspaceDir,
|
||||
env,
|
||||
});
|
||||
const onlyPluginIds = resolveWebSearchCandidatePluginIds({
|
||||
config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
});
|
||||
return {
|
||||
env,
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
...(onlyPluginIds ? { onlyPluginIds } : {}),
|
||||
logger: createPluginLoaderLogger(log),
|
||||
} satisfies PluginLoadOptions;
|
||||
}
|
||||
|
||||
function mapRegistryWebSearchProviders(params: {
|
||||
registry: ReturnType<typeof loadOpenClawPlugins>;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return sortWebSearchProviders(
|
||||
params.registry.webSearchProviders
|
||||
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
|
||||
.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
return mapRegistryProviders({
|
||||
entries: params.registry.webSearchProviders,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
sortProviders: sortWebSearchProviders,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginWebSearchProviders(params: {
|
||||
@@ -180,83 +66,12 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
mode?: "runtime" | "setup";
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir();
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
resolveWebSearchCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds,
|
||||
}),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: pluginIds,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return mapRegistryWebSearchProviders({ registry, onlyPluginIds: pluginIds });
|
||||
}
|
||||
const cacheOwnerConfig = params.config;
|
||||
const shouldMemoizeSnapshot =
|
||||
params.activate !== true && params.cache !== true && shouldUsePluginSnapshotCache(env);
|
||||
const cacheKey = buildWebSearchSnapshotCacheKey({
|
||||
config: cacheOwnerConfig,
|
||||
workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
env,
|
||||
return resolvePluginWebProviders(params, {
|
||||
snapshotCache: webSearchProviderSnapshotCache,
|
||||
resolveBundledResolutionConfig: resolveBundledWebSearchResolutionConfig,
|
||||
resolveCandidatePluginIds: resolveWebSearchCandidatePluginIds,
|
||||
mapRegistryProviders: mapRegistryWebSearchProviders,
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const configCache = webSearchProviderSnapshotCache.get(cacheOwnerConfig);
|
||||
const envCache = configCache?.get(env);
|
||||
const cached = envCache?.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.providers;
|
||||
}
|
||||
}
|
||||
const loadOptions = resolveWebSearchLoadOptions({ ...params, workspaceDir });
|
||||
// Prefer the compatible active registry so repeated runtime reads do not
|
||||
// re-import the same plugin set through the snapshot path.
|
||||
const resolved = mapRegistryWebSearchProviders({
|
||||
registry:
|
||||
resolveCompatibleRuntimePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions),
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
let configCache = webSearchProviderSnapshotCache.get(cacheOwnerConfig);
|
||||
if (!configCache) {
|
||||
configCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, WebSearchProviderSnapshotCacheEntry>
|
||||
>();
|
||||
webSearchProviderSnapshotCache.set(cacheOwnerConfig, configCache);
|
||||
}
|
||||
let envCache = configCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, WebSearchProviderSnapshotCacheEntry>();
|
||||
configCache.set(env, envCache);
|
||||
}
|
||||
envCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
providers: resolved,
|
||||
});
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function resolveRuntimeWebSearchProviders(params: {
|
||||
@@ -267,19 +82,10 @@ export function resolveRuntimeWebSearchProviders(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(
|
||||
params.config === undefined
|
||||
? undefined
|
||||
: resolveWebSearchLoadOptions({
|
||||
...params,
|
||||
workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(),
|
||||
}),
|
||||
);
|
||||
if (runtimeRegistry) {
|
||||
return mapRegistryWebSearchProviders({
|
||||
registry: runtimeRegistry,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
}
|
||||
return resolvePluginWebSearchProviders(params);
|
||||
return resolveRuntimeWebProviders(params, {
|
||||
snapshotCache: webSearchProviderSnapshotCache,
|
||||
resolveBundledResolutionConfig: resolveBundledWebSearchResolutionConfig,
|
||||
resolveCandidatePluginIds: resolveWebSearchCandidatePluginIds,
|
||||
mapRegistryProviders: mapRegistryWebSearchProviders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { type NormalizedPluginsConfig } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { resolveManifestContractPluginIds } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
function resolveBundledWebSearchCompatPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
function compareWebSearchProvidersAlphabetically(
|
||||
left: Pick<PluginWebSearchProviderEntry, "id" | "pluginId">,
|
||||
right: Pick<PluginWebSearchProviderEntry, "id" | "pluginId">,
|
||||
): number {
|
||||
return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId);
|
||||
}
|
||||
import {
|
||||
resolveBundledWebProviderResolutionConfig,
|
||||
sortPluginProviders,
|
||||
sortPluginProvidersForAutoDetect,
|
||||
} from "./web-provider-resolution-shared.js";
|
||||
|
||||
export function sortWebSearchProviders(
|
||||
providers: PluginWebSearchProviderEntry[],
|
||||
): PluginWebSearchProviderEntry[] {
|
||||
return providers.toSorted(compareWebSearchProvidersAlphabetically);
|
||||
return sortPluginProviders(providers);
|
||||
}
|
||||
|
||||
export function sortWebSearchProvidersForAutoDetect(
|
||||
providers: PluginWebSearchProviderEntry[],
|
||||
): PluginWebSearchProviderEntry[] {
|
||||
return providers.toSorted((left, right) => {
|
||||
const leftOrder = left.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightOrder = right.autoDetectOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
return compareWebSearchProvidersAlphabetically(left, right);
|
||||
});
|
||||
return sortPluginProvidersForAutoDetect(providers);
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchResolutionConfig(params: {
|
||||
@@ -55,23 +30,11 @@ export function resolveBundledWebSearchResolutionConfig(params: {
|
||||
activationSourceConfig?: PluginLoadOptions["config"];
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
} {
|
||||
const activation = resolveBundledPluginCompatibleActivationInputs({
|
||||
rawConfig: params.config,
|
||||
env: params.env,
|
||||
return resolveBundledWebProviderResolutionConfig({
|
||||
contract: "webSearchProviders",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
applyAutoEnable: true,
|
||||
compatMode: {
|
||||
allowlist: params.bundledAllowlistCompat,
|
||||
enablement: "always",
|
||||
vitest: true,
|
||||
},
|
||||
resolveCompatPluginIds: resolveBundledWebSearchCompatPluginIds,
|
||||
env: params.env,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
});
|
||||
|
||||
return {
|
||||
config: activation.config,
|
||||
normalized: activation.normalized,
|
||||
activationSourceConfig: activation.activationSourceConfig,
|
||||
autoEnabledReasons: activation.autoEnabledReasons,
|
||||
};
|
||||
}
|
||||
|
||||
459
src/secrets/runtime-web-tools.shared.ts
Normal file
459
src/secrets/runtime-web-tools.shared.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../plugins/manifest-registry.js";
|
||||
import type { ResolverContext, SecretDefaults } from "./runtime-shared.js";
|
||||
import { pushInactiveSurfaceWarning, pushWarning } from "./runtime-shared.js";
|
||||
import type { RuntimeWebDiagnostic, RuntimeWebDiagnosticCode } from "./runtime-web-tools.types.js";
|
||||
|
||||
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: string;
|
||||
noFallbackCode: string;
|
||||
autoDetectSelectedCode: string;
|
||||
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 isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
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 {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
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: RuntimeWebDiagnosticCode;
|
||||
sourceConfig: OpenClawConfig;
|
||||
context: ResolverContext;
|
||||
resolveProviders: (params: { configuredBundledPluginId?: string }) => TProvider[];
|
||||
sortProviders: (providers: TProvider[]) => TProvider[];
|
||||
readConfiguredCredential: (params: {
|
||||
provider: TProvider;
|
||||
config: OpenClawConfig;
|
||||
toolConfig: TToolConfig;
|
||||
}) => unknown;
|
||||
ignoreKeylessProvidersForConfiguredSurface?: boolean;
|
||||
emptyProvidersWhenSurfaceMissing?: boolean;
|
||||
normalizeConfiguredProviderAgainstActiveProviders?: boolean;
|
||||
};
|
||||
|
||||
export function resolveRuntimeWebProviderSurface<
|
||||
TProvider extends {
|
||||
id: string;
|
||||
requiresCredential?: boolean;
|
||||
},
|
||||
TToolConfig extends Record<string, unknown> | undefined,
|
||||
>(
|
||||
params: ResolveRuntimeWebProviderSurfaceParams<TProvider, TToolConfig>,
|
||||
): RuntimeWebProviderSurface<TProvider> {
|
||||
const configuredBundledPluginId = resolveManifestContractOwnerPluginId({
|
||||
contract: params.contract,
|
||||
value: params.rawProvider,
|
||||
origin: "bundled",
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
});
|
||||
|
||||
const allProviders = params.sortProviders(
|
||||
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}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import {
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import { resolveManifestContractPluginIds } from "../plugins/manifest-registry.js";
|
||||
import type {
|
||||
PluginWebFetchProviderEntry,
|
||||
PluginWebSearchProviderEntry,
|
||||
@@ -17,12 +14,15 @@ import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-provi
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { secretRefKey } from "./ref-contract.js";
|
||||
import { resolveSecretRefValues } from "./resolve.js";
|
||||
import type { ResolverContext, SecretDefaults } from "./runtime-shared.js";
|
||||
import {
|
||||
pushInactiveSurfaceWarning,
|
||||
pushWarning,
|
||||
type ResolverContext,
|
||||
type SecretDefaults,
|
||||
} from "./runtime-shared.js";
|
||||
ensureObject,
|
||||
hasConfiguredSecretRef,
|
||||
isRecord,
|
||||
resolveRuntimeWebProviderSurface,
|
||||
resolveRuntimeWebProviderSelection,
|
||||
type SecretResolutionResult,
|
||||
} from "./runtime-web-tools.shared.js";
|
||||
import type {
|
||||
RuntimeWebDiagnostic,
|
||||
RuntimeWebDiagnosticCode,
|
||||
@@ -31,9 +31,6 @@ import type {
|
||||
RuntimeWebToolsMetadata,
|
||||
} from "./runtime-web-tools.types.js";
|
||||
|
||||
type WebSearchProvider = string;
|
||||
type WebFetchProvider = string;
|
||||
|
||||
export type {
|
||||
RuntimeWebDiagnostic,
|
||||
RuntimeWebDiagnosticCode,
|
||||
@@ -48,18 +45,9 @@ type FetchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
type SecretResolutionResult = {
|
||||
value?: string;
|
||||
source: WebSearchCredentialResolutionSource | WebFetchCredentialResolutionSource;
|
||||
secretRefConfigured: boolean;
|
||||
unresolvedRefReason?: string;
|
||||
fallbackEnvVar?: string;
|
||||
fallbackUsedAfterRefFailure: boolean;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
type SecretResolutionSource =
|
||||
| WebSearchCredentialResolutionSource
|
||||
| WebFetchCredentialResolutionSource;
|
||||
|
||||
function hasPluginWebToolConfig(config: OpenClawConfig): boolean {
|
||||
const entries = config.plugins?.entries;
|
||||
@@ -75,34 +63,6 @@ function hasPluginWebToolConfig(config: OpenClawConfig): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeProvider(
|
||||
value: unknown,
|
||||
providers: ReturnType<typeof resolvePluginWebSearchProviders>,
|
||||
): WebSearchProvider | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (providers.some((provider) => provider.id === normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeFetchProvider(
|
||||
value: unknown,
|
||||
providers: PluginWebFetchProviderEntry[],
|
||||
): WebFetchProvider | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (providers.some((provider) => provider.id === normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasCustomWebSearchPluginRisk(config: OpenClawConfig): boolean {
|
||||
const plugins = config.plugins;
|
||||
if (!plugins) {
|
||||
@@ -172,7 +132,7 @@ async function resolveSecretInputWithEnvFallback(params: {
|
||||
path: string;
|
||||
envVars: string[];
|
||||
restrictEnvRefsToEnvVars?: boolean;
|
||||
}): Promise<SecretResolutionResult> {
|
||||
}): Promise<SecretResolutionResult<SecretResolutionSource>> {
|
||||
const { ref } = resolveSecretInputRef({
|
||||
value: params.value,
|
||||
defaults: params.defaults,
|
||||
@@ -277,16 +237,6 @@ async function resolveSecretInputWithEnvFallback(params: {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function setResolvedWebSearchApiKey(params: {
|
||||
resolvedConfig: OpenClawConfig;
|
||||
provider: PluginWebSearchProviderEntry;
|
||||
@@ -304,10 +254,6 @@ function setResolvedWebSearchApiKey(params: {
|
||||
params.provider.setCredentialValue(search, params.value);
|
||||
}
|
||||
|
||||
function keyPathForProvider(provider: PluginWebSearchProviderEntry): string {
|
||||
return provider.credentialPath;
|
||||
}
|
||||
|
||||
function readConfiguredProviderCredential(params: {
|
||||
provider: PluginWebSearchProviderEntry;
|
||||
config: OpenClawConfig;
|
||||
@@ -341,10 +287,6 @@ function setResolvedWebFetchApiKey(params: {
|
||||
params.provider.setCredentialValue(fetch, params.value);
|
||||
}
|
||||
|
||||
function keyPathForFetchProvider(provider: PluginWebFetchProviderEntry): string {
|
||||
return provider.credentialPath;
|
||||
}
|
||||
|
||||
function readConfiguredFetchProviderCredential(params: {
|
||||
provider: PluginWebFetchProviderEntry;
|
||||
config: OpenClawConfig;
|
||||
@@ -363,15 +305,6 @@ function inactivePathsForFetchProvider(provider: PluginWebFetchProviderEntry): s
|
||||
: [provider.credentialPath];
|
||||
}
|
||||
|
||||
function hasConfiguredSecretRef(value: unknown, defaults: SecretDefaults | undefined): boolean {
|
||||
return Boolean(
|
||||
resolveSecretInputRef({
|
||||
value,
|
||||
defaults,
|
||||
}).ref,
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveRuntimeWebTools(params: {
|
||||
sourceConfig: OpenClawConfig;
|
||||
resolvedConfig: OpenClawConfig;
|
||||
@@ -413,594 +346,215 @@ export async function resolveRuntimeWebTools(params: {
|
||||
}
|
||||
const rawProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledPluginId = resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: rawProvider,
|
||||
origin: "bundled",
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
});
|
||||
|
||||
const searchMetadata: RuntimeWebSearchMetadata = {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
const searchProviders = sortWebSearchProvidersForAutoDetect(
|
||||
configuredBundledPluginId
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: !hasCustomWebSearchPluginRisk(params.sourceConfig)
|
||||
const searchSurface = resolveRuntimeWebProviderSurface({
|
||||
contract: "webSearchProviders",
|
||||
rawProvider,
|
||||
providerPath: "tools.web.search.provider",
|
||||
toolConfig: search,
|
||||
diagnostics,
|
||||
metadataDiagnostics: searchMetadata.diagnostics,
|
||||
invalidAutoDetectCode: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT",
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
resolveProviders: ({ configuredBundledPluginId }) =>
|
||||
configuredBundledPluginId
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
}),
|
||||
);
|
||||
const searchConfigured = Boolean(search);
|
||||
const hasConfiguredSearchSurface =
|
||||
searchConfigured ||
|
||||
searchProviders.some((provider) => {
|
||||
if (provider.requiresCredential === false) {
|
||||
return false;
|
||||
}
|
||||
const value = readConfiguredProviderCredential({
|
||||
: !hasCustomWebSearchPluginRisk(params.sourceConfig)
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
}),
|
||||
sortProviders: sortWebSearchProvidersForAutoDetect,
|
||||
readConfiguredCredential: ({ provider, config, toolConfig }) =>
|
||||
readConfiguredProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
search,
|
||||
});
|
||||
return value !== undefined;
|
||||
});
|
||||
const searchEnabled = hasConfiguredSearchSurface && search?.enabled !== false;
|
||||
const providers = hasConfiguredSearchSurface ? searchProviders : [];
|
||||
const configuredProvider = normalizeProvider(rawProvider, providers);
|
||||
config,
|
||||
search: toolConfig,
|
||||
}),
|
||||
ignoreKeylessProvidersForConfiguredSurface: true,
|
||||
emptyProvidersWhenSurfaceMissing: true,
|
||||
normalizeConfiguredProviderAgainstActiveProviders: true,
|
||||
});
|
||||
|
||||
if (rawProvider && !configuredProvider) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT",
|
||||
message: `tools.web.search.provider is "${rawProvider}". Falling back to auto-detect precedence.`,
|
||||
path: "tools.web.search.provider",
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
searchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT",
|
||||
path: "tools.web.search.provider",
|
||||
message: diagnostic.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (configuredProvider) {
|
||||
searchMetadata.providerConfigured = configuredProvider;
|
||||
searchMetadata.providerSource = "configured";
|
||||
}
|
||||
|
||||
if (searchEnabled) {
|
||||
const candidates = configuredProvider
|
||||
? providers.filter((provider) => provider.id === configuredProvider)
|
||||
: providers;
|
||||
const unresolvedWithoutFallback: Array<{
|
||||
provider: WebSearchProvider;
|
||||
path: string;
|
||||
reason: string;
|
||||
}> = [];
|
||||
|
||||
let selectedProvider: WebSearchProvider | undefined;
|
||||
let selectedResolution: SecretResolutionResult | undefined;
|
||||
let keylessFallbackProvider: PluginWebSearchProviderEntry | undefined;
|
||||
|
||||
for (const provider of candidates) {
|
||||
if (provider.requiresCredential === false) {
|
||||
if (!keylessFallbackProvider) {
|
||||
keylessFallbackProvider = provider;
|
||||
}
|
||||
if (configuredProvider) {
|
||||
selectedProvider = provider.id;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const path = keyPathForProvider(provider);
|
||||
const value = readConfiguredProviderCredential({
|
||||
await resolveRuntimeWebProviderSelection({
|
||||
scopePath: "tools.web.search",
|
||||
toolConfig: search,
|
||||
enabled: searchSurface.enabled,
|
||||
providers: searchSurface.providers,
|
||||
configuredProvider: searchSurface.configuredProvider,
|
||||
metadata: searchMetadata,
|
||||
diagnostics,
|
||||
sourceConfig: params.sourceConfig,
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
deferKeylessFallback: true,
|
||||
fallbackUsedCode: "WEB_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
noFallbackCode: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
autoDetectSelectedCode: "WEB_SEARCH_AUTODETECT_SELECTED",
|
||||
readConfiguredCredential: ({ provider, config, toolConfig }) =>
|
||||
readConfiguredProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
search,
|
||||
});
|
||||
const resolution = await resolveSecretInputWithEnvFallback({
|
||||
config,
|
||||
search: toolConfig,
|
||||
}),
|
||||
resolveSecretInput: ({ value, path, envVars }) =>
|
||||
resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
value,
|
||||
path,
|
||||
envVars: provider.envVars,
|
||||
});
|
||||
|
||||
if (resolution.secretRefConfigured && resolution.fallbackUsedAfterRefFailure) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
message:
|
||||
`${path} SecretRef could not be resolved; using ${resolution.fallbackEnvVar ?? "env fallback"}. ` +
|
||||
(resolution.unresolvedRefReason ?? "").trim(),
|
||||
path,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
searchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
path,
|
||||
message: diagnostic.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolution.secretRefConfigured && !resolution.value && resolution.unresolvedRefReason) {
|
||||
unresolvedWithoutFallback.push({
|
||||
provider: provider.id,
|
||||
path,
|
||||
reason: resolution.unresolvedRefReason,
|
||||
});
|
||||
}
|
||||
|
||||
if (configuredProvider) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = resolution;
|
||||
if (resolution.value) {
|
||||
setResolvedWebSearchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (resolution.value) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = resolution;
|
||||
setResolvedWebSearchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedProvider && keylessFallbackProvider) {
|
||||
selectedProvider = keylessFallbackProvider.id;
|
||||
selectedResolution = {
|
||||
source: "missing",
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
}
|
||||
|
||||
const failUnresolvedSearchNoFallback = (unresolved: { path: string; reason: string }) => {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
message: unresolved.reason,
|
||||
path: unresolved.path,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
searchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
path: unresolved.path,
|
||||
message: unresolved.reason,
|
||||
});
|
||||
throw new Error(`[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK] ${unresolved.reason}`);
|
||||
};
|
||||
|
||||
if (configuredProvider) {
|
||||
const unresolved = unresolvedWithoutFallback[0];
|
||||
if (unresolved) {
|
||||
failUnresolvedSearchNoFallback(unresolved);
|
||||
}
|
||||
} else {
|
||||
if (!selectedProvider && unresolvedWithoutFallback.length > 0) {
|
||||
failUnresolvedSearchNoFallback(unresolvedWithoutFallback[0]);
|
||||
}
|
||||
|
||||
if (selectedProvider) {
|
||||
const selectedProviderEntry = providers.find((entry) => entry.id === selectedProvider);
|
||||
const selectedDetails =
|
||||
selectedProviderEntry?.requiresCredential === false
|
||||
? `tools.web.search auto-detected keyless provider "${selectedProvider}" as the default fallback.`
|
||||
: `tools.web.search auto-detected provider "${selectedProvider}" from available credentials.`;
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_SEARCH_AUTODETECT_SELECTED",
|
||||
message: selectedDetails,
|
||||
path: "tools.web.search.provider",
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
searchMetadata.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider) {
|
||||
searchMetadata.selectedProvider = selectedProvider;
|
||||
searchMetadata.selectedProviderKeySource = selectedResolution?.source;
|
||||
if (!configuredProvider) {
|
||||
searchMetadata.providerSource = "auto-detect";
|
||||
}
|
||||
const provider = providers.find((entry) => entry.id === selectedProvider);
|
||||
if (provider?.resolveRuntimeMetadata) {
|
||||
Object.assign(
|
||||
searchMetadata,
|
||||
await provider.resolveRuntimeMetadata({
|
||||
config: params.sourceConfig,
|
||||
searchConfig: search,
|
||||
runtimeMetadata: searchMetadata,
|
||||
resolvedCredential: selectedResolution
|
||||
? {
|
||||
value: selectedResolution.value,
|
||||
source: selectedResolution.source,
|
||||
fallbackEnvVar: selectedResolution.fallbackEnvVar,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchEnabled && !configuredProvider && searchMetadata.selectedProvider) {
|
||||
for (const provider of providers) {
|
||||
if (provider.id === searchMetadata.selectedProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = readConfiguredProviderCredential({
|
||||
envVars,
|
||||
}),
|
||||
setResolvedCredential: ({ resolvedConfig, provider, value }) =>
|
||||
setResolvedWebSearchApiKey({
|
||||
resolvedConfig,
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
search,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
value,
|
||||
}),
|
||||
inactivePathsForProvider,
|
||||
hasConfiguredSecretRef,
|
||||
mergeRuntimeMetadata: async ({ provider, metadata, toolConfig, selectedResolution }) => {
|
||||
if (!provider.resolveRuntimeMetadata) {
|
||||
return;
|
||||
}
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search auto-detected provider is "${searchMetadata.selectedProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (search && !searchEnabled) {
|
||||
for (const provider of providers) {
|
||||
const value = readConfiguredProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
search,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: "tools.web.search is disabled.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchEnabled && search && configuredProvider) {
|
||||
for (const provider of providers) {
|
||||
if (provider.id === configuredProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = readConfiguredProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
search,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search.provider is "${configuredProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.assign(
|
||||
metadata,
|
||||
await provider.resolveRuntimeMetadata({
|
||||
config: params.sourceConfig,
|
||||
searchConfig: toolConfig,
|
||||
runtimeMetadata: metadata,
|
||||
resolvedCredential: selectedResolution
|
||||
? {
|
||||
value: selectedResolution.value,
|
||||
source: selectedResolution.source,
|
||||
fallbackEnvVar: selectedResolution.fallbackEnvVar,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const rawFetchProvider =
|
||||
typeof fetch?.provider === "string" ? fetch.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledFetchPluginId = resolveManifestContractOwnerPluginId({
|
||||
contract: "webFetchProviders",
|
||||
value: rawFetchProvider,
|
||||
origin: "bundled",
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
});
|
||||
const fetchMetadata: RuntimeWebFetchMetadata = {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
};
|
||||
const fetchProviders = sortWebFetchProvidersForAutoDetect(
|
||||
configuredBundledFetchPluginId
|
||||
? resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledFetchPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
const hasConfiguredFetchSurface =
|
||||
Boolean(fetch) ||
|
||||
fetchProviders.some((provider) => {
|
||||
const value = readConfiguredFetchProviderCredential({
|
||||
const fetchSurface = resolveRuntimeWebProviderSurface({
|
||||
contract: "webFetchProviders",
|
||||
rawProvider: rawFetchProvider,
|
||||
providerPath: "tools.web.fetch.provider",
|
||||
toolConfig: fetch,
|
||||
diagnostics,
|
||||
metadataDiagnostics: fetchMetadata.diagnostics,
|
||||
invalidAutoDetectCode: "WEB_FETCH_PROVIDER_INVALID_AUTODETECT",
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
resolveProviders: ({ configuredBundledPluginId }) =>
|
||||
configuredBundledPluginId
|
||||
? resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
sortProviders: sortWebFetchProvidersForAutoDetect,
|
||||
readConfiguredCredential: ({ provider, config, toolConfig }) =>
|
||||
readConfiguredFetchProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
fetch,
|
||||
});
|
||||
return value !== undefined;
|
||||
});
|
||||
const fetchEnabled = hasConfiguredFetchSurface && fetch?.enabled !== false;
|
||||
const configuredFetchProvider = normalizeFetchProvider(rawFetchProvider, fetchProviders);
|
||||
config,
|
||||
fetch: toolConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
if (rawFetchProvider && !configuredFetchProvider) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_FETCH_PROVIDER_INVALID_AUTODETECT",
|
||||
message: `tools.web.fetch.provider is "${rawFetchProvider}". Falling back to auto-detect precedence.`,
|
||||
path: "tools.web.fetch.provider",
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
fetchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_FETCH_PROVIDER_INVALID_AUTODETECT",
|
||||
path: "tools.web.fetch.provider",
|
||||
message: diagnostic.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (configuredFetchProvider) {
|
||||
fetchMetadata.providerConfigured = configuredFetchProvider;
|
||||
fetchMetadata.providerSource = "configured";
|
||||
}
|
||||
|
||||
if (fetchEnabled) {
|
||||
const candidates = configuredFetchProvider
|
||||
? fetchProviders.filter((provider) => provider.id === configuredFetchProvider)
|
||||
: fetchProviders;
|
||||
const unresolvedWithoutFallback: Array<{
|
||||
provider: WebFetchProvider;
|
||||
path: string;
|
||||
reason: string;
|
||||
}> = [];
|
||||
|
||||
let selectedProvider: WebFetchProvider | undefined;
|
||||
let selectedResolution: SecretResolutionResult | undefined;
|
||||
|
||||
for (const provider of candidates) {
|
||||
if (provider.requiresCredential === false) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = {
|
||||
source: "missing",
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
break;
|
||||
}
|
||||
const path = keyPathForFetchProvider(provider);
|
||||
const value = readConfiguredFetchProviderCredential({
|
||||
await resolveRuntimeWebProviderSelection({
|
||||
scopePath: "tools.web.fetch",
|
||||
toolConfig: fetch,
|
||||
enabled: fetchSurface.enabled,
|
||||
providers: fetchSurface.providers,
|
||||
configuredProvider: fetchSurface.configuredProvider,
|
||||
metadata: fetchMetadata,
|
||||
diagnostics,
|
||||
sourceConfig: params.sourceConfig,
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
deferKeylessFallback: false,
|
||||
fallbackUsedCode: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
noFallbackCode: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
autoDetectSelectedCode: "WEB_FETCH_AUTODETECT_SELECTED",
|
||||
readConfiguredCredential: ({ provider, config, toolConfig }) =>
|
||||
readConfiguredFetchProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
fetch,
|
||||
});
|
||||
const resolution = await resolveSecretInputWithEnvFallback({
|
||||
config,
|
||||
fetch: toolConfig,
|
||||
}),
|
||||
resolveSecretInput: ({ value, path, envVars }) =>
|
||||
resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
value,
|
||||
path,
|
||||
envVars: provider.envVars,
|
||||
envVars,
|
||||
restrictEnvRefsToEnvVars: true,
|
||||
});
|
||||
|
||||
if (resolution.secretRefConfigured && resolution.fallbackUsedAfterRefFailure) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
message:
|
||||
`${path} SecretRef could not be resolved; using ${resolution.fallbackEnvVar ?? "env fallback"}. ` +
|
||||
(resolution.unresolvedRefReason ?? "").trim(),
|
||||
path,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
fetchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
path,
|
||||
message: diagnostic.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolution.secretRefConfigured && !resolution.value && resolution.unresolvedRefReason) {
|
||||
unresolvedWithoutFallback.push({
|
||||
provider: provider.id,
|
||||
path,
|
||||
reason: resolution.unresolvedRefReason,
|
||||
});
|
||||
}
|
||||
|
||||
if (configuredFetchProvider) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = resolution;
|
||||
if (resolution.value) {
|
||||
setResolvedWebFetchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (resolution.value) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = resolution;
|
||||
setResolvedWebFetchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const failUnresolvedFetchNoFallback = (unresolved: { path: string; reason: string }) => {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
message: unresolved.reason,
|
||||
path: unresolved.path,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
fetchMetadata.diagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_FETCH_PROVIDER_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
path: unresolved.path,
|
||||
message: unresolved.reason,
|
||||
});
|
||||
throw new Error(`[WEB_FETCH_PROVIDER_KEY_UNRESOLVED_NO_FALLBACK] ${unresolved.reason}`);
|
||||
};
|
||||
|
||||
if (configuredFetchProvider) {
|
||||
const unresolved = unresolvedWithoutFallback[0];
|
||||
if (unresolved) {
|
||||
failUnresolvedFetchNoFallback(unresolved);
|
||||
}
|
||||
} else {
|
||||
if (!selectedProvider && unresolvedWithoutFallback.length > 0) {
|
||||
failUnresolvedFetchNoFallback(unresolvedWithoutFallback[0]);
|
||||
}
|
||||
|
||||
if (selectedProvider) {
|
||||
const selectedProviderEntry = fetchProviders.find((entry) => entry.id === selectedProvider);
|
||||
const selectedDetails =
|
||||
selectedProviderEntry?.requiresCredential === false
|
||||
? `tools.web.fetch auto-detected keyless provider "${selectedProvider}" as the default fallback.`
|
||||
: `tools.web.fetch auto-detected provider "${selectedProvider}" from available credentials.`;
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_FETCH_AUTODETECT_SELECTED",
|
||||
message: selectedDetails,
|
||||
path: "tools.web.fetch.provider",
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
fetchMetadata.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider) {
|
||||
fetchMetadata.selectedProvider = selectedProvider;
|
||||
fetchMetadata.selectedProviderKeySource = selectedResolution?.source;
|
||||
if (!configuredFetchProvider) {
|
||||
fetchMetadata.providerSource = "auto-detect";
|
||||
}
|
||||
const provider = fetchProviders.find((entry) => entry.id === selectedProvider);
|
||||
if (provider?.resolveRuntimeMetadata) {
|
||||
Object.assign(
|
||||
fetchMetadata,
|
||||
await provider.resolveRuntimeMetadata({
|
||||
config: params.sourceConfig,
|
||||
fetchConfig: fetch,
|
||||
runtimeMetadata: fetchMetadata,
|
||||
resolvedCredential: selectedResolution
|
||||
? {
|
||||
value: selectedResolution.value,
|
||||
source: selectedResolution.source,
|
||||
fallbackEnvVar: selectedResolution.fallbackEnvVar,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchEnabled && !configuredFetchProvider && fetchMetadata.selectedProvider) {
|
||||
for (const provider of fetchProviders) {
|
||||
if (provider.id === fetchMetadata.selectedProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = readConfiguredFetchProviderCredential({
|
||||
}),
|
||||
setResolvedCredential: ({ resolvedConfig, provider, value }) =>
|
||||
setResolvedWebFetchApiKey({
|
||||
resolvedConfig,
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
fetch,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
value,
|
||||
}),
|
||||
inactivePathsForProvider: inactivePathsForFetchProvider,
|
||||
hasConfiguredSecretRef,
|
||||
mergeRuntimeMetadata: async ({ provider, metadata, toolConfig, selectedResolution }) => {
|
||||
if (!provider.resolveRuntimeMetadata) {
|
||||
return;
|
||||
}
|
||||
for (const path of inactivePathsForFetchProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.fetch auto-detected provider is "${fetchMetadata.selectedProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fetch && !fetchEnabled) {
|
||||
for (const provider of fetchProviders) {
|
||||
const value = readConfiguredFetchProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
fetch,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
for (const path of inactivePathsForFetchProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: "tools.web.fetch is disabled.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchEnabled && fetch && configuredFetchProvider) {
|
||||
for (const provider of fetchProviders) {
|
||||
if (provider.id === configuredFetchProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = readConfiguredFetchProviderCredential({
|
||||
provider,
|
||||
config: params.sourceConfig,
|
||||
fetch,
|
||||
});
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
for (const path of inactivePathsForFetchProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.fetch.provider is "${configuredFetchProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.assign(
|
||||
metadata,
|
||||
await provider.resolveRuntimeMetadata({
|
||||
config: params.sourceConfig,
|
||||
fetchConfig: toolConfig,
|
||||
runtimeMetadata: metadata,
|
||||
resolvedCredential: selectedResolution
|
||||
? {
|
||||
value: selectedResolution.value,
|
||||
source: selectedResolution.source,
|
||||
fallbackEnvVar: selectedResolution.fallbackEnvVar,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
search: searchMetadata,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import type {
|
||||
PluginWebFetchProviderEntry,
|
||||
@@ -9,7 +8,13 @@ import { resolvePluginWebFetchProviders } from "../plugins/web-fetch-providers.r
|
||||
import { sortWebFetchProvidersForAutoDetect } from "../plugins/web-fetch-providers.shared.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
|
||||
import type { RuntimeWebFetchMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import {
|
||||
hasWebProviderEntryCredential,
|
||||
providerRequiresCredential,
|
||||
readWebProviderEnvValue,
|
||||
resolveWebProviderConfig,
|
||||
resolveWebProviderDefinition,
|
||||
} from "../web/provider-runtime-shared.js";
|
||||
|
||||
type WebFetchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { fetch?: infer Fetch }
|
||||
@@ -26,11 +31,7 @@ export type ResolveWebFetchDefinitionParams = {
|
||||
};
|
||||
|
||||
function resolveFetchConfig(cfg?: OpenClawConfig): WebFetchConfig {
|
||||
const fetch = cfg?.tools?.web?.fetch;
|
||||
if (!fetch || typeof fetch !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return fetch as WebFetchConfig;
|
||||
return resolveWebProviderConfig<"fetch", NonNullable<WebFetchConfig>>(cfg, "fetch");
|
||||
}
|
||||
|
||||
export function resolveWebFetchEnabled(params: {
|
||||
@@ -43,22 +44,6 @@ export function resolveWebFetchEnabled(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function providerRequiresCredential(
|
||||
provider: Pick<PluginWebFetchProviderEntry, "requiresCredential">,
|
||||
): boolean {
|
||||
return provider.requiresCredential !== false;
|
||||
}
|
||||
|
||||
function hasEntryCredential(
|
||||
provider: Pick<
|
||||
PluginWebFetchProviderEntry,
|
||||
@@ -67,19 +52,16 @@ function hasEntryCredential(
|
||||
config: OpenClawConfig | undefined,
|
||||
fetch: WebFetchConfig | undefined,
|
||||
): boolean {
|
||||
if (!providerRequiresCredential(provider)) {
|
||||
return true;
|
||||
}
|
||||
const configuredValue = provider.getConfiguredCredentialValue?.(config);
|
||||
const rawValue = configuredValue ?? provider.getCredentialValue(fetch as Record<string, unknown>);
|
||||
const configuredRef = resolveSecretInputRef({
|
||||
value: rawValue,
|
||||
}).ref;
|
||||
if (configuredRef && configuredRef.source !== "env") {
|
||||
return true;
|
||||
}
|
||||
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
|
||||
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
|
||||
return hasWebProviderEntryCredential({
|
||||
provider,
|
||||
config,
|
||||
toolConfig: fetch as Record<string, unknown> | undefined,
|
||||
resolveRawValue: ({ provider: currentProvider, config: currentConfig, toolConfig }) =>
|
||||
currentProvider.getConfiguredCredentialValue?.(currentConfig) ??
|
||||
currentProvider.getCredentialValue(toolConfig),
|
||||
resolveEnvValue: ({ provider: currentProvider }) =>
|
||||
readWebProviderEnvValue(currentProvider.envVars),
|
||||
});
|
||||
}
|
||||
|
||||
export function listWebFetchProviders(params?: {
|
||||
@@ -150,42 +132,36 @@ export function resolveWebFetchDefinition(
|
||||
): { provider: PluginWebFetchProviderEntry; definition: WebFetchProviderToolDefinition } | null {
|
||||
const fetch = resolveFetchConfig(options?.config);
|
||||
const runtimeWebFetch = options?.runtimeWebFetch ?? getActiveRuntimeWebToolsMetadata()?.fetch;
|
||||
if (!resolveWebFetchEnabled({ fetch, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providers = sortWebFetchProvidersForAutoDetect(
|
||||
resolvePluginWebFetchProviders({
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
).filter(Boolean);
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerId =
|
||||
options?.providerId ??
|
||||
runtimeWebFetch?.selectedProvider ??
|
||||
runtimeWebFetch?.providerConfigured ??
|
||||
resolveWebFetchProviderId({ config: options?.config, fetch, providers });
|
||||
if (!providerId) {
|
||||
return null;
|
||||
}
|
||||
const provider = providers.find((entry) => entry.id === providerId);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = provider.createTool({
|
||||
);
|
||||
return resolveWebProviderDefinition({
|
||||
config: options?.config,
|
||||
fetchConfig: fetch as Record<string, unknown> | undefined,
|
||||
toolConfig: fetch as Record<string, unknown> | undefined,
|
||||
runtimeMetadata: runtimeWebFetch,
|
||||
sandboxed: options?.sandboxed,
|
||||
providerId: options?.providerId,
|
||||
providers,
|
||||
resolveEnabled: ({ toolConfig, sandboxed }) =>
|
||||
resolveWebFetchEnabled({
|
||||
fetch: toolConfig as WebFetchConfig | undefined,
|
||||
sandboxed,
|
||||
}),
|
||||
resolveAutoProviderId: ({ config, toolConfig, providers }) =>
|
||||
resolveWebFetchProviderId({
|
||||
config,
|
||||
fetch: toolConfig as WebFetchConfig | undefined,
|
||||
providers,
|
||||
}),
|
||||
createTool: ({ provider, config, toolConfig, runtimeMetadata }) =>
|
||||
provider.createTool({
|
||||
config,
|
||||
fetchConfig: toolConfig,
|
||||
runtimeMetadata,
|
||||
}),
|
||||
});
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { provider, definition };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import type {
|
||||
PluginWebSearchProviderEntry,
|
||||
@@ -10,7 +9,13 @@ import { resolveRuntimeWebSearchProviders } from "../plugins/web-search-provider
|
||||
import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import {
|
||||
hasWebProviderEntryCredential,
|
||||
providerRequiresCredential,
|
||||
readWebProviderEnvValue,
|
||||
resolveWebProviderConfig,
|
||||
resolveWebProviderDefinition,
|
||||
} from "../web/provider-runtime-shared.js";
|
||||
|
||||
type WebSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { search?: infer Search }
|
||||
@@ -31,11 +36,7 @@ export type RunWebSearchParams = ResolveWebSearchDefinitionParams & {
|
||||
};
|
||||
|
||||
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
||||
const search = cfg?.tools?.web?.search;
|
||||
if (!search || typeof search !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return search as WebSearchConfig;
|
||||
return resolveWebProviderConfig<"search", NonNullable<WebSearchConfig>>(cfg, "search");
|
||||
}
|
||||
|
||||
export function resolveWebSearchEnabled(params: {
|
||||
@@ -51,22 +52,6 @@ export function resolveWebSearchEnabled(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function providerRequiresCredential(
|
||||
provider: Pick<PluginWebSearchProviderEntry, "requiresCredential">,
|
||||
): boolean {
|
||||
return provider.requiresCredential !== false;
|
||||
}
|
||||
|
||||
function hasEntryCredential(
|
||||
provider: Pick<
|
||||
PluginWebSearchProviderEntry,
|
||||
@@ -80,28 +65,17 @@ function hasEntryCredential(
|
||||
config: OpenClawConfig | undefined,
|
||||
search: WebSearchConfig | undefined,
|
||||
): boolean {
|
||||
if (!providerRequiresCredential(provider)) {
|
||||
return true;
|
||||
}
|
||||
const configuredValue = provider.getConfiguredCredentialValue?.(config);
|
||||
const rawValue =
|
||||
configuredValue ??
|
||||
(provider.id === "brave"
|
||||
? provider.getCredentialValue(search as Record<string, unknown> | undefined)
|
||||
: undefined);
|
||||
const configuredRef = resolveSecretInputRef({
|
||||
value: rawValue,
|
||||
}).ref;
|
||||
if (configuredRef && configuredRef.source !== "env") {
|
||||
return true;
|
||||
}
|
||||
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
|
||||
if (configuredRef?.source === "env") {
|
||||
return Boolean(
|
||||
normalizeSecretInput(process.env[configuredRef.id]) || readProviderEnvValue(provider.envVars),
|
||||
);
|
||||
}
|
||||
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
|
||||
return hasWebProviderEntryCredential({
|
||||
provider,
|
||||
config,
|
||||
toolConfig: search as Record<string, unknown> | undefined,
|
||||
resolveRawValue: ({ provider: currentProvider, config: currentConfig, toolConfig }) =>
|
||||
currentProvider.getConfiguredCredentialValue?.(currentConfig) ??
|
||||
(currentProvider.id === "brave" ? currentProvider.getCredentialValue(toolConfig) : undefined),
|
||||
resolveEnvValue: ({ provider: currentProvider, configuredEnvVarId }) =>
|
||||
(configuredEnvVarId ? readWebProviderEnvValue([configuredEnvVarId]) : undefined) ??
|
||||
readWebProviderEnvValue(currentProvider.envVars),
|
||||
});
|
||||
}
|
||||
|
||||
export function listWebSearchProviders(params?: {
|
||||
@@ -178,10 +152,6 @@ export function resolveWebSearchDefinition(
|
||||
): { provider: PluginWebSearchProviderEntry; definition: WebSearchProviderToolDefinition } | null {
|
||||
const search = resolveSearchConfig(options?.config);
|
||||
const runtimeWebSearch = options?.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search;
|
||||
if (!resolveWebSearchEnabled({ search, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providers = sortWebSearchProvidersForAutoDetect(
|
||||
options?.preferRuntimeProviders
|
||||
? resolveRuntimeWebSearchProviders({
|
||||
@@ -193,37 +163,38 @@ export function resolveWebSearchDefinition(
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
).filter(Boolean);
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerId =
|
||||
options?.providerId ??
|
||||
runtimeWebSearch?.selectedProvider ??
|
||||
runtimeWebSearch?.providerConfigured ??
|
||||
resolveWebSearchProviderId({ config: options?.config, search, providers });
|
||||
const provider =
|
||||
providers.find((entry) => entry.id === providerId) ??
|
||||
providers.find(
|
||||
(entry) =>
|
||||
entry.id === resolveWebSearchProviderId({ config: options?.config, search, providers }),
|
||||
) ??
|
||||
providers[0];
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = provider.createTool({
|
||||
);
|
||||
return resolveWebProviderDefinition({
|
||||
config: options?.config,
|
||||
searchConfig: search as Record<string, unknown> | undefined,
|
||||
toolConfig: search as Record<string, unknown> | undefined,
|
||||
runtimeMetadata: runtimeWebSearch,
|
||||
sandboxed: options?.sandboxed,
|
||||
providerId: options?.providerId,
|
||||
providers,
|
||||
resolveEnabled: ({ toolConfig, sandboxed }) =>
|
||||
resolveWebSearchEnabled({
|
||||
search: toolConfig as WebSearchConfig | undefined,
|
||||
sandboxed,
|
||||
}),
|
||||
resolveAutoProviderId: ({ config, toolConfig, providers }) =>
|
||||
resolveWebSearchProviderId({
|
||||
config,
|
||||
search: toolConfig as WebSearchConfig | undefined,
|
||||
providers,
|
||||
}),
|
||||
resolveFallbackProviderId: ({ config, toolConfig, providers }) =>
|
||||
resolveWebSearchProviderId({
|
||||
config,
|
||||
search: toolConfig as WebSearchConfig | undefined,
|
||||
providers,
|
||||
}) || providers[0]?.id,
|
||||
createTool: ({ provider, config, toolConfig, runtimeMetadata }) =>
|
||||
provider.createTool({
|
||||
config,
|
||||
searchConfig: toolConfig,
|
||||
runtimeMetadata,
|
||||
}),
|
||||
});
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { provider, definition };
|
||||
}
|
||||
|
||||
export async function runWebSearch(
|
||||
|
||||
165
src/web/provider-runtime-shared.ts
Normal file
165
src/web/provider-runtime-shared.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
|
||||
type RuntimeWebProviderMetadata = {
|
||||
providerConfigured?: string;
|
||||
selectedProvider?: string;
|
||||
};
|
||||
|
||||
type ProviderWithCredential = {
|
||||
envVars: string[];
|
||||
requiresCredential?: boolean;
|
||||
};
|
||||
|
||||
export function resolveWebProviderConfig<
|
||||
TKind extends "search" | "fetch",
|
||||
TConfig extends Record<string, unknown>,
|
||||
>(cfg: OpenClawConfig | undefined, kind: TKind): TConfig | undefined {
|
||||
const webConfig = cfg?.tools?.web;
|
||||
if (!webConfig || typeof webConfig !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const toolConfig = webConfig[kind];
|
||||
if (!toolConfig || typeof toolConfig !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return toolConfig as TConfig;
|
||||
}
|
||||
|
||||
export function readWebProviderEnvValue(
|
||||
envVars: string[],
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(processEnv[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function providerRequiresCredential(
|
||||
provider: Pick<ProviderWithCredential, "requiresCredential">,
|
||||
): boolean {
|
||||
return provider.requiresCredential !== false;
|
||||
}
|
||||
|
||||
export function hasWebProviderEntryCredential<
|
||||
TProvider extends ProviderWithCredential,
|
||||
TConfig extends Record<string, unknown> | undefined,
|
||||
>(params: {
|
||||
provider: TProvider;
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
resolveRawValue: (params: {
|
||||
provider: TProvider;
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
}) => unknown;
|
||||
resolveEnvValue: (params: {
|
||||
provider: TProvider;
|
||||
configuredEnvVarId?: string;
|
||||
}) => string | undefined;
|
||||
}): boolean {
|
||||
if (!providerRequiresCredential(params.provider)) {
|
||||
return true;
|
||||
}
|
||||
const rawValue = params.resolveRawValue({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
toolConfig: params.toolConfig,
|
||||
});
|
||||
const configuredRef = resolveSecretInputRef({
|
||||
value: rawValue,
|
||||
}).ref;
|
||||
if (configuredRef && configuredRef.source !== "env") {
|
||||
return true;
|
||||
}
|
||||
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
|
||||
if (fromConfig) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(
|
||||
params.resolveEnvValue({
|
||||
provider: params.provider,
|
||||
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveWebProviderDefinition<
|
||||
TProvider extends { id: string },
|
||||
TConfig extends Record<string, unknown> | undefined,
|
||||
TRuntimeMetadata extends RuntimeWebProviderMetadata,
|
||||
TDefinition,
|
||||
>(params: {
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
runtimeMetadata: TRuntimeMetadata | undefined;
|
||||
sandboxed?: boolean;
|
||||
providerId?: string;
|
||||
providers: TProvider[];
|
||||
resolveEnabled: (params: { toolConfig: TConfig; sandboxed?: boolean }) => boolean;
|
||||
resolveAutoProviderId: (params: {
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
providers: TProvider[];
|
||||
}) => string;
|
||||
resolveFallbackProviderId?: (params: {
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
providers: TProvider[];
|
||||
providerId: string;
|
||||
}) => string | undefined;
|
||||
createTool: (params: {
|
||||
provider: TProvider;
|
||||
config: OpenClawConfig | undefined;
|
||||
toolConfig: TConfig;
|
||||
runtimeMetadata: TRuntimeMetadata | undefined;
|
||||
}) => TDefinition | null;
|
||||
}): { provider: TProvider; definition: TDefinition } | null {
|
||||
if (!params.resolveEnabled({ toolConfig: params.toolConfig, sandboxed: params.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
const providers = params.providers.filter(Boolean);
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const autoProviderId = params.resolveAutoProviderId({
|
||||
config: params.config,
|
||||
toolConfig: params.toolConfig,
|
||||
providers,
|
||||
});
|
||||
const providerId =
|
||||
params.providerId ??
|
||||
params.runtimeMetadata?.selectedProvider ??
|
||||
params.runtimeMetadata?.providerConfigured ??
|
||||
autoProviderId;
|
||||
const provider =
|
||||
providers.find((entry) => entry.id === providerId) ??
|
||||
providers.find(
|
||||
(entry) =>
|
||||
entry.id ===
|
||||
params.resolveFallbackProviderId?.({
|
||||
config: params.config,
|
||||
toolConfig: params.toolConfig,
|
||||
providers,
|
||||
providerId,
|
||||
}),
|
||||
);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
const definition = params.createTool({
|
||||
provider,
|
||||
config: params.config,
|
||||
toolConfig: params.toolConfig,
|
||||
runtimeMetadata: params.runtimeMetadata,
|
||||
});
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
return { provider, definition };
|
||||
}
|
||||
Reference in New Issue
Block a user