diff --git a/CHANGELOG.md b/CHANGELOG.md index 645c7590930..99a8f357737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects (WebSocket close, timeout, connection drop) with bounded exponential backoff (up to 8 retries, capped at 30 s) and stderr retry warnings, while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059) Thanks @shashank-poola. - CLI/logs: announce `--follow` recovery with a `[logs] gateway reconnected` notice once a poll succeeds after a transient outage, and emit JSON `notice` records in `--json` mode for both the retry warning and the reconnect transition, so live monitoring scripts can react to the recovery. Carries forward #75059. (#75372) Thanks @romneyda. - Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc. +- Agents/web_search: keep installed runtime provider discovery enabled when web-search metadata is missing, so externally installed official providers such as Brave remain visible to agent and cron turns instead of falling back to bundled-only lookup. Fixes #76626. Thanks @amknight. - Tests/plugins: expose the Discord npm onboarding Docker lane as a package script and assert planned Docker lanes point at real scripts, so external-channel onboarding coverage can actually run. Thanks @vincentkoc. - Plugins/ClawHub: explain unreleased ClawHub plugin artifacts as a rollout-state fallback to `npm:` installs instead of leaking raw archive metadata fields. Thanks @vincentkoc. - Tests/onboarding: assert packaged channel onboarding leaves `openclaw channels status --json` and plain `openclaw status` showing the configured channel, covering the empty Channels table regression path. Thanks @vincentkoc. diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 451648e6c95..4e1392195d6 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -98,7 +98,7 @@ export function createWebSearchTool(options?: { ? (getActiveSecretsRuntimeSnapshot()?.config ?? options?.config) : options?.config; const preferRuntimeProviders = - Boolean(runtimeProviderId) && + !runtimeProviderId || !resolveManifestContractOwnerPluginId({ contract: "webSearchProviders", value: runtimeProviderId, diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index 39df65d87f7..8a412e0b995 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -8,12 +8,37 @@ import { import { createWebFetchTool, createWebSearchTool } from "./web-tools.js"; const runWebSearchCalls = vi.hoisted( - () => [] as Array<{ config?: unknown; runtimeWebSearch?: unknown }>, + () => + [] as Array<{ + config?: unknown; + preferRuntimeProviders?: boolean; + runtimeWebSearch?: unknown; + }>, ); const activeSecretsRuntimeSnapshot = vi.hoisted(() => ({ current: null as null | { config: unknown }, })); +function readConfiguredSearchProvider(config: unknown): string | undefined { + if (!config || typeof config !== "object") { + return undefined; + } + const tools = (config as { tools?: unknown }).tools; + if (!tools || typeof tools !== "object") { + return undefined; + } + const web = (tools as { web?: unknown }).web; + if (!web || typeof web !== "object") { + return undefined; + } + const search = (web as { search?: unknown }).search; + if (!search || typeof search !== "object") { + return undefined; + } + const provider = (search as { provider?: unknown }).provider; + return typeof provider === "string" ? provider : undefined; +} + vi.mock("../../secrets/runtime.js", () => ({ getActiveSecretsRuntimeSnapshot: () => activeSecretsRuntimeSnapshot.current, })); @@ -30,7 +55,8 @@ vi.mock("../../web-search/runtime.js", async () => { options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured ?? getActiveRuntimeWebToolsMetadata()?.search?.selectedProvider ?? - getActiveRuntimeWebToolsMetadata()?.search?.providerConfigured; + getActiveRuntimeWebToolsMetadata()?.search?.providerConfigured ?? + readConfiguredSearchProvider(options?.config); const registration = getActivePluginRegistry()?.webSearchProviders.find( (entry) => entry.provider.id === providerId, ); @@ -54,10 +80,12 @@ vi.mock("../../web-search/runtime.js", async () => { runWebSearch: async (options: { config?: unknown; args: Record; + preferRuntimeProviders?: boolean; runtimeWebSearch?: unknown; }) => { runWebSearchCalls.push({ config: options.config, + preferRuntimeProviders: options.preferRuntimeProviders, runtimeWebSearch: options.runtimeWebSearch, }); const resolved = resolveRuntimeDefinition(options as never); @@ -142,6 +170,52 @@ describe("web tools defaults", () => { expect(result?.details).toMatchObject({ ok: true }); }); + it("keeps runtime provider discovery enabled when runtime web_search metadata is missing", async () => { + const registry = createEmptyPluginRegistry(); + registry.webSearchProviders.push({ + pluginId: "custom-search", + pluginName: "Custom Search", + source: "test", + provider: { + id: "custom", + label: "Custom Search", + hint: "Custom runtime provider", + envVars: ["CUSTOM_SEARCH_API_KEY"], + placeholder: "custom-...", + signupUrl: "https://example.com/signup", + autoDetectOrder: 1, + credentialPath: "plugins.entries.custom-search.config.webSearch.apiKey", + getCredentialValue: () => "configured", + setCredentialValue: () => {}, + createTool: () => ({ + description: "custom runtime tool", + parameters: {}, + execute: async () => ({ provider: "custom" }), + }), + }, + }); + setActivePluginRegistry(registry); + + const tool = createWebSearchTool({ + config: { + tools: { + web: { + search: { + provider: "custom", + }, + }, + }, + }, + sandboxed: true, + }); + + const result = await tool?.execute?.("call-runtime-provider-without-metadata", {}); + + expect(result?.details).toMatchObject({ provider: "custom" }); + expect(runWebSearchCalls).toHaveLength(1); + expect(runWebSearchCalls[0]?.preferRuntimeProviders).toBe(true); + }); + it("late-binds managed web_search execution to the current runtime snapshot", async () => { const registry = createEmptyPluginRegistry(); registry.webSearchProviders.push(