mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
refactor: simplify web provider plugin discovery
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveBundledWebSearchPluginId } from "../../plugins/bundled-web-search-provider-ids.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../../plugins/manifest-registry.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import {
|
||||
resolveWebSearchDefinition,
|
||||
@@ -19,7 +19,13 @@ export function createWebSearchTool(options?: {
|
||||
const resolved = resolveWebSearchDefinition({
|
||||
...options,
|
||||
preferRuntimeProviders:
|
||||
Boolean(runtimeProviderId) && !resolveBundledWebSearchPluginId(runtimeProviderId),
|
||||
Boolean(runtimeProviderId) &&
|
||||
!resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: runtimeProviderId,
|
||||
origin: "bundled",
|
||||
config: options?.config,
|
||||
}),
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { validateSecretsResolveResult } from "../gateway/protocol/index.js";
|
||||
import { resolveBundledWebFetchPluginId } from "../plugins/bundled-web-fetch-provider-ids.js";
|
||||
import { resolveBundledWebSearchPluginId } from "../plugins/bundled-web-search-provider-ids.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
analyzeCommandSecretAssignmentsFromSnapshot,
|
||||
type UnresolvedCommandSecretAssignment,
|
||||
@@ -118,7 +117,12 @@ function classifyRuntimeWebTargetPathState(params: {
|
||||
if (!configuredProvider) {
|
||||
return "active";
|
||||
}
|
||||
return resolveBundledWebFetchPluginId(configuredProvider) === pluginId
|
||||
return resolveManifestContractOwnerPluginId({
|
||||
contract: "webFetchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
}) === pluginId
|
||||
? "active"
|
||||
: "inactive";
|
||||
}
|
||||
@@ -131,7 +135,14 @@ function classifyRuntimeWebTargetPathState(params: {
|
||||
if (!configuredProvider) {
|
||||
return "active";
|
||||
}
|
||||
return resolveBundledWebSearchPluginId(configuredProvider) === pluginId ? "active" : "inactive";
|
||||
return resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
}) === pluginId
|
||||
? "active"
|
||||
: "inactive";
|
||||
}
|
||||
|
||||
const match = /^tools\.web\.search\.([^.]+)\.apiKey$/.exec(params.path);
|
||||
@@ -184,7 +195,12 @@ function describeInactiveRuntimeWebTargetPath(params: {
|
||||
const configuredProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredPluginId = configuredProvider
|
||||
? resolveBundledWebSearchPluginId(configuredProvider)
|
||||
? resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
})
|
||||
: undefined;
|
||||
if (configuredPluginId && configuredPluginId !== pluginId) {
|
||||
return `tools.web.search.provider is "${configuredProvider}".`;
|
||||
|
||||
@@ -33,6 +33,7 @@ const getConfiguredPluginWebSearchCredential =
|
||||
const mockWebSearchProviders = [
|
||||
{
|
||||
id: "brave",
|
||||
pluginId: "brave",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
getCredentialValue: (search?: Record<string, unknown>) => search?.apiKey,
|
||||
@@ -40,6 +41,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "firecrawl",
|
||||
pluginId: "firecrawl",
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("firecrawl"),
|
||||
@@ -47,6 +49,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
pluginId: "google",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("gemini"),
|
||||
@@ -54,6 +57,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "grok",
|
||||
pluginId: "xai",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("grok"),
|
||||
@@ -61,6 +65,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "kimi",
|
||||
pluginId: "moonshot",
|
||||
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
|
||||
credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("kimi"),
|
||||
@@ -68,6 +73,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
pluginId: "minimax",
|
||||
envVars: ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"],
|
||||
credentialPath: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("minimax"),
|
||||
@@ -75,6 +81,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
pluginId: "perplexity",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("perplexity"),
|
||||
@@ -82,6 +89,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "searxng",
|
||||
pluginId: "searxng",
|
||||
envVars: ["SEARXNG_BASE_URL"],
|
||||
credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl",
|
||||
getCredentialValue: (search?: Record<string, unknown>) =>
|
||||
@@ -91,6 +99,7 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
{
|
||||
id: "tavily",
|
||||
pluginId: "tavily",
|
||||
envVars: ["TAVILY_API_KEY"],
|
||||
credentialPath: "plugins.entries.tavily.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("tavily"),
|
||||
@@ -98,9 +107,8 @@ const mockWebSearchProviders = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => {
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => {
|
||||
return {
|
||||
resolveBundledPluginWebSearchProviders: () => mockWebSearchProviders,
|
||||
resolvePluginWebSearchProviders: () => mockWebSearchProviders,
|
||||
};
|
||||
});
|
||||
@@ -158,6 +166,9 @@ vi.mock("../plugins/manifest-registry.js", () => {
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
contracts: {
|
||||
webSearchProviders: ["brave"],
|
||||
},
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
@@ -181,6 +192,9 @@ vi.mock("../plugins/manifest-registry.js", () => {
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
contracts: {
|
||||
webSearchProviders: [id],
|
||||
},
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
@@ -193,6 +207,17 @@ vi.mock("../plugins/manifest-registry.js", () => {
|
||||
],
|
||||
diagnostics: [],
|
||||
}),
|
||||
resolveManifestContractPluginIds: (params?: { contract?: string; origin?: string }) =>
|
||||
params?.contract === "webSearchProviders" && params.origin === "bundled"
|
||||
? mockWebSearchProviders
|
||||
.map((provider) => provider.pluginId)
|
||||
.filter((value, index, array) => array.indexOf(value) === index)
|
||||
.toSorted((left, right) => left.localeCompare(right))
|
||||
: [],
|
||||
resolveManifestContractOwnerPluginId: (params?: { contract?: string; value?: string }) =>
|
||||
params?.contract === "webSearchProviders"
|
||||
? mockWebSearchProviders.find((provider) => provider.id === params.value)?.pluginId
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -861,9 +861,9 @@ describe("applyPluginAutoEnable", () => {
|
||||
it("prefers bluebubbles: skips imessage auto-configure when both are configured", () => {
|
||||
const result = applyWithBluebubblesImessageConfig();
|
||||
|
||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
||||
expect(result.config.channels?.bluebubbles?.enabled).toBe(true);
|
||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBeUndefined();
|
||||
expect(result.changes.join("\n")).toContain("bluebubbles configured, enabled automatically.");
|
||||
expect(result.changes.join("\n")).toContain("BlueBubbles configured, enabled automatically.");
|
||||
expect(result.changes.join("\n")).not.toContain(
|
||||
"iMessage configured, enabled automatically.",
|
||||
);
|
||||
@@ -874,7 +874,7 @@ describe("applyPluginAutoEnable", () => {
|
||||
plugins: { entries: { imessage: { enabled: true } } },
|
||||
});
|
||||
|
||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
||||
expect(result.config.channels?.bluebubbles?.enabled).toBe(true);
|
||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ import {
|
||||
listPotentialConfiguredChannelIds,
|
||||
} from "../channels/config-presence.js";
|
||||
import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
|
||||
import {
|
||||
BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS,
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS,
|
||||
} from "../plugins/bundled-capability-metadata.js";
|
||||
import { resolveBundledWebFetchPluginId } from "../plugins/bundled-web-fetch-provider-ids.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
type PluginManifestRegistry,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import { resolveOwningPluginIdsForModelRef } from "../plugins/providers.js";
|
||||
@@ -81,7 +77,7 @@ const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALO
|
||||
function resolveAutoEnableProviderPluginIds(
|
||||
registry: PluginManifestRegistry,
|
||||
): Readonly<Record<string, string>> {
|
||||
const entries = new Map<string, string>(Object.entries(BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS));
|
||||
const entries = new Map<string, string>();
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const providerId of plugin.autoEnableWhenConfiguredProviders ?? []) {
|
||||
if (!entries.has(providerId)) {
|
||||
@@ -214,11 +210,7 @@ function hasPluginOwnedToolConfig(cfg: OpenClawConfig, pluginId: string): boolea
|
||||
function resolveProviderPluginsWithOwnedWebSearch(
|
||||
registry: PluginManifestRegistry,
|
||||
): ReadonlySet<string> {
|
||||
const pluginIds = new Set(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter(
|
||||
(entry) => entry.providerIds.length > 0 && entry.webSearchProviderIds.length > 0,
|
||||
).map((entry) => entry.pluginId),
|
||||
);
|
||||
const pluginIds = new Set<string>();
|
||||
for (const plugin of registry.plugins) {
|
||||
if (plugin.providers.length > 0 && (plugin.contracts?.webSearchProviders?.length ?? 0) > 0) {
|
||||
pluginIds.add(plugin.id);
|
||||
@@ -227,26 +219,25 @@ function resolveProviderPluginsWithOwnedWebSearch(
|
||||
return pluginIds;
|
||||
}
|
||||
|
||||
const BUNDLED_WEB_FETCH_OWNER_PLUGIN_IDS = new Set(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => entry.webFetchProviderIds.length > 0).map(
|
||||
(entry) => entry.pluginId,
|
||||
),
|
||||
);
|
||||
|
||||
function resolveProviderPluginsWithOwnedWebFetch(): ReadonlySet<string> {
|
||||
function resolveProviderPluginsWithOwnedWebFetch(
|
||||
registry: PluginManifestRegistry,
|
||||
): ReadonlySet<string> {
|
||||
return new Set(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => entry.webFetchProviderIds.length > 0).map(
|
||||
(entry) => entry.pluginId,
|
||||
),
|
||||
registry.plugins
|
||||
.filter((plugin) => (plugin.contracts?.webFetchProviders?.length ?? 0) > 0)
|
||||
.map((plugin) => plugin.id),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePluginIdForConfiguredWebFetchProvider(
|
||||
providerId: string | undefined,
|
||||
): string | undefined {
|
||||
return resolveBundledWebFetchPluginId(
|
||||
typeof providerId === "string" ? providerId.trim().toLowerCase() : "",
|
||||
);
|
||||
return resolveManifestContractOwnerPluginId({
|
||||
contract: "webFetchProviders",
|
||||
value: typeof providerId === "string" ? providerId.trim().toLowerCase() : "",
|
||||
origin: "bundled",
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
function buildChannelToPluginIdMap(registry: PluginManifestRegistry): Map<string, string> {
|
||||
@@ -378,16 +369,19 @@ function hasConfiguredWebFetchPluginEntry(cfg: OpenClawConfig): boolean {
|
||||
if (!entries || typeof entries !== "object") {
|
||||
return false;
|
||||
}
|
||||
return Object.entries(entries).some(
|
||||
([pluginId, entry]) =>
|
||||
BUNDLED_WEB_FETCH_OWNER_PLUGIN_IDS.has(pluginId) &&
|
||||
isRecord(entry) &&
|
||||
isRecord(entry.config) &&
|
||||
isRecord(entry.config.webFetch),
|
||||
return Object.values(entries).some(
|
||||
(entry) => isRecord(entry) && isRecord(entry.config) && isRecord(entry.config.webFetch),
|
||||
);
|
||||
}
|
||||
|
||||
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean {
|
||||
const pluginEntries = cfg.plugins?.entries;
|
||||
if (
|
||||
pluginEntries &&
|
||||
Object.values(pluginEntries).some((entry) => isRecord(entry) && isRecord(entry.config))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (cfg.auth?.profiles && Object.keys(cfg.auth.profiles).length > 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -601,7 +595,7 @@ function resolveConfiguredPlugins(
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const pluginId of resolveProviderPluginsWithOwnedWebFetch()) {
|
||||
for (const pluginId of resolveProviderPluginsWithOwnedWebFetch(registry)) {
|
||||
if (hasPluginOwnedWebFetchConfig(cfg, pluginId)) {
|
||||
changes.push({
|
||||
pluginId,
|
||||
|
||||
@@ -2,13 +2,15 @@ import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/ids.js";
|
||||
import { withBundledPluginAllowlistCompat } from "../plugins/bundled-compat.js";
|
||||
import { listBundledWebSearchPluginIds } from "../plugins/bundled-web-search-ids.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
||||
import { hasKind } from "../plugins/slots.js";
|
||||
import { collectUnsupportedSecretRefConfigCandidates } from "../secrets/unsupported-surface-policy.js";
|
||||
@@ -582,6 +584,31 @@ function validateConfigObjectWithPluginsBase(
|
||||
|
||||
let registryInfo: RegistryInfo | null = null;
|
||||
let compatConfig: OpenClawConfig | null | undefined;
|
||||
let compatPluginIds: ReadonlySet<string> | null = null;
|
||||
let compatPluginIdsResolved = false;
|
||||
|
||||
const ensureCompatPluginIds = (): ReadonlySet<string> => {
|
||||
if (compatPluginIdsResolved) {
|
||||
return compatPluginIds ?? new Set<string>();
|
||||
}
|
||||
compatPluginIdsResolved = true;
|
||||
const allow = config.plugins?.allow;
|
||||
if (!Array.isArray(allow) || allow.length === 0) {
|
||||
compatPluginIds = new Set<string>();
|
||||
return compatPluginIds;
|
||||
}
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
compatPluginIds = new Set(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
config,
|
||||
workspaceDir: workspaceDir ?? undefined,
|
||||
env: opts.env,
|
||||
}),
|
||||
);
|
||||
return compatPluginIds;
|
||||
};
|
||||
|
||||
const ensureCompatConfig = (): OpenClawConfig => {
|
||||
if (compatConfig !== undefined) {
|
||||
@@ -594,27 +621,9 @@ function validateConfigObjectWithPluginsBase(
|
||||
return config;
|
||||
}
|
||||
|
||||
const bundledWebSearchPluginIds = new Set(listBundledWebSearchPluginIds());
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const seenCompatPluginIds = new Set<string>();
|
||||
const compatPluginIds = loadPluginManifestRegistry({
|
||||
config,
|
||||
workspaceDir: workspaceDir ?? undefined,
|
||||
env: opts.env,
|
||||
})
|
||||
.plugins.filter((plugin) => {
|
||||
if (seenCompatPluginIds.has(plugin.id)) {
|
||||
return false;
|
||||
}
|
||||
seenCompatPluginIds.add(plugin.id);
|
||||
return plugin.origin === "bundled" && bundledWebSearchPluginIds.has(plugin.id);
|
||||
})
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
compatConfig = withBundledPluginAllowlistCompat({
|
||||
config,
|
||||
pluginIds: compatPluginIds,
|
||||
pluginIds: [...ensureCompatPluginIds()],
|
||||
});
|
||||
return compatConfig ?? config;
|
||||
};
|
||||
@@ -978,7 +987,7 @@ function validateConfigObjectWithPluginsBase(
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled && entryHasConfig) {
|
||||
if (!enabled && entryHasConfig && !ensureCompatPluginIds().has(pluginId)) {
|
||||
warnings.push({
|
||||
path: `plugins.entries.${pluginId}`,
|
||||
message: `plugin disabled (${reason ?? "disabled"}) but config is present`,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { resolveBundledWebFetchPluginIds as listBundledWebFetchPluginIds } from "./bundled-web-fetch.js";
|
||||
@@ -1 +0,0 @@
|
||||
export { resolveBundledWebFetchPluginId } from "./bundled-web-fetch.js";
|
||||
@@ -1,92 +0,0 @@
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebFetchProviderEntry = PluginWebFetchProviderEntry & { pluginId: string };
|
||||
|
||||
const bundledWebFetchProvidersCache = new Map<string, BundledWebFetchProviderEntry[]>();
|
||||
|
||||
function resolveBundledWebFetchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webFetchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebFetchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebFetchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebFetchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webFetchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebFetchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebFetchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
return loadBundledWebFetchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebFetchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webFetchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { listBundledWebSearchPluginIds as listBundledWebSearchPluginIdsImpl } from "./bundled-web-search.js";
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = listBundledWebSearchPluginIdsImpl();
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return listBundledWebSearchPluginIdsImpl();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { resolveBundledWebSearchPluginId } from "./bundled-web-search.js";
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveBundledPluginWebSearchProviders } from "./web-search-providers.js";
|
||||
import { resolvePluginWebSearchProviders } from "./web-search-providers.runtime.js";
|
||||
|
||||
function hasConfiguredCredentialValue(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
@@ -16,10 +16,11 @@ export function hasBundledWebSearchCredential(params: {
|
||||
const searchConfig =
|
||||
params.searchConfig ??
|
||||
(params.config.tools?.web?.search as Record<string, unknown> | undefined);
|
||||
return resolveBundledPluginWebSearchProviders({
|
||||
return resolvePluginWebSearchProviders({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}).some((provider) => {
|
||||
const configuredCredential =
|
||||
provider.getConfiguredCredentialValue?.(params.config) ??
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { hasBundledWebSearchCredential } from "./bundled-web-search-registry.js";
|
||||
import {
|
||||
listBundledWebSearchPluginIds,
|
||||
listBundledWebSearchProviders,
|
||||
resolveBundledWebSearchPluginId,
|
||||
resolveBundledWebSearchPluginIds,
|
||||
} from "./bundled-web-search.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
|
||||
vi.mock("./manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
const resolveBundledPluginWebSearchProvidersMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./web-search-providers.js", () => ({
|
||||
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
function createMockedBundledWebSearchProvider(params: {
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
configuredCredential?: unknown;
|
||||
scopedCredential?: unknown;
|
||||
envVars?: string[];
|
||||
}) {
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.providerId,
|
||||
label: params.providerId,
|
||||
hint: `${params.providerId} provider`,
|
||||
envVars: params.envVars ?? [],
|
||||
placeholder: `${params.providerId}-key`,
|
||||
signupUrl: `https://example.com/${params.providerId}`,
|
||||
autoDetectOrder: 10,
|
||||
credentialPath: `plugins.entries.${params.pluginId}.config.webSearch.apiKey`,
|
||||
getCredentialValue: () => params.scopedCredential,
|
||||
getConfiguredCredentialValue: () => params.configuredCredential,
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => ({
|
||||
description: params.providerId,
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("bundled web search helpers", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(loadPluginManifestRegistry).mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "xai", origin: "bundled", contracts: { webSearchProviders: ["grok"] } },
|
||||
{ id: "google", origin: "bundled", contracts: { webSearchProviders: ["gemini"] } },
|
||||
{ id: "minimax", origin: "bundled", contracts: { webSearchProviders: ["minimax"] } },
|
||||
{ id: "noise", origin: "bundled" },
|
||||
{ id: "external-google", origin: "workspace" },
|
||||
] as never[],
|
||||
diagnostics: [],
|
||||
});
|
||||
vi.mocked(loadBundledCapabilityRuntimeRegistry).mockReturnValue({
|
||||
webSearchProviders: [
|
||||
{
|
||||
pluginId: "minimax",
|
||||
provider: createMockedBundledWebSearchProvider({
|
||||
pluginId: "minimax",
|
||||
providerId: "minimax",
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: createMockedBundledWebSearchProvider({
|
||||
pluginId: "xai",
|
||||
providerId: "grok",
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: createMockedBundledWebSearchProvider({
|
||||
pluginId: "google",
|
||||
providerId: "gemini",
|
||||
}),
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
});
|
||||
|
||||
it("returns bundled manifest-derived web search plugins from the registry", () => {
|
||||
expect(
|
||||
resolveBundledWebSearchPluginIds({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["google", "xai"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" },
|
||||
}),
|
||||
).toEqual(["google", "minimax", "xai"]);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["google", "xai"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" },
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a copy of the bundled plugin id fast-path list", () => {
|
||||
const listed = listBundledWebSearchPluginIds();
|
||||
expect(listed).toEqual(["google", "minimax", "xai"]);
|
||||
expect(listed).not.toBe(listBundledWebSearchPluginIds());
|
||||
});
|
||||
|
||||
it("maps bundled provider ids back to their owning plugins", () => {
|
||||
expect(resolveBundledWebSearchPluginId(" gemini ")).toBe("google");
|
||||
expect(resolveBundledWebSearchPluginId(" minimax ")).toBe("minimax");
|
||||
expect(resolveBundledWebSearchPluginId("missing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads bundled provider entries through the capability runtime registry once", () => {
|
||||
expect(listBundledWebSearchProviders()).toEqual([
|
||||
expect.objectContaining({ pluginId: "minimax", id: "minimax" }),
|
||||
expect.objectContaining({ pluginId: "xai", id: "grok" }),
|
||||
expect.objectContaining({ pluginId: "google", id: "gemini" }),
|
||||
]);
|
||||
expect(listBundledWebSearchProviders()).toEqual([
|
||||
expect.objectContaining({ pluginId: "minimax", id: "minimax" }),
|
||||
expect.objectContaining({ pluginId: "xai", id: "grok" }),
|
||||
expect.objectContaining({ pluginId: "google", id: "gemini" }),
|
||||
]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
||||
pluginIds: ["google", "minimax", "xai"],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasBundledWebSearchCredential", () => {
|
||||
const baseCfg = {
|
||||
agents: { defaults: { model: { primary: "ollama/mistral-8b" } } },
|
||||
browser: { enabled: false },
|
||||
tools: { web: { fetch: { enabled: false } } },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "detects configured plugin credentials",
|
||||
providers: [
|
||||
createMockedBundledWebSearchProvider({
|
||||
pluginId: "google",
|
||||
providerId: "gemini",
|
||||
configuredCredential: "AIza-test",
|
||||
}),
|
||||
],
|
||||
config: baseCfg,
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "detects scoped tool credentials",
|
||||
providers: [
|
||||
createMockedBundledWebSearchProvider({
|
||||
pluginId: "google",
|
||||
providerId: "gemini",
|
||||
scopedCredential: "AIza-test",
|
||||
}),
|
||||
],
|
||||
config: baseCfg,
|
||||
env: {},
|
||||
searchConfig: { provider: "gemini" },
|
||||
},
|
||||
{
|
||||
name: "detects env credentials",
|
||||
providers: [
|
||||
createMockedBundledWebSearchProvider({
|
||||
pluginId: "xai",
|
||||
providerId: "grok",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
}),
|
||||
],
|
||||
config: baseCfg,
|
||||
env: { XAI_API_KEY: "xai-test" },
|
||||
},
|
||||
] as const)("$name", ({ providers, config, env, searchConfig }) => {
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue(providers);
|
||||
|
||||
expect(hasBundledWebSearchCredential({ config, env, searchConfig })).toBe(true);
|
||||
expect(resolveBundledPluginWebSearchProvidersMock).toHaveBeenCalledWith({
|
||||
config,
|
||||
env,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when no bundled provider exposes a configured credential", () => {
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([
|
||||
createMockedBundledWebSearchProvider({
|
||||
pluginId: "google",
|
||||
providerId: "gemini",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(hasBundledWebSearchCredential({ config: baseCfg, env: {} })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebSearchProviderEntry = PluginWebSearchProviderEntry & { pluginId: string };
|
||||
|
||||
const bundledWebSearchProvidersCache = new Map<string, BundledWebSearchProviderEntry[]>();
|
||||
|
||||
function resolveBundledWebSearchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webSearchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebSearchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebSearchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebSearchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebSearchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebSearchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebSearchPluginIds(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebSearchPluginIds(params ?? {});
|
||||
}
|
||||
|
||||
export function listBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
return loadBundledWebSearchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebSearchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webSearchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withBundledPluginAllowlistCompat } from "../bundled-compat.js";
|
||||
import { resolveBundledWebSearchPluginIds } from "../bundled-web-search.js";
|
||||
import { loadPluginManifestRegistry } from "../manifest-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../manifest-registry.js";
|
||||
import { __testing as providerTesting } from "../providers.js";
|
||||
import { resolveBundledPluginWebSearchProviders } from "../web-search-providers.js";
|
||||
import { resolvePluginWebSearchProviders } from "../web-search-providers.runtime.js";
|
||||
import { providerContractCompatPluginIds } from "./registry.js";
|
||||
import { uniqueSortedStrings } from "./testkit.js";
|
||||
|
||||
@@ -66,9 +68,14 @@ describe("plugin loader contract", () => {
|
||||
env: { VITEST: "1" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
webSearchPluginIds = uniqueSortedStrings(
|
||||
resolveBundledPluginWebSearchProviders({}).map((entry) => entry.pluginId),
|
||||
resolvePluginWebSearchProviders({ origin: "bundled" }).map((entry) => entry.pluginId),
|
||||
);
|
||||
bundledWebSearchPluginIds = uniqueSortedStrings(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
bundledWebSearchPluginIds = uniqueSortedStrings(resolveBundledWebSearchPluginIds({}));
|
||||
webSearchAllowlistCompatConfig = createAllowlistCompatConfig(webSearchPluginIds);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveBundledWebFetchPluginIds } from "../bundled-web-fetch.js";
|
||||
import { resolveBundledWebSearchPluginIds } from "../bundled-web-search.js";
|
||||
import { loadPluginManifestRegistry } from "../manifest-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../manifest-registry.js";
|
||||
import {
|
||||
imageGenerationProviderContractRegistry,
|
||||
mediaUnderstandingProviderContractRegistry,
|
||||
@@ -137,7 +138,10 @@ describe("plugin contract registry", () => {
|
||||
});
|
||||
|
||||
it("covers every bundled web fetch plugin from the shared resolver", () => {
|
||||
const bundledWebFetchPluginIds = resolveBundledWebFetchPluginIds({});
|
||||
const bundledWebFetchPluginIds = resolveManifestContractPluginIds({
|
||||
contract: "webFetchProviders",
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(
|
||||
uniqueSortedStrings(
|
||||
@@ -152,7 +156,10 @@ describe("plugin contract registry", () => {
|
||||
"loads bundled web fetch providers for each shared-resolver plugin",
|
||||
{ timeout: REGISTRY_CONTRACT_TIMEOUT_MS },
|
||||
() => {
|
||||
for (const pluginId of resolveBundledWebFetchPluginIds({})) {
|
||||
for (const pluginId of resolveManifestContractPluginIds({
|
||||
contract: "webFetchProviders",
|
||||
origin: "bundled",
|
||||
})) {
|
||||
expect(resolveWebFetchProviderContractEntriesForPluginId(pluginId).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
@@ -162,7 +169,10 @@ describe("plugin contract registry", () => {
|
||||
);
|
||||
|
||||
it("covers every bundled web search plugin from the shared resolver", () => {
|
||||
const bundledWebSearchPluginIds = resolveBundledWebSearchPluginIds({});
|
||||
const bundledWebSearchPluginIds = resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(
|
||||
uniqueSortedStrings(
|
||||
@@ -177,7 +187,10 @@ describe("plugin contract registry", () => {
|
||||
"loads bundled web search providers for each shared-resolver plugin",
|
||||
{ timeout: REGISTRY_CONTRACT_TIMEOUT_MS },
|
||||
() => {
|
||||
for (const pluginId of resolveBundledWebSearchPluginIds({})) {
|
||||
for (const pluginId of resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
})) {
|
||||
expect(resolveWebSearchProviderContractEntriesForPluginId(pluginId).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -29,6 +29,8 @@ import type {
|
||||
PluginOrigin,
|
||||
} from "./types.js";
|
||||
|
||||
type PluginManifestContractListKey = "webFetchProviders" | "webSearchProviders";
|
||||
|
||||
type SeenIdEntry = {
|
||||
candidate: PluginCandidate;
|
||||
recordIndex: number;
|
||||
@@ -98,6 +100,63 @@ export function clearPluginManifestRegistryCache(): void {
|
||||
registryCache.clear();
|
||||
}
|
||||
|
||||
function listContractValues(
|
||||
plugin: PluginManifestRecord,
|
||||
contract: PluginManifestContractListKey,
|
||||
): readonly string[] {
|
||||
return plugin.contracts?.[contract] ?? [];
|
||||
}
|
||||
|
||||
export function resolveManifestContractPluginIds(params: {
|
||||
contract: PluginManifestContractListKey;
|
||||
origin?: PluginOrigin;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
|
||||
listContractValues(plugin, params.contract).length > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveManifestContractOwnerPluginId(params: {
|
||||
contract: PluginManifestContractListKey;
|
||||
value: string | undefined;
|
||||
origin?: PluginOrigin;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string | undefined {
|
||||
const normalizedValue = params.value?.trim().toLowerCase();
|
||||
if (!normalizedValue) {
|
||||
return undefined;
|
||||
}
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.find(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
listContractValues(plugin, params.contract).some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedValue,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
|
||||
function resolveManifestCacheMs(env: NodeJS.ProcessEnv): number {
|
||||
const raw = env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS?.trim();
|
||||
if (raw === "" || raw === "0") {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
@@ -13,7 +14,11 @@ import {
|
||||
} from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
type PluginManifestRecord,
|
||||
} from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
import {
|
||||
resolveBundledWebFetchResolutionConfig,
|
||||
@@ -46,11 +51,13 @@ function buildWebFetchSnapshotCacheKey(params: {
|
||||
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),
|
||||
),
|
||||
@@ -79,19 +86,30 @@ function resolveWebFetchCandidatePluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): string[] | undefined {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
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({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = registry.plugins
|
||||
.filter(
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
pluginManifestDeclaresWebFetch(plugin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
(!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));
|
||||
@@ -106,6 +124,7 @@ function resolveWebFetchLoadOptions(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
@@ -118,6 +137,7 @@ function resolveWebFetchLoadOptions(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
});
|
||||
return {
|
||||
env,
|
||||
@@ -156,8 +176,38 @@ export function resolvePluginWebFetchProviders(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
mode?: "runtime" | "setup";
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
resolveWebFetchCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.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: params.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);
|
||||
@@ -166,6 +216,7 @@ export function resolvePluginWebFetchProviders(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
env,
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
@@ -212,6 +263,7 @@ export function resolveRuntimeWebFetchProviders(params: {
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(
|
||||
params.config === undefined ? undefined : resolveWebFetchLoadOptions(params),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { resolveBundledWebFetchPluginIds } from "./bundled-web-fetch.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: {
|
||||
@@ -9,7 +9,9 @@ function resolveBundledWebFetchCompatPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebFetchPluginIds({
|
||||
return resolveManifestContractPluginIds({
|
||||
contract: "webFetchProviders",
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { listBundledWebFetchProviders as listBundledWebFetchProviderEntries } from "./bundled-web-fetch.js";
|
||||
import { resolveEffectiveEnableState } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
import {
|
||||
resolveBundledWebFetchResolutionConfig,
|
||||
sortWebFetchProviders,
|
||||
} from "./web-fetch-providers.shared.js";
|
||||
|
||||
function listBundledWebFetchProviders(): PluginWebFetchProviderEntry[] {
|
||||
return sortWebFetchProviders(listBundledWebFetchProviderEntries());
|
||||
}
|
||||
|
||||
export function resolveBundledPluginWebFetchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
const { config, normalized } = resolveBundledWebFetchResolutionConfig(params);
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
|
||||
return listBundledWebFetchProviders().filter((provider) => {
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(provider.pluginId)) {
|
||||
return false;
|
||||
}
|
||||
return resolveEffectiveEnableState({
|
||||
id: provider.pluginId,
|
||||
origin: "bundled",
|
||||
config: normalized,
|
||||
rootConfig: config,
|
||||
}).enabled;
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
} from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
type PluginManifestRecord,
|
||||
} from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
import {
|
||||
resolveBundledWebSearchResolutionConfig,
|
||||
@@ -46,11 +50,13 @@ function buildWebSearchSnapshotCacheKey(params: {
|
||||
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),
|
||||
),
|
||||
@@ -79,19 +85,30 @@ function resolveWebSearchCandidatePluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): string[] | undefined {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
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({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = registry.plugins
|
||||
.filter(
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
pluginManifestDeclaresWebSearch(plugin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
(!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));
|
||||
@@ -106,6 +123,7 @@ function resolveWebSearchLoadOptions(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
@@ -118,6 +136,7 @@ function resolveWebSearchLoadOptions(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
});
|
||||
return {
|
||||
env,
|
||||
@@ -157,6 +176,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
mode?: "runtime" | "setup";
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
if (params.mode === "setup") {
|
||||
@@ -166,6 +186,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
@@ -194,6 +215,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
origin: params.origin,
|
||||
env,
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
@@ -240,6 +262,7 @@ export function resolveRuntimeWebSearchProviders(params: {
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(
|
||||
params.config === undefined ? undefined : resolveWebSearchLoadOptions(params),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { resolveBundledWebSearchPluginIds } from "./bundled-web-search.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: {
|
||||
@@ -9,7 +9,9 @@ function resolveBundledWebSearchCompatPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebSearchPluginIds({
|
||||
return resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
import { resolveBundledPluginWebSearchProviders } from "./web-search-providers.js";
|
||||
|
||||
const listBundledWebSearchProvidersMock = vi.hoisted(() => vi.fn());
|
||||
const resolveBundledWebSearchPluginIdsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./bundled-web-search.js", () => ({
|
||||
listBundledWebSearchProviders: listBundledWebSearchProvidersMock,
|
||||
resolveBundledWebSearchPluginIds: resolveBundledWebSearchPluginIdsMock,
|
||||
}));
|
||||
|
||||
const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_KEYS = [
|
||||
"brave:brave",
|
||||
"duckduckgo:duckduckgo",
|
||||
"exa:exa",
|
||||
"firecrawl:firecrawl",
|
||||
"google:gemini",
|
||||
"xai:grok",
|
||||
"moonshot:kimi",
|
||||
"perplexity:perplexity",
|
||||
"searxng:searxng",
|
||||
"tavily:tavily",
|
||||
] as const;
|
||||
const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = [
|
||||
"brave",
|
||||
"duckduckgo",
|
||||
"exa",
|
||||
"firecrawl",
|
||||
"google",
|
||||
"xai",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"searxng",
|
||||
"tavily",
|
||||
] as const;
|
||||
const EXPECTED_BUNDLED_WEB_SEARCH_CREDENTIAL_PATHS = [
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
"",
|
||||
"plugins.entries.exa.config.webSearch.apiKey",
|
||||
"plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"plugins.entries.google.config.webSearch.apiKey",
|
||||
"plugins.entries.xai.config.webSearch.apiKey",
|
||||
"plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"plugins.entries.searxng.config.webSearch.baseUrl",
|
||||
"plugins.entries.tavily.config.webSearch.apiKey",
|
||||
] as const;
|
||||
|
||||
function createBundledWebSearchProviderEntry(params: {
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
credentialPath: string;
|
||||
order: number;
|
||||
withApplySelectionConfig?: boolean;
|
||||
withResolveRuntimeMetadata?: boolean;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.providerId,
|
||||
label: params.providerId,
|
||||
hint: `${params.providerId} provider`,
|
||||
envVars: [],
|
||||
placeholder: `${params.providerId}-key`,
|
||||
signupUrl: `https://example.com/${params.providerId}`,
|
||||
autoDetectOrder: params.order,
|
||||
credentialPath: params.credentialPath,
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue: () => {},
|
||||
...(params.withApplySelectionConfig
|
||||
? {
|
||||
applySelectionConfig: () => ({
|
||||
plugins: {
|
||||
entries: {
|
||||
[params.pluginId]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(params.withResolveRuntimeMetadata
|
||||
? {
|
||||
resolveRuntimeMetadata: () => ({
|
||||
selectedProvider: params.providerId,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
createTool: () => ({
|
||||
description: params.providerId,
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const BUNDLED_WEB_SEARCH_PROVIDERS: PluginWebSearchProviderEntry[] = [
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "duckduckgo",
|
||||
providerId: "duckduckgo",
|
||||
credentialPath: "",
|
||||
order: 100,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "moonshot",
|
||||
providerId: "kimi",
|
||||
credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
order: 40,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "brave",
|
||||
providerId: "brave",
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
order: 10,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "perplexity",
|
||||
providerId: "perplexity",
|
||||
credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
order: 50,
|
||||
withResolveRuntimeMetadata: true,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "firecrawl",
|
||||
providerId: "firecrawl",
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
order: 60,
|
||||
withApplySelectionConfig: true,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "google",
|
||||
providerId: "gemini",
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
order: 20,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "tavily",
|
||||
providerId: "tavily",
|
||||
credentialPath: "plugins.entries.tavily.config.webSearch.apiKey",
|
||||
order: 80,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "exa",
|
||||
providerId: "exa",
|
||||
credentialPath: "plugins.entries.exa.config.webSearch.apiKey",
|
||||
order: 55,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "searxng",
|
||||
providerId: "searxng",
|
||||
credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl",
|
||||
order: 70,
|
||||
}),
|
||||
createBundledWebSearchProviderEntry({
|
||||
pluginId: "xai",
|
||||
providerId: "grok",
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
order: 30,
|
||||
}),
|
||||
];
|
||||
|
||||
function toProviderKeys(
|
||||
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
|
||||
): string[] {
|
||||
return providers.map((provider) => `${provider.pluginId}:${provider.id}`);
|
||||
}
|
||||
|
||||
function expectBundledWebSearchProviders(
|
||||
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
|
||||
) {
|
||||
expect(toProviderKeys(providers)).toEqual(EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_KEYS);
|
||||
expect(providers.map((provider) => provider.credentialPath)).toEqual(
|
||||
EXPECTED_BUNDLED_WEB_SEARCH_CREDENTIAL_PATHS,
|
||||
);
|
||||
}
|
||||
|
||||
function expectResolvedPluginIds(
|
||||
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
|
||||
expectedPluginIds: readonly string[],
|
||||
) {
|
||||
expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds);
|
||||
}
|
||||
|
||||
function expectResolvedPluginIdsExcluding(
|
||||
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
|
||||
unexpectedPluginIds: readonly string[],
|
||||
) {
|
||||
const pluginIds = providers.map((provider) => provider.pluginId);
|
||||
for (const pluginId of unexpectedPluginIds) {
|
||||
expect(pluginIds).not.toContain(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
function expectBundledWebSearchResolution(params: {
|
||||
options?: Parameters<typeof resolveBundledPluginWebSearchProviders>[0];
|
||||
expectedProviders?: "full";
|
||||
expectedPluginIds?: readonly string[];
|
||||
excludedPluginIds?: readonly string[];
|
||||
}) {
|
||||
const providers = resolveBundledPluginWebSearchProviders(params.options ?? {});
|
||||
|
||||
if (params.expectedProviders === "full") {
|
||||
expectBundledWebSearchProviders(providers);
|
||||
}
|
||||
if (params.expectedPluginIds) {
|
||||
expectResolvedPluginIds(providers, params.expectedPluginIds);
|
||||
}
|
||||
if (params.excludedPluginIds) {
|
||||
expectResolvedPluginIdsExcluding(providers, params.excludedPluginIds);
|
||||
}
|
||||
}
|
||||
|
||||
describe("resolveBundledPluginWebSearchProviders", () => {
|
||||
beforeEach(() => {
|
||||
listBundledWebSearchProvidersMock.mockReset();
|
||||
listBundledWebSearchProvidersMock.mockReturnValue(BUNDLED_WEB_SEARCH_PROVIDERS);
|
||||
resolveBundledWebSearchPluginIdsMock.mockReset();
|
||||
resolveBundledWebSearchPluginIdsMock.mockReturnValue([
|
||||
...EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: "returns bundled providers in alphabetical order",
|
||||
options: {},
|
||||
},
|
||||
{
|
||||
title: "can resolve bundled providers through the manifest-scoped loader path",
|
||||
options: {
|
||||
bundledAllowlistCompat: true,
|
||||
},
|
||||
},
|
||||
] as const)("$title", ({ options }) => {
|
||||
const providers = resolveBundledPluginWebSearchProviders(options);
|
||||
|
||||
expectBundledWebSearchProviders(providers);
|
||||
expect(providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(
|
||||
providers.find((provider) => provider.id === "perplexity")?.resolveRuntimeMetadata,
|
||||
).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: "can augment restrictive allowlists for bundled compatibility",
|
||||
params: {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo-other-plugin"],
|
||||
},
|
||||
},
|
||||
bundledAllowlistCompat: true,
|
||||
},
|
||||
expectedPluginIds: EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS,
|
||||
},
|
||||
{
|
||||
title: "does not return bundled providers excluded by a restrictive allowlist without compat",
|
||||
params: {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo-other-plugin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPluginIds: [],
|
||||
},
|
||||
{
|
||||
title: "returns no providers when plugins are globally disabled",
|
||||
params: {
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPluginIds: [],
|
||||
},
|
||||
{
|
||||
title: "can scope bundled resolution to one plugin id",
|
||||
params: {
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: ["google"],
|
||||
},
|
||||
expectedPluginIds: ["google"],
|
||||
},
|
||||
{
|
||||
title: "preserves explicit bundled provider entry state",
|
||||
params: {
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
perplexity: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
excludedPluginIds: ["perplexity"],
|
||||
},
|
||||
])("$title", ({ params, expectedPluginIds, excludedPluginIds }) => {
|
||||
expectBundledWebSearchResolution({
|
||||
options: params,
|
||||
expectedPluginIds,
|
||||
excludedPluginIds,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { listBundledWebSearchProviders as listBundledWebSearchProviderEntries } from "./bundled-web-search.js";
|
||||
import { resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
import {
|
||||
resolveBundledWebSearchResolutionConfig,
|
||||
sortWebSearchProviders,
|
||||
} from "./web-search-providers.shared.js";
|
||||
|
||||
function listBundledWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
return sortWebSearchProviders(listBundledWebSearchProviderEntries());
|
||||
}
|
||||
|
||||
export function resolveBundledPluginWebSearchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const { config, normalized } = resolveBundledWebSearchResolutionConfig(params);
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
|
||||
return listBundledWebSearchProviders().filter((provider) => {
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(provider.pluginId)) {
|
||||
return false;
|
||||
}
|
||||
return resolveEffectivePluginActivationState({
|
||||
id: provider.pluginId,
|
||||
origin: "bundled",
|
||||
config: normalized,
|
||||
rootConfig: config,
|
||||
}).activated;
|
||||
});
|
||||
}
|
||||
@@ -11,30 +11,15 @@ const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
const { resolveBundledPluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolveBundledPluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
const { resolvePluginWebFetchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()),
|
||||
}));
|
||||
|
||||
const { resolveBundledPluginWebFetchProvidersMock } = vi.hoisted(() => ({
|
||||
resolveBundledPluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()),
|
||||
}));
|
||||
|
||||
let bundledWebSearchProviders: typeof import("../plugins/web-search-providers.js");
|
||||
let runtimeWebSearchProviders: typeof import("../plugins/web-search-providers.runtime.js");
|
||||
let bundledWebFetchProviders: typeof import("../plugins/web-fetch-providers.js");
|
||||
let runtimeWebFetchProviders: typeof import("../plugins/web-fetch-providers.runtime.js");
|
||||
let secretResolve: typeof import("./resolve.js");
|
||||
let createResolverContext: typeof import("./runtime-shared.js").createResolverContext;
|
||||
let resolveRuntimeWebTools: typeof import("./runtime-web-tools.js").resolveRuntimeWebTools;
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => ({
|
||||
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/web-search-providers.runtime.js")>(
|
||||
"../plugins/web-search-providers.runtime.js",
|
||||
@@ -45,10 +30,6 @@ vi.mock("../plugins/web-search-providers.runtime.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.js", () => ({
|
||||
resolveBundledPluginWebFetchProviders: resolveBundledPluginWebFetchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/web-fetch-providers.runtime.js")>(
|
||||
"../plugins/web-fetch-providers.runtime.js",
|
||||
@@ -264,9 +245,7 @@ function expectInactiveWebFetchProviderSecretRef(params: {
|
||||
|
||||
describe("runtime web tools resolution", () => {
|
||||
beforeAll(async () => {
|
||||
bundledWebSearchProviders = await import("../plugins/web-search-providers.js");
|
||||
runtimeWebSearchProviders = await import("../plugins/web-search-providers.runtime.js");
|
||||
bundledWebFetchProviders = await import("../plugins/web-fetch-providers.js");
|
||||
runtimeWebFetchProviders = await import("../plugins/web-fetch-providers.runtime.js");
|
||||
secretResolve = await import("./resolve.js");
|
||||
({ createResolverContext } = await import("./runtime-shared.js"));
|
||||
@@ -275,9 +254,7 @@ describe("runtime web tools resolution", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
runtimeWebSearchProviders.__testing.resetWebSearchProviderSnapshotCacheForTests();
|
||||
vi.mocked(bundledWebSearchProviders.resolveBundledPluginWebSearchProviders).mockClear();
|
||||
vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders).mockClear();
|
||||
vi.mocked(bundledWebFetchProviders.resolveBundledPluginWebFetchProviders).mockClear();
|
||||
vi.mocked(runtimeWebFetchProviders.resolvePluginWebFetchProviders).mockClear();
|
||||
});
|
||||
|
||||
@@ -665,9 +642,8 @@ describe("runtime web tools resolution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses bundled provider resolution for configured bundled providers", async () => {
|
||||
const bundledSpy = vi.mocked(bundledWebSearchProviders.resolveBundledPluginWebSearchProviders);
|
||||
const genericSpy = vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders);
|
||||
it("uses bundled-only runtime provider resolution for configured bundled providers", async () => {
|
||||
const runtimeSpy = vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders);
|
||||
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
@@ -698,13 +674,13 @@ describe("runtime web tools resolution", () => {
|
||||
});
|
||||
|
||||
expect(metadata.search.selectedProvider).toBe("gemini");
|
||||
expect(bundledSpy).toHaveBeenCalledWith(
|
||||
expect(runtimeSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: ["google"],
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
expect(genericSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not resolve web fetch provider SecretRef when web fetch is inactive", async () => {
|
||||
@@ -955,7 +931,6 @@ describe("runtime web tools resolution", () => {
|
||||
});
|
||||
|
||||
it("keeps web fetch provider discovery bundled-only during runtime secret resolution", async () => {
|
||||
const bundledSpy = vi.mocked(bundledWebFetchProviders.resolveBundledPluginWebFetchProviders);
|
||||
const runtimeSpy = vi.mocked(runtimeWebFetchProviders.resolvePluginWebFetchProviders);
|
||||
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
@@ -986,7 +961,11 @@ describe("runtime web tools resolution", () => {
|
||||
});
|
||||
|
||||
expect(metadata.fetch.selectedProvider).toBe("firecrawl");
|
||||
expect(bundledSpy).toHaveBeenCalled();
|
||||
expect(runtimeSpy).not.toHaveBeenCalled();
|
||||
expect(runtimeSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { resolveBundledWebFetchPluginId } from "../plugins/bundled-web-fetch-provider-ids.js";
|
||||
import { listBundledWebSearchPluginIds } from "../plugins/bundled-web-search-ids.js";
|
||||
import { resolveBundledWebSearchPluginId } from "../plugins/bundled-web-search-provider-ids.js";
|
||||
import {
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import type {
|
||||
PluginWebFetchProviderEntry,
|
||||
PluginWebSearchProviderEntry,
|
||||
WebFetchCredentialResolutionSource,
|
||||
WebSearchCredentialResolutionSource,
|
||||
} from "../plugins/types.js";
|
||||
import { resolveBundledPluginWebFetchProviders } from "../plugins/web-fetch-providers.js";
|
||||
import { resolvePluginWebFetchProviders } from "../plugins/web-fetch-providers.runtime.js";
|
||||
import { sortWebFetchProvidersForAutoDetect } from "../plugins/web-fetch-providers.shared.js";
|
||||
import { resolveBundledPluginWebSearchProviders } from "../plugins/web-search-providers.js";
|
||||
import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js";
|
||||
import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
@@ -101,7 +101,14 @@ function hasCustomWebSearchPluginRisk(config: OpenClawConfig): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bundledPluginIds = new Set<string>(listBundledWebSearchPluginIds());
|
||||
const bundledPluginIds = new Set<string>(
|
||||
resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
config,
|
||||
env: process.env,
|
||||
}),
|
||||
);
|
||||
const hasNonBundledPluginId = (pluginId: string) => !bundledPluginIds.has(pluginId.trim());
|
||||
if (Array.isArray(plugins.allow) && plugins.allow.some(hasNonBundledPluginId)) {
|
||||
return true;
|
||||
@@ -364,7 +371,13 @@ export async function resolveRuntimeWebTools(params: {
|
||||
const search = isRecord(web?.search) ? web.search : undefined;
|
||||
const rawProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledPluginId = resolveBundledWebSearchPluginId(rawProvider);
|
||||
const configuredBundledPluginId = resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: rawProvider,
|
||||
origin: "bundled",
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
});
|
||||
|
||||
const searchMetadata: RuntimeWebSearchMetadata = {
|
||||
providerSource: "none",
|
||||
@@ -373,17 +386,19 @@ export async function resolveRuntimeWebTools(params: {
|
||||
|
||||
const searchProviders = sortWebSearchProvidersForAutoDetect(
|
||||
configuredBundledPluginId
|
||||
? resolveBundledPluginWebSearchProviders({
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: !hasCustomWebSearchPluginRisk(params.sourceConfig)
|
||||
? resolveBundledPluginWebSearchProviders({
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
@@ -664,23 +679,31 @@ export async function resolveRuntimeWebTools(params: {
|
||||
const fetch = isRecord(web?.fetch) ? (web.fetch as FetchConfig) : undefined;
|
||||
const rawFetchProvider =
|
||||
typeof fetch?.provider === "string" ? fetch.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledFetchPluginId = resolveBundledWebFetchPluginId(rawFetchProvider);
|
||||
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
|
||||
? resolveBundledPluginWebFetchProviders({
|
||||
? resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
onlyPluginIds: [configuredBundledFetchPluginId],
|
||||
origin: "bundled",
|
||||
})
|
||||
: resolveBundledPluginWebFetchProviders({
|
||||
: resolvePluginWebFetchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.context.env },
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
const hasConfiguredFetchSurface =
|
||||
|
||||
@@ -11,32 +11,20 @@ import { listSecretTargetRegistryEntries } from "./target-registry.js";
|
||||
|
||||
type SecretRegistryEntry = ReturnType<typeof listSecretTargetRegistryEntries>[number];
|
||||
|
||||
const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvidersMock } =
|
||||
vi.hoisted(() => ({
|
||||
resolveBundledPluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
const { resolveBundledPluginWebFetchProvidersMock, resolvePluginWebFetchProvidersMock } =
|
||||
vi.hoisted(() => ({
|
||||
resolveBundledPluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()),
|
||||
resolvePluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()),
|
||||
}));
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
const { resolvePluginWebFetchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebFetchProvidersMock: vi.fn(() => buildTestWebFetchProviders()),
|
||||
}));
|
||||
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => ({
|
||||
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.js", () => ({
|
||||
resolveBundledPluginWebFetchProviders: resolveBundledPluginWebFetchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.runtime.js", () => ({
|
||||
resolvePluginWebFetchProviders: resolvePluginWebFetchProvidersMock,
|
||||
}));
|
||||
@@ -343,9 +331,7 @@ describe("secrets runtime target coverage", () => {
|
||||
|
||||
afterEach(() => {
|
||||
clearSecretsRuntimeSnapshot();
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReset();
|
||||
resolvePluginWebFetchProvidersMock.mockReset();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,14 +10,8 @@ import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
|
||||
type WebProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl";
|
||||
|
||||
const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvidersMock } =
|
||||
vi.hoisted(() => ({
|
||||
resolveBundledPluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => ({
|
||||
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
@@ -132,8 +126,6 @@ describe("secrets runtime snapshot", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReset();
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
});
|
||||
|
||||
@@ -9,18 +9,15 @@ type TestPluginWebFetchConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
const { resolveBundledPluginWebFetchProvidersMock, resolveRuntimeWebFetchProvidersMock } =
|
||||
vi.hoisted(() => ({
|
||||
resolveBundledPluginWebFetchProvidersMock: vi.fn<() => PluginWebFetchProviderEntry[]>(() => []),
|
||||
const { resolvePluginWebFetchProvidersMock, resolveRuntimeWebFetchProvidersMock } = vi.hoisted(
|
||||
() => ({
|
||||
resolvePluginWebFetchProvidersMock: vi.fn<() => PluginWebFetchProviderEntry[]>(() => []),
|
||||
resolveRuntimeWebFetchProvidersMock: vi.fn<() => PluginWebFetchProviderEntry[]>(() => []),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.js", () => ({
|
||||
resolveBundledPluginWebFetchProviders: resolveBundledPluginWebFetchProvidersMock,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/web-fetch-providers.runtime.js", () => ({
|
||||
resolvePluginWebFetchProviders: resolveRuntimeWebFetchProvidersMock,
|
||||
resolvePluginWebFetchProviders: resolvePluginWebFetchProvidersMock,
|
||||
resolveRuntimeWebFetchProviders: resolveRuntimeWebFetchProvidersMock,
|
||||
}));
|
||||
|
||||
@@ -69,9 +66,9 @@ describe("web fetch runtime", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReset();
|
||||
resolvePluginWebFetchProvidersMock.mockReset();
|
||||
resolveRuntimeWebFetchProvidersMock.mockReset();
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([]);
|
||||
resolveRuntimeWebFetchProvidersMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
@@ -92,7 +89,7 @@ describe("web fetch runtime", () => {
|
||||
return pluginConfig?.webFetch?.apiKey;
|
||||
},
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
@@ -133,7 +130,7 @@ describe("web fetch runtime", () => {
|
||||
}),
|
||||
}),
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
resolveRuntimeWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const runtimeWebFetch: RuntimeWebFetchMetadata = {
|
||||
@@ -171,7 +168,7 @@ describe("web fetch runtime", () => {
|
||||
credentialPath: "plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
autoDetectOrder: 1,
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "firecrawl-env-key");
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
@@ -189,7 +186,7 @@ describe("web fetch runtime", () => {
|
||||
autoDetectOrder: 1,
|
||||
getConfiguredCredentialValue: () => "firecrawl-key",
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
config: {
|
||||
@@ -221,7 +218,7 @@ describe("web fetch runtime", () => {
|
||||
autoDetectOrder: 0,
|
||||
getConfiguredCredentialValue: () => "runtime-key",
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([bundled]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([bundled]);
|
||||
resolveRuntimeWebFetchProvidersMock.mockReturnValue([runtimeOnly]);
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
@@ -248,7 +245,7 @@ describe("web fetch runtime", () => {
|
||||
autoDetectOrder: 0,
|
||||
getConfiguredCredentialValue: () => "runtime-key",
|
||||
});
|
||||
resolveBundledPluginWebFetchProvidersMock.mockReturnValue([bundled]);
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([bundled]);
|
||||
resolveRuntimeWebFetchProvidersMock.mockReturnValue([runtimeOnly]);
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
PluginWebFetchProviderEntry,
|
||||
WebFetchProviderToolDefinition,
|
||||
} from "../plugins/types.js";
|
||||
import { resolveBundledPluginWebFetchProviders } from "../plugins/web-fetch-providers.js";
|
||||
import { resolvePluginWebFetchProviders } from "../plugins/web-fetch-providers.runtime.js";
|
||||
import { sortWebFetchProvidersForAutoDetect } from "../plugins/web-fetch-providers.shared.js";
|
||||
import type { RuntimeWebFetchMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
@@ -86,9 +85,10 @@ function hasEntryCredential(
|
||||
export function listWebFetchProviders(params?: {
|
||||
config?: OpenClawConfig;
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
return resolveBundledPluginWebFetchProviders({
|
||||
return resolvePluginWebFetchProviders({
|
||||
config: params?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,9 +108,10 @@ export function resolveWebFetchProviderId(params: {
|
||||
}): string {
|
||||
const providers = sortWebFetchProvidersForAutoDetect(
|
||||
params.providers ??
|
||||
resolveBundledPluginWebFetchProviders({
|
||||
resolvePluginWebFetchProviders({
|
||||
config: params.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
const raw =
|
||||
@@ -154,9 +155,10 @@ export function resolveWebFetchDefinition(
|
||||
}
|
||||
|
||||
const providers = sortWebFetchProvidersForAutoDetect(
|
||||
resolveBundledPluginWebFetchProviders({
|
||||
resolvePluginWebFetchProviders({
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
).filter(Boolean);
|
||||
if (providers.length === 0) {
|
||||
|
||||
@@ -8,20 +8,15 @@ type TestPluginWebSearchConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
const { resolveBundledPluginWebSearchProvidersMock, resolveRuntimeWebSearchProvidersMock } =
|
||||
vi.hoisted(() => ({
|
||||
resolveBundledPluginWebSearchProvidersMock: vi.fn<() => PluginWebSearchProviderEntry[]>(
|
||||
() => [],
|
||||
),
|
||||
const { resolvePluginWebSearchProvidersMock, resolveRuntimeWebSearchProvidersMock } = vi.hoisted(
|
||||
() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn<() => PluginWebSearchProviderEntry[]>(() => []),
|
||||
resolveRuntimeWebSearchProvidersMock: vi.fn<() => PluginWebSearchProviderEntry[]>(() => []),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => ({
|
||||
resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolveRuntimeWebSearchProvidersMock,
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
resolveRuntimeWebSearchProviders: resolveRuntimeWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
@@ -71,9 +66,9 @@ describe("web search runtime", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolveRuntimeWebSearchProvidersMock.mockReset();
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([]);
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue([]);
|
||||
resolveRuntimeWebSearchProvidersMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
@@ -127,7 +122,7 @@ describe("web search runtime", () => {
|
||||
}),
|
||||
});
|
||||
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
@@ -174,7 +169,7 @@ describe("web search runtime", () => {
|
||||
}),
|
||||
});
|
||||
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
resolveBundledPluginWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
PluginWebSearchProviderEntry,
|
||||
WebSearchProviderToolDefinition,
|
||||
} from "../plugins/types.js";
|
||||
import { resolveBundledPluginWebSearchProviders } from "../plugins/web-search-providers.js";
|
||||
import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js";
|
||||
import { resolveRuntimeWebSearchProviders } from "../plugins/web-search-providers.runtime.js";
|
||||
import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js";
|
||||
@@ -130,9 +129,10 @@ export function resolveWebSearchProviderId(params: {
|
||||
}): string {
|
||||
const providers = sortWebSearchProvidersForAutoDetect(
|
||||
params.providers ??
|
||||
resolveBundledPluginWebSearchProviders({
|
||||
resolvePluginWebSearchProviders({
|
||||
config: params.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
);
|
||||
const raw =
|
||||
@@ -188,9 +188,10 @@ export function resolveWebSearchDefinition(
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
})
|
||||
: resolveBundledPluginWebSearchProviders({
|
||||
: resolvePluginWebSearchProviders({
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
origin: "bundled",
|
||||
}),
|
||||
).filter(Boolean);
|
||||
if (providers.length === 0) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "../../../src/plugins/bundled-capability-runtime.js";
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "../../../src/plugins/bundled-web-search-ids.js";
|
||||
import { resolveBundledWebSearchPluginId } from "../../../src/plugins/bundled-web-search-provider-ids.js";
|
||||
import { listBundledWebSearchProviders } from "../../../src/plugins/bundled-web-search.js";
|
||||
import {
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../../../src/plugins/manifest-registry.js";
|
||||
import { resolvePluginWebSearchProviders } from "../../../src/plugins/web-search-providers.runtime.js";
|
||||
|
||||
type ComparableProvider = {
|
||||
pluginId: string;
|
||||
@@ -79,21 +81,31 @@ function sortComparableEntries(entries: ComparableProvider[]): ComparableProvide
|
||||
export function describeBundledWebSearchFastPathContract(pluginId: string) {
|
||||
describe(`${pluginId} bundled web search fast-path contract`, () => {
|
||||
it("keeps provider-to-plugin ids aligned with bundled contracts", () => {
|
||||
const providers = listBundledWebSearchProviders().filter(
|
||||
(provider) => provider.pluginId === pluginId,
|
||||
);
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
origin: "bundled",
|
||||
}).filter((provider) => provider.pluginId === pluginId);
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
for (const provider of providers) {
|
||||
expect(resolveBundledWebSearchPluginId(provider.id)).toBe(pluginId);
|
||||
expect(
|
||||
resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: provider.id,
|
||||
origin: "bundled",
|
||||
}),
|
||||
).toBe(pluginId);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps fast-path provider metadata aligned with the bundled runtime registry", async () => {
|
||||
const fastPathProviders = listBundledWebSearchProviders().filter(
|
||||
(provider) => provider.pluginId === pluginId,
|
||||
);
|
||||
const bundledWebSearchPluginIds = resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
origin: "bundled",
|
||||
});
|
||||
const fastPathProviders = resolvePluginWebSearchProviders({
|
||||
origin: "bundled",
|
||||
}).filter((provider) => provider.pluginId === pluginId);
|
||||
const bundledProviderEntries = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
pluginIds: bundledWebSearchPluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
})
|
||||
.webSearchProviders.filter((entry) => entry.pluginId === pluginId)
|
||||
|
||||
Reference in New Issue
Block a user