fix(doctor): preserve catalog repair specs

This commit is contained in:
Vincent Koc
2026-05-03 02:53:45 -07:00
parent 4bb4127a33
commit a4c1c28a17
3 changed files with 53 additions and 2 deletions

View File

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

View File

@@ -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([
{

View File

@@ -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) =>