diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d33192179..4370a0c5358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/ClawHub: resolve plugin API compatibility against the active runtime version at install time, and add regression coverage for current `>=2026.3.22` ClawHub package checks so installs no longer fail behind the stale `1.2.0` constant. (#53157) Thanks @futhgar. - CLI/channel auth: auto-select the single login-capable configured channel for `channels login`/`logout` instead of relying on the outbound message-channel resolver, so env-only or non-auth channels no longer cause false ambiguity errors. (#53254) Thanks @BunsDev. - Control UI/auth: preserve operator scopes through the device-auth bypass path, ignore cached under-scoped operator tokens, and show a clear `operator.read` fallback message when a connection really lacks read scope, so operator sessions stop failing or blanking on read-backed pages. (#53110) Thanks @BunsDev. - Plugins/uninstall: accept installed `clawhub:` specs and versionless ClawHub package names as uninstall targets, so `openclaw plugins uninstall clawhub:` works again even when the recorded install was pinned to a version. diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 78ca25349bc..6c94567af17 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -61,19 +61,19 @@ describe("installPluginFromClawHub", () => { createdAt: 0, updatedAt: 0, compatibility: { - pluginApiRange: "^1.2.0", + pluginApiRange: ">=2026.3.22", minGatewayVersion: "2026.3.0", }, }, }); - resolveLatestVersionFromPackageMock.mockReturnValue("1.2.3"); + resolveLatestVersionFromPackageMock.mockReturnValue("2026.3.22"); fetchClawHubPackageVersionMock.mockResolvedValue({ version: { - version: "1.2.3", + version: "2026.3.22", createdAt: 0, changelog: "", compatibility: { - pluginApiRange: "^1.2.0", + pluginApiRange: ">=2026.3.22", minGatewayVersion: "2026.3.0", }, }, @@ -89,7 +89,7 @@ describe("installPluginFromClawHub", () => { ok: true, pluginId: "demo", targetDir: "/tmp/openclaw/plugins/demo", - version: "1.2.3", + version: "2026.3.22", }); }); @@ -116,7 +116,7 @@ describe("installPluginFromClawHub", () => { expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( expect.objectContaining({ name: "demo", - version: "1.2.3", + version: "2026.3.22", }), ); expect(installPluginFromArchiveMock).toHaveBeenCalledWith( @@ -127,7 +127,7 @@ describe("installPluginFromClawHub", () => { expect(result).toMatchObject({ ok: true, pluginId: "demo", - version: "1.2.3", + version: "2026.3.22", clawhub: { source: "clawhub", clawhubPackage: "demo", @@ -136,11 +136,27 @@ describe("installPluginFromClawHub", () => { integrity: "sha256-demo", }, }); - expect(info).toHaveBeenCalledWith("ClawHub code-plugin demo@1.2.3 channel=official"); - expect(info).toHaveBeenCalledWith("Compatibility: pluginApi=^1.2.0 minGateway=2026.3.0"); + expect(satisfiesPluginApiRangeMock).toHaveBeenCalledWith("2026.3.22", ">=2026.3.22"); + expect(satisfiesGatewayMinimumMock).toHaveBeenCalledWith("2026.3.22", "2026.3.0"); + expect(info).toHaveBeenCalledWith("ClawHub code-plugin demo@2026.3.22 channel=official"); + expect(info).toHaveBeenCalledWith("Compatibility: pluginApi=>=2026.3.22 minGateway=2026.3.0"); expect(warn).not.toHaveBeenCalled(); }); + it("rejects packages whose plugin API range exceeds the runtime version", async () => { + resolveRuntimeServiceVersionMock.mockReturnValueOnce("2026.3.21"); + satisfiesPluginApiRangeMock.mockReturnValueOnce(false); + + await expect(installPluginFromClawHub({ spec: "clawhub:demo" })).resolves.toMatchObject({ + ok: false, + code: CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API, + error: + 'Plugin "demo" requires plugin API >=2026.3.22, but this OpenClaw runtime exposes 2026.3.21.', + }); + + expect(satisfiesPluginApiRangeMock).toHaveBeenCalledWith("2026.3.21", ">=2026.3.22"); + }); + it("rejects skill families and redirects to skills install", async () => { fetchClawHubPackageDetailMock.mockResolvedValueOnce({ package: { diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index ba928e59bf3..38b31e141b1 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -15,8 +15,6 @@ import { } from "../infra/clawhub.js"; import { resolveRuntimeServiceVersion } from "../version.js"; import { installPluginFromArchive, type InstallPluginResult } from "./install.js"; - -export const OPENCLAW_PLUGIN_API_VERSION = resolveRuntimeServiceVersion(); export const CLAWHUB_INSTALL_ERROR_CODE = { INVALID_SPEC: "invalid_spec", PACKAGE_NOT_FOUND: "package_not_found", @@ -175,17 +173,17 @@ function validateClawHubPluginPackage(params: { } const compatibility = params.compatibility; + const runtimeVersion = resolveRuntimeServiceVersion(); if ( compatibility?.pluginApiRange && - !satisfiesPluginApiRange(OPENCLAW_PLUGIN_API_VERSION, compatibility.pluginApiRange) + !satisfiesPluginApiRange(runtimeVersion, compatibility.pluginApiRange) ) { return buildClawHubInstallFailure( - `Plugin "${pkg.name}" requires plugin API ${compatibility.pluginApiRange}, but this OpenClaw runtime exposes ${OPENCLAW_PLUGIN_API_VERSION}.`, + `Plugin "${pkg.name}" requires plugin API ${compatibility.pluginApiRange}, but this OpenClaw runtime exposes ${runtimeVersion}.`, CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API, ); } - const runtimeVersion = resolveRuntimeServiceVersion(); if ( compatibility?.minGatewayVersion && !satisfiesGatewayMinimum(runtimeVersion, compatibility.minGatewayVersion)