From ed214817fbcd5d97c6ee26b8fc488036a365d927 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 03:18:26 +0100 Subject: [PATCH] fix(release): tolerate legacy installed plugin min host floors --- src/plugins/manifest-registry.test.ts | 35 +++++++++++++++++++++++++++ src/plugins/manifest-registry.ts | 10 ++++++++ src/plugins/min-host-version.test.ts | 20 +++++++++++++++ src/plugins/min-host-version.ts | 15 +++++++++--- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index 7537bbe2acc..0fc5d8e4ba0 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -1528,6 +1528,41 @@ describe("loadPluginManifestRegistry", () => { } }); + it("accepts legacy bare minHostVersion metadata for recorded installed globals", () => { + const dir = makeTempDir(); + writeManifest(dir, { id: "codex", configSchema: { type: "object" } }); + + const registry = loadPluginManifestRegistry({ + installRecords: { + codex: { + source: "npm", + installPath: dir, + }, + }, + candidates: [ + createPluginCandidate({ + idHint: "codex", + rootDir: dir, + packageDir: dir, + origin: "global", + packageManifest: { + install: { + npmSpec: "@openclaw/codex", + minHostVersion: "2026.3.22", + }, + }, + }), + ], + }); + + expect(registry.plugins.map((plugin) => plugin.id)).toEqual(["codex"]); + expect( + registry.diagnostics.some((diag) => + diag.message.includes("openclaw.install.minHostVersion must use"), + ), + ).toBe(false); + }); + it.each([ { name: "reports bundled plugins as the duplicate winner for auto-discovered globals", diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 6315c061c59..b9c4b856cd7 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -617,9 +617,19 @@ export function loadPluginManifestRegistry( continue; } const manifest = manifestRes.manifest; + const allowLegacyBareMinHostVersion = + candidate.origin === "global" && + matchesInstalledPluginRecord({ + pluginId: manifest.id, + candidate, + config, + env, + installRecords: getInstallRecords(), + }); const minHostVersionCheck = checkMinHostVersion({ currentVersion: currentHostVersion, minHostVersion: candidate.packageManifest?.install?.minHostVersion, + allowLegacyBareSemver: allowLegacyBareMinHostVersion, }); if (!minHostVersionCheck.ok) { const packageManifestSource = path.join( diff --git a/src/plugins/min-host-version.test.ts b/src/plugins/min-host-version.test.ts index daf11fc6450..ce45d3e1876 100644 --- a/src/plugins/min-host-version.test.ts +++ b/src/plugins/min-host-version.test.ts @@ -59,6 +59,26 @@ describe("min-host-version", () => { expect(parseMinHostVersionRequirement(">=2026.3.22")).toEqual(MIN_HOST_REQUIREMENT); }); + it("can parse legacy bare semver floors for runtime upgrade compatibility", () => { + expect(parseMinHostVersionRequirement("2026.3.22", { allowLegacyBareSemver: true })).toEqual({ + raw: "2026.3.22", + minimumLabel: "2026.3.22", + }); + expect( + checkMinHostVersion({ + currentVersion: "2026.3.22", + minHostVersion: "2026.3.22", + allowLegacyBareSemver: true, + }), + ).toEqual({ + ok: true, + requirement: { + raw: "2026.3.22", + minimumLabel: "2026.3.22", + }, + }); + }); + it.each(["2026.3.22", 123, ">=2026.3.22 garbage"] as const)( "rejects invalid floor syntax and host checks: %p", (minHostVersion) => { diff --git a/src/plugins/min-host-version.ts b/src/plugins/min-host-version.ts index 207f545ff0d..feda1ace857 100644 --- a/src/plugins/min-host-version.ts +++ b/src/plugins/min-host-version.ts @@ -3,6 +3,7 @@ import { isAtLeast, parseSemver } from "../infra/runtime-guard.js"; export const MIN_HOST_VERSION_FORMAT = 'openclaw.install.minHostVersion must use a semver floor in the form ">=x.y.z"'; const MIN_HOST_VERSION_RE = /^>=(\d+)\.(\d+)\.(\d+)$/; +const LEGACY_MIN_HOST_VERSION_RE = /^(\d+)\.(\d+)\.(\d+)$/; export type MinHostVersionRequirement = { raw: string; @@ -22,7 +23,10 @@ export type MinHostVersionCheckResult = currentVersion: string; }; -export function parseMinHostVersionRequirement(raw: unknown): MinHostVersionRequirement | null { +export function parseMinHostVersionRequirement( + raw: unknown, + options: { allowLegacyBareSemver?: boolean } = {}, +): MinHostVersionRequirement | null { if (typeof raw !== "string") { return null; } @@ -30,7 +34,9 @@ export function parseMinHostVersionRequirement(raw: unknown): MinHostVersionRequ if (!trimmed) { return null; } - const match = trimmed.match(MIN_HOST_VERSION_RE); + const match = + trimmed.match(MIN_HOST_VERSION_RE) ?? + (options.allowLegacyBareSemver ? trimmed.match(LEGACY_MIN_HOST_VERSION_RE) : null); if (!match) { return null; } @@ -54,11 +60,14 @@ export function validateMinHostVersion(raw: unknown): string | null { export function checkMinHostVersion(params: { currentVersion: string | undefined; minHostVersion: unknown; + allowLegacyBareSemver?: boolean; }): MinHostVersionCheckResult { if (params.minHostVersion === undefined) { return { ok: true, requirement: null }; } - const requirement = parseMinHostVersionRequirement(params.minHostVersion); + const requirement = parseMinHostVersionRequirement(params.minHostVersion, { + allowLegacyBareSemver: params.allowLegacyBareSemver, + }); if (!requirement) { return { ok: false, kind: "invalid", error: MIN_HOST_VERSION_FORMAT }; }