diff --git a/scripts/openclaw-build-messaging-plugins.py b/scripts/openclaw-build-messaging-plugins.py index 73dfd7ba38c..0edd3249b31 100755 --- a/scripts/openclaw-build-messaging-plugins.py +++ b/scripts/openclaw-build-messaging-plugins.py @@ -6,7 +6,8 @@ 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 pin external messaging plugins to that same version. +activation must force explicit npm installs for external messaging plugins and +pin them to that same version. """ from __future__ import annotations @@ -90,7 +91,7 @@ def plugin_specs(channels: Iterable[str], openclaw_version: str) -> list[str]: for channel in channels: package_name = EXTERNAL_CHANNEL_PACKAGES.get(channel) if package_name: - specs.append(f"{package_name}@{openclaw_version}") + specs.append(f"npm:{package_name}@{openclaw_version}") return specs @@ -137,7 +138,7 @@ def main(argv: list[str]) -> int: return 0 for spec in specs: - run_command(["openclaw", "plugins", "install", spec]) + run_command(["openclaw", "plugins", "install", spec, "--pin"]) doctor_env = os.environ.copy() doctor_env.update(env_overrides) diff --git a/test/openclaw-build-messaging-plugins.test.ts b/test/openclaw-build-messaging-plugins.test.ts index 5b03f7d7812..358feb839b7 100644 --- a/test/openclaw-build-messaging-plugins.test.ts +++ b/test/openclaw-build-messaging-plugins.test.ts @@ -74,9 +74,9 @@ describe("openclaw-build-messaging-plugins.py", () => { }); expect(payload.installSpecs).toEqual([ - "@openclaw/discord@2026.5.22", - "@openclaw/slack@2026.5.22", - "@openclaw/whatsapp@2026.5.22", + "npm:@openclaw/discord@2026.5.22", + "npm:@openclaw/slack@2026.5.22", + "npm:@openclaw/whatsapp@2026.5.22", ]); expect(payload.doctorEnv).toEqual({ DISCORD_BOT_TOKEN: "openshell:resolve:env:DISCORD_BOT_TOKEN", @@ -93,7 +93,7 @@ describe("openclaw-build-messaging-plugins.py", () => { }); expect(payload.channels).toEqual(["discord"]); - expect(payload.installSpecs).toEqual(["@openclaw/discord@2026.5.22"]); + expect(payload.installSpecs).toEqual(["npm:@openclaw/discord@2026.5.22"]); expect(payload.doctorEnv).toEqual({ DISCORD_BOT_TOKEN: "openshell:resolve:env:DISCORD_BOT_TOKEN", }); @@ -110,6 +110,15 @@ describe("openclaw-build-messaging-plugins.py", () => { }); }); + it("forces WhatsApp to the OpenClaw runtime version on 2026.5.18 sandboxes", () => { + const payload = parseDryRun({ + OPENCLAW_VERSION: "2026.5.18", + NEMOCLAW_MESSAGING_CHANNELS_B64: channelsB64(["whatsapp"]), + }); + + expect(payload.installSpecs).toEqual(["npm:@openclaw/whatsapp@2026.5.18"]); + }); + it("fails fast on malformed channel payloads", () => { const result = runDryRun({ OPENCLAW_VERSION: "2026.5.22", @@ -155,9 +164,9 @@ describe("openclaw-build-messaging-plugins.py", () => { expect(result.status, result.stderr).toBe(0); expect(fs.readFileSync(tracePath, "utf-8").trim().split("\n")).toEqual([ - "plugins|install|@openclaw/discord@2026.5.22||||", - "plugins|install|@openclaw/slack@2026.5.22||||", - "plugins|install|@openclaw/whatsapp@2026.5.22||||", + "plugins|install|npm:@openclaw/discord@2026.5.22|--pin|||", + "plugins|install|npm:@openclaw/slack@2026.5.22|--pin|||", + "plugins|install|npm:@openclaw/whatsapp@2026.5.22|--pin|||", [ "doctor", "--fix", @@ -186,7 +195,8 @@ describe("openclaw-build-messaging-plugins.py", () => { "const args = process.argv.slice(2);", 'fs.appendFileSync(process.env.OPENCLAW_TRACE, `${args.join("|")}|${process.env.DISCORD_BOT_TOKEN || ""}\\n`);', 'if (args[0] === "plugins" && args[1] === "install") {', - ' if (args[2] !== "@openclaw/discord@2026.5.22") process.exit(41);', + ' if (args[2] !== "npm:@openclaw/discord@2026.5.22") process.exit(41);', + ' if (args[3] !== "--pin") process.exit(47);', " process.exit(0);", "}", 'if (args[0] === "doctor" && args[1] === "--fix" && args[2] === "--non-interactive") {', @@ -233,7 +243,7 @@ describe("openclaw-build-messaging-plugins.py", () => { expect(pluginResult.status, pluginResult.stderr).toBe(0); expect(fs.readFileSync(tracePath, "utf-8").trim().split("\n")).toEqual([ - "plugins|install|@openclaw/discord@2026.5.22|", + "plugins|install|npm:@openclaw/discord@2026.5.22|--pin|", "doctor|--fix|--non-interactive|openshell:resolve:env:DISCORD_BOT_TOKEN", ]); } finally {