fix(plugins): enforce minimum host versions for installable plugins (#52094)

* fix(plugins): enforce min host versions

* fix(plugins): tighten min host version validation

* chore(plugins): trim dead min host version code

* fix(plugins): handle malformed min host metadata

* fix(plugins): key manifest cache by host version
This commit is contained in:
Vincent Koc
2026-03-22 09:12:08 -07:00
committed by GitHub
parent 6b7206ed35
commit 3ce5a8366a
29 changed files with 653 additions and 21 deletions

View File

@@ -493,6 +493,7 @@ beforeAll(async () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
});
describe("installPluginFromArchive", () => {
@@ -775,6 +776,95 @@ describe("installPluginFromDir", () => {
expect(manifest.devDependencies?.vitest).toBe("^3.0.0");
});
it("rejects plugins whose minHostVersion is newer than the current host", async () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.3.13");
const { pluginDir, extensionsDir } = setupInstallPluginFromDirFixture();
const packageJsonPath = path.join(pluginDir, "package.json");
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
openclaw?: { install?: Record<string, unknown> };
};
manifest.openclaw = {
...manifest.openclaw,
install: {
...manifest.openclaw?.install,
minHostVersion: ">=2026.3.14",
},
};
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest), "utf-8");
const result = await installPluginFromDir({
dirPath: pluginDir,
extensionsDir,
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.INCOMPATIBLE_HOST_VERSION);
expect(result.error).toContain("requires OpenClaw >=2026.3.14, but this host is 2026.3.13");
expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled();
});
it("rejects plugins with invalid minHostVersion metadata", async () => {
const { pluginDir, extensionsDir } = setupInstallPluginFromDirFixture();
const packageJsonPath = path.join(pluginDir, "package.json");
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
openclaw?: { install?: Record<string, unknown> };
};
manifest.openclaw = {
...manifest.openclaw,
install: {
...manifest.openclaw?.install,
minHostVersion: "2026.3.14",
},
};
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest), "utf-8");
const result = await installPluginFromDir({
dirPath: pluginDir,
extensionsDir,
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.INVALID_MIN_HOST_VERSION);
expect(result.error).toContain("invalid package.json openclaw.install.minHostVersion");
expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled();
});
it("reports unknown host versions distinctly for minHostVersion-gated plugins", async () => {
vi.stubEnv("OPENCLAW_VERSION", "unknown");
const { pluginDir, extensionsDir } = setupInstallPluginFromDirFixture();
const packageJsonPath = path.join(pluginDir, "package.json");
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
openclaw?: { install?: Record<string, unknown> };
};
manifest.openclaw = {
...manifest.openclaw,
install: {
...manifest.openclaw?.install,
minHostVersion: ">=2026.3.14",
},
};
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest), "utf-8");
const result = await installPluginFromDir({
dirPath: pluginDir,
extensionsDir,
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.UNKNOWN_HOST_VERSION);
expect(result.error).toContain("host version could not be determined");
expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled();
});
it("uses openclaw.plugin.json id as install key when it differs from package name", async () => {
const { pluginDir, extensionsDir } = setupManifestInstallFixture({
manifestId: "memory-cognee",