mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix(update): trust catalog-matched npm updates
This commit is contained in:
@@ -22,7 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Plugins/catalog: preserve ClawHub install specs when generating the packaged channel catalog so future storepack-first channel plugins keep their remote source instead of becoming npm-only. Thanks @vincentkoc.
|
||||
- Plugins/update: treat OpenClaw-authored externalized-bundled npm bridges as trusted official installs so launch-code plugins can migrate out of the bundled tree without scanner false positives. Thanks @vincentkoc.
|
||||
- Plugins/update: treat catalog-matched official npm updates and OpenClaw-authored externalized-bundled npm bridges as trusted official installs so launch-code plugins can update or migrate out of the bundled tree without scanner false positives. 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.
|
||||
|
||||
@@ -469,6 +469,80 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("trusts official catalog npm updates when the installed package matches the catalog", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@openclaw/acpx",
|
||||
version: "2026.5.2-beta.1",
|
||||
});
|
||||
mockNpmViewMetadata({
|
||||
name: "@openclaw/acpx",
|
||||
version: "2026.5.2-beta.2",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "acpx",
|
||||
targetDir: installPath,
|
||||
version: "2026.5.2-beta.2",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateNpmInstalledPlugins({
|
||||
config: createNpmInstallConfig({
|
||||
pluginId: "acpx",
|
||||
spec: "@openclaw/acpx@beta",
|
||||
installPath,
|
||||
resolvedName: "@openclaw/acpx",
|
||||
resolvedSpec: "@openclaw/acpx@2026.5.2-beta.1",
|
||||
resolvedVersion: "2026.5.2-beta.1",
|
||||
}),
|
||||
pluginIds: ["acpx"],
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/acpx@beta",
|
||||
expectedPluginId: "acpx",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not trust official npm updates when the install record package mismatches", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@vendor/acpx-fork",
|
||||
version: "1.0.0",
|
||||
});
|
||||
mockNpmViewMetadata({
|
||||
name: "@vendor/acpx-fork",
|
||||
version: "1.0.1",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "acpx",
|
||||
targetDir: installPath,
|
||||
version: "1.0.1",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateNpmInstalledPlugins({
|
||||
config: createNpmInstallConfig({
|
||||
pluginId: "acpx",
|
||||
spec: "@vendor/acpx-fork",
|
||||
installPath,
|
||||
resolvedName: "@vendor/acpx-fork",
|
||||
resolvedSpec: "@vendor/acpx-fork@1.0.0",
|
||||
resolvedVersion: "1.0.0",
|
||||
}),
|
||||
pluginIds: ["acpx"],
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips npm reinstall and config rewrite when the installed artifact is unchanged", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
} from "./install.js";
|
||||
import { buildNpmResolutionInstallFields, recordPluginInstall } from "./installs.js";
|
||||
import { installPluginFromMarketplace } from "./marketplace.js";
|
||||
import {
|
||||
getOfficialExternalPluginCatalogEntry,
|
||||
resolveOfficialExternalPluginInstall,
|
||||
} from "./official-external-plugin-catalog.js";
|
||||
|
||||
export type PluginUpdateLogger = {
|
||||
info?: (message: string) => void;
|
||||
@@ -424,6 +428,37 @@ function isDefaultNpmSpecForBetaUpdate(spec: string): { name: string } | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveNpmSpecPackageName(spec: string | undefined): string | undefined {
|
||||
return spec ? parseRegistryNpmSpec(spec)?.name : undefined;
|
||||
}
|
||||
|
||||
function isTrustedSourceLinkedOfficialNpmUpdate(params: {
|
||||
pluginId: string;
|
||||
spec: string | undefined;
|
||||
record: PluginInstallRecord;
|
||||
}): boolean {
|
||||
if (params.record.source !== "npm") {
|
||||
return false;
|
||||
}
|
||||
const entry = getOfficialExternalPluginCatalogEntry(params.pluginId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const officialPackageName = resolveNpmSpecPackageName(
|
||||
resolveOfficialExternalPluginInstall(entry)?.npmSpec,
|
||||
);
|
||||
const requestedPackageName = resolveNpmSpecPackageName(params.spec);
|
||||
if (!officialPackageName || requestedPackageName !== officialPackageName) {
|
||||
return false;
|
||||
}
|
||||
const recordedPackageNames = [
|
||||
params.record.resolvedName,
|
||||
resolveNpmSpecPackageName(params.record.spec),
|
||||
resolveNpmSpecPackageName(params.record.resolvedSpec),
|
||||
].filter((value): value is string => Boolean(value));
|
||||
return recordedPackageNames.includes(officialPackageName);
|
||||
}
|
||||
|
||||
function resolveNpmUpdateSpecs(params: {
|
||||
record: PluginInstallRecord;
|
||||
specOverride?: string;
|
||||
@@ -727,6 +762,11 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
record.source === "npm" && npmSpecs?.fallbackSpec === record.spec
|
||||
? expectedIntegrityForUpdate(record.spec, record.integrity)
|
||||
: undefined;
|
||||
const trustedSourceLinkedOfficialInstall = isTrustedSourceLinkedOfficialNpmUpdate({
|
||||
pluginId,
|
||||
spec: effectiveSpec,
|
||||
record,
|
||||
});
|
||||
|
||||
if (record.source === "npm" && !effectiveSpec) {
|
||||
outcomes.push({
|
||||
@@ -851,6 +891,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
@@ -919,6 +960,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity: fallbackExpectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
@@ -1033,6 +1075,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
@@ -1097,6 +1140,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity: fallbackExpectedIntegrity,
|
||||
onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({
|
||||
|
||||
Reference in New Issue
Block a user