fix(agents): keep web_search runtime providers visible (#76685)

Summary:
- The PR changes the agent web_search wrapper to keep runtime provider discovery enabled when runtime metadata is absent, adds focused regression coverage, and records an unreleased changelog fix.
- Reproducibility: yes. at source level: current main passes preferRuntimeProviders: false when runtime web-se ... d issue supplies live Brave CLI-vs-agent evidence; this read-only review did not rerun a live Gateway call.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head e7f379c68d.
- Required merge gates passed before the squash merge.

Prepared head SHA: e7f379c68d
Review: https://github.com/openclaw/openclaw/pull/76685#issuecomment-4366216450

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
This commit is contained in:
Alex Knight
2026-05-03 23:11:35 +10:00
committed by GitHub
parent a4a4cac8e9
commit ccce342a24
3 changed files with 78 additions and 3 deletions

View File

@@ -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.

View File

@@ -98,7 +98,7 @@ export function createWebSearchTool(options?: {
? (getActiveSecretsRuntimeSnapshot()?.config ?? options?.config)
: options?.config;
const preferRuntimeProviders =
Boolean(runtimeProviderId) &&
!runtimeProviderId ||
!resolveManifestContractOwnerPluginId({
contract: "webSearchProviders",
value: runtimeProviderId,

View File

@@ -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<string, unknown>;
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(