From 80206bf20a423e0dfabcea72888e447207095e12 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:19:48 -0500 Subject: [PATCH] feat: migrate core search providers to bundled plugins --- extensions/search-brave/index.ts | 12 ++ extensions/search-brave/openclaw.plugin.json | 4 + extensions/search-brave/package.json | 12 ++ extensions/search-gemini/index.ts | 12 ++ extensions/search-gemini/openclaw.plugin.json | 4 + extensions/search-gemini/package.json | 12 ++ extensions/search-grok/index.ts | 12 ++ extensions/search-grok/openclaw.plugin.json | 4 + extensions/search-grok/package.json | 12 ++ extensions/search-kimi/index.ts | 12 ++ extensions/search-kimi/openclaw.plugin.json | 4 + extensions/search-kimi/package.json | 12 ++ extensions/search-perplexity/index.ts | 12 ++ .../search-perplexity/openclaw.plugin.json | 4 + extensions/search-perplexity/package.json | 12 ++ .../tools/web-search-provider-catalog.ts | 12 ++ src/agents/tools/web-search.ts | 107 +++++++++++++----- .../tools/web-tools.enabled-defaults.test.ts | 69 +++++++++++ src/commands/configure.wizard.test.ts | 2 +- src/commands/onboard-search.test.ts | 58 ++++++++++ src/commands/onboard-search.ts | 18 ++- src/plugin-sdk/index.test.ts | 1 + src/plugin-sdk/index.ts | 1 + src/plugins/config-state.test.ts | 26 +++++ src/plugins/config-state.ts | 2 + src/plugins/hooks.ts | 80 ++++++------- src/plugins/types.ts | 1 + 27 files changed, 438 insertions(+), 79 deletions(-) create mode 100644 extensions/search-brave/index.ts create mode 100644 extensions/search-brave/openclaw.plugin.json create mode 100644 extensions/search-brave/package.json create mode 100644 extensions/search-gemini/index.ts create mode 100644 extensions/search-gemini/openclaw.plugin.json create mode 100644 extensions/search-gemini/package.json create mode 100644 extensions/search-grok/index.ts create mode 100644 extensions/search-grok/openclaw.plugin.json create mode 100644 extensions/search-grok/package.json create mode 100644 extensions/search-kimi/index.ts create mode 100644 extensions/search-kimi/openclaw.plugin.json create mode 100644 extensions/search-kimi/package.json create mode 100644 extensions/search-perplexity/index.ts create mode 100644 extensions/search-perplexity/openclaw.plugin.json create mode 100644 extensions/search-perplexity/package.json diff --git a/extensions/search-brave/index.ts b/extensions/search-brave/index.ts new file mode 100644 index 00000000000..aa34520a7c8 --- /dev/null +++ b/extensions/search-brave/index.ts @@ -0,0 +1,12 @@ +import { createBundledBuiltinSearchProvider, type OpenClawPluginApi } from "openclaw/plugin-sdk"; + +const plugin = { + id: "search-brave", + name: "Brave Search", + description: "Bundled Brave web search provider for OpenClaw.", + register(api: OpenClawPluginApi) { + api.registerSearchProvider(createBundledBuiltinSearchProvider("brave")); + }, +}; + +export default plugin; diff --git a/extensions/search-brave/openclaw.plugin.json b/extensions/search-brave/openclaw.plugin.json new file mode 100644 index 00000000000..060c043872e --- /dev/null +++ b/extensions/search-brave/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "search-brave", + "provides": ["providers.search.brave"] +} diff --git a/extensions/search-brave/package.json b/extensions/search-brave/package.json new file mode 100644 index 00000000000..0c3cf7af6fe --- /dev/null +++ b/extensions/search-brave/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/search-brave", + "version": "2026.3.12", + "private": true, + "description": "OpenClaw bundled Brave search provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/search-gemini/index.ts b/extensions/search-gemini/index.ts new file mode 100644 index 00000000000..c55b8a04e2d --- /dev/null +++ b/extensions/search-gemini/index.ts @@ -0,0 +1,12 @@ +import { createBundledBuiltinSearchProvider, type OpenClawPluginApi } from "openclaw/plugin-sdk"; + +const plugin = { + id: "search-gemini", + name: "Gemini Search", + description: "Bundled Gemini web search provider for OpenClaw.", + register(api: OpenClawPluginApi) { + api.registerSearchProvider(createBundledBuiltinSearchProvider("gemini")); + }, +}; + +export default plugin; diff --git a/extensions/search-gemini/openclaw.plugin.json b/extensions/search-gemini/openclaw.plugin.json new file mode 100644 index 00000000000..a774390b13a --- /dev/null +++ b/extensions/search-gemini/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "search-gemini", + "provides": ["providers.search.gemini"] +} diff --git a/extensions/search-gemini/package.json b/extensions/search-gemini/package.json new file mode 100644 index 00000000000..8f03543e77b --- /dev/null +++ b/extensions/search-gemini/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/search-gemini", + "version": "2026.3.12", + "private": true, + "description": "OpenClaw bundled Gemini search provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/search-grok/index.ts b/extensions/search-grok/index.ts new file mode 100644 index 00000000000..88802da23c2 --- /dev/null +++ b/extensions/search-grok/index.ts @@ -0,0 +1,12 @@ +import { createBundledBuiltinSearchProvider, type OpenClawPluginApi } from "openclaw/plugin-sdk"; + +const plugin = { + id: "search-grok", + name: "Grok Search", + description: "Bundled xAI Grok web search provider for OpenClaw.", + register(api: OpenClawPluginApi) { + api.registerSearchProvider(createBundledBuiltinSearchProvider("grok")); + }, +}; + +export default plugin; diff --git a/extensions/search-grok/openclaw.plugin.json b/extensions/search-grok/openclaw.plugin.json new file mode 100644 index 00000000000..f28e593ec25 --- /dev/null +++ b/extensions/search-grok/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "search-grok", + "provides": ["providers.search.grok"] +} diff --git a/extensions/search-grok/package.json b/extensions/search-grok/package.json new file mode 100644 index 00000000000..ace1527f22c --- /dev/null +++ b/extensions/search-grok/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/search-grok", + "version": "2026.3.12", + "private": true, + "description": "OpenClaw bundled Grok search provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/search-kimi/index.ts b/extensions/search-kimi/index.ts new file mode 100644 index 00000000000..908f8704bfd --- /dev/null +++ b/extensions/search-kimi/index.ts @@ -0,0 +1,12 @@ +import { createBundledBuiltinSearchProvider, type OpenClawPluginApi } from "openclaw/plugin-sdk"; + +const plugin = { + id: "search-kimi", + name: "Kimi Search", + description: "Bundled Kimi web search provider for OpenClaw.", + register(api: OpenClawPluginApi) { + api.registerSearchProvider(createBundledBuiltinSearchProvider("kimi")); + }, +}; + +export default plugin; diff --git a/extensions/search-kimi/openclaw.plugin.json b/extensions/search-kimi/openclaw.plugin.json new file mode 100644 index 00000000000..5811bfdbeff --- /dev/null +++ b/extensions/search-kimi/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "search-kimi", + "provides": ["providers.search.kimi"] +} diff --git a/extensions/search-kimi/package.json b/extensions/search-kimi/package.json new file mode 100644 index 00000000000..1a19b0ad14f --- /dev/null +++ b/extensions/search-kimi/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/search-kimi", + "version": "2026.3.12", + "private": true, + "description": "OpenClaw bundled Kimi search provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/search-perplexity/index.ts b/extensions/search-perplexity/index.ts new file mode 100644 index 00000000000..351742b0675 --- /dev/null +++ b/extensions/search-perplexity/index.ts @@ -0,0 +1,12 @@ +import { createBundledBuiltinSearchProvider, type OpenClawPluginApi } from "openclaw/plugin-sdk"; + +const plugin = { + id: "search-perplexity", + name: "Perplexity Search", + description: "Bundled Perplexity web search provider for OpenClaw.", + register(api: OpenClawPluginApi) { + api.registerSearchProvider(createBundledBuiltinSearchProvider("perplexity")); + }, +}; + +export default plugin; diff --git a/extensions/search-perplexity/openclaw.plugin.json b/extensions/search-perplexity/openclaw.plugin.json new file mode 100644 index 00000000000..e2cb10c2642 --- /dev/null +++ b/extensions/search-perplexity/openclaw.plugin.json @@ -0,0 +1,4 @@ +{ + "id": "search-perplexity", + "provides": ["providers.search.perplexity"] +} diff --git a/extensions/search-perplexity/package.json b/extensions/search-perplexity/package.json new file mode 100644 index 00000000000..32cdc1a19eb --- /dev/null +++ b/extensions/search-perplexity/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/search-perplexity", + "version": "2026.3.12", + "private": true, + "description": "OpenClaw bundled Perplexity search provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/src/agents/tools/web-search-provider-catalog.ts b/src/agents/tools/web-search-provider-catalog.ts index 0352344c9fd..d8c82f9b5cc 100644 --- a/src/agents/tools/web-search-provider-catalog.ts +++ b/src/agents/tools/web-search-provider-catalog.ts @@ -8,6 +8,18 @@ export const BUILTIN_WEB_SEARCH_PROVIDER_IDS = [ export type BuiltinWebSearchProviderId = (typeof BUILTIN_WEB_SEARCH_PROVIDER_IDS)[number]; +export const MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_IDS = BUILTIN_WEB_SEARCH_PROVIDER_IDS; + +export type MigratedBundledWebSearchProviderId = + (typeof MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_IDS)[number]; + +export const bundledCoreWebSearchPluginId = (providerId: BuiltinWebSearchProviderId): string => + `search-${providerId}`; + +export const MIGRATED_BUNDLED_WEB_SEARCH_PLUGIN_IDS = MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_IDS.map( + bundledCoreWebSearchPluginId, +); + export type BuiltinWebSearchProviderEntry = { value: BuiltinWebSearchProviderId; label: string; diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index b96f67dead1..d4320577adb 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -2461,10 +2461,56 @@ function getBuiltinSearchProviders(search?: WebSearchConfig): SearchProviderPlug ]; } +export function createBundledBuiltinSearchProvider( + providerId: BuiltinWebSearchProviderId, +): SearchProviderPlugin { + const providers = getBuiltinSearchProviders(); + switch (providerId) { + case "brave": + return { + ...providers[0], + builtinProviderId: "brave", + }; + case "gemini": + return { + ...providers[1], + builtinProviderId: "gemini", + }; + case "grok": + return { + ...providers[2], + builtinProviderId: "grok", + }; + case "kimi": + return { + ...providers[3], + builtinProviderId: "kimi", + }; + case "perplexity": + return { + ...providers[4], + builtinProviderId: "perplexity", + }; + } +} + function getPluginSearchProviders(): SearchProviderPlugin[] { return getActivePluginRegistry()?.searchProviders.map((entry) => entry.provider) ?? []; } +function resolveBuiltinSchemaProviderId( + provider: SearchProviderPlugin, +): BuiltinWebSearchProviderId | undefined { + if (provider.builtinProviderId && isBuiltinSearchProviderId(provider.builtinProviderId)) { + return provider.builtinProviderId; + } + if (!provider.pluginId) { + const candidate = normalizeSearchProviderId(provider.id); + return isBuiltinSearchProviderId(candidate) ? candidate : undefined; + } + return undefined; +} + function resolveConfiguredSearchProviderId(params: { config?: OpenClawConfig; search?: WebSearchConfig; @@ -2582,8 +2628,8 @@ function createSearchProviderSchema(params: { search?: WebSearchConfig; runtimeWebSearch?: RuntimeWebSearchMetadata; }) { - const providerId = normalizeSearchProviderId(params.provider.id); - if (!params.provider.pluginId && isBuiltinSearchProviderId(providerId)) { + const providerId = resolveBuiltinSchemaProviderId(params.provider); + if (providerId) { const perplexityTransport = params.runtimeWebSearch?.selectedProvider === "perplexity" ? params.runtimeWebSearch.perplexityTransport @@ -2719,36 +2765,35 @@ export function createWebSearchTool(options?: { }); } - const providerId = normalizeSearchProviderId(provider.id); + const builtinProviderId = resolveBuiltinSchemaProviderId(provider); logVerbose(formatWebSearchExecutionLog(provider)); - const result = - !provider.pluginId && isBuiltinSearchProviderId(providerId) - ? await executeBuiltinSearchProvider({ - provider: providerId, - request, - context: { - config: options?.config ?? {}, - timeoutSeconds: resolveTimeoutSeconds( - search?.timeoutSeconds, - DEFAULT_TIMEOUT_SECONDS, - ), - cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), - pluginConfig: resolveSearchProviderPluginConfig(options?.config, provider), - }, - }) - : await executePluginSearchProvider({ - provider, - request, - context: { - config: options?.config ?? {}, - timeoutSeconds: resolveTimeoutSeconds( - search?.timeoutSeconds, - DEFAULT_TIMEOUT_SECONDS, - ), - cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), - pluginConfig: resolveSearchProviderPluginConfig(options?.config, provider), - }, - }); + const result = builtinProviderId + ? await executeBuiltinSearchProvider({ + provider: builtinProviderId, + request, + context: { + config: options?.config ?? {}, + timeoutSeconds: resolveTimeoutSeconds( + search?.timeoutSeconds, + DEFAULT_TIMEOUT_SECONDS, + ), + cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), + pluginConfig: resolveSearchProviderPluginConfig(options?.config, provider), + }, + }) + : await executePluginSearchProvider({ + provider, + request, + context: { + config: options?.config ?? {}, + timeoutSeconds: resolveTimeoutSeconds( + search?.timeoutSeconds, + DEFAULT_TIMEOUT_SECONDS, + ), + cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), + pluginConfig: resolveSearchProviderPluginConfig(options?.config, provider), + }, + }); return jsonResult(result); }, }; diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index f0937434f46..be3f138d396 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -210,6 +210,75 @@ describe("web tools defaults", () => { }); describe("web_search plugin providers", () => { + it.each(["brave", "perplexity", "grok", "gemini", "kimi"] as const)( + "resolves configured built-in provider %s through bundled plugin registrations when available", + async (providerId) => { + const registry = createEmptyPluginRegistry(); + registry.searchProviders.push({ + pluginId: `search-${providerId}`, + source: `/plugins/search-${providerId}`, + provider: { + id: providerId, + name: `${providerId} bundled provider`, + pluginId: `search-${providerId}`, + builtinProviderId: providerId, + isAvailable: () => true, + search: async () => ({ content: "unused" }), + }, + }); + setActivePluginRegistry(registry); + + const mockFetch = installMockFetch(createProviderSuccessPayload(providerId)); + const provider = webSearchTesting.resolveRegisteredSearchProvider({ + config: { + tools: { + web: { + search: + providerId === "perplexity" + ? { provider: providerId, perplexity: { apiKey: "pplx-config-test" } } + : providerId === "grok" + ? { provider: providerId, grok: { apiKey: "xai-config-test" } } + : providerId === "gemini" + ? { provider: providerId, gemini: { apiKey: "gemini-config-test" } } + : providerId === "kimi" + ? { provider: providerId, kimi: { apiKey: "moonshot-config-test" } } + : { provider: providerId, apiKey: "brave-config-test" }, + }, + }, + }, + }); + + expect(provider.pluginId).toBe(`search-${providerId}`); + + const tool = createWebSearchTool({ + config: { + tools: { + web: { + search: + providerId === "perplexity" + ? { provider: providerId, perplexity: { apiKey: "pplx-config-test" } } + : providerId === "grok" + ? { provider: providerId, grok: { apiKey: "xai-config-test" } } + : providerId === "gemini" + ? { provider: providerId, gemini: { apiKey: "gemini-config-test" } } + : providerId === "kimi" + ? { provider: providerId, kimi: { apiKey: "moonshot-config-test" } } + : { provider: providerId, apiKey: "brave-config-test" }, + }, + }, + }, + sandboxed: true, + }); + + const result = await tool?.execute?.(`call-bundled-${providerId}`, { + query: `bundled ${providerId}`, + }); + + expect(mockFetch).toHaveBeenCalled(); + expect((result?.details as { provider?: string } | undefined)?.provider).toBe(providerId); + }, + ); + it("prefers an explicitly configured plugin provider over a built-in provider with the same id", async () => { const searchMock = vi.fn(async () => ({ results: [ diff --git a/src/commands/configure.wizard.test.ts b/src/commands/configure.wizard.test.ts index 743dbce2d48..2c690c45862 100644 --- a/src/commands/configure.wizard.test.ts +++ b/src/commands/configure.wizard.test.ts @@ -335,7 +335,7 @@ describe("runConfigureWizard", () => { }), }), ); - expect(mocks.writeConfigFile.mock.calls[0]?.[0]?.tools?.web?.search?.provider).toBeUndefined(); + expect(mocks.writeConfigFile.mock.calls[0]?.[0]?.tools?.web?.search?.provider).toBe("brave"); }); it("re-prompts invalid plugin config values during configure", async () => { diff --git a/src/commands/onboard-search.test.ts b/src/commands/onboard-search.test.ts index 6fb3a66f7a6..192158c9d1c 100644 --- a/src/commands/onboard-search.test.ts +++ b/src/commands/onboard-search.test.ts @@ -242,6 +242,64 @@ describe("setupSearch", () => { ); }); + it.each([ + ["brave", "Brave Search"], + ["gemini", "Gemini (Google Search)"], + ["grok", "Grok (xAI)"], + ["kimi", "Kimi (Moonshot)"], + ["perplexity", "Perplexity Search"], + ] as const)( + "does not duplicate built-in provider %s when a bundled search plugin registers the same provider id", + async (providerId, providerLabel) => { + loadOpenClawPlugins.mockReturnValue({ + searchProviders: [ + { + pluginId: `search-${providerId}`, + provider: { + id: providerId, + name: providerLabel, + description: `Bundled ${providerLabel} provider`, + pluginId: `search-${providerId}`, + builtinProviderId: providerId, + isAvailable: () => true, + search: async () => ({ content: "ok" }), + }, + }, + ], + plugins: [ + { + id: `search-${providerId}`, + name: providerLabel, + description: `Bundled ${providerLabel} provider`, + origin: "bundled", + source: `/tmp/bundled/search-${providerId}`, + configJsonSchema: undefined, + configUiHints: undefined, + }, + ], + typedHooks: [], + }); + loadPluginManifestRegistry.mockReturnValue({ + plugins: [], + diagnostics: [], + }); + + const cfg: OpenClawConfig = {}; + const { prompter } = createPrompter({ selectValue: "__skip__" }); + await setupSearch(cfg, runtime, prompter); + + const providerSelectCall = (prompter.select as ReturnType).mock.calls.find( + (call) => call[0]?.message === "Choose active web search provider", + ); + const matchingOptions = + providerSelectCall?.[0]?.options?.filter( + (option: { value?: string }) => option.value === providerId, + ) ?? []; + expect(matchingOptions).toHaveLength(1); + expect(matchingOptions[0]?.hint).toContain("Bundled plugin"); + }, + ); + it("uses the updated configure-or-install action label", async () => { vi.stubEnv("BRAVE_API_KEY", "BSA-test-key"); loadOpenClawPlugins.mockReturnValue({ diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index c8097f2238f..110a4a2b161 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -1,5 +1,6 @@ import { BUILTIN_WEB_SEARCH_PROVIDER_OPTIONS, + MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_IDS, type BuiltinWebSearchProviderEntry, type BuiltinWebSearchProviderId, isBuiltinWebSearchProviderId, @@ -49,6 +50,10 @@ const SEARCH_PROVIDER_SKIP_SENTINEL = "__skip__" as const; const SEARCH_PROVIDER_SWITCH_ACTIVE_SENTINEL = "__switch_active__" as const; const SEARCH_PROVIDER_CONFIGURE_SENTINEL = "__configure_provider__" as const; +const MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_ID_SET = new Set( + MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_IDS, +); + type PluginSearchProviderEntry = { kind: "plugin"; value: string; @@ -517,7 +522,9 @@ export async function resolveSearchProviderPickerEntries( config: OpenClawConfig, workspaceDir?: string, ): Promise { - const builtins: SearchProviderPickerEntry[] = SEARCH_PROVIDER_OPTIONS.map((entry) => ({ + const builtins: SearchProviderPickerEntry[] = SEARCH_PROVIDER_OPTIONS.filter( + (entry) => !MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_ID_SET.has(entry.value), + ).map((entry) => ({ ...entry, kind: "builtin", configured: hasExistingKey(config, entry.value) || hasKeyInEnv(entry), @@ -568,6 +575,15 @@ export async function resolveSearchProviderPickerEntries( configUiHints: pluginRecord.configUiHints, }; }) + .filter((entry) => { + if (!entry) { + return false; + } + return !( + entry.origin === "bundled" && + !MIGRATED_BUNDLED_WEB_SEARCH_PROVIDER_ID_SET.has(entry.value) + ); + }) .filter(Boolean) as PluginSearchProviderEntry[]; pluginEntries = resolvedPluginEntries.toSorted((left, right) => left.label.localeCompare(right.label), diff --git a/src/plugin-sdk/index.test.ts b/src/plugin-sdk/index.test.ts index 61d1cccb10c..6cbeb9440e7 100644 --- a/src/plugin-sdk/index.test.ts +++ b/src/plugin-sdk/index.test.ts @@ -139,6 +139,7 @@ describe("plugin-sdk exports", () => { "formatInboundFromLabel", "resolveRuntimeGroupPolicy", "emptyPluginConfigSchema", + "createBundledBuiltinSearchProvider", "normalizePluginHttpPath", "registerPluginHttpRoute", "buildBaseAccountStatusSnapshot", diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index eaae5d08968..b7a6d3e965e 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -125,6 +125,7 @@ export type { export { normalizePluginHttpPath } from "../plugins/http-path.js"; export { registerPluginHttpRoute } from "../plugins/http-registry.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export { createBundledBuiltinSearchProvider } from "../agents/tools/web-search.js"; export type { OpenClawConfig } from "../config/config.js"; /** @deprecated Use OpenClawConfig instead */ export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js"; diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index 2d287a71e34..67cd4bf12d8 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -114,6 +114,32 @@ describe("resolveEffectiveEnableState", () => { }); expect(state).toEqual({ enabled: false, reason: "disabled in config" }); }); + + it("enables bundled search provider plugins by default", () => { + const normalized = normalizePluginsConfig({ + enabled: true, + }); + const state = resolveEffectiveEnableState({ + id: "search-brave", + origin: "bundled", + config: normalized, + rootConfig: {}, + }); + expect(state).toEqual({ enabled: true }); + }); + + it("enables other migrated bundled search provider plugins by default", () => { + const normalized = normalizePluginsConfig({ + enabled: true, + }); + const state = resolveEffectiveEnableState({ + id: "search-gemini", + origin: "bundled", + config: normalized, + rootConfig: {}, + }); + expect(state).toEqual({ enabled: true }); + }); }); describe("resolveEnableState", () => { diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index b8b89609049..f39ef0581cd 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -1,3 +1,4 @@ +import { MIGRATED_BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "../agents/tools/web-search-provider-catalog.js"; import { normalizeChatChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginRecord } from "./registry.js"; @@ -28,6 +29,7 @@ export const BUNDLED_ENABLED_BY_DEFAULT = new Set([ "ollama", "phone-control", "sglang", + ...MIGRATED_BUNDLED_WEB_SEARCH_PLUGIN_IDS, "talk-voice", "vllm", ]); diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 3efd9423893..37dd41c555e 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -148,21 +148,28 @@ function getHooksForNameWithoutPluginIds(params: { ); } -type SearchProviderAliasDescriptor< - TGenericName extends - | "before_provider_configure" - | "after_provider_configure" - | "after_provider_activate", - TLegacyName extends - | "before_search_provider_configure" - | "after_search_provider_configure" - | "after_search_provider_activate", - TGenericEvent, - TLegacyEvent, -> = { - genericHookName: TGenericName; - legacyHookName: TLegacyName; - toLegacyEvent: (event: TGenericEvent) => TLegacyEvent; +type SearchBeforeProviderAliasDescriptor = { + genericHookName: "before_provider_configure"; + legacyHookName: "before_search_provider_configure"; + toLegacyEvent: ( + event: PluginHookBeforeProviderConfigureEvent, + ) => PluginHookBeforeSearchProviderConfigureEvent; +}; + +type SearchAfterProviderConfigureAliasDescriptor = { + genericHookName: "after_provider_configure"; + legacyHookName: "after_search_provider_configure"; + toLegacyEvent: ( + event: PluginHookAfterProviderConfigureEvent, + ) => PluginHookAfterSearchProviderConfigureEvent; +}; + +type SearchAfterProviderActivateAliasDescriptor = { + genericHookName: "after_provider_activate"; + legacyHookName: "after_search_provider_activate"; + toLegacyEvent: ( + event: PluginHookAfterProviderActivateEvent, + ) => PluginHookAfterSearchProviderActivateEvent; }; const SEARCH_PROVIDER_ALIAS_HOOKS = { @@ -180,12 +187,7 @@ const SEARCH_PROVIDER_ALIAS_HOOKS = { activeProviderId: event.activeProviderId, configured: event.configured, }), - } satisfies SearchProviderAliasDescriptor< - "before_provider_configure", - "before_search_provider_configure", - PluginHookBeforeProviderConfigureEvent, - PluginHookBeforeSearchProviderConfigureEvent - >, + } satisfies SearchBeforeProviderAliasDescriptor, afterConfigure: { genericHookName: "after_provider_configure", legacyHookName: "after_search_provider_configure", @@ -200,12 +202,7 @@ const SEARCH_PROVIDER_ALIAS_HOOKS = { activeProviderId: event.activeProviderId, configured: event.configured, }), - } satisfies SearchProviderAliasDescriptor< - "after_provider_configure", - "after_search_provider_configure", - PluginHookAfterProviderConfigureEvent, - PluginHookAfterSearchProviderConfigureEvent - >, + } satisfies SearchAfterProviderConfigureAliasDescriptor, afterActivate: { genericHookName: "after_provider_activate", legacyHookName: "after_search_provider_activate", @@ -219,12 +216,7 @@ const SEARCH_PROVIDER_ALIAS_HOOKS = { previousProviderId: event.previousProviderId, intent: event.intent, }), - } satisfies SearchProviderAliasDescriptor< - "after_provider_activate", - "after_search_provider_activate", - PluginHookAfterProviderActivateEvent, - PluginHookAfterSearchProviderActivateEvent - >, + } satisfies SearchAfterProviderActivateAliasDescriptor, } as const; /** @@ -409,16 +401,12 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp return result; } - function getSearchProviderLegacyHooks< - TGenericName extends - | "before_provider_configure" - | "after_provider_configure" - | "after_provider_activate", - TLegacyName extends - | "before_search_provider_configure" - | "after_search_provider_configure" - | "after_search_provider_activate", - >(descriptor: SearchProviderAliasDescriptor) { + function getSearchProviderLegacyHooks( + descriptor: + | SearchBeforeProviderAliasDescriptor + | SearchAfterProviderConfigureAliasDescriptor + | SearchAfterProviderActivateAliasDescriptor, + ) { const genericHooks = getHooksForName(registry, descriptor.genericHookName); const genericPluginIds = new Set(genericHooks.map((hook) => hook.pluginId)); return getHooksForNameWithoutPluginIds({ @@ -512,7 +500,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp PluginHookBeforeSearchProviderConfigureResult >( aliasDescriptor.legacyHookName, - legacyHooks, + legacyHooks as PluginHookRegistration<"before_search_provider_configure">[], aliasDescriptor.toLegacyEvent(event), ctx, mergeBeforeSearchProviderConfigure, @@ -538,7 +526,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp const legacyHooks = getSearchProviderLegacyHooks(aliasDescriptor); await runVoidHookRegistrations( aliasDescriptor.legacyHookName, - legacyHooks, + legacyHooks as PluginHookRegistration<"after_search_provider_configure">[], aliasDescriptor.toLegacyEvent(event), ctx, ); @@ -557,7 +545,7 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp const legacyHooks = getSearchProviderLegacyHooks(aliasDescriptor); await runVoidHookRegistrations( aliasDescriptor.legacyHookName, - legacyHooks, + legacyHooks as PluginHookRegistration<"after_search_provider_activate">[], aliasDescriptor.toLegacyEvent(event), ctx, ); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 0569d218f3f..a0e0fa6c886 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -299,6 +299,7 @@ export type SearchProviderPlugin = { name: string; description?: string; pluginId?: string; + builtinProviderId?: string; docsUrl?: string; configFieldOrder?: string[]; isAvailable?: (config?: OpenClawConfig) => boolean;