From 112d1d3a7c409d8de1bd68dcd153a2153f0a762f Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:29:52 -0500 Subject: [PATCH] refactor web search config ownership into extensions --- extensions/brave/openclaw.plugin.json | 28 ++- .../brave/src/brave-web-search-provider.ts | 55 ++++- extensions/firecrawl/openclaw.plugin.json | 27 ++- extensions/firecrawl/src/config.ts | 18 ++ .../src/firecrawl-search-provider.ts | 13 +- extensions/google/openclaw.plugin.json | 27 ++- .../google/src/gemini-web-search-provider.ts | 34 ++- extensions/moonshot/openclaw.plugin.json | 33 ++- .../moonshot/src/kimi-web-search-provider.ts | 31 ++- extensions/perplexity/openclaw.plugin.json | 34 ++- .../src/perplexity-web-search-provider.ts | 44 ++-- extensions/xai/openclaw.plugin.json | 33 ++- .../xai/src/grok-web-search-provider.ts | 31 ++- .../tools/web-search-provider-config.ts | 35 ++++ src/commands/onboard-search.ts | 23 ++- src/config/config.web-search-provider.test.ts | 38 ++-- src/config/legacy-web-search.ts | 146 +++++++++++++ src/config/schema.help.ts | 25 +-- src/config/schema.labels.ts | 14 -- src/config/test-helpers.ts | 24 ++- src/config/types.tools.ts | 52 +---- src/config/validation.ts | 16 +- src/config/zod-schema.agent-runtime.ts | 58 +----- src/plugins/types.ts | 2 + src/plugins/web-search-providers.test.ts | 12 +- src/secrets/runtime-web-tools.test.ts | 193 ++++++++++++------ src/secrets/runtime-web-tools.ts | 19 +- src/secrets/target-registry-data.ts | 66 ++++++ src/web-search/runtime.ts | 29 ++- 29 files changed, 856 insertions(+), 304 deletions(-) create mode 100644 src/config/legacy-web-search.ts diff --git a/extensions/brave/openclaw.plugin.json b/extensions/brave/openclaw.plugin.json index 404382996d7..2077f174d62 100644 --- a/extensions/brave/openclaw.plugin.json +++ b/extensions/brave/openclaw.plugin.json @@ -1,8 +1,34 @@ { "id": "brave", + "uiHints": { + "webSearch.apiKey": { + "label": "Brave Search API Key", + "help": "Brave Search API key (fallback: BRAVE_API_KEY env var).", + "sensitive": true, + "placeholder": "BSA..." + }, + "webSearch.mode": { + "label": "Brave Search Mode", + "help": "Brave Search mode: web or llm-context." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "mode": { + "type": "string", + "enum": ["web", "llm-context"] + } + } + } + } } } diff --git a/extensions/brave/src/brave-web-search-provider.ts b/extensions/brave/src/brave-web-search-provider.ts index b33f1ab0575..266f7dd666c 100644 --- a/extensions/brave/src/brave-web-search-provider.ts +++ b/extensions/brave/src/brave-web-search-provider.ts @@ -17,7 +17,12 @@ import { withTrustedWebSearchEndpoint, writeCachedSearchPayload, } from "../../../src/agents/tools/web-search-provider-common.js"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; import { formatCliCommand } from "../../../src/cli/command-format.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; import type { WebSearchProviderPlugin, WebSearchProviderToolDefinition, @@ -90,6 +95,7 @@ const BRAVE_SEARCH_LANG_ALIASES: Record = { const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i; type BraveConfig = { + apiKey?: unknown; mode?: string; }; @@ -112,18 +118,41 @@ type BraveLlmContextResponse = { sources?: { url?: string; hostname?: string; date?: string }[]; }; -function resolveBraveConfig(searchConfig?: SearchConfigRecord): BraveConfig { - const brave = searchConfig?.brave; - return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {}; +function resolveBraveConfig( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): BraveConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "brave"); + if (pluginConfig) { + return pluginConfig as BraveConfig; + } + const scoped = (searchConfig as Record | undefined)?.brave; + return scoped && typeof scoped === "object" && !Array.isArray(scoped) + ? ({ + ...(scoped as BraveConfig), + apiKey: (searchConfig as Record | undefined)?.apiKey, + } as BraveConfig) + : ({ apiKey: (searchConfig as Record | undefined)?.apiKey } as BraveConfig); } function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" { return brave.mode === "llm-context" ? "llm-context" : "web"; } -function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined { +function resolveBraveApiKey( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): string | undefined { + const braveConfig = resolveBraveConfig(config, searchConfig); return ( - readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ?? + readConfiguredSecretString( + braveConfig.apiKey, + "plugins.entries.brave.config.webSearch.apiKey", + ) ?? + readConfiguredSecretString( + (searchConfig as Record | undefined)?.apiKey, + "tools.web.search.apiKey", + ) ?? readProviderEnvValue(["BRAVE_API_KEY"]) ); } @@ -384,9 +413,10 @@ function missingBraveKeyPayload() { } function createBraveToolDefinition( + config?: OpenClawConfig, searchConfig?: SearchConfigRecord, ): WebSearchProviderToolDefinition { - const braveConfig = resolveBraveConfig(searchConfig); + const braveConfig = resolveBraveConfig(config, searchConfig); const braveMode = resolveBraveMode(braveConfig); return { @@ -396,7 +426,7 @@ function createBraveToolDefinition( : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.", parameters: createBraveSchema(), execute: async (args) => { - const apiKey = resolveBraveApiKey(searchConfig); + const apiKey = resolveBraveApiKey(config, searchConfig); if (!apiKey) { return missingBraveKeyPayload(); } @@ -594,14 +624,19 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://brave.com/search/api/", docsUrl: "https://docs.openclaw.ai/brave-search", autoDetectOrder: 10, - credentialPath: "tools.web.search.apiKey", - inactiveSecretPaths: ["tools.web.search.apiKey"], + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"], getCredentialValue: (searchConfig) => searchConfig?.apiKey, setCredentialValue: (searchConfigTarget, value) => { searchConfigTarget.apiKey = value; }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value); + }, createTool: (ctx) => - createBraveToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined), + createBraveToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined), }; } diff --git a/extensions/firecrawl/openclaw.plugin.json b/extensions/firecrawl/openclaw.plugin.json index 52289f0711a..e9c50c589d2 100644 --- a/extensions/firecrawl/openclaw.plugin.json +++ b/extensions/firecrawl/openclaw.plugin.json @@ -1,8 +1,33 @@ { "id": "firecrawl", + "uiHints": { + "webSearch.apiKey": { + "label": "Firecrawl Search API Key", + "help": "Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).", + "sensitive": true, + "placeholder": "fc-..." + }, + "webSearch.baseUrl": { + "label": "Firecrawl Search Base URL", + "help": "Firecrawl Search base URL override." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "baseUrl": { + "type": "string" + } + } + } + } } } diff --git a/extensions/firecrawl/src/config.ts b/extensions/firecrawl/src/config.ts index 5558c0dce0a..3f2d6a82f8a 100644 --- a/extensions/firecrawl/src/config.ts +++ b/extensions/firecrawl/src/config.ts @@ -26,6 +26,15 @@ type FirecrawlSearchConfig = } | undefined; +type PluginEntryConfig = + | { + webSearch?: { + apiKey?: unknown; + baseUrl?: string; + }; + } + | undefined; + type FirecrawlFetchConfig = | { apiKey?: unknown; @@ -53,6 +62,11 @@ function resolveFetchConfig(cfg?: OpenClawConfig): WebFetchConfig { } export function resolveFirecrawlSearchConfig(cfg?: OpenClawConfig): FirecrawlSearchConfig { + const pluginConfig = cfg?.plugins?.entries?.firecrawl?.config as PluginEntryConfig; + const pluginWebSearch = pluginConfig?.webSearch; + if (pluginWebSearch && typeof pluginWebSearch === "object" && !Array.isArray(pluginWebSearch)) { + return pluginWebSearch; + } const search = resolveSearchConfig(cfg); if (!search || typeof search !== "object") { return undefined; @@ -89,6 +103,10 @@ export function resolveFirecrawlApiKey(cfg?: OpenClawConfig): string | undefined const search = resolveFirecrawlSearchConfig(cfg); const fetch = resolveFirecrawlFetchConfig(cfg); return ( + normalizeConfiguredSecret( + search?.apiKey, + "plugins.entries.firecrawl.config.webSearch.apiKey", + ) || normalizeConfiguredSecret(search?.apiKey, "tools.web.search.firecrawl.apiKey") || normalizeConfiguredSecret(fetch?.apiKey, "tools.web.fetch.firecrawl.apiKey") || normalizeSecretInput(process.env.FIRECRAWL_API_KEY) || diff --git a/extensions/firecrawl/src/firecrawl-search-provider.ts b/extensions/firecrawl/src/firecrawl-search-provider.ts index dd222b02d63..bb2a8aa2864 100644 --- a/extensions/firecrawl/src/firecrawl-search-provider.ts +++ b/extensions/firecrawl/src/firecrawl-search-provider.ts @@ -1,4 +1,8 @@ import { Type } from "@sinclair/typebox"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; import { enablePluginInConfig } from "../../../src/plugins/enable.js"; import type { WebSearchProviderPlugin } from "../../../src/plugins/types.js"; import { runFirecrawlSearch } from "./firecrawl-client.js"; @@ -47,10 +51,15 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://www.firecrawl.dev/", docsUrl: "https://docs.openclaw.ai/tools/firecrawl", autoDetectOrder: 60, - credentialPath: "tools.web.search.firecrawl.apiKey", - inactiveSecretPaths: ["tools.web.search.firecrawl.apiKey"], + credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.firecrawl.config.webSearch.apiKey"], getCredentialValue: getScopedCredentialValue, setCredentialValue: setScopedCredentialValue, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "firecrawl")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "firecrawl", "apiKey", value); + }, applySelectionConfig: (config) => enablePluginInConfig(config, "firecrawl").config, createTool: (ctx) => ({ description: diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index b779c292375..dc41496cb1c 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -29,9 +29,34 @@ "groupHint": "Gemini API key + OAuth" } ], + "uiHints": { + "webSearch.apiKey": { + "label": "Gemini Search API Key", + "help": "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).", + "sensitive": true, + "placeholder": "AIza..." + }, + "webSearch.model": { + "label": "Gemini Search Model", + "help": "Gemini model override for web search grounding." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "model": { + "type": "string" + } + } + } + } } } diff --git a/extensions/google/src/gemini-web-search-provider.ts b/extensions/google/src/gemini-web-search-provider.ts index 85325cd132e..1d56f36e13f 100644 --- a/extensions/google/src/gemini-web-search-provider.ts +++ b/extensions/google/src/gemini-web-search-provider.ts @@ -15,6 +15,11 @@ import { withTrustedWebSearchEndpoint, writeCachedSearchPayload, } from "../../../src/agents/tools/web-search-provider-common.js"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; import type { WebSearchProviderPlugin, WebSearchProviderToolDefinition, @@ -52,8 +57,15 @@ type GeminiGroundingResponse = { }; }; -function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig { - const gemini = searchConfig?.gemini; +function resolveGeminiConfig( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): GeminiConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "google"); + if (pluginConfig) { + return pluginConfig as GeminiConfig; + } + const gemini = (searchConfig as Record | undefined)?.gemini; return gemini && typeof gemini === "object" && !Array.isArray(gemini) ? (gemini as GeminiConfig) : {}; @@ -61,7 +73,7 @@ function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig { function resolveGeminiApiKey(gemini?: GeminiConfig): string | undefined { return ( - readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ?? + readConfiguredSecretString(gemini?.apiKey, "plugins.entries.google.config.webSearch.apiKey") ?? readProviderEnvValue(["GEMINI_API_KEY"]) ); } @@ -168,6 +180,7 @@ function createGeminiSchema() { } function createGeminiToolDefinition( + config?: OpenClawConfig, searchConfig?: SearchConfigRecord, ): WebSearchProviderToolDefinition { return { @@ -194,13 +207,13 @@ function createGeminiToolDefinition( } } - const geminiConfig = resolveGeminiConfig(searchConfig); + const geminiConfig = resolveGeminiConfig(config, searchConfig); const apiKey = resolveGeminiApiKey(geminiConfig); if (!apiKey) { return { error: "missing_gemini_api_key", message: - "web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure tools.web.search.gemini.apiKey.", + "web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure plugins.entries.google.config.webSearch.apiKey.", docs: "https://docs.openclaw.ai/tools/web", }; } @@ -259,8 +272,8 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://aistudio.google.com/apikey", docsUrl: "https://docs.openclaw.ai/tools/web", autoDetectOrder: 20, - credentialPath: "tools.web.search.gemini.apiKey", - inactiveSecretPaths: ["tools.web.search.gemini.apiKey"], + credentialPath: "plugins.entries.google.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.google.config.webSearch.apiKey"], getCredentialValue: (searchConfig) => { const gemini = searchConfig?.gemini; return gemini && typeof gemini === "object" && !Array.isArray(gemini) @@ -275,8 +288,13 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin { } (scoped as Record).apiKey = value; }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "google")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "google", "apiKey", value); + }, createTool: (ctx) => - createGeminiToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined), + createGeminiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined), }; } diff --git a/extensions/moonshot/openclaw.plugin.json b/extensions/moonshot/openclaw.plugin.json index 66bbfd2b6c8..a5756e05623 100644 --- a/extensions/moonshot/openclaw.plugin.json +++ b/extensions/moonshot/openclaw.plugin.json @@ -32,9 +32,40 @@ "cliDescription": "Moonshot API key" } ], + "uiHints": { + "webSearch.apiKey": { + "label": "Kimi Search API Key", + "help": "Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).", + "sensitive": true + }, + "webSearch.baseUrl": { + "label": "Kimi Search Base URL", + "help": "Kimi base URL override." + }, + "webSearch.model": { + "label": "Kimi Search Model", + "help": "Kimi model override." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "baseUrl": { + "type": "string" + }, + "model": { + "type": "string" + } + } + } + } } } diff --git a/extensions/moonshot/src/kimi-web-search-provider.ts b/extensions/moonshot/src/kimi-web-search-provider.ts index f71a889bb1f..ab76814201a 100644 --- a/extensions/moonshot/src/kimi-web-search-provider.ts +++ b/extensions/moonshot/src/kimi-web-search-provider.ts @@ -14,6 +14,11 @@ import { withTrustedWebSearchEndpoint, writeCachedSearchPayload, } from "../../../src/agents/tools/web-search-provider-common.js"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; import type { WebSearchProviderPlugin, WebSearchProviderToolDefinition, @@ -61,14 +66,18 @@ type KimiSearchResponse = { }>; }; -function resolveKimiConfig(searchConfig?: SearchConfigRecord): KimiConfig { - const kimi = searchConfig?.kimi; +function resolveKimiConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): KimiConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "moonshot"); + if (pluginConfig) { + return pluginConfig as KimiConfig; + } + const kimi = (searchConfig as Record | undefined)?.kimi; return kimi && typeof kimi === "object" && !Array.isArray(kimi) ? (kimi as KimiConfig) : {}; } function resolveKimiApiKey(kimi?: KimiConfig): string | undefined { return ( - readConfiguredSecretString(kimi?.apiKey, "tools.web.search.kimi.apiKey") ?? + readConfiguredSecretString(kimi?.apiKey, "plugins.entries.moonshot.config.webSearch.apiKey") ?? readProviderEnvValue(["KIMI_API_KEY", "MOONSHOT_API_KEY"]) ); } @@ -237,6 +246,7 @@ function createKimiSchema() { } function createKimiToolDefinition( + config?: OpenClawConfig, searchConfig?: SearchConfigRecord, ): WebSearchProviderToolDefinition { return { @@ -263,13 +273,13 @@ function createKimiToolDefinition( } } - const kimiConfig = resolveKimiConfig(searchConfig); + const kimiConfig = resolveKimiConfig(config, searchConfig); const apiKey = resolveKimiApiKey(kimiConfig); if (!apiKey) { return { error: "missing_kimi_api_key", message: - "web_search (kimi) needs a Moonshot API key. Set KIMI_API_KEY or MOONSHOT_API_KEY in the Gateway environment, or configure tools.web.search.kimi.apiKey.", + "web_search (kimi) needs a Moonshot API key. Set KIMI_API_KEY or MOONSHOT_API_KEY in the Gateway environment, or configure plugins.entries.moonshot.config.webSearch.apiKey.", docs: "https://docs.openclaw.ai/tools/web", }; } @@ -331,8 +341,8 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://platform.moonshot.cn/", docsUrl: "https://docs.openclaw.ai/tools/web", autoDetectOrder: 40, - credentialPath: "tools.web.search.kimi.apiKey", - inactiveSecretPaths: ["tools.web.search.kimi.apiKey"], + credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.moonshot.config.webSearch.apiKey"], getCredentialValue: (searchConfig) => { const kimi = searchConfig?.kimi; return kimi && typeof kimi === "object" && !Array.isArray(kimi) @@ -347,8 +357,13 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin { } (scoped as Record).apiKey = value; }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "moonshot")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "moonshot", "apiKey", value); + }, createTool: (ctx) => - createKimiToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined), + createKimiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined), }; } diff --git a/extensions/perplexity/openclaw.plugin.json b/extensions/perplexity/openclaw.plugin.json index 6b976506b65..89c7a0fb902 100644 --- a/extensions/perplexity/openclaw.plugin.json +++ b/extensions/perplexity/openclaw.plugin.json @@ -1,8 +1,40 @@ { "id": "perplexity", + "uiHints": { + "webSearch.apiKey": { + "label": "Perplexity API Key", + "help": "Perplexity or OpenRouter API key for web search.", + "sensitive": true, + "placeholder": "pplx-..." + }, + "webSearch.baseUrl": { + "label": "Perplexity Base URL", + "help": "Optional Perplexity/OpenRouter chat-completions base URL override." + }, + "webSearch.model": { + "label": "Perplexity Model", + "help": "Optional Sonar/OpenRouter model override." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "baseUrl": { + "type": "string" + }, + "model": { + "type": "string" + } + } + } + } } } diff --git a/extensions/perplexity/src/perplexity-web-search-provider.ts b/extensions/perplexity/src/perplexity-web-search-provider.ts index 4b2a618ea39..6a150d64b53 100644 --- a/extensions/perplexity/src/perplexity-web-search-provider.ts +++ b/extensions/perplexity/src/perplexity-web-search-provider.ts @@ -23,6 +23,11 @@ import { withTrustedWebSearchEndpoint, writeCachedSearchPayload, } from "../../../src/agents/tools/web-search-provider-common.js"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; import type { WebSearchCredentialResolutionSource, WebSearchProviderPlugin, @@ -71,8 +76,15 @@ type PerplexitySearchApiResponse = { }>; }; -function resolvePerplexityConfig(searchConfig?: SearchConfigRecord): PerplexityConfig { - const perplexity = searchConfig?.perplexity; +function resolvePerplexityConfig( + config?: OpenClawConfig, + searchConfig?: SearchConfigRecord, +): PerplexityConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "perplexity"); + if (pluginConfig) { + return pluginConfig as PerplexityConfig; + } + const perplexity = (searchConfig as Record | undefined)?.perplexity; return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity) ? (perplexity as PerplexityConfig) : {}; @@ -98,7 +110,7 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): { } { const fromConfig = readConfiguredSecretString( perplexity?.apiKey, - "tools.web.search.perplexity.apiKey", + "plugins.entries.perplexity.config.webSearch.apiKey", ); if (fromConfig) { return { apiKey: fromConfig, source: "config" }; @@ -313,16 +325,16 @@ async function runPerplexitySearch(params: { } function resolveRuntimeTransport(params: { + config?: OpenClawConfig; searchConfig?: Record; resolvedKey?: string; keySource: WebSearchCredentialResolutionSource; fallbackEnvVar?: string; }): PerplexityTransport | undefined { - const perplexity = params.searchConfig?.perplexity; - const scoped = - perplexity && typeof perplexity === "object" && !Array.isArray(perplexity) - ? (perplexity as { baseUrl?: string; model?: string }) - : undefined; + const scoped = resolvePerplexityConfig( + params.config, + params.searchConfig as SearchConfigRecord | undefined, + ); const configuredBaseUrl = typeof scoped?.baseUrl === "string" ? scoped.baseUrl.trim() : ""; const configuredModel = typeof scoped?.model === "string" ? scoped.model.trim() : ""; const baseUrl = (() => { @@ -404,10 +416,11 @@ function createPerplexitySchema(transport?: PerplexityTransport) { } function createPerplexityToolDefinition( + config?: OpenClawConfig, searchConfig?: SearchConfigRecord, runtimeTransport?: PerplexityTransport, ): WebSearchProviderToolDefinition { - const perplexityConfig = resolvePerplexityConfig(searchConfig); + const perplexityConfig = resolvePerplexityConfig(config, searchConfig); const schemaTransport = runtimeTransport ?? (perplexityConfig.baseUrl || perplexityConfig.model ? "chat_completions" : undefined); @@ -424,7 +437,7 @@ function createPerplexityToolDefinition( return { error: "missing_perplexity_api_key", message: - "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.", + "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure plugins.entries.perplexity.config.webSearch.apiKey.", docs: "https://docs.openclaw.ai/tools/web", }; } @@ -656,8 +669,8 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://www.perplexity.ai/settings/api", docsUrl: "https://docs.openclaw.ai/perplexity", autoDetectOrder: 50, - credentialPath: "tools.web.search.perplexity.apiKey", - inactiveSecretPaths: ["tools.web.search.perplexity.apiKey"], + credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.perplexity.config.webSearch.apiKey"], getCredentialValue: (searchConfig) => { const perplexity = searchConfig?.perplexity; return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity) @@ -672,8 +685,14 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin { } (scoped as Record).apiKey = value; }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "perplexity")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "perplexity", "apiKey", value); + }, resolveRuntimeMetadata: (ctx) => ({ perplexityTransport: resolveRuntimeTransport({ + config: ctx.config, searchConfig: ctx.searchConfig, resolvedKey: ctx.resolvedCredential?.value, keySource: ctx.resolvedCredential?.source ?? "missing", @@ -682,6 +701,7 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin { }), createTool: (ctx) => createPerplexityToolDefinition( + ctx.config, ctx.searchConfig as SearchConfigRecord | undefined, ctx.runtimeMetadata?.perplexityTransport as PerplexityTransport | undefined, ), diff --git a/extensions/xai/openclaw.plugin.json b/extensions/xai/openclaw.plugin.json index 8cb2d8f5cfc..69ec2574083 100644 --- a/extensions/xai/openclaw.plugin.json +++ b/extensions/xai/openclaw.plugin.json @@ -19,9 +19,40 @@ "cliDescription": "xAI API key" } ], + "uiHints": { + "webSearch.apiKey": { + "label": "Grok Search API Key", + "help": "xAI API key for Grok web search (fallback: XAI_API_KEY env var).", + "sensitive": true + }, + "webSearch.model": { + "label": "Grok Search Model", + "help": "Grok model override for web search." + }, + "webSearch.inlineCitations": { + "label": "Inline Citations", + "help": "Include inline markdown citations in Grok responses." + } + }, "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": ["string", "object"] + }, + "model": { + "type": "string" + }, + "inlineCitations": { + "type": "boolean" + } + } + } + } } } diff --git a/extensions/xai/src/grok-web-search-provider.ts b/extensions/xai/src/grok-web-search-provider.ts index bb9af38f91a..e18b9a156ef 100644 --- a/extensions/xai/src/grok-web-search-provider.ts +++ b/extensions/xai/src/grok-web-search-provider.ts @@ -14,6 +14,11 @@ import { withTrustedWebSearchEndpoint, writeCachedSearchPayload, } from "../../../src/agents/tools/web-search-provider-common.js"; +import { + resolveProviderWebSearchPluginConfig, + setProviderWebSearchPluginConfigValue, +} from "../../../src/agents/tools/web-search-provider-config.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; import type { WebSearchProviderPlugin, WebSearchProviderToolDefinition, @@ -60,14 +65,18 @@ type GrokSearchResponse = { }>; }; -function resolveGrokConfig(searchConfig?: SearchConfigRecord): GrokConfig { - const grok = searchConfig?.grok; +function resolveGrokConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): GrokConfig { + const pluginConfig = resolveProviderWebSearchPluginConfig(config, "xai"); + if (pluginConfig) { + return pluginConfig as GrokConfig; + } + const grok = (searchConfig as Record | undefined)?.grok; return grok && typeof grok === "object" && !Array.isArray(grok) ? (grok as GrokConfig) : {}; } function resolveGrokApiKey(grok?: GrokConfig): string | undefined { return ( - readConfiguredSecretString(grok?.apiKey, "tools.web.search.grok.apiKey") ?? + readConfiguredSecretString(grok?.apiKey, "plugins.entries.xai.config.webSearch.apiKey") ?? readProviderEnvValue(["XAI_API_KEY"]) ); } @@ -179,6 +188,7 @@ function createGrokSchema() { } function createGrokToolDefinition( + config?: OpenClawConfig, searchConfig?: SearchConfigRecord, ): WebSearchProviderToolDefinition { return { @@ -205,13 +215,13 @@ function createGrokToolDefinition( } } - const grokConfig = resolveGrokConfig(searchConfig); + const grokConfig = resolveGrokConfig(config, searchConfig); const apiKey = resolveGrokApiKey(grokConfig); if (!apiKey) { return { error: "missing_xai_api_key", message: - "web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.", + "web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.", docs: "https://docs.openclaw.ai/tools/web", }; } @@ -274,8 +284,8 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin { signupUrl: "https://console.x.ai/", docsUrl: "https://docs.openclaw.ai/tools/web", autoDetectOrder: 30, - credentialPath: "tools.web.search.grok.apiKey", - inactiveSecretPaths: ["tools.web.search.grok.apiKey"], + credentialPath: "plugins.entries.xai.config.webSearch.apiKey", + inactiveSecretPaths: ["plugins.entries.xai.config.webSearch.apiKey"], getCredentialValue: (searchConfig) => { const grok = searchConfig?.grok; return grok && typeof grok === "object" && !Array.isArray(grok) @@ -290,8 +300,13 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin { } (scoped as Record).apiKey = value; }, + getConfiguredCredentialValue: (config) => + resolveProviderWebSearchPluginConfig(config, "xai")?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value); + }, createTool: (ctx) => - createGrokToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined), + createGrokToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined), }; } diff --git a/src/agents/tools/web-search-provider-config.ts b/src/agents/tools/web-search-provider-config.ts index 861898d0b53..3e246b93068 100644 --- a/src/agents/tools/web-search-provider-config.ts +++ b/src/agents/tools/web-search-provider-config.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../../config/config.js"; +import { resolvePluginWebSearchConfig } from "../../config/legacy-web-search.js"; type ConfiguredWebSearchProvider = NonNullable< NonNullable["web"]>["search"] @@ -78,6 +79,40 @@ export function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig { return search as WebSearchConfig; } +export function resolveProviderWebSearchPluginConfig( + config: OpenClawConfig | undefined, + pluginId: string, +): Record | undefined { + return resolvePluginWebSearchConfig(config, pluginId); +} + +function ensureObject(target: Record, key: string): Record { + const current = target[key]; + if (current && typeof current === "object" && !Array.isArray(current)) { + return current as Record; + } + const next: Record = {}; + target[key] = next; + return next; +} + +export function setProviderWebSearchPluginConfigValue( + configTarget: OpenClawConfig, + pluginId: string, + key: string, + value: unknown, +): void { + const plugins = ensureObject(configTarget as Record, "plugins"); + const entries = ensureObject(plugins, "entries"); + const entry = ensureObject(entries, pluginId); + if (entry.enabled === undefined) { + entry.enabled = true; + } + const config = ensureObject(entry, "config"); + const webSearch = ensureObject(config, "webSearch"); + webSearch[key] = value; +} + export function resolveSearchEnabled(params: { search?: WebSearchConfig; sandboxed?: boolean; diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index 7562ae55041..bc2b1e8aac2 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -44,12 +44,14 @@ export function hasKeyInEnv(entry: SearchProviderEntry): boolean { } function rawKeyValue(config: OpenClawConfig, provider: SearchProvider): unknown { - const search = config.tools?.web?.search; const entry = resolvePluginWebSearchProviders({ config, bundledAllowlistCompat: true, }).find((candidate) => candidate.id === provider); - return entry?.getCredentialValue(search as Record | undefined); + return ( + entry?.getConfiguredCredentialValue?.(config) ?? + entry?.getCredentialValue(config.tools?.web?.search as Record | undefined) + ); } /** Returns the plaintext key string, or undefined for SecretRefs/missing. */ @@ -99,17 +101,24 @@ export function applySearchKey( config, bundledAllowlistCompat: true, }).find((candidate) => candidate.id === provider); - const search = { ...config.tools?.web?.search, provider, enabled: true }; - if (providerEntry) { - providerEntry.setCredentialValue(search as Record, key); - } const nextBase = { ...config, tools: { ...config.tools, - web: { ...config.tools?.web, search }, + web: { + ...config.tools?.web, + search: { ...config.tools?.web?.search, provider, enabled: true }, + }, }, }; + if (providerEntry?.setConfiguredCredentialValue) { + providerEntry.setConfiguredCredentialValue(nextBase, key); + } else { + const search = nextBase.tools?.web?.search as Record | undefined; + if (providerEntry && search) { + providerEntry.setCredentialValue(search, key); + } + } return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase; } diff --git a/src/config/config.web-search-provider.test.ts b/src/config/config.web-search-provider.test.ts index 882f406eb85..85ce1c2700a 100644 --- a/src/config/config.web-search-provider.test.ts +++ b/src/config/config.web-search-provider.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { validateConfigObject } from "./config.js"; +import { validateConfigObjectWithPlugins } from "./config.js"; import { buildWebSearchProviderConfig } from "./test-helpers.js"; vi.mock("../runtime.js", () => ({ @@ -9,43 +9,55 @@ vi.mock("../runtime.js", () => ({ vi.mock("../plugins/web-search-providers.js", () => { const getScoped = (key: string) => (search?: Record) => (search?.[key] as { apiKey?: unknown } | undefined)?.apiKey; + const getConfigured = (pluginId: string) => (config?: Record) => + ( + config?.plugins as + | { entries?: Record } + | undefined + )?.entries?.[pluginId]?.config?.webSearch?.apiKey; return { resolvePluginWebSearchProviders: () => [ { id: "brave", envVars: ["BRAVE_API_KEY"], - credentialPath: "tools.web.search.apiKey", + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", getCredentialValue: (search?: Record) => search?.apiKey, + getConfiguredCredentialValue: getConfigured("brave"), }, { id: "firecrawl", envVars: ["FIRECRAWL_API_KEY"], - credentialPath: "tools.web.search.firecrawl.apiKey", + credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey", getCredentialValue: getScoped("firecrawl"), + getConfiguredCredentialValue: getConfigured("firecrawl"), }, { id: "gemini", envVars: ["GEMINI_API_KEY"], - credentialPath: "tools.web.search.gemini.apiKey", + credentialPath: "plugins.entries.google.config.webSearch.apiKey", getCredentialValue: getScoped("gemini"), + getConfiguredCredentialValue: getConfigured("google"), }, { id: "grok", envVars: ["XAI_API_KEY"], - credentialPath: "tools.web.search.grok.apiKey", + credentialPath: "plugins.entries.xai.config.webSearch.apiKey", getCredentialValue: getScoped("grok"), + getConfiguredCredentialValue: getConfigured("xai"), }, { id: "kimi", envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"], - credentialPath: "tools.web.search.kimi.apiKey", + credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey", getCredentialValue: getScoped("kimi"), + getConfiguredCredentialValue: getConfigured("moonshot"), }, { id: "perplexity", envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"], - credentialPath: "tools.web.search.perplexity.apiKey", + credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey", getCredentialValue: getScoped("perplexity"), + getConfiguredCredentialValue: getConfigured("perplexity"), }, ], }; @@ -56,7 +68,7 @@ const { resolveSearchProvider } = __testing; describe("web search provider config", () => { it("accepts perplexity provider and config", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ enabled: true, provider: "perplexity", @@ -72,7 +84,7 @@ describe("web search provider config", () => { }); it("accepts gemini provider and config", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ enabled: true, provider: "gemini", @@ -87,7 +99,7 @@ describe("web search provider config", () => { }); it("accepts firecrawl provider and config", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ enabled: true, provider: "firecrawl", @@ -102,7 +114,7 @@ describe("web search provider config", () => { }); it("accepts gemini provider with no extra config", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ provider: "gemini", }), @@ -112,7 +124,7 @@ describe("web search provider config", () => { }); it("accepts brave llm-context mode config", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ provider: "brave", providerConfig: { @@ -125,7 +137,7 @@ describe("web search provider config", () => { }); it("rejects invalid brave mode config values", () => { - const res = validateConfigObject( + const res = validateConfigObjectWithPlugins( buildWebSearchProviderConfig({ provider: "brave", providerConfig: { diff --git a/src/config/legacy-web-search.ts b/src/config/legacy-web-search.ts new file mode 100644 index 00000000000..4b42eca8311 --- /dev/null +++ b/src/config/legacy-web-search.ts @@ -0,0 +1,146 @@ +import type { OpenClawConfig } from "./config.js"; + +type JsonRecord = Record; + +const GENERIC_WEB_SEARCH_KEYS = new Set([ + "enabled", + "provider", + "maxResults", + "timeoutSeconds", + "cacheTtlMinutes", +]); + +const LEGACY_PROVIDER_MAP = { + brave: "brave", + firecrawl: "firecrawl", + gemini: "google", + grok: "xai", + kimi: "moonshot", + perplexity: "perplexity", +} as const; + +type LegacyProviderId = keyof typeof LEGACY_PROVIDER_MAP; + +function isRecord(value: unknown): value is JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function cloneRecord(value: T | undefined): T { + return { ...value } as T; +} + +function ensureRecord(target: JsonRecord, key: string): JsonRecord { + const current = target[key]; + if (isRecord(current)) { + return current; + } + const next: JsonRecord = {}; + target[key] = next; + return next; +} + +function resolveLegacySearchConfig(raw: unknown): JsonRecord | undefined { + if (!isRecord(raw)) { + return undefined; + } + const tools = isRecord(raw.tools) ? raw.tools : undefined; + const web = isRecord(tools?.web) ? tools.web : undefined; + return isRecord(web?.search) ? web.search : undefined; +} + +function copyLegacyProviderConfig( + search: JsonRecord, + providerKey: LegacyProviderId, +): JsonRecord | undefined { + const current = search[providerKey]; + return isRecord(current) ? cloneRecord(current) : undefined; +} + +function setPluginWebSearchConfig( + target: JsonRecord, + pluginId: string, + webSearchConfig: JsonRecord, +): void { + const plugins = ensureRecord(target, "plugins"); + const entries = ensureRecord(plugins, "entries"); + const entry = ensureRecord(entries, pluginId); + if (entry.enabled === undefined) { + entry.enabled = true; + } + const config = ensureRecord(entry, "config"); + config.webSearch = webSearchConfig; +} + +export function listLegacyWebSearchConfigPaths(raw: unknown): string[] { + const search = resolveLegacySearchConfig(raw); + if (!search) { + return []; + } + const paths: string[] = []; + + if ("apiKey" in search) { + paths.push("tools.web.search.apiKey"); + } + for (const providerId of Object.keys(LEGACY_PROVIDER_MAP) as LegacyProviderId[]) { + const scoped = search[providerId]; + if (isRecord(scoped)) { + for (const key of Object.keys(scoped)) { + paths.push(`tools.web.search.${providerId}.${key}`); + } + } + } + return paths; +} + +export function normalizeLegacyWebSearchConfig(raw: T): T { + if (!isRecord(raw)) { + return raw; + } + + const search = resolveLegacySearchConfig(raw); + if (!search) { + return raw; + } + + const nextRoot = cloneRecord(raw); + const tools = ensureRecord(nextRoot, "tools"); + const web = ensureRecord(tools, "web"); + const nextSearch: JsonRecord = {}; + + for (const [key, value] of Object.entries(search)) { + if (GENERIC_WEB_SEARCH_KEYS.has(key)) { + nextSearch[key] = value; + } + } + web.search = nextSearch; + + const braveConfig = copyLegacyProviderConfig(search, "brave") ?? {}; + if ("apiKey" in search) { + braveConfig.apiKey = search.apiKey; + } + if (Object.keys(braveConfig).length > 0) { + setPluginWebSearchConfig(nextRoot, LEGACY_PROVIDER_MAP.brave, braveConfig); + } + + for (const providerId of ["firecrawl", "gemini", "grok", "kimi", "perplexity"] as const) { + const scoped = copyLegacyProviderConfig(search, providerId); + if (!scoped || Object.keys(scoped).length === 0) { + continue; + } + setPluginWebSearchConfig(nextRoot, LEGACY_PROVIDER_MAP[providerId], scoped); + } + + return nextRoot as T; +} + +export function resolvePluginWebSearchConfig( + config: OpenClawConfig | undefined, + pluginId: string, +): Record | undefined { + const pluginConfig = config?.plugins?.entries?.[pluginId]?.config; + if (!isRecord(pluginConfig)) { + return undefined; + } + const webSearch = pluginConfig.webSearch; + return isRecord(webSearch) ? webSearch : undefined; +} diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 4518d393ed2..72ec1074135 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -667,33 +667,10 @@ export const FIELD_HELP: Record = { "tools.message.broadcast.enabled": "Enable broadcast action (default: true).", "tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).", "tools.web.search.provider": - 'Search provider ("brave", "firecrawl", "gemini", "grok", "kimi", or "perplexity"). Auto-detected from available API keys if omitted.', - "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).", + "Search provider id. Auto-detected from available API keys if omitted.", "tools.web.search.maxResults": "Number of results to return (1-10).", "tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.", "tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.", - "tools.web.search.brave.mode": - 'Brave Search mode: "web" (URL results) or "llm-context" (pre-extracted page content for LLM grounding).', - "tools.web.search.firecrawl.apiKey": - "Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).", - "tools.web.search.firecrawl.baseUrl": - 'Firecrawl Search base URL override (default: "https://api.firecrawl.dev").', - "tools.web.search.gemini.apiKey": - "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).", - "tools.web.search.gemini.model": 'Gemini model override (default: "gemini-2.5-flash").', - "tools.web.search.grok.apiKey": "Grok (xAI) API key (fallback: XAI_API_KEY env var).", // pragma: allowlist secret - "tools.web.search.grok.model": 'Grok model override (default: "grok-4-1-fast").', - "tools.web.search.kimi.apiKey": - "Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).", - "tools.web.search.kimi.baseUrl": - 'Kimi base URL override (default: "https://api.moonshot.ai/v1").', - "tools.web.search.kimi.model": 'Kimi model override (default: "moonshot-v1-128k").', - "tools.web.search.perplexity.apiKey": - "Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var). Direct Perplexity keys default to the Search API; OpenRouter keys use Sonar chat completions.", - "tools.web.search.perplexity.baseUrl": - "Optional Perplexity/OpenRouter chat-completions base URL override. Setting this opts Perplexity into the legacy Sonar/OpenRouter compatibility path.", - "tools.web.search.perplexity.model": - 'Optional Sonar/OpenRouter model override (default: "perplexity/sonar-pro"). Setting this opts Perplexity into the legacy chat-completions compatibility path.', "tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).", "tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).", "tools.web.fetch.maxCharsCap": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index ae1c8d2829d..1684d3c3ee6 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -216,23 +216,9 @@ export const FIELD_LABELS: Record = { "tools.message.broadcast.enabled": "Enable Message Broadcast", "tools.web.search.enabled": "Enable Web Search Tool", "tools.web.search.provider": "Web Search Provider", - "tools.web.search.apiKey": "Brave Search API Key", "tools.web.search.maxResults": "Web Search Max Results", "tools.web.search.timeoutSeconds": "Web Search Timeout (sec)", "tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)", - "tools.web.search.brave.mode": "Brave Search Mode", - "tools.web.search.firecrawl.apiKey": "Firecrawl Search API Key", // pragma: allowlist secret - "tools.web.search.firecrawl.baseUrl": "Firecrawl Search Base URL", - "tools.web.search.gemini.apiKey": "Gemini Search API Key", // pragma: allowlist secret - "tools.web.search.gemini.model": "Gemini Search Model", - "tools.web.search.grok.apiKey": "Grok Search API Key", // pragma: allowlist secret - "tools.web.search.grok.model": "Grok Search Model", - "tools.web.search.kimi.apiKey": "Kimi Search API Key", // pragma: allowlist secret - "tools.web.search.kimi.baseUrl": "Kimi Search Base URL", - "tools.web.search.kimi.model": "Kimi Search Model", - "tools.web.search.perplexity.apiKey": "Perplexity API Key", // pragma: allowlist secret - "tools.web.search.perplexity.baseUrl": "Perplexity Base URL", - "tools.web.search.perplexity.model": "Perplexity Model", "tools.web.fetch.enabled": "Enable Web Fetch Tool", "tools.web.fetch.maxChars": "Web Fetch Max Chars", "tools.web.fetch.maxCharsCap": "Web Fetch Hard Max Chars", diff --git a/src/config/test-helpers.ts b/src/config/test-helpers.ts index 5809a37da2d..c8e3c539d14 100644 --- a/src/config/test-helpers.ts +++ b/src/config/test-helpers.ts @@ -64,14 +64,32 @@ export function buildWebSearchProviderConfig(params: { if (params.enabled !== undefined) { search.enabled = params.enabled; } - if (params.providerConfig) { - search[params.provider] = params.providerConfig; - } + const pluginId = + params.provider === "gemini" + ? "google" + : params.provider === "grok" + ? "xai" + : params.provider === "kimi" + ? "moonshot" + : params.provider; return { tools: { web: { search, }, }, + ...(params.providerConfig + ? { + plugins: { + entries: { + [pluginId]: { + config: { + webSearch: params.providerConfig, + }, + }, + }, + }, + } + : {}), }; } diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index d1195ace393..0a1e68a16a7 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -457,62 +457,14 @@ export type ToolsConfig = { search?: { /** Enable web search tool (default: true when API key is present). */ enabled?: boolean; - /** Search provider ("brave", "firecrawl", "gemini", "grok", "kimi", or "perplexity"). */ - provider?: "brave" | "firecrawl" | "gemini" | "grok" | "kimi" | "perplexity"; - /** Brave Search API key (optional; defaults to BRAVE_API_KEY env var). */ - apiKey?: SecretInput; + /** Search provider id. */ + provider?: string; /** Default search results count (1-10). */ maxResults?: number; /** Timeout in seconds for search requests. */ timeoutSeconds?: number; /** Cache TTL in minutes for search results. */ cacheTtlMinutes?: number; - /** Brave-specific configuration (used when provider="brave"). */ - brave?: { - /** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */ - mode?: "web" | "llm-context"; - }; - /** Gemini-specific configuration (used when provider="gemini"). */ - gemini?: { - /** Gemini API key (defaults to GEMINI_API_KEY env var). */ - apiKey?: SecretInput; - /** Model to use for grounded search (defaults to "gemini-2.5-flash"). */ - model?: string; - }; - /** Firecrawl-specific configuration (used when provider="firecrawl"). */ - firecrawl?: { - /** Firecrawl API key (defaults to FIRECRAWL_API_KEY env var). */ - apiKey?: SecretInput; - /** Base URL for API requests (defaults to "https://api.firecrawl.dev"). */ - baseUrl?: string; - }; - /** Grok-specific configuration (used when provider="grok"). */ - grok?: { - /** API key for xAI (defaults to XAI_API_KEY env var). */ - apiKey?: SecretInput; - /** Model to use (defaults to "grok-4-1-fast"). */ - model?: string; - /** Include inline citations in response text as markdown links (default: false). */ - inlineCitations?: boolean; - }; - /** Kimi-specific configuration (used when provider="kimi"). */ - kimi?: { - /** Moonshot/Kimi API key (defaults to KIMI_API_KEY or MOONSHOT_API_KEY env var). */ - apiKey?: SecretInput; - /** Base URL for API requests (defaults to "https://api.moonshot.ai/v1"). */ - baseUrl?: string; - /** Model to use (defaults to "moonshot-v1-128k"). */ - model?: string; - }; - /** Perplexity-specific configuration (used when provider="perplexity"). */ - perplexity?: { - /** API key for Perplexity (defaults to PERPLEXITY_API_KEY env var). */ - apiKey?: SecretInput; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ - baseUrl?: string; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ - model?: string; - }; }; fetch?: { /** Enable web fetch tool (default: true). */ diff --git a/src/config/validation.ts b/src/config/validation.ts index 2a2c08b96ee..0c2bba53aae 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -20,6 +20,10 @@ import { isRecord } from "../utils.js"; import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js"; import { appendAllowedValuesHint, summarizeAllowedValues } from "./allowed-values.js"; import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js"; +import { + listLegacyWebSearchConfigPaths, + normalizeLegacyWebSearchConfig, +} from "./legacy-web-search.js"; import { findLegacyConfigIssues } from "./legacy.js"; import type { OpenClawConfig, ConfigValidationIssue } from "./types.js"; import { OpenClawSchema } from "./zod-schema.js"; @@ -229,7 +233,8 @@ function validateGatewayTailscaleBind(config: OpenClawConfig): ConfigValidationI export function validateConfigObjectRaw( raw: unknown, ): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } { - const legacyIssues = findLegacyConfigIssues(raw); + const normalizedRaw = normalizeLegacyWebSearchConfig(raw); + const legacyIssues = findLegacyConfigIssues(normalizedRaw); if (legacyIssues.length > 0) { return { ok: false, @@ -239,7 +244,7 @@ export function validateConfigObjectRaw( })), }; } - const validated = OpenClawSchema.safeParse(raw); + const validated = OpenClawSchema.safeParse(normalizedRaw); if (!validated.success) { return { ok: false, @@ -322,7 +327,12 @@ function validateConfigObjectWithPluginsBase( const config = base.config; const issues: ConfigValidationIssue[] = []; - const warnings: ConfigValidationIssue[] = []; + const warnings: ConfigValidationIssue[] = listLegacyWebSearchConfigPaths(raw).map((path) => ({ + path, + message: + `${path} is deprecated for web search provider config. ` + + "Move it under plugins.entries..config.webSearch.*; OpenClaw mapped it automatically for compatibility.", + })); const hasExplicitPluginsConfig = isRecord(raw) && Object.prototype.hasOwnProperty.call(raw, "plugins"); diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 10cef396275..2763697c2d9 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -263,66 +263,10 @@ export const ToolPolicySchema = ToolPolicyBaseSchema.superRefine((value, ctx) => export const ToolsWebSearchSchema = z .object({ enabled: z.boolean().optional(), - provider: z - .union([ - z.literal("brave"), - z.literal("firecrawl"), - z.literal("perplexity"), - z.literal("grok"), - z.literal("gemini"), - z.literal("kimi"), - ]) - .optional(), - apiKey: SecretInputSchema.optional().register(sensitive), + provider: z.string().optional(), maxResults: z.number().int().positive().optional(), timeoutSeconds: z.number().int().positive().optional(), cacheTtlMinutes: z.number().nonnegative().optional(), - perplexity: z - .object({ - apiKey: SecretInputSchema.optional().register(sensitive), - // Legacy Sonar/OpenRouter compatibility fields. - // Setting either opts Perplexity back into the chat-completions path. - baseUrl: z.string().optional(), - model: z.string().optional(), - }) - .strict() - .optional(), - grok: z - .object({ - apiKey: SecretInputSchema.optional().register(sensitive), - model: z.string().optional(), - inlineCitations: z.boolean().optional(), - }) - .strict() - .optional(), - gemini: z - .object({ - apiKey: SecretInputSchema.optional().register(sensitive), - model: z.string().optional(), - }) - .strict() - .optional(), - firecrawl: z - .object({ - apiKey: SecretInputSchema.optional().register(sensitive), - baseUrl: z.string().optional(), - }) - .strict() - .optional(), - kimi: z - .object({ - apiKey: SecretInputSchema.optional().register(sensitive), - baseUrl: z.string().optional(), - model: z.string().optional(), - }) - .strict() - .optional(), - brave: z - .object({ - mode: z.union([z.literal("web"), z.literal("llm-context")]).optional(), - }) - .strict() - .optional(), }) .strict() .optional(); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index ab903ff0cc9..0fa61a466c8 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -892,6 +892,8 @@ export type WebSearchProviderPlugin = { inactiveSecretPaths?: string[]; getCredentialValue: (searchConfig?: Record) => unknown; setCredentialValue: (searchConfigTarget: Record, value: unknown) => void; + getConfiguredCredentialValue?: (config?: OpenClawConfig) => unknown; + setConfiguredCredentialValue?: (configTarget: OpenClawConfig, value: unknown) => void; applySelectionConfig?: (config: OpenClawConfig) => OpenClawConfig; resolveRuntimeMetadata?: ( ctx: WebSearchRuntimeMetadataContext, diff --git a/src/plugins/web-search-providers.test.ts b/src/plugins/web-search-providers.test.ts index d88fbe96431..ffffdea6d5d 100644 --- a/src/plugins/web-search-providers.test.ts +++ b/src/plugins/web-search-providers.test.ts @@ -23,12 +23,12 @@ describe("resolvePluginWebSearchProviders", () => { "firecrawl:firecrawl", ]); expect(providers.map((provider) => provider.credentialPath)).toEqual([ - "tools.web.search.apiKey", - "tools.web.search.gemini.apiKey", - "tools.web.search.grok.apiKey", - "tools.web.search.kimi.apiKey", - "tools.web.search.perplexity.apiKey", - "tools.web.search.firecrawl.apiKey", + "plugins.entries.brave.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.firecrawl.config.webSearch.apiKey", ]); expect(providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig).toEqual( expect.any(Function), diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index c67f6af6573..94f7b9be99f 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -11,6 +11,19 @@ function asConfig(value: unknown): OpenClawConfig { return value as OpenClawConfig; } +function providerPluginId(provider: ProviderUnderTest): string { + switch (provider) { + case "gemini": + return "google"; + case "grok": + return "xai"; + case "kimi": + return "moonshot"; + default: + return provider; + } +} + async function runRuntimeWebTools(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv }) { const sourceConfig = structuredClone(params.config); const resolvedConfig = structuredClone(params.config); @@ -30,40 +43,32 @@ function createProviderSecretRefConfig( provider: ProviderUnderTest, envRefId: string, ): OpenClawConfig { - const search: Record = { - enabled: true, - provider, - }; - if (provider === "brave") { - search.apiKey = { source: "env", provider: "default", id: envRefId }; - } else { - search[provider] = { - apiKey: { source: "env", provider: "default", id: envRefId }, - }; - } return asConfig({ tools: { web: { - search, + search: { + enabled: true, + provider, + }, + }, + }, + plugins: { + entries: { + [providerPluginId(provider)]: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: envRefId }, + }, + }, + }, }, }, }); } function readProviderKey(config: OpenClawConfig, provider: ProviderUnderTest): unknown { - if (provider === "brave") { - return config.tools?.web?.search?.apiKey; - } - if (provider === "gemini") { - return config.tools?.web?.search?.gemini?.apiKey; - } - if (provider === "grok") { - return config.tools?.web?.search?.grok?.apiKey; - } - if (provider === "kimi") { - return config.tools?.web?.search?.kimi?.apiKey; - } - return config.tools?.web?.search?.perplexity?.apiKey; + return config.plugins?.entries?.[providerPluginId(provider)]?.config?.webSearch?.apiKey; } function expectInactiveFirecrawlSecretRef(params: { @@ -171,18 +176,40 @@ describe("runtime web tools resolution", () => { tools: { web: { search: { - apiKey: { source: "env", provider: "default", id: "BRAVE_REF" }, - gemini: { - apiKey: { source: "env", provider: "default", id: "GEMINI_REF" }, + enabled: true, + }, + }, + }, + plugins: { + entries: { + brave: { + enabled: true, + config: { + webSearch: { apiKey: { source: "env", provider: "default", id: "BRAVE_REF" } }, }, - grok: { - apiKey: { source: "env", provider: "default", id: "GROK_REF" }, + }, + google: { + enabled: true, + config: { + webSearch: { apiKey: { source: "env", provider: "default", id: "GEMINI_REF" } }, }, - kimi: { - apiKey: { source: "env", provider: "default", id: "KIMI_REF" }, + }, + xai: { + enabled: true, + config: { + webSearch: { apiKey: { source: "env", provider: "default", id: "GROK_REF" } }, }, - perplexity: { - apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" }, + }, + moonshot: { + enabled: true, + config: { + webSearch: { apiKey: { source: "env", provider: "default", id: "KIMI_REF" } }, + }, + }, + perplexity: { + enabled: true, + config: { + webSearch: { apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" } }, }, }, }, @@ -199,13 +226,13 @@ describe("runtime web tools resolution", () => { expect(metadata.search.providerSource).toBe("auto-detect"); expect(metadata.search.selectedProvider).toBe("brave"); - expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-precedence-key"); + expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-precedence-key"); expect(context.warnings).toEqual( expect.arrayContaining([ - expect.objectContaining({ path: "tools.web.search.gemini.apiKey" }), - expect.objectContaining({ path: "tools.web.search.grok.apiKey" }), - expect.objectContaining({ path: "tools.web.search.kimi.apiKey" }), - expect.objectContaining({ path: "tools.web.search.perplexity.apiKey" }), + expect.objectContaining({ path: "plugins.entries.google.config.webSearch.apiKey" }), + expect.objectContaining({ path: "plugins.entries.xai.config.webSearch.apiKey" }), + expect.objectContaining({ path: "plugins.entries.moonshot.config.webSearch.apiKey" }), + expect.objectContaining({ path: "plugins.entries.perplexity.config.webSearch.apiKey" }), ]), ); }); @@ -216,12 +243,25 @@ describe("runtime web tools resolution", () => { tools: { web: { search: { - apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" }, - gemini: { - apiKey: { - source: "env", - provider: "default", - id: "MISSING_GEMINI_API_KEY_REF", + enabled: true, + }, + }, + }, + plugins: { + entries: { + brave: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" }, + }, + }, + }, + google: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" }, }, }, }, @@ -236,8 +276,8 @@ describe("runtime web tools resolution", () => { expect(metadata.search.providerSource).toBe("auto-detect"); expect(metadata.search.selectedProvider).toBe("brave"); expect(metadata.search.selectedProviderKeySource).toBe("secretRef"); - expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-runtime-key"); - expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toEqual({ + expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-runtime-key"); + expect(readProviderKey(resolvedConfig, "gemini")).toEqual({ source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF", @@ -246,7 +286,7 @@ describe("runtime web tools resolution", () => { expect.arrayContaining([ expect.objectContaining({ code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", - path: "tools.web.search.gemini.apiKey", + path: "plugins.entries.google.config.webSearch.apiKey", }), ]), ); @@ -261,9 +301,26 @@ describe("runtime web tools resolution", () => { tools: { web: { search: { - apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" }, - gemini: { - apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" }, + enabled: true, + }, + }, + }, + plugins: { + entries: { + brave: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" }, + }, + }, + }, + google: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" }, + }, }, }, }, @@ -276,12 +333,12 @@ describe("runtime web tools resolution", () => { expect(metadata.search.providerSource).toBe("auto-detect"); expect(metadata.search.selectedProvider).toBe("gemini"); - expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key"); + expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key"); expect(context.warnings).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", - path: "tools.web.search.apiKey", + path: "plugins.entries.brave.config.webSearch.apiKey", }), ]), ); @@ -297,8 +354,17 @@ describe("runtime web tools resolution", () => { web: { search: { provider: "invalid-provider", - gemini: { - apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" }, + }, + }, + }, + plugins: { + entries: { + google: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" }, + }, }, }, }, @@ -312,7 +378,7 @@ describe("runtime web tools resolution", () => { expect(metadata.search.providerConfigured).toBeUndefined(); expect(metadata.search.providerSource).toBe("auto-detect"); expect(metadata.search.selectedProvider).toBe("gemini"); - expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key"); + expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key"); expect(metadata.search.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -337,8 +403,17 @@ describe("runtime web tools resolution", () => { web: { search: { provider: "gemini", - gemini: { - apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" }, + }, + }, + }, + plugins: { + entries: { + google: { + enabled: true, + config: { + webSearch: { + apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" }, + }, }, }, }, @@ -361,7 +436,7 @@ describe("runtime web tools resolution", () => { expect.arrayContaining([ expect.objectContaining({ code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK", - path: "tools.web.search.gemini.apiKey", + path: "plugins.entries.google.config.webSearch.apiKey", }), ]), ); diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index baa41d68ed2..4a2ec996589 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -221,6 +221,9 @@ function setResolvedWebSearchApiKey(params: { env: params.env, bundledAllowlistCompat: true, }).find((entry) => entry.id === params.provider); + if (provider?.setConfiguredCredentialValue) { + provider.setConfiguredCredentialValue(params.resolvedConfig, params.value); + } provider?.setCredentialValue(search, params.value); } @@ -318,7 +321,9 @@ export async function resolveRuntimeWebTools(params: { for (const provider of candidates) { const path = keyPathForProvider(provider); - const value = provider.getCredentialValue(search); + const value = + provider.getConfiguredCredentialValue?.(params.sourceConfig) ?? + provider.getCredentialValue(search); const resolution = await resolveSecretInputWithEnvFallback({ sourceConfig: params.sourceConfig, context: params.context, @@ -451,7 +456,9 @@ export async function resolveRuntimeWebTools(params: { if (provider.id === searchMetadata.selectedProvider) { continue; } - const value = provider.getCredentialValue(search); + const value = + provider.getConfiguredCredentialValue?.(params.sourceConfig) ?? + provider.getCredentialValue(search); if (!hasConfiguredSecretRef(value, defaults)) { continue; } @@ -465,7 +472,9 @@ export async function resolveRuntimeWebTools(params: { } } else if (search && !searchEnabled) { for (const provider of providers) { - const value = provider.getCredentialValue(search); + const value = + provider.getConfiguredCredentialValue?.(params.sourceConfig) ?? + provider.getCredentialValue(search); if (!hasConfiguredSecretRef(value, defaults)) { continue; } @@ -484,7 +493,9 @@ export async function resolveRuntimeWebTools(params: { if (provider.id === configuredProvider) { continue; } - const value = provider.getCredentialValue(search); + const value = + provider.getConfiguredCredentialValue?.(params.sourceConfig) ?? + provider.getCredentialValue(search); if (!hasConfiguredSecretRef(value, defaults)) { continue; } diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 67f622a56fa..30aa096004b 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -733,6 +733,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "plugins.entries.brave.config.webSearch.apiKey", + targetType: "plugins.entries.brave.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.brave.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "tools.web.search.gemini.apiKey", targetType: "tools.web.search.gemini.apiKey", @@ -744,6 +755,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "plugins.entries.google.config.webSearch.apiKey", + targetType: "plugins.entries.google.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.google.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "tools.web.search.grok.apiKey", targetType: "tools.web.search.grok.apiKey", @@ -755,6 +777,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "plugins.entries.xai.config.webSearch.apiKey", + targetType: "plugins.entries.xai.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.xai.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "tools.web.search.kimi.apiKey", targetType: "tools.web.search.kimi.apiKey", @@ -766,6 +799,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "plugins.entries.moonshot.config.webSearch.apiKey", + targetType: "plugins.entries.moonshot.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.moonshot.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, { id: "tools.web.search.perplexity.apiKey", targetType: "tools.web.search.perplexity.apiKey", @@ -777,6 +821,28 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, + { + id: "plugins.entries.perplexity.config.webSearch.apiKey", + targetType: "plugins.entries.perplexity.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.perplexity.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, + { + id: "plugins.entries.firecrawl.config.webSearch.apiKey", + targetType: "plugins.entries.firecrawl.config.webSearch.apiKey", + configFile: "openclaw.json", + pathPattern: "plugins.entries.firecrawl.config.webSearch.apiKey", + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, ]; export { SECRET_TARGET_REGISTRY }; diff --git a/src/web-search/runtime.ts b/src/web-search/runtime.ts index cf11dfcb667..4861ad12480 100644 --- a/src/web-search/runtime.ts +++ b/src/web-search/runtime.ts @@ -61,22 +61,26 @@ function readProviderEnvValue(envVars: string[]): string | undefined { return undefined; } -function hasProviderCredential(providerId: string, search: WebSearchConfig | undefined): boolean { +function hasProviderCredential( + providerId: string, + config: OpenClawConfig | undefined, + search: WebSearchConfig | undefined, +): boolean { const providers = resolvePluginWebSearchProviders({ + config, bundledAllowlistCompat: true, }); const provider = providers.find((entry) => entry.id === providerId); if (!provider) { return false; } - const rawValue = provider.getCredentialValue(search as Record | undefined); + const rawValue = + provider.getConfiguredCredentialValue?.(config) ?? + provider.getCredentialValue(search as Record | undefined); const fromConfig = normalizeSecretInput( normalizeResolvedSecretInputString({ value: rawValue, - path: - providerId === "brave" - ? "tools.web.search.apiKey" - : `tools.web.search.${providerId}.apiKey`, + path: provider.credentialPath, }), ); return Boolean(fromConfig || readProviderEnvValue(provider.envVars)); @@ -93,11 +97,13 @@ export function listWebSearchProviders(params?: { export function resolveWebSearchProviderId(params: { search?: WebSearchConfig; + config?: OpenClawConfig; providers?: PluginWebSearchProviderEntry[]; }): string { const providers = params.providers ?? resolvePluginWebSearchProviders({ + config: params.config, bundledAllowlistCompat: true, }); const raw = @@ -114,7 +120,7 @@ export function resolveWebSearchProviderId(params: { if (!raw) { for (const provider of providers) { - if (!hasProviderCredential(provider.id, params.search)) { + if (!hasProviderCredential(provider.id, params.config, params.search)) { continue; } logVerbose( @@ -124,7 +130,7 @@ export function resolveWebSearchProviderId(params: { } } - return providers[0]?.id ?? "brave"; + return providers[0]?.id ?? ""; } export function resolveWebSearchDefinition( @@ -154,10 +160,13 @@ export function resolveWebSearchDefinition( options?.providerId ?? options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured ?? - resolveWebSearchProviderId({ search, providers }); + resolveWebSearchProviderId({ config: options?.config, search, providers }); const provider = providers.find((entry) => entry.id === providerId) ?? - providers.find((entry) => entry.id === resolveWebSearchProviderId({ search, providers })) ?? + providers.find( + (entry) => + entry.id === resolveWebSearchProviderId({ config: options?.config, search, providers }), + ) ?? providers[0]; if (!provider) { return null;