Plugins: reuse compatible runtime web search registries

This commit is contained in:
Gustavo Madeira Santana
2026-03-28 00:07:24 -04:00
parent a00127bf5b
commit 9b405f88d4
2 changed files with 114 additions and 39 deletions

View File

@@ -5,6 +5,7 @@ type RuntimeModule = typeof import("./runtime.js");
type WebSearchProvidersRuntimeModule = typeof import("./web-search-providers.runtime.js");
type ManifestRegistryModule = typeof import("./manifest-registry.js");
type PluginAutoEnableModule = typeof import("../config/plugin-auto-enable.js");
type WebSearchProvidersSharedModule = typeof import("./web-search-providers.shared.js");
const BUNDLED_WEB_SEARCH_PROVIDERS = [
{ pluginId: "brave", id: "brave", order: 10 },
@@ -29,6 +30,7 @@ let loaderModule: typeof import("./loader.js");
let manifestRegistryModule: ManifestRegistryModule;
let pluginAutoEnableModule: PluginAutoEnableModule;
let applyPluginAutoEnableSpy: ReturnType<typeof vi.fn>;
let webSearchProvidersSharedModule: WebSearchProvidersSharedModule;
const DEFAULT_WEB_SEARCH_WORKSPACE = "/tmp/workspace";
const EXPECTED_BUNDLED_RUNTIME_WEB_SEARCH_PROVIDER_KEYS = [
@@ -206,6 +208,7 @@ describe("resolvePluginWebSearchProviders", () => {
manifestRegistryModule = await import("./manifest-registry.js");
loaderModule = await import("./loader.js");
pluginAutoEnableModule = await import("../config/plugin-auto-enable.js");
webSearchProvidersSharedModule = await import("./web-search-providers.shared.js");
({ setActivePluginRegistry } = await import("./runtime.js"));
({
resolvePluginWebSearchProviders,
@@ -417,4 +420,55 @@ describe("resolvePluginWebSearchProviders", () => {
expect(toRuntimeProviderKeys(providers)).toEqual(["custom-search:custom"]);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("reuses a compatible active registry for runtime resolution when config is provided", () => {
const env = createWebSearchEnv();
const { config } = webSearchProvidersSharedModule.resolveBundledWebSearchResolutionConfig({
config: createBraveAllowConfig(),
bundledAllowlistCompat: true,
env,
});
const { cacheKey } = loaderModule.resolvePluginLoadCacheContext({
config,
workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
env,
onlyPluginIds: ["brave"],
cache: false,
activate: false,
});
const registry = createEmptyPluginRegistry();
registry.webSearchProviders.push({
pluginId: "brave",
pluginName: "Brave",
provider: {
id: "brave",
label: "Brave Search",
hint: "Brave runtime provider",
envVars: ["BRAVE_API_KEY"],
placeholder: "brave-...",
signupUrl: "https://example.com/brave",
autoDetectOrder: 1,
credentialPath: "tools.web.search.brave.apiKey",
getCredentialValue: () => "configured",
setCredentialValue: () => {},
createTool: () => ({
description: "brave",
parameters: {},
execute: async () => ({}),
}),
},
source: "test",
});
setActivePluginRegistry(registry, cacheKey);
const providers = resolveRuntimeWebSearchProviders({
config: createBraveAllowConfig(),
bundledAllowlistCompat: true,
workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
env,
});
expect(toRuntimeProviderKeys(providers)).toEqual(["brave:brave"]);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -6,11 +6,10 @@ import {
resolvePluginSnapshotCacheTtlMs,
shouldUsePluginSnapshotCache,
} from "./cache-controls.js";
import { loadOpenClawPlugins } from "./loader.js";
import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js";
import type { PluginLoadOptions } from "./loader.js";
import { createPluginLoaderLogger } from "./logger.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import { getActivePluginRegistry } from "./runtime.js";
import type { PluginWebSearchProviderEntry } from "./types.js";
import {
resolveBundledWebSearchResolutionConfig,
@@ -94,6 +93,53 @@ function resolveWebSearchCandidatePluginIds(params: {
return ids.length > 0 ? ids : undefined;
}
function resolveWebSearchLoadOptions(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
bundledAllowlistCompat?: boolean;
onlyPluginIds?: readonly string[];
activate?: boolean;
cache?: boolean;
}) {
const env = params.env ?? process.env;
const { config } = resolveBundledWebSearchResolutionConfig({
...params,
env,
});
const onlyPluginIds = resolveWebSearchCandidatePluginIds({
config,
workspaceDir: params.workspaceDir,
env,
onlyPluginIds: params.onlyPluginIds,
});
return {
env,
config,
workspaceDir: params.workspaceDir,
cache: params.cache ?? false,
activate: params.activate ?? false,
...(onlyPluginIds ? { onlyPluginIds } : {}),
logger: createPluginLoaderLogger(log),
} satisfies PluginLoadOptions;
}
function mapRegistryWebSearchProviders(params: {
registry: ReturnType<typeof loadOpenClawPlugins>;
onlyPluginIds?: readonly string[];
}): PluginWebSearchProviderEntry[] {
const onlyPluginIdSet =
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
return sortWebSearchProviders(
params.registry.webSearchProviders
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
})),
);
}
export function resolvePluginWebSearchProviders(params: {
config?: PluginLoadOptions["config"];
workspaceDir?: string;
@@ -122,32 +168,10 @@ export function resolvePluginWebSearchProviders(params: {
return cached.providers;
}
}
const { config } = resolveBundledWebSearchResolutionConfig({
...params,
env,
const loadOptions = resolveWebSearchLoadOptions(params);
const resolved = mapRegistryWebSearchProviders({
registry: loadOpenClawPlugins(loadOptions),
});
const onlyPluginIds = resolveWebSearchCandidatePluginIds({
config,
workspaceDir: params.workspaceDir,
env,
onlyPluginIds: params.onlyPluginIds,
});
const registry = loadOpenClawPlugins({
config,
workspaceDir: params.workspaceDir,
env,
cache: params.cache ?? false,
activate: params.activate ?? false,
...(onlyPluginIds ? { onlyPluginIds } : {}),
logger: createPluginLoaderLogger(log),
});
const resolved = sortWebSearchProviders(
registry.webSearchProviders.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
})),
);
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
let configCache = webSearchProviderSnapshotCache.get(cacheOwnerConfig);
@@ -178,18 +202,15 @@ export function resolveRuntimeWebSearchProviders(params: {
bundledAllowlistCompat?: boolean;
onlyPluginIds?: readonly string[];
}): PluginWebSearchProviderEntry[] {
const runtimeProviders = getActivePluginRegistry()?.webSearchProviders ?? [];
const onlyPluginIdSet =
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
if (runtimeProviders.length > 0) {
return sortWebSearchProviders(
runtimeProviders
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
})),
);
const runtimeRegistry =
params.config === undefined
? getCompatibleActivePluginRegistry()
: getCompatibleActivePluginRegistry(resolveWebSearchLoadOptions(params));
if (runtimeRegistry) {
return mapRegistryWebSearchProviders({
registry: runtimeRegistry,
onlyPluginIds: params.onlyPluginIds,
});
}
return resolvePluginWebSearchProviders(params);
}