diff --git a/CHANGELOG.md b/CHANGELOG.md index b61abe98104..18bf0c2dc83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc. - Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars. - Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97. +- Web search: late-bind managed agent `web_search` calls to the current runtime config snapshot, so existing sessions do not keep stale unresolved SecretRefs after secrets reload. Fixes #75420. Thanks @richardmqq. - Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570. - macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc. - Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129. diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 2a366d1f6a4..078aeb1389e 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -194,6 +194,7 @@ export function createOpenClawTools( config: options?.config, sandboxed: options?.sandboxed, runtimeWebSearch: runtimeWebTools?.search, + lateBindRuntimeConfig: true, }); const webFetchTool = createWebFetchTool({ config: options?.config, diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 4a3565e2e40..bc10e8dd805 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveManifestContractOwnerPluginId } from "../../plugins/plugin-registry.js"; +import { getActiveRuntimeWebToolsMetadata } from "../../secrets/runtime-web-tools-state.js"; import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js"; import { resolveWebSearchProviderId, runWebSearch } from "../../web-search/runtime.js"; import type { AnyAgentTool } from "./common.js"; @@ -72,20 +73,11 @@ export function createWebSearchTool(options?: { config?: OpenClawConfig; sandboxed?: boolean; runtimeWebSearch?: RuntimeWebSearchMetadata; + lateBindRuntimeConfig?: boolean; }): AnyAgentTool | null { if (isWebSearchDisabled(options?.config)) { return null; } - const runtimeProviderId = - options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured; - const preferRuntimeProviders = - Boolean(runtimeProviderId) && - !resolveManifestContractOwnerPluginId({ - contract: "webSearchProviders", - value: runtimeProviderId, - origin: "bundled", - config: options?.config, - }); return { label: "Web Search", @@ -94,10 +86,25 @@ export function createWebSearchTool(options?: { "Search the web. Returns provider-normalized results for current information lookup.", parameters: WebSearchSchema, execute: async (_toolCallId, args) => { + const runtimeWebSearch = + options?.lateBindRuntimeConfig === true + ? getActiveRuntimeWebToolsMetadata()?.search + : options?.runtimeWebSearch; + const runtimeProviderId = + runtimeWebSearch?.selectedProvider ?? runtimeWebSearch?.providerConfigured; + const config = options?.lateBindRuntimeConfig === true ? undefined : options?.config; + const preferRuntimeProviders = + Boolean(runtimeProviderId) && + !resolveManifestContractOwnerPluginId({ + contract: "webSearchProviders", + value: runtimeProviderId, + origin: "bundled", + config, + }); const result = await runWebSearch({ - config: options?.config, + config, sandboxed: options?.sandboxed, - runtimeWebSearch: options?.runtimeWebSearch, + runtimeWebSearch, preferRuntimeProviders, args: asToolParamsRecord(args), }); diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index 9d794af118e..2db169c22b0 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -1,17 +1,29 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; -import { clearActiveRuntimeWebToolsMetadata } from "../../secrets/runtime-web-tools-state.js"; +import { + clearActiveRuntimeWebToolsMetadata, + setActiveRuntimeWebToolsMetadata, +} from "../../secrets/runtime-web-tools-state.js"; import { createWebFetchTool, createWebSearchTool } from "./web-tools.js"; +const runWebSearchCalls = vi.hoisted( + () => [] as Array<{ config?: unknown; runtimeWebSearch?: unknown }>, +); + vi.mock("../../web-search/runtime.js", async () => { const { getActivePluginRegistry } = await import("../../plugins/runtime.js"); + const { getActiveRuntimeWebToolsMetadata } = + await import("../../secrets/runtime-web-tools-state.js"); const resolveRuntimeDefinition = (options?: { config?: unknown; runtimeWebSearch?: { selectedProvider?: string; providerConfigured?: string }; }) => { const providerId = - options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured; + options?.runtimeWebSearch?.selectedProvider ?? + options?.runtimeWebSearch?.providerConfigured ?? + getActiveRuntimeWebToolsMetadata()?.search?.selectedProvider ?? + getActiveRuntimeWebToolsMetadata()?.search?.providerConfigured; const registration = getActivePluginRegistry()?.webSearchProviders.find( (entry) => entry.provider.id === providerId, ); @@ -33,9 +45,14 @@ vi.mock("../../web-search/runtime.js", async () => { resolveWebSearchDefinition: resolveRuntimeDefinition, resolveWebSearchProviderId: () => "", runWebSearch: async (options: { + config?: unknown; args: Record; runtimeWebSearch?: unknown; }) => { + runWebSearchCalls.push({ + config: options.config, + runtimeWebSearch: options.runtimeWebSearch, + }); const resolved = resolveRuntimeDefinition(options as never); if (!resolved) { throw new Error("web_search is disabled or no provider is available."); @@ -51,6 +68,7 @@ vi.mock("../../web-search/runtime.js", async () => { beforeEach(() => { setActivePluginRegistry(createEmptyPluginRegistry()); clearActiveRuntimeWebToolsMetadata(); + runWebSearchCalls.length = 0; }); afterEach(() => { @@ -114,4 +132,91 @@ describe("web tools defaults", () => { expect(tool?.description).toContain("Search the web"); expect(result?.details).toMatchObject({ ok: true }); }); + + it("late-binds managed web_search execution to the current runtime snapshot", async () => { + const registry = createEmptyPluginRegistry(); + registry.webSearchProviders.push( + { + pluginId: "stale-search", + pluginName: "Stale Search", + source: "test", + provider: { + id: "stale", + label: "Stale Search", + hint: "Stale runtime provider", + envVars: [], + placeholder: "stale-...", + signupUrl: "https://example.com/stale", + autoDetectOrder: 1, + credentialPath: "tools.web.search.stale.apiKey", + getCredentialValue: () => "configured", + setCredentialValue: () => {}, + createTool: () => ({ + description: "stale runtime tool", + parameters: {}, + execute: async () => ({ provider: "stale" }), + }), + }, + }, + { + pluginId: "fresh-search", + pluginName: "Fresh Search", + source: "test", + provider: { + id: "fresh", + label: "Fresh Search", + hint: "Fresh runtime provider", + envVars: [], + placeholder: "fresh-...", + signupUrl: "https://example.com/fresh", + autoDetectOrder: 2, + credentialPath: "tools.web.search.fresh.apiKey", + getCredentialValue: () => "configured", + setCredentialValue: () => {}, + createTool: () => ({ + description: "fresh runtime tool", + parameters: {}, + execute: async () => ({ provider: "fresh" }), + }), + }, + }, + ); + setActivePluginRegistry(registry); + setActiveRuntimeWebToolsMetadata({ + search: { + providerConfigured: "fresh", + providerSource: "configured", + selectedProvider: "fresh", + selectedProviderKeySource: "config", + diagnostics: [], + }, + fetch: { + providerSource: "none", + diagnostics: [], + }, + diagnostics: [], + }); + + const tool = createWebSearchTool({ + config: { tools: { web: { search: { provider: "stale" } } } }, + sandboxed: true, + runtimeWebSearch: { + providerConfigured: "stale", + providerSource: "configured", + selectedProvider: "stale", + selectedProviderKeySource: "config", + diagnostics: [], + }, + lateBindRuntimeConfig: true, + }); + + const result = await tool?.execute?.("call-runtime-provider", {}); + + expect(result?.details).toMatchObject({ provider: "fresh" }); + expect(runWebSearchCalls).toHaveLength(1); + expect(runWebSearchCalls[0]?.config).toBeUndefined(); + expect(runWebSearchCalls[0]?.runtimeWebSearch).toMatchObject({ + selectedProvider: "fresh", + }); + }); });