fix(catalog): pin prerelease channel npm specs

This commit is contained in:
Peter Steinberger
2026-05-03 12:18:56 +01:00
parent 8dd6a2d323
commit 4ec1efbcbc
3 changed files with 54 additions and 1 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
- Plugins/externalization: keep official ACPX, Google Chat, and LINE install specs on production package names, leaving beta-tag probing to the explicit OpenClaw beta update channel. Thanks @vincentkoc.
- CLI/doctor: keep missing-plugin repair from overriding official catalog metadata with runtime fallbacks, so ACPX repairs preserve the official 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/catalog: pin bare npm specs from prerelease external channel catalog entries to the catalog entry version, so beta catalogs do not silently install the latest stable package.
- 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.
- Plugins/onboarding: fall back from ClawHub to npm only for missing package/version errors, keeping integrity and verification failures fail-closed during storepack 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.

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
import { isPrereleaseSemverVersion, parseRegistryNpmSpec } from "../../infra/npm-registry-spec.js";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
import {
@@ -240,12 +241,26 @@ function toChannelMeta(params: {
function resolveInstallInfo(params: {
install?: PluginPackageInstall;
packageName?: string;
packageVersion?: string;
packageDir?: string;
workspaceDir?: string;
}): ChannelPluginCatalogEntry["install"] | null {
const clawhubSpec = normalizeOptionalString(params.install?.clawhubSpec);
const npmSpec =
let npmSpec =
normalizeOptionalString(params.install?.npmSpec) ?? normalizeOptionalString(params.packageName);
const packageVersion = normalizeOptionalString(params.packageVersion);
const parsedNpmSpec = npmSpec ? parseRegistryNpmSpec(npmSpec) : null;
const expectedPackageName = normalizeOptionalString(params.packageName);
const parsedPackageName = expectedPackageName ? parseRegistryNpmSpec(expectedPackageName) : null;
if (
npmSpec &&
packageVersion &&
isPrereleaseSemverVersion(packageVersion) &&
parsedNpmSpec?.selectorKind === "none" &&
(!parsedPackageName || parsedNpmSpec.name === parsedPackageName.name)
) {
npmSpec = `${parsedNpmSpec.name}@${packageVersion}`;
}
if (!clawhubSpec && !npmSpec) {
return null;
}
@@ -296,6 +311,7 @@ function resolveInstallInfo(params: {
function buildCatalogEntryFromManifest(params: {
pluginId?: string;
packageName?: string;
packageVersion?: string;
packageDir?: string;
origin?: PluginOrigin;
trustedSourceLinkedOfficialInstall?: boolean;
@@ -317,6 +333,7 @@ function buildCatalogEntryFromManifest(params: {
const install = resolveInstallInfo({
install: params.install,
packageName: params.packageName,
packageVersion: params.packageVersion,
packageDir: params.packageDir,
workspaceDir: params.workspaceDir,
});
@@ -349,6 +366,7 @@ function buildExternalCatalogEntry(
return buildCatalogEntryFromManifest({
pluginId: manifest?.plugin?.id,
packageName: entry.name,
packageVersion: entry.version,
trustedSourceLinkedOfficialInstall: options?.trustedSourceLinkedOfficialInstall,
channel: manifest?.channel,
install: manifest?.install,

View File

@@ -287,6 +287,40 @@ export function describeChannelPluginCatalogEntriesContract() {
};
},
},
{
name: "pins bare external prerelease package specs to the entry version",
setup: () => {
const dir = fs.mkdtempSync(
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-catalog-prerelease-"),
);
const catalogPath = path.join(dir, "catalog.json");
writeCatalogFile(catalogPath, {
...createCatalogEntry({
packageName: "@openclaw/prerelease-demo-channel",
channelId: "prerelease-demo",
label: "Prerelease Demo",
blurb: "Prerelease package pinning fixture",
}),
version: "2026.5.3-beta.1",
});
return {
channelId: "prerelease-demo",
catalogPaths: [catalogPath],
expected: {
install: { npmSpec: "@openclaw/prerelease-demo-channel@2026.5.3-beta.1" },
installSource: {
npm: {
spec: "@openclaw/prerelease-demo-channel@2026.5.3-beta.1",
packageName: "@openclaw/prerelease-demo-channel",
selector: "2026.5.3-beta.1",
selectorKind: "exact-version",
exactVersion: true,
},
},
},
};
},
},
{
name: "accepts external manifest entries with ClawHub-only install metadata",
setup: () => {