fix: use runtime version for ClawHub plugin API checks (#53157) (thanks @futhgar)

This commit is contained in:
Peter Steinberger 2026-03-23 18:39:43 -07:00
parent 447e074bf4
commit 5b4fd6bf31
3 changed files with 29 additions and 14 deletions

View File

@ -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:<package>` works again even when the recorded install was pinned to a version.

View File

@ -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: {

View File

@ -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)