From b57664be7c60c9b8b8e6ef53425397bf10687ac8 Mon Sep 17 00:00:00 2001 From: John Ellison Date: Mon, 8 Jun 2026 19:47:03 +0800 Subject: [PATCH 1/3] fix(onboard): pin Brave web-search plugin to OPENCLAW_VERSION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Brave web-search provider is an external plugin (@openclaw/brave-plugin), like the messaging channels and diagnostics OTEL exporter — but it was never pinned. `openclaw doctor --fix` installs it from the official catalog's unversioned npmSpec, so it resolves to npm `latest`. Once OpenClaw publishes a release the NemoClaw OPENCLAW_VERSION pin has not caught up to, `latest` drifts ahead of the host: the newer plugin imports plugin-SDK symbols the older host does not export, so web_search fails at runtime with (0 , _providerWebSearch.readPositiveIntegerParam) is not a function This is timing-dependent — it only breaks once `latest` has drifted past the pin — which is why it reproduces intermittently. Pin and build-install @openclaw/brave-plugin to OPENCLAW_VERSION when NEMOCLAW_WEB_SEARCH_ENABLED is set, mirroring the existing messaging-channel and diagnostics-OTEL handling. NEMOCLAW_WEB_SEARCH_ENABLED is already plumbed into this build step (Dockerfile ARG + ENV), so no Dockerfile change is needed. The plugin publishes a build for every core calver, so the pin always resolves. Addresses #3948 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: John Ellison --- scripts/openclaw-build-messaging-plugins.py | 31 ++++++++++++++--- test/openclaw-build-messaging-plugins.test.ts | 33 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/scripts/openclaw-build-messaging-plugins.py b/scripts/openclaw-build-messaging-plugins.py index 9b2b3f413a5..b992d41da4e 100755 --- a/scripts/openclaw-build-messaging-plugins.py +++ b/scripts/openclaw-build-messaging-plugins.py @@ -4,10 +4,11 @@ """Install OpenClaw plugins that match the bundled OpenClaw version. OpenClaw's doctor repair uses the official catalog's unversioned plugin specs. -That can drift to a newer external messaging plugin than the host OpenClaw -runtime. NemoClaw pins the runtime with OPENCLAW_VERSION, so build-time channel -activation must force explicit npm installs for external messaging plugins and -pin them to that same version. +That can drift to a newer external plugin than the host OpenClaw runtime — for +messaging channels, the diagnostics OTEL exporter, and the Brave web-search +provider alike. NemoClaw pins the runtime with OPENCLAW_VERSION, so build-time +activation must force explicit npm installs for every external plugin and pin +them to that same version. """ from __future__ import annotations @@ -29,6 +30,16 @@ "whatsapp": "@openclaw/whatsapp", } DIAGNOSTICS_OTEL_PACKAGE = "@openclaw/diagnostics-otel" +# The Brave web-search provider is an external plugin too. Without an explicit +# pin, `openclaw doctor --fix` installs it from the official catalog's +# unversioned npmSpec, which resolves to the newest published brave-plugin — +# newer than the host runtime once OpenClaw cuts a release the NemoClaw pin has +# not caught up to. The newer plugin imports plugin-SDK symbols the older host +# does not export, so web_search fails at runtime with +# (0, _providerWebSearch.readPositiveIntegerParam) is not a function +# Pin it to OPENCLAW_VERSION like the messaging channels and diagnostics OTEL +# exporter above so the installed plugin always matches the host runtime. +WEB_SEARCH_PLUGIN_PACKAGE = "@openclaw/brave-plugin" DOCTOR_ENV_BY_CHANNEL = { "telegram": { @@ -88,9 +99,12 @@ def require_openclaw_version( env: dict[str, str], *, diagnostics_otel_enabled: bool, + web_search_enabled: bool, ) -> str: needs_external_install = any(channel in EXTERNAL_CHANNEL_PACKAGES for channel in channels) - needs_external_install = needs_external_install or diagnostics_otel_enabled + needs_external_install = ( + needs_external_install or diagnostics_otel_enabled or web_search_enabled + ) version = (env.get("OPENCLAW_VERSION") or "").strip() if needs_external_install and not version: raise BuildMessagingPluginError( @@ -104,6 +118,7 @@ def plugin_specs( openclaw_version: str, *, diagnostics_otel_enabled: bool, + web_search_enabled: bool, ) -> list[str]: specs: list[str] = [] for channel in channels: @@ -112,6 +127,8 @@ def plugin_specs( specs.append(f"npm:{package_name}@{openclaw_version}") if diagnostics_otel_enabled: specs.append(f"npm:{DIAGNOSTICS_OTEL_PACKAGE}@{openclaw_version}") + if web_search_enabled: + specs.append(f"npm:{WEB_SEARCH_PLUGIN_PACKAGE}@{openclaw_version}") return specs @@ -139,15 +156,18 @@ def main(argv: list[str]) -> int: raw_channels = os.environ.get("NEMOCLAW_MESSAGING_CHANNELS_B64", DEFAULT_CHANNELS_B64) channels = decode_channels(raw_channels or DEFAULT_CHANNELS_B64) diagnostics_otel_enabled = is_truthy_env(os.environ.get("NEMOCLAW_OPENCLAW_OTEL")) + web_search_enabled = is_truthy_env(os.environ.get("NEMOCLAW_WEB_SEARCH_ENABLED")) openclaw_version = require_openclaw_version( channels, os.environ, diagnostics_otel_enabled=diagnostics_otel_enabled, + web_search_enabled=web_search_enabled, ) specs = plugin_specs( channels, openclaw_version, diagnostics_otel_enabled=diagnostics_otel_enabled, + web_search_enabled=web_search_enabled, ) env_overrides = doctor_env_overrides(channels) @@ -160,6 +180,7 @@ def main(argv: list[str]) -> int: "doctorEnv": env_overrides, "installSpecs": specs, "openclawVersion": openclaw_version, + "webSearchEnabled": web_search_enabled, }, indent=2, sort_keys=True, diff --git a/test/openclaw-build-messaging-plugins.test.ts b/test/openclaw-build-messaging-plugins.test.ts index cf4e74591c4..358655930ef 100644 --- a/test/openclaw-build-messaging-plugins.test.ts +++ b/test/openclaw-build-messaging-plugins.test.ts @@ -138,6 +138,39 @@ describe("openclaw-build-messaging-plugins.py", () => { expect(result.stderr).toContain("OPENCLAW_VERSION is required"); }); + it("pins the Brave web-search plugin to OPENCLAW_VERSION when web search is enabled", () => { + const payload = parseDryRun({ + OPENCLAW_VERSION: "2026.5.22", + NEMOCLAW_WEB_SEARCH_ENABLED: "1", + NEMOCLAW_MESSAGING_CHANNELS_B64: channelsB64(["slack"]), + }); + + expect(payload.webSearchEnabled).toBe(true); + expect(payload.installSpecs).toEqual([ + "npm:@openclaw/slack@2026.5.22", + "npm:@openclaw/brave-plugin@2026.5.22", + ]); + }); + + it("does not install the Brave plugin when web search is disabled", () => { + const payload = parseDryRun({ + OPENCLAW_VERSION: "2026.5.22", + NEMOCLAW_MESSAGING_CHANNELS_B64: channelsB64(["slack"]), + }); + + expect(payload.webSearchEnabled).toBe(false); + expect(payload.installSpecs).toEqual(["npm:@openclaw/slack@2026.5.22"]); + }); + + it("requires OPENCLAW_VERSION when web search is enabled", () => { + const result = runDryRun({ + NEMOCLAW_WEB_SEARCH_ENABLED: "1", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("OPENCLAW_VERSION is required"); + }); + it("fails fast on malformed channel payloads", () => { const result = runDryRun({ OPENCLAW_VERSION: "2026.5.22", From 2fd02f051c312175fde3c6aa1f7c2fae90def990 Mon Sep 17 00:00:00 2001 From: John Ellison Date: Tue, 9 Jun 2026 17:56:47 +0800 Subject: [PATCH 2/3] fix(onboard): preserve Brave apiKey placeholder through build-time doctor The generated config sets tools.web.search.apiKey to openshell:resolve:env:BRAVE_API_KEY, but the build-time `openclaw doctor --fix` runs with only the messaging-channel placeholders in its env. Without BRAVE_API_KEY present, doctor can mutate/strip the web-search block while repairing the config, leaving Brave search without a usable key at runtime. Inject the BRAVE_API_KEY placeholder into the doctor env when web search is enabled, the same way the channel bot tokens are already injected, so doctor preserves the resolvable web-search config. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: John Ellison --- scripts/openclaw-build-messaging-plugins.py | 16 ++++++++++++++-- test/openclaw-build-messaging-plugins.test.ts | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/openclaw-build-messaging-plugins.py b/scripts/openclaw-build-messaging-plugins.py index b992d41da4e..b1841ce8039 100755 --- a/scripts/openclaw-build-messaging-plugins.py +++ b/scripts/openclaw-build-messaging-plugins.py @@ -132,10 +132,20 @@ def plugin_specs( return specs -def doctor_env_overrides(channels: Iterable[str]) -> dict[str, str]: +def doctor_env_overrides( + channels: Iterable[str], + *, + web_search_enabled: bool, +) -> dict[str, str]: overrides: dict[str, str] = {} for channel in channels: overrides.update(DOCTOR_ENV_BY_CHANNEL.get(channel, {})) + # The generated config references openshell:resolve:env:BRAVE_API_KEY in + # tools.web.search.apiKey. `openclaw doctor --fix` runs with only this env, + # so without the placeholder set it can mutate/strip the web-search block. + # Inject it the same way the messaging channel tokens above are injected. + if web_search_enabled: + overrides["BRAVE_API_KEY"] = "openshell:resolve:env:BRAVE_API_KEY" return overrides @@ -169,7 +179,9 @@ def main(argv: list[str]) -> int: diagnostics_otel_enabled=diagnostics_otel_enabled, web_search_enabled=web_search_enabled, ) - env_overrides = doctor_env_overrides(channels) + env_overrides = doctor_env_overrides( + channels, web_search_enabled=web_search_enabled + ) if args.dry_run: print( diff --git a/test/openclaw-build-messaging-plugins.test.ts b/test/openclaw-build-messaging-plugins.test.ts index 358655930ef..fb9ffc903f2 100644 --- a/test/openclaw-build-messaging-plugins.test.ts +++ b/test/openclaw-build-messaging-plugins.test.ts @@ -150,6 +150,9 @@ describe("openclaw-build-messaging-plugins.py", () => { "npm:@openclaw/slack@2026.5.22", "npm:@openclaw/brave-plugin@2026.5.22", ]); + expect(payload.doctorEnv.BRAVE_API_KEY).toBe( + "openshell:resolve:env:BRAVE_API_KEY", + ); }); it("does not install the Brave plugin when web search is disabled", () => { @@ -160,6 +163,7 @@ describe("openclaw-build-messaging-plugins.py", () => { expect(payload.webSearchEnabled).toBe(false); expect(payload.installSpecs).toEqual(["npm:@openclaw/slack@2026.5.22"]); + expect(payload.doctorEnv.BRAVE_API_KEY).toBeUndefined(); }); it("requires OPENCLAW_VERSION when web search is enabled", () => { From bbe8be717bd0f61b6279725bc3597bdf8af54ca5 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 9 Jun 2026 18:47:34 -0700 Subject: [PATCH 3/3] style(test): biome-format openclaw-build-messaging-plugins test Run Biome on the new test so static-checks' formatter hook leaves it unchanged (it was collapsing a multi-line expect()). Formatting only; no behavior change. Co-authored-by: johnellison Signed-off-by: Prekshi Vyas Co-Authored-By: Claude Opus 4.8 (1M context) --- test/openclaw-build-messaging-plugins.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/openclaw-build-messaging-plugins.test.ts b/test/openclaw-build-messaging-plugins.test.ts index a2a01c44a8f..b1f2190b2eb 100644 --- a/test/openclaw-build-messaging-plugins.test.ts +++ b/test/openclaw-build-messaging-plugins.test.ts @@ -145,9 +145,7 @@ describe("openclaw-build-messaging-plugins.py", () => { "npm:@openclaw/slack@2026.5.22", "npm:@openclaw/brave-plugin@2026.5.22", ]); - expect(payload.doctorEnv.BRAVE_API_KEY).toBe( - "openshell:resolve:env:BRAVE_API_KEY", - ); + expect(payload.doctorEnv.BRAVE_API_KEY).toBe("openshell:resolve:env:BRAVE_API_KEY"); }); it("does not install the Brave plugin when web search is disabled", () => {