fix(plugins): update trusted prerelease installs

This commit is contained in:
Vincent Koc
2026-05-03 17:16:58 -07:00
parent 40b8d52240
commit f5927cbb43
3 changed files with 78 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
- Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc.
- Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc.
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.

View File

@@ -507,6 +507,59 @@ describe("updateNpmInstalledPlugins", () => {
);
});
it("does not skip trusted official default updates when latest resolves to the installed prerelease", async () => {
const installPath = createInstalledPackageDir({
name: "@openclaw/acpx",
version: "2026.5.2-beta.2",
});
mockNpmViewMetadata({
name: "@openclaw/acpx",
version: "2026.5.2-beta.2",
integrity: "sha512-beta",
shasum: "beta",
});
installPluginFromNpmSpecMock.mockResolvedValue(
createSuccessfulNpmUpdateResult({
pluginId: "acpx",
targetDir: installPath,
version: "2026.5.2",
npmResolution: {
name: "@openclaw/acpx",
version: "2026.5.2",
resolvedSpec: "@openclaw/acpx@2026.5.2",
},
}),
);
const result = await updateNpmInstalledPlugins({
config: createNpmInstallConfig({
pluginId: "acpx",
spec: "@openclaw/acpx",
installPath,
integrity: "sha512-beta",
shasum: "beta",
resolvedName: "@openclaw/acpx",
resolvedSpec: "@openclaw/acpx@2026.5.2-beta.2",
resolvedVersion: "2026.5.2-beta.2",
}),
pluginIds: ["acpx"],
});
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/acpx",
expectedPluginId: "acpx",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(result.outcomes[0]).toMatchObject({
pluginId: "acpx",
status: "updated",
currentVersion: "2026.5.2-beta.2",
nextVersion: "2026.5.2",
});
});
it("does not trust official npm updates when the install record package mismatches", async () => {
const installPath = createInstalledPackageDir({
name: "@vendor/acpx-fork",

View File

@@ -4,7 +4,7 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
import { resolveNpmSpecMetadata } from "../infra/install-source-utils.js";
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { isPrereleaseResolutionAllowed, parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import {
expectedIntegrityForUpdate,
readInstalledPackageVersion,
@@ -179,6 +179,24 @@ function shouldSkipUnchangedNpmInstall(params: {
);
}
function shouldBypassTrustedOfficialUnchangedNpmCheck(params: {
metadata: NpmSpecResolution;
spec: string;
trustedSourceLinkedOfficialInstall: boolean;
}): boolean {
if (!params.trustedSourceLinkedOfficialInstall || !params.metadata.version) {
return false;
}
const parsedSpec = parseRegistryNpmSpec(params.spec);
return Boolean(
parsedSpec &&
!isPrereleaseResolutionAllowed({
spec: parsedSpec,
resolvedVersion: params.metadata.version,
}),
);
}
function isBundledVersionNewer(bundledVersion: string, installedVersion: string): boolean {
const bundled = parseComparableSemver(bundledVersion);
const installed = parseComparableSemver(installedVersion);
@@ -853,6 +871,11 @@ export async function updateNpmInstalledPlugins(params: {
});
if (metadataResult.ok) {
if (
!shouldBypassTrustedOfficialUnchangedNpmCheck({
metadata: metadataResult.metadata,
spec: effectiveSpec!,
trustedSourceLinkedOfficialInstall,
}) &&
shouldSkipUnchangedNpmInstall({
currentVersion,
record,