fix(web-search): keep runtime legacy merge out of validation (#86818)

Runtime-injected web_search provider config from plugins.entries.<plugin>.config.webSearch now stays available to provider execution without being validated as user-authored legacy tools.web.search.<provider> config.

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
This commit is contained in:
狼哥
2026-05-27 04:15:44 +08:00
committed by GitHub
parent 3127808473
commit 4a85cd76f6
5 changed files with 62 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
import { resolvePluginWebSearchConfig } from "../../config/plugin-web-search-config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { isLegacyWebSearchProviderConfigKey } from "../../config/web-search-legacy-provider-keys.js";
export function getTopLevelCredentialValue(searchConfig?: Record<string, unknown>): unknown {
return searchConfig?.apiKey;
@@ -52,13 +53,22 @@ export function mergeScopedSearchConfig(
!Array.isArray(searchConfig[key])
? (searchConfig[key] as Record<string, unknown>)
: {};
const next: Record<string, unknown> = {
...searchConfig,
[key]: {
const next: Record<string, unknown> = { ...searchConfig };
const existingDescriptor = searchConfig
? Object.getOwnPropertyDescriptor(searchConfig, key)
: undefined;
const shouldHideRuntimeInjectedLegacyShape =
isLegacyWebSearchProviderConfigKey(key) && existingDescriptor === undefined;
Object.defineProperty(next, key, {
value: {
...currentScoped,
...pluginConfig,
},
};
enumerable: !shouldHideRuntimeInjectedLegacyShape,
configurable: true,
writable: true,
});
if (options?.mirrorApiKeyToTopLevel && pluginConfig.apiKey !== undefined) {
next.apiKey = pluginConfig.apiKey;

View File

@@ -144,4 +144,15 @@ describe("web_search scoped config merge", () => {
brave: { count: 5, apiKey: "brave-test-key" },
});
});
it("keeps newly injected legacy provider config runtime-only for validation", () => {
const merged = mergeScopedSearchConfig({ enabled: true, provider: "gemini" }, "perplexity", {
apiKey: "perplexity-test-key",
});
expect(merged?.perplexity).toEqual({ apiKey: "perplexity-test-key" });
expect(Object.keys(merged ?? {})).toEqual(["enabled", "provider"]);
expect(Object.getOwnPropertyDescriptor(merged, "perplexity")?.enumerable).toBe(false);
});
});

View File

@@ -1,5 +1,6 @@
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import { mergeScopedSearchConfig } from "../agents/tools/web-search-provider-config.js";
import { validateConfigObjectRaw } from "./validation.js";
describe("web search Codex native config validation", () => {
@@ -54,6 +55,23 @@ describe("web search Codex native config validation", () => {
}
});
it("accepts runtime-only legacy provider entries injected by web search merge", () => {
const search = mergeScopedSearchConfig({ enabled: true, provider: "gemini" }, "perplexity", {
apiKey: "perplexity-test-key",
});
const result = validateConfigObjectRaw({
tools: {
web: {
search,
},
},
});
expect(search?.perplexity).toEqual({ apiKey: "perplexity-test-key" });
expect(Object.keys(search ?? {})).toEqual(["enabled", "provider"]);
expect(result.ok).toBe(true);
});
it.each(["__proto__", "prototype", "constructor"])(
"rejects blocked tools.web.search key %s",
(key) => {

View File

@@ -0,0 +1,18 @@
export const LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS = new Set([
"brave",
"duckduckgo",
"exa",
"firecrawl",
"gemini",
"grok",
"kimi",
"minimax",
"ollama",
"perplexity",
"searxng",
"tavily",
]);
export function isLegacyWebSearchProviderConfigKey(key: string): boolean {
return LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS.has(key);
}

View File

@@ -10,6 +10,7 @@ import {
} from "../shared/string-coerce.js";
import { uniqueStrings } from "../shared/string-normalization.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
import { LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS } from "./web-search-legacy-provider-keys.js";
import { AgentModelSchema, AgentToolModelSchema } from "./zod-schema.agent-model.js";
import {
GroupChatSchema,
@@ -352,21 +353,6 @@ const CodexUserLocationSchema = z
})
.optional();
const LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS = new Set([
"brave",
"duckduckgo",
"exa",
"firecrawl",
"gemini",
"grok",
"kimi",
"minimax",
"ollama",
"perplexity",
"searxng",
"tavily",
]);
const BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD = "__openclawBlockedWebSearchKeys";
const ToolsWebSearchSchema = z