diff --git a/CHANGELOG.md b/CHANGELOG.md index 553d6463caf..686a3c02e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Plugins/install: stage bundled plugin runtime dependencies before Gateway startup and drain update restarts while preserving per-plugin isolation when pre-stage scan or install fails. Thanks @codex. - CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc. - CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras. +- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc. - Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras. - Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors. - Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech. diff --git a/docs/tools/web.md b/docs/tools/web.md index a3d93bdb361..c4d3ee1e7a0 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -185,7 +185,8 @@ error prompting you to configure one). All provider key fields support SecretRef objects. Plugin-scoped SecretRefs under `plugins.entries..config.webSearch.apiKey` are resolved for the - bundled Exa, Firecrawl, Gemini, Grok, Kimi, Perplexity, and Tavily providers + bundled API-backed web search providers, including Brave, Exa, Firecrawl, + Gemini, Grok, Kimi, MiniMax, Perplexity, and Tavily, whether the provider is picked explicitly via `tools.web.search.provider` or selected through auto-detect. In auto-detect mode, OpenClaw resolves only the selected provider key -- non-selected SecretRefs stay inactive, so you can diff --git a/src/web-search/runtime.test.ts b/src/web-search/runtime.test.ts index f516e6b441d..0be4d68eb56 100644 --- a/src/web-search/runtime.test.ts +++ b/src/web-search/runtime.test.ts @@ -155,6 +155,60 @@ describe("web search runtime", () => { }); }); + it("uses the active resolved runtime config for matching source config callers", async () => { + const provider = createCustomSearchProvider({ + createTool: ({ config }) => ({ + description: "custom", + parameters: {}, + execute: async (args) => ({ + ...args, + apiKey: getCustomSearchApiKey(config), + }), + }), + }); + resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]); + resolvePluginWebSearchProvidersMock.mockReturnValue([provider]); + + const sourceConfig = createCustomSearchConfig({ + source: "exec", + provider: "mockexec", + id: "custom-search/api-key", + }); + const resolvedConfig = createCustomSearchConfig("resolved-custom-key"); + + activateSecretsRuntimeSnapshot({ + sourceConfig, + config: resolvedConfig, + authStores: [], + warnings: [], + webTools: { + search: { + providerSource: "auto-detect", + selectedProvider: "custom", + diagnostics: [], + }, + fetch: { + providerSource: "none", + diagnostics: [], + }, + diagnostics: [], + }, + }); + + await expect( + runWebSearch({ + config: structuredClone(sourceConfig), + args: { query: "runtime-source" }, + }), + ).resolves.toEqual({ + provider: "custom", + result: { + query: "runtime-source", + apiKey: "resolved-custom-key", + }, + }); + }); + it("treats non-env SecretRefs as configured credentials for provider auto-detect", async () => { const provider = createCustomSearchProvider(); resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]); diff --git a/src/web-search/runtime.ts b/src/web-search/runtime.ts index d6828d12c0a..79556397e1d 100644 --- a/src/web-search/runtime.ts +++ b/src/web-search/runtime.ts @@ -1,3 +1,8 @@ +import { + getRuntimeConfigSnapshot, + getRuntimeConfigSourceSnapshot, + selectApplicableRuntimeConfig, +} from "../config/runtime-snapshot.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { logVerbose } from "../globals.js"; import type { @@ -41,6 +46,14 @@ function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig { return resolveWebProviderConfig(cfg, "search") as NonNullable | undefined; } +function resolveWebSearchRuntimeConfig(config?: OpenClawConfig): OpenClawConfig | undefined { + return selectApplicableRuntimeConfig({ + inputConfig: config, + runtimeConfig: getRuntimeConfigSnapshot(), + runtimeSourceConfig: getRuntimeConfigSourceSnapshot(), + }); +} + export function resolveWebSearchEnabled(params: { search?: WebSearchConfig; sandboxed?: boolean; @@ -91,14 +104,16 @@ export function isWebSearchProviderConfigured(params: { >; config?: OpenClawConfig; }): boolean { - return hasEntryCredential(params.provider, params.config, resolveSearchConfig(params.config)); + const config = resolveWebSearchRuntimeConfig(params.config); + return hasEntryCredential(params.provider, config, resolveSearchConfig(config)); } export function listWebSearchProviders(params?: { config?: OpenClawConfig; }): PluginWebSearchProviderEntry[] { + const config = resolveWebSearchRuntimeConfig(params?.config); return resolveRuntimeWebSearchProviders({ - config: params?.config, + config, bundledAllowlistCompat: true, }); } @@ -106,8 +121,9 @@ export function listWebSearchProviders(params?: { export function listConfiguredWebSearchProviders(params?: { config?: OpenClawConfig; }): PluginWebSearchProviderEntry[] { + const config = resolveWebSearchRuntimeConfig(params?.config); return resolvePluginWebSearchProviders({ - config: params?.config, + config, bundledAllowlistCompat: true, }); } @@ -117,18 +133,18 @@ export function resolveWebSearchProviderId(params: { config?: OpenClawConfig; providers?: PluginWebSearchProviderEntry[]; }): string { + const config = resolveWebSearchRuntimeConfig(params.config); + const search = params.search ?? resolveSearchConfig(config); const providers = sortWebSearchProvidersForAutoDetect( params.providers ?? resolvePluginWebSearchProviders({ - config: params.config, + config, bundledAllowlistCompat: true, origin: "bundled", }), ); const raw = - params.search && "provider" in params.search - ? normalizeLowercaseStringOrEmpty(params.search.provider) - : ""; + search && "provider" in search ? normalizeLowercaseStringOrEmpty(search.provider) : ""; if (raw) { const explicit = providers.find((provider) => provider.id === raw); @@ -144,7 +160,7 @@ export function resolveWebSearchProviderId(params: { keylessFallbackProviderId ||= provider.id; continue; } - if (!hasEntryCredential(provider, params.config, params.search)) { + if (!hasEntryCredential(provider, config, search)) { continue; } logVerbose( @@ -166,22 +182,23 @@ export function resolveWebSearchProviderId(params: { export function resolveWebSearchDefinition( options?: ResolveWebSearchDefinitionParams, ): { provider: PluginWebSearchProviderEntry; definition: WebSearchProviderToolDefinition } | null { - const search = resolveSearchConfig(options?.config); + const config = resolveWebSearchRuntimeConfig(options?.config); + const search = resolveSearchConfig(config); const runtimeWebSearch = options?.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search; const providers = sortWebSearchProvidersForAutoDetect( options?.preferRuntimeProviders ? resolveRuntimeWebSearchProviders({ - config: options?.config, + config, bundledAllowlistCompat: true, }) : resolvePluginWebSearchProviders({ - config: options?.config, + config, bundledAllowlistCompat: true, origin: "bundled", }), ); return resolveWebProviderDefinition({ - config: options?.config, + config, toolConfig: search as Record | undefined, runtimeMetadata: runtimeWebSearch, sandboxed: options?.sandboxed, @@ -216,7 +233,8 @@ export function resolveWebSearchDefinition( function resolveWebSearchCandidates( options?: ResolveWebSearchDefinitionParams, ): PluginWebSearchProviderEntry[] { - const search = resolveSearchConfig(options?.config); + const config = resolveWebSearchRuntimeConfig(options?.config); + const search = resolveSearchConfig(config); const runtimeWebSearch = options?.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search; if (!resolveWebSearchEnabled({ search, sandboxed: options?.sandboxed })) { return []; @@ -225,11 +243,11 @@ function resolveWebSearchCandidates( const providers = sortWebSearchProvidersForAutoDetect( options?.preferRuntimeProviders ? resolveRuntimeWebSearchProviders({ - config: options?.config, + config, bundledAllowlistCompat: true, }) : resolvePluginWebSearchProviders({ - config: options?.config, + config, bundledAllowlistCompat: true, origin: "bundled", }), @@ -242,7 +260,7 @@ function resolveWebSearchCandidates( options?.providerId, runtimeWebSearch?.selectedProvider, runtimeWebSearch?.providerConfigured, - resolveWebSearchProviderId({ config: options?.config, search, providers }), + resolveWebSearchProviderId({ config, search, providers }), ].filter( (value, index, array): value is string => Boolean(value) && array.indexOf(value) === index, ); @@ -294,10 +312,12 @@ function hasExplicitWebSearchSelection(params: { } export async function runWebSearch(params: RunWebSearchParams): Promise { - const search = resolveSearchConfig(params.config); + const config = resolveWebSearchRuntimeConfig(params.config); + const search = resolveSearchConfig(config); const runtimeWebSearch = params.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search; const candidates = resolveWebSearchCandidates({ ...params, + config, runtimeWebSearch, preferRuntimeProviders: params.preferRuntimeProviders ?? true, }); @@ -316,7 +336,7 @@ export async function runWebSearch(params: RunWebSearchParams): Promise | undefined, runtimeMetadata: runtimeWebSearch, });