From b6a8759b29c7d2109300ba9c1593acd8b8f996cb Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Tue, 21 Apr 2026 04:34:24 +0300 Subject: [PATCH] fix(web-search): restore SecretRef runtime compatibility for bundled providers (#68424) Adds missing compatibility runtime path metadata for bundled SecretRef-capable web-search providers and keeps the manifest registry covered by a regression test.\n\nThanks @afurm! --- CHANGELOG.md | 1 + extensions/exa/openclaw.plugin.json | 3 ++ extensions/firecrawl/openclaw.plugin.json | 3 ++ extensions/google/openclaw.plugin.json | 3 ++ extensions/moonshot/openclaw.plugin.json | 3 ++ extensions/perplexity/openclaw.plugin.json | 3 ++ extensions/tavily/openclaw.plugin.json | 3 ++ extensions/xai/openclaw.plugin.json | 3 ++ .../web-provider-public-artifacts.test.ts | 47 +++++++++++++++++++ 9 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b8a0648882..9f72e07dfe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Telegram/status reactions: honor `messages.removeAckAfterReply` when lifecycle status reactions are enabled, clearing or restoring the reaction after success/error using the configured hold timings. (#68067) Thanks @poiskgit. +- Web search/plugins: resolve plugin-scoped SecretRef API keys for bundled Exa, Firecrawl, Gemini, Kimi, Perplexity, Tavily, and Grok web-search providers when they are selected through the shared web-search config. (#68424) Thanks @afurm. - Telegram/polling: raise the default polling watchdog threshold from 90s to 120s and add configurable `channels.telegram.pollingStallThresholdMs` (also per-account) so long-running Telegram work gets more room before polling is treated as stalled. (#57737) Thanks @Vitalcheffe. - Telegram/polling: bound the persisted-offset confirmation `getUpdates` probe with a client-side timeout so a zombie socket cannot hang polling recovery before the runner watchdog starts. (#50368) Thanks @boticlaw. - Agents/Pi runner: retry silent `stopReason=error` turns with no output when no side effects ran, so non-frontier providers that briefly return empty error turns get another chance instead of ending the session early. (#68310) Thanks @Chased1k. diff --git a/extensions/exa/openclaw.plugin.json b/extensions/exa/openclaw.plugin.json index 1e54139b04a..e6a7a2be985 100644 --- a/extensions/exa/openclaw.plugin.json +++ b/extensions/exa/openclaw.plugin.json @@ -14,6 +14,9 @@ "contracts": { "webSearchProviders": ["exa"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/firecrawl/openclaw.plugin.json b/extensions/firecrawl/openclaw.plugin.json index c55e4827a49..e765e8484af 100644 --- a/extensions/firecrawl/openclaw.plugin.json +++ b/extensions/firecrawl/openclaw.plugin.json @@ -30,6 +30,9 @@ "webSearchProviders": ["firecrawl"], "tools": ["firecrawl_search", "firecrawl_scrape"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index 5ce20ace302..85ea4851984 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -53,6 +53,9 @@ "videoGenerationProviders": ["google"], "webSearchProviders": ["gemini"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/moonshot/openclaw.plugin.json b/extensions/moonshot/openclaw.plugin.json index 1fe1ef1cbdb..0ded65b8c34 100644 --- a/extensions/moonshot/openclaw.plugin.json +++ b/extensions/moonshot/openclaw.plugin.json @@ -52,6 +52,9 @@ "mediaUnderstandingProviders": ["moonshot"], "webSearchProviders": ["kimi"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/perplexity/openclaw.plugin.json b/extensions/perplexity/openclaw.plugin.json index 06858badea0..252f796a04c 100644 --- a/extensions/perplexity/openclaw.plugin.json +++ b/extensions/perplexity/openclaw.plugin.json @@ -22,6 +22,9 @@ "contracts": { "webSearchProviders": ["perplexity"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/tavily/openclaw.plugin.json b/extensions/tavily/openclaw.plugin.json index 37c9450b195..1815b07f159 100644 --- a/extensions/tavily/openclaw.plugin.json +++ b/extensions/tavily/openclaw.plugin.json @@ -20,6 +20,9 @@ "webSearchProviders": ["tavily"], "tools": ["tavily_search", "tavily_extract"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/xai/openclaw.plugin.json b/extensions/xai/openclaw.plugin.json index baebd2d1659..20c6f1f932f 100644 --- a/extensions/xai/openclaw.plugin.json +++ b/extensions/xai/openclaw.plugin.json @@ -87,6 +87,9 @@ "videoGenerationProviders": ["xai"], "tools": ["code_execution", "x_search"] }, + "configContracts": { + "compatibilityRuntimePaths": ["tools.web.search.apiKey"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/src/plugins/web-provider-public-artifacts.test.ts b/src/plugins/web-provider-public-artifacts.test.ts index f2b349d2f1c..65d7a5f5d4f 100644 --- a/src/plugins/web-provider-public-artifacts.test.ts +++ b/src/plugins/web-provider-public-artifacts.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "vitest"; import { + loadPluginManifestRegistry, resolveManifestContractOwnerPluginId, resolveManifestContractPluginIds, + resolveManifestContractPluginIdsByCompatibilityRuntimePath, } from "./manifest-registry.js"; import { hasBundledWebFetchProviderPublicArtifact, @@ -9,6 +11,29 @@ import { resolveBundledExplicitWebSearchProvidersFromPublicArtifacts, } from "./web-provider-public-artifacts.explicit.js"; +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function supportsSecretRefWebSearchApiKey( + plugin: ReturnType["plugins"][number], +): boolean { + const configProperties = isRecord(plugin.configSchema?.["properties"]) + ? plugin.configSchema["properties"] + : undefined; + const webSearch = configProperties?.["webSearch"]; + if (!isRecord(webSearch)) { + return false; + } + const properties = isRecord(webSearch["properties"]) ? webSearch["properties"] : undefined; + const apiKey = properties?.["apiKey"]; + if (!isRecord(apiKey)) { + return false; + } + const typeValue = apiKey["type"]; + return Array.isArray(typeValue) && typeValue.includes("object"); +} + describe("web provider public artifacts", () => { it("has a public artifact for every bundled web search provider declared in manifests", () => { const pluginIds = resolveManifestContractPluginIds({ @@ -44,6 +69,28 @@ describe("web provider public artifacts", () => { } }); + it("registers compatibility runtime paths for bundled SecretRef-capable web search providers", () => { + const registry = loadPluginManifestRegistry({ cache: false }); + const expectedPluginIds = registry.plugins + .filter( + (plugin) => + plugin.origin === "bundled" && + (plugin.contracts?.webSearchProviders?.length ?? 0) > 0 && + supportsSecretRefWebSearchApiKey(plugin), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); + + expect(expectedPluginIds).not.toHaveLength(0); + expect( + resolveManifestContractPluginIdsByCompatibilityRuntimePath({ + contract: "webSearchProviders", + path: "tools.web.search.apiKey", + origin: "bundled", + }), + ).toEqual(expectedPluginIds); + }); + it("has a public artifact for every bundled web fetch provider declared in manifests", () => { const pluginIds = resolveManifestContractPluginIds({ contract: "webFetchProviders",