diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fdde3d670c..f5071899cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -433,6 +433,7 @@ Docs: https://docs.openclaw.ai - Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit. - Agents/compaction: disable Pi auto-compaction whenever OpenClaw effectively owns safeguard compaction, including provider-backed safeguard mode, so Pi and OpenClaw no longer fight over long-session compaction. Fixes #73003. (#73839) Thanks @bradhallett. - Telegram/streaming: finalize text replies by stopping the edited stream message instead of sending a second answer bubble, so Telegram turns cannot duplicate the streamed final response. (#77947) Thanks @obviyus. +- web_search/Brave: fix provider selection when Brave is installed as an external plugin and `tools.web.search.provider: "brave"` is explicitly configured — a redundant provider re-resolution at startup could race and return an empty list, causing a spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and treating the explicitly configured provider as absent. Fixes #77676. Thanks @openperf. ## 2026.5.3-1 diff --git a/src/secrets/runtime-web-tools.shared.ts b/src/secrets/runtime-web-tools.shared.ts index eb91134f94d..930761867a3 100644 --- a/src/secrets/runtime-web-tools.shared.ts +++ b/src/secrets/runtime-web-tools.shared.ts @@ -234,7 +234,11 @@ export async function resolveRuntimeWebProviderSurface< ) { configuredBundledPluginId = undefined; } - if (params.rawProvider && !configuredBundledPluginId) { + if ( + params.rawProvider && + !configuredBundledPluginId && + !allProviders.some((provider) => provider.id === params.rawProvider) + ) { const resolveManifestContractOwnerPluginId = await loadResolveManifestContractOwnerPluginId(); configuredBundledPluginId = resolveManifestContractOwnerPluginId({ contract: params.contract, diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 1870ee357b5..d7df13a71c5 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -1564,4 +1564,94 @@ describe("runtime web tools resolution", () => { expect(resolveBundledWebFetchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled(); expect(resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled(); }); + + describe("when brave is installed as an external plugin and explicitly configured", () => { + const externalBraveImpl = ({ + value, + origin, + }: { + value: string; + origin?: string; + }): string | undefined => { + if (origin === "bundled" && value === "brave") { + return undefined; + } + return ( + { + brave: "brave", + firecrawl: "firecrawl", + gemini: "google", + grok: "xai", + kimi: "moonshot", + perplexity: "perplexity", + } as Record + )[value]; + }; + + const defaultImpl = ({ value }: { value: string }): string | undefined => + ( + ({ + brave: "brave", + firecrawl: "firecrawl", + gemini: "google", + grok: "xai", + kimi: "moonshot", + perplexity: "perplexity", + }) as Record + )[value]; + + beforeEach(() => { + loadInstalledPluginIndexInstallRecordsSyncMock.mockReturnValue({ + brave: { source: "npm", spec: "@openclaw/brave-search" }, + }); + resolveManifestContractOwnerPluginIdMock.mockImplementation(externalBraveImpl); + }); + + afterEach(() => { + resolveManifestContractOwnerPluginIdMock.mockImplementation(defaultImpl); + }); + + it("selects the configured provider without re-invoking provider discovery when found in the first pass", async () => { + resolvePluginWebSearchProvidersMock + .mockReturnValueOnce(buildTestWebSearchProviders()) + .mockReturnValueOnce([]); + + const { metadata, context } = await runRuntimeWebTools({ + config: asConfig({ + tools: { + web: { + search: { + provider: "brave", + }, + }, + }, + plugins: { + entries: { + brave: { + config: { + webSearch: { + apiKey: "brave-api-key", // pragma: allowlist secret + }, + }, + }, + }, + }, + }), + }); + + expect(metadata.search.selectedProvider).toBe("brave"); + expect(metadata.search.providerSource).toBe("configured"); + expect(metadata.search.selectedProviderKeySource).toBe("config"); + expect(context.warnings).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT" }), + ]), + ); + expect(resolvePluginWebSearchProvidersMock).toHaveBeenCalledTimes(1); + expect( + resolveBundledExplicitWebSearchProvidersFromPublicArtifactsMock, + ).not.toHaveBeenCalled(); + expect(resolveBundledWebSearchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled(); + }); + }); });