fix: preserve clawhub install selectors

This commit is contained in:
Peter Steinberger
2026-04-27 10:25:16 +01:00
parent 1b81f75654
commit 3af34316f2
5 changed files with 90 additions and 13 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/plugins: preserve unversioned ClawHub install specs so `plugins update` can follow newer ClawHub releases instead of pinning to the initially resolved version. Fixes #63010; supersedes #58426. Thanks @kangsen1234 and @robinspt.
- 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/update: honor `OPENCLAW_NO_AUTO_UPDATE=1` as a gateway startup kill-switch for configured background package auto-updates, so operators can hold a deliberate downgrade during incident recovery without editing config first. Fixes #72715. Thanks @Xivi08.
- Agents/Claude CLI: force live-session launches to include `--output-format stream-json` whenever OpenClaw adds `--input-format stream-json`, so new Claude CLI sessions no longer fail immediately while reusable sessions keep working. Fixes #72206. Thanks @kwangwonkoh and @Xivi08.

View File

@@ -138,6 +138,7 @@ openclaw plugins install npm:@scope/plugin-name@1.0.1
```
OpenClaw downloads the package archive from ClawHub, checks the advertised plugin API / minimum gateway compatibility, then installs it through the normal archive path. Recorded installs keep their ClawHub source metadata for later updates.
Unversioned ClawHub installs keep an unversioned recorded spec so `openclaw plugins update` can follow newer ClawHub releases; explicit version or tag selectors such as `clawhub:pkg@1.2.3` and `clawhub:pkg@beta` remain pinned to that selector.
#### Marketplace shorthand

View File

@@ -479,8 +479,6 @@ vi.mock("../plugins/clawhub.js", () => ({
installPluginFromClawHub,
...args,
)) as (typeof import("../plugins/clawhub.js"))["installPluginFromClawHub"],
formatClawHubSpecifier: ({ name, version }: { name: string; version?: string }) =>
`clawhub:${name}${version ? `@${version}` : ""}`,
}));
vi.mock("../infra/clawhub.js", () => ({

View File

@@ -408,8 +408,9 @@ describe("plugins cli install", () => {
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
demo: expect.objectContaining({
source: "clawhub",
spec: "clawhub:demo@1.2.3",
spec: "clawhub:demo",
installPath: cliInstallPath("demo"),
version: "1.2.3",
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
@@ -491,6 +492,48 @@ describe("plugins cli install", () => {
);
});
it("keeps explicit ClawHub versions pinned in install records", async () => {
const cfg = {
plugins: {
entries: {},
},
} as OpenClawConfig;
const enabledCfg = createEnabledPluginConfig("demo");
loadConfig.mockReturnValue(cfg);
parseClawHubPluginSpec.mockReturnValue({ name: "demo", version: "1.2.3" });
installPluginFromClawHub.mockResolvedValue(
createClawHubInstallResult({
pluginId: "demo",
packageName: "demo",
version: "1.2.3",
channel: "official",
}),
);
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "clawhub:demo@1.2.3"]);
expect(installPluginFromClawHub).toHaveBeenCalledWith(
expect.objectContaining({
spec: "clawhub:demo@1.2.3",
}),
);
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
demo: expect.objectContaining({
source: "clawhub",
spec: "clawhub:demo@1.2.3",
installPath: cliInstallPath("demo"),
version: "1.2.3",
clawhubPackage: "demo",
}),
});
});
it("prefers ClawHub before npm for bare plugin specs", async () => {
const cfg = {
plugins: {
@@ -524,14 +567,54 @@ describe("plugins cli install", () => {
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
demo: expect.objectContaining({
source: "clawhub",
spec: "clawhub:demo@1.2.3",
spec: "clawhub:demo",
installPath: cliInstallPath("demo"),
version: "1.2.3",
clawhubPackage: "demo",
}),
});
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
});
it("keeps explicit bare ClawHub selectors in install records", async () => {
const cfg = {
plugins: {
entries: {},
},
} as OpenClawConfig;
const enabledCfg = createEnabledPluginConfig("demo");
loadConfig.mockReturnValue(cfg);
installPluginFromClawHub.mockResolvedValue(
createClawHubInstallResult({
pluginId: "demo",
packageName: "demo",
version: "1.2.3-beta.1",
channel: "community",
}),
);
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "demo@beta"]);
expect(installPluginFromClawHub).toHaveBeenCalledWith(
expect.objectContaining({
spec: "clawhub:demo@beta",
}),
);
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
demo: expect.objectContaining({
source: "clawhub",
spec: "clawhub:demo@beta",
version: "1.2.3-beta.1",
clawhubPackage: "demo",
}),
});
});
it("falls back to npm when ClawHub does not have the package", async () => {
primeNpmPluginFallback();

View File

@@ -7,7 +7,7 @@ import { resolveArchiveKind } from "../infra/archive.js";
import { parseClawHubPluginSpec } from "../infra/clawhub.js";
import { formatErrorMessage } from "../infra/errors.js";
import { type BundledPluginSource, findBundledPluginSource } from "../plugins/bundled-sources.js";
import { formatClawHubSpecifier, installPluginFromClawHub } from "../plugins/clawhub.js";
import { installPluginFromClawHub } from "../plugins/clawhub.js";
import type { InstallSafetyOverrides } from "../plugins/install-security-scan.js";
import {
PLUGIN_INSTALL_ERROR_CODE,
@@ -672,10 +672,7 @@ export async function runPluginInstallCommand(params: {
pluginId: result.pluginId,
install: {
source: "clawhub",
spec: formatClawHubSpecifier({
name: result.clawhub.clawhubPackage,
version: result.clawhub.version,
}),
spec: raw,
installPath: result.targetDir,
version: result.version,
integrity: result.clawhub.integrity,
@@ -704,10 +701,7 @@ export async function runPluginInstallCommand(params: {
pluginId: clawhubResult.pluginId,
install: {
source: "clawhub",
spec: formatClawHubSpecifier({
name: clawhubResult.clawhub.clawhubPackage,
version: clawhubResult.clawhub.version,
}),
spec: preferredClawHubSpec,
installPath: clawhubResult.targetDir,
version: clawhubResult.version,
integrity: clawhubResult.clawhub.integrity,