Skip to content

Commit a4c1c28

Browse files
committed
fix(doctor): preserve catalog repair specs
1 parent 4bb4127 commit a4c1c28

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
2020
### Fixes
2121

2222
- 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.
23+
- 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.
2324
- 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.
2425
- 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.
2526
- 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.

src/commands/doctor/shared/missing-configured-plugin-install.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,54 @@ describe("repairMissingConfiguredPluginInstalls", () => {
404404
]);
405405
});
406406

407+
it("does not let runtime fallback metadata override official catalog install specs", async () => {
408+
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
409+
ok: true,
410+
pluginId: "acpx",
411+
targetDir: "/tmp/openclaw-plugins/acpx",
412+
version: "2026.5.2-beta.2",
413+
npmResolution: {
414+
name: "@openclaw/acpx",
415+
version: "2026.5.2-beta.2",
416+
resolvedSpec: "@openclaw/acpx@2026.5.2-beta.2",
417+
integrity: "sha512-acpx",
418+
resolvedAt: "2026-05-01T00:00:00.000Z",
419+
},
420+
});
421+
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
422+
{
423+
id: "acpx",
424+
label: "ACPX Runtime",
425+
install: {
426+
npmSpec: "@openclaw/acpx@beta",
427+
defaultChoice: "npm",
428+
},
429+
},
430+
]);
431+
432+
const { repairMissingConfiguredPluginInstalls } =
433+
await import("./missing-configured-plugin-install.js");
434+
const result = await repairMissingConfiguredPluginInstalls({
435+
cfg: {
436+
acp: {
437+
backend: "acpx",
438+
},
439+
},
440+
env: {},
441+
});
442+
443+
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
444+
expect.objectContaining({
445+
spec: "@openclaw/acpx@beta",
446+
expectedPluginId: "acpx",
447+
trustedSourceLinkedOfficialInstall: true,
448+
}),
449+
);
450+
expect(result.changes).toEqual([
451+
'Installed missing configured plugin "acpx" from @openclaw/acpx@beta.',
452+
]);
453+
});
454+
407455
it("does not install disabled configured plugin entries", async () => {
408456
mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
409457
{

src/commands/doctor/shared/missing-configured-plugin-install.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[]
4444
{
4545
pluginId: "acpx",
4646
label: "ACPX Runtime",
47-
npmSpec: "@openclaw/acpx",
47+
npmSpec: "@openclaw/acpx@beta",
4848
trustedSourceLinkedOfficialInstall: true,
4949
},
5050
// Runtime-only configs do not have a provider/channel integration catalog entry.
@@ -275,7 +275,9 @@ function collectDownloadableInstallCandidates(params: {
275275
if (params.blockedPluginIds?.has(entry.pluginId)) {
276276
continue;
277277
}
278-
candidates.set(entry.pluginId, entry);
278+
if (!candidates.has(entry.pluginId)) {
279+
candidates.set(entry.pluginId, entry);
280+
}
279281
}
280282

281283
return [...candidates.values()].toSorted((left, right) =>

0 commit comments

Comments
 (0)