fix: accept clawhub plugin api wildcards

This commit is contained in:
Peter Steinberger
2026-04-27 09:47:26 +01:00
parent 6c8f0d04c3
commit 6fddf17632
4 changed files with 92 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/models: move local-provider pricing opt-outs, OpenRouter/LiteLLM aliases, and proxy passthrough pricing lookup into plugin manifest metadata so core no longer carries extension-specific pricing tables. Thanks @codex.
- CLI/plugins: accept ClawHub plugin API wildcard ranges such as `*` without rejecting compatible plugin installs, while still requiring a valid runtime API version. Fixes #56446; supersedes #56466. Thanks @darconada and @claygeo.
- Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work. Thanks @codex.
- Browser/plugins: auto-start the bundled browser plugin when root `browser` config is present, including restrictive plugin allowlists, and ignore stale persisted plugin registries whose package paths no longer exist. Thanks @codex.
- Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex.

View File

@@ -87,6 +87,26 @@ describe("clawhub helpers", () => {
expect(satisfiesPluginApiRange("invalid", "^1.2.0")).toBe(false);
});
it.each(["*", "x", "X", "=*", "=x", ">=*", ">=x", "<=*", "^*", "~*"] as const)(
"accepts plugin api wildcard range %s for valid runtime versions",
(range) => {
expect(satisfiesPluginApiRange("2026.3.24", range)).toBe(true);
expect(satisfiesPluginApiRange("1.0.0", range)).toBe(true);
},
);
it("keeps wildcard plugin api ranges intersected with concrete comparators", () => {
expect(satisfiesPluginApiRange("2026.3.24", "* >=2026.3.22")).toBe(true);
expect(satisfiesPluginApiRange("2026.3.21", "* >=2026.3.22")).toBe(false);
expect(satisfiesPluginApiRange("2026.3.24", "x <2026.3.24")).toBe(false);
});
it("rejects invalid runtime versions and impossible wildcard comparators", () => {
expect(satisfiesPluginApiRange("invalid", "*")).toBe(false);
expect(satisfiesPluginApiRange("2026.3.24", ">*")).toBe(false);
expect(satisfiesPluginApiRange("2026.3.24", "<*")).toBe(false);
});
it("checks min gateway versions with loose host labels", () => {
expect(satisfiesGatewayMinimum("2026.3.22", "2026.3.0")).toBe(true);
expect(satisfiesGatewayMinimum("OpenClaw 2026.3.22", "2026.3.0")).toBe(true);

View File

@@ -299,11 +299,24 @@ function upperBoundForCaret(version: string): string | null {
return `0.0.${parsed.patch + 1}`;
}
function matchWildcardComparator(token: string): "any" | "none" | null {
const match = /^(>=|<=|>|<|=|\^|~)?\s*([*xX])$/.exec(token);
if (!match) {
return null;
}
const operator = match[1];
return operator === ">" || operator === "<" ? "none" : "any";
}
function satisfiesComparator(version: string, token: string): boolean {
const trimmed = token.trim();
if (!trimmed) {
return true;
}
const wildcard = matchWildcardComparator(trimmed);
if (wildcard) {
return wildcard === "any" && parseComparableSemver(version) != null;
}
if (trimmed.startsWith("^")) {
const base = trimmed.slice(1).trim();
const upperBound = upperBoundForCaret(base);

View File

@@ -249,6 +249,64 @@ describe("installPluginFromClawHub", () => {
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
});
it("installs when ClawHub advertises a wildcard plugin API range", async () => {
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
version: {
version: "2026.3.22",
createdAt: 0,
changelog: "",
sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
compatibility: {
pluginApiRange: "*",
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({
version: {
version: "2026.3.22",
createdAt: 0,
changelog: "",
sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
compatibility: {
pluginApiRange: "*",
minGatewayVersion: "2026.3.0",
},
},
});
const result = await installPluginFromClawHub({
spec: "clawhub:demo",
});
expect(result).toMatchObject({
ok: false,
code: CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API,
error: 'Plugin "demo" requires plugin API *, but this OpenClaw runtime exposes invalid.',
});
expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled();
expect(installPluginFromArchiveMock).not.toHaveBeenCalled();
expect(archiveCleanupMock).not.toHaveBeenCalled();
});
it("passes dangerous force unsafe install through to archive installs", async () => {
await installPluginFromClawHub({
spec: "clawhub:demo",