diff --git a/CHANGELOG.md b/CHANGELOG.md index dcce92716c8..34256c64fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob. - CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc. - Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc. +- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan. - fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987. - Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987. - Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333. diff --git a/src/infra/clawhub.test.ts b/src/infra/clawhub.test.ts index 1e404208df7..45e8d85b61c 100644 --- a/src/infra/clawhub.test.ts +++ b/src/infra/clawhub.test.ts @@ -100,6 +100,12 @@ describe("clawhub helpers", () => { expect(satisfiesPluginApiRange("invalid", "^1.2.0")).toBe(false); }); + it("treats OpenClaw CalVer correction versions as stable plugin API hosts", () => { + expect(satisfiesPluginApiRange("2026.5.3-1", ">=2026.5.3")).toBe(true); + expect(satisfiesPluginApiRange("2026.5.3-2", ">=2026.5.3")).toBe(true); + expect(satisfiesPluginApiRange("2026.5.3-beta.1", ">=2026.5.3")).toBe(false); + }); + it("accepts legacy bare major.minor plugin api ranges as lower bounds", () => { expect(satisfiesPluginApiRange("2026.5.2", "2026.4")).toBe(true); expect(satisfiesPluginApiRange("2026.4.0", "2026.4")).toBe(true); diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index 1d2b0472348..d8f4a4df59c 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -542,6 +542,13 @@ function satisfiesSemverRange(version: string, range: string): boolean { return tokens.every((token) => satisfiesComparator(version, token)); } +const OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN = /^[vV]?(\d{4}\.\d{1,2}\.\d{1,2})-\d+$/; + +function normalizeCalVerCorrectionForPluginApi(pluginApiVersion: string): string { + const match = OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN.exec(pluginApiVersion.trim()); + return match?.[1] ?? pluginApiVersion; +} + function buildUrl(params: Pick): URL { const url = new URL(params.path, `${normalizeBaseUrl(params.baseUrl)}/`); for (const [key, value] of Object.entries(params.search ?? {})) { @@ -1046,7 +1053,10 @@ export function satisfiesPluginApiRange( if (!pluginApiRange) { return true; } - return satisfiesSemverRange(pluginApiVersion, pluginApiRange); + return satisfiesSemverRange( + normalizeCalVerCorrectionForPluginApi(pluginApiVersion), + pluginApiRange, + ); } export function satisfiesGatewayMinimum( diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 99cc2a922a9..63d0ebb3eb2 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -852,6 +852,36 @@ describe("installPluginFromClawHub", () => { expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); + it("installs when a CalVer correction runtime satisfies the base plugin API range", async () => { + resolveCompatibilityHostVersionMock.mockReturnValueOnce("2026.5.3-1"); + fetchClawHubPackageVersionMock.mockResolvedValueOnce({ + version: { + version: "2026.5.3", + createdAt: 0, + changelog: "", + sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af", + compatibility: { + pluginApiRange: ">=2026.5.3", + minGatewayVersion: "2026.3.0", + }, + }, + }); + + const result = await installPluginFromClawHub({ + spec: "clawhub:demo", + baseUrl: "https://clawhub.ai", + }); + + expectSuccessfulClawHubInstall(result); + expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledTimes(1); + expect(installPluginFromArchiveMock).toHaveBeenCalledWith( + expect.objectContaining({ + archivePath: "/tmp/clawhub-demo/archive.zip", + }), + ); + expect(archiveCleanupMock).toHaveBeenCalledTimes(1); + }); + it("does not let a wildcard plugin API range hide an invalid runtime version", async () => { resolveCompatibilityHostVersionMock.mockReturnValueOnce("invalid"); fetchClawHubPackageVersionMock.mockResolvedValueOnce({