From a4c1c28a1731eb1beff27a58d5ce10860aa52290 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 02:53:45 -0700 Subject: [PATCH] fix(doctor): preserve catalog repair specs --- CHANGELOG.md | 1 + .../missing-configured-plugin-install.test.ts | 48 +++++++++++++++++++ .../missing-configured-plugin-install.ts | 6 ++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a62bc0989d..845c87642a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Plugins/externalization: pin beta-only official launch packages for ACPX, Google Chat, and LINE to explicit npm beta specs so catalog-driven installs do not trip the prerelease safety guard while npm `latest` still points at beta. Thanks @vincentkoc. +- CLI/doctor: keep missing-plugin repair from overriding official catalog metadata with runtime fallbacks, so ACPX repairs preserve the beta npm spec during the externalization rollout. Thanks @vincentkoc. - Control UI/Talk: fix Talk (OpenAI Realtime WebRTC) CORS failure by stripping server-side-only attribution headers (`originator`, `version`, `User-Agent`) from browser offer headers; `api.openai.com/v1/realtime/calls` only allows `authorization` and `content-type` in its CORS preflight, so forwarding these headers caused the browser SDP exchange to fail. Fixes #76435. Thanks @hclsys. - 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. - 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. diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts index 8b317a9a8ca..771cdbca1e8 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts @@ -404,6 +404,54 @@ describe("repairMissingConfiguredPluginInstalls", () => { ]); }); + it("does not let runtime fallback metadata override official catalog install specs", async () => { + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "acpx", + targetDir: "/tmp/openclaw-plugins/acpx", + version: "2026.5.2-beta.2", + npmResolution: { + name: "@openclaw/acpx", + version: "2026.5.2-beta.2", + resolvedSpec: "@openclaw/acpx@2026.5.2-beta.2", + integrity: "sha512-acpx", + resolvedAt: "2026-05-01T00:00:00.000Z", + }, + }); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "acpx", + label: "ACPX Runtime", + install: { + npmSpec: "@openclaw/acpx@beta", + defaultChoice: "npm", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + acp: { + backend: "acpx", + }, + }, + env: {}, + }); + + expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( + expect.objectContaining({ + spec: "@openclaw/acpx@beta", + expectedPluginId: "acpx", + trustedSourceLinkedOfficialInstall: true, + }), + ); + expect(result.changes).toEqual([ + 'Installed missing configured plugin "acpx" from @openclaw/acpx@beta.', + ]); + }); + it("does not install disabled configured plugin entries", async () => { mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ { diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 0f1ac03d66f..687b64df981 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -44,7 +44,7 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[] { pluginId: "acpx", label: "ACPX Runtime", - npmSpec: "@openclaw/acpx", + npmSpec: "@openclaw/acpx@beta", trustedSourceLinkedOfficialInstall: true, }, // Runtime-only configs do not have a provider/channel integration catalog entry. @@ -275,7 +275,9 @@ function collectDownloadableInstallCandidates(params: { if (params.blockedPluginIds?.has(entry.pluginId)) { continue; } - candidates.set(entry.pluginId, entry); + if (!candidates.has(entry.pluginId)) { + candidates.set(entry.pluginId, entry); + } } return [...candidates.values()].toSorted((left, right) =>