Skip to content

Commit 6a3f5d0

Browse files
ai-hpchxy91819
andauthored
fix(cli): reject missing plugin ids before config writes (#73554)
Merged via squash. Prepared head SHA: f0d3e61 Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com> Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com> Reviewed-by: @hxy91819
1 parent c81c017 commit 6a3f5d0

3 files changed

Lines changed: 108 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515
### Fixes
1616

1717
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
18+
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
1819
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
1920
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
2021
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.

src/cli/plugins-cli.policy.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import { beforeEach, describe, expect, it } from "vitest";
22
import type { OpenClawConfig } from "../config/config.js";
33
import {
4+
buildPluginRegistrySnapshotReport,
45
enablePluginInConfig,
56
loadConfig,
67
refreshPluginRegistry,
78
resetPluginsCliTestState,
9+
runtimeErrors,
810
runPluginsCommand,
911
writeConfigFile,
1012
} from "./plugins-cli-test-helpers.js";
1113

1214
describe("plugins cli policy mutations", () => {
15+
const compatibilityPluginIds = [
16+
{ alias: "openai-codex", pluginId: "openai" },
17+
{ alias: "google-gemini-cli", pluginId: "google" },
18+
{ alias: "minimax-portal-auth", pluginId: "minimax" },
19+
] as const;
20+
1321
beforeEach(() => {
1422
resetPluginsCliTestState();
1523
});
1624

25+
function mockPluginRegistry(ids: string[]) {
26+
buildPluginRegistrySnapshotReport.mockReturnValue({
27+
plugins: ids.map((id) => ({ id })),
28+
diagnostics: [],
29+
registrySource: "derived",
30+
registryDiagnostics: [],
31+
});
32+
}
33+
1734
it("refreshes the persisted plugin registry after enabling a plugin", async () => {
1835
const enabledConfig = {
1936
plugins: {
@@ -28,6 +45,7 @@ describe("plugins cli policy mutations", () => {
2845
enabled: true,
2946
pluginId: "alpha",
3047
});
48+
mockPluginRegistry(["alpha"]);
3149

3250
await runPluginsCommand(["plugins", "enable", "alpha"]);
3351

@@ -48,6 +66,7 @@ describe("plugins cli policy mutations", () => {
4866
},
4967
},
5068
} as OpenClawConfig);
69+
mockPluginRegistry(["alpha"]);
5170

5271
await runPluginsCommand(["plugins", "disable", "alpha"]);
5372

@@ -60,4 +79,67 @@ describe("plugins cli policy mutations", () => {
6079
reason: "policy-changed",
6180
});
6281
});
82+
83+
it.each(compatibilityPluginIds)(
84+
"enables compatibility id $alias through canonical plugin $pluginId",
85+
async ({ alias, pluginId }) => {
86+
const sourceConfig = {} as OpenClawConfig;
87+
const enabledConfig = {
88+
plugins: {
89+
entries: {
90+
[pluginId]: { enabled: true },
91+
},
92+
},
93+
} as OpenClawConfig;
94+
loadConfig.mockReturnValue(sourceConfig);
95+
enablePluginInConfig.mockReturnValue({
96+
config: enabledConfig,
97+
enabled: true,
98+
});
99+
mockPluginRegistry([pluginId]);
100+
101+
await runPluginsCommand(["plugins", "enable", alias]);
102+
103+
expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, pluginId);
104+
expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig);
105+
},
106+
);
107+
108+
it.each(compatibilityPluginIds)(
109+
"disables compatibility id $alias through canonical plugin $pluginId",
110+
async ({ alias, pluginId }) => {
111+
loadConfig.mockReturnValue({
112+
plugins: {
113+
entries: {
114+
[pluginId]: { enabled: true },
115+
},
116+
},
117+
} as OpenClawConfig);
118+
mockPluginRegistry([pluginId]);
119+
120+
await runPluginsCommand(["plugins", "disable", alias]);
121+
122+
const nextConfig = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig;
123+
expect(nextConfig.plugins?.entries?.[pluginId]?.enabled).toBe(false);
124+
expect(nextConfig.plugins?.entries?.[alias]).toBeUndefined();
125+
},
126+
);
127+
128+
it.each(["enable", "disable"] as const)(
129+
"rejects %s for a plugin that is not discovered",
130+
async (command) => {
131+
mockPluginRegistry(["alpha"]);
132+
133+
await expect(runPluginsCommand(["plugins", command, "missing-plugin"])).rejects.toThrow(
134+
"__exit__:1",
135+
);
136+
137+
expect(runtimeErrors).toContain(
138+
"Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins.",
139+
);
140+
expect(enablePluginInConfig).not.toHaveBeenCalled();
141+
expect(writeConfigFile).not.toHaveBeenCalled();
142+
expect(refreshPluginRegistry).not.toHaveBeenCalled();
143+
},
144+
);
63145
});

src/cli/plugins-cli.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ function formatRegistryState(state: "missing" | "fresh" | "stale"): string {
5151
return theme.warn(state);
5252
}
5353

54+
function reportMissingPlugin(id: string) {
55+
defaultRuntime.error(
56+
`Plugin not found: ${id}. Run \`openclaw plugins list\` to see installed plugins.`,
57+
);
58+
return defaultRuntime.exit(1);
59+
}
60+
61+
function matchesPluginId(plugin: { id: string }, id: string) {
62+
return plugin.id === id;
63+
}
64+
5465
export function registerPluginsCli(program: Command) {
5566
const plugins = program
5667
.command("plugins")
@@ -102,12 +113,19 @@ export function registerPluginsCli(program: Command) {
102113
.argument("<id>", "Plugin id")
103114
.action(async (id: string) => {
104115
const { enablePluginInConfig } = await import("../plugins/enable.js");
116+
const { normalizePluginId } = await import("../plugins/config-state.js");
117+
const { buildPluginRegistrySnapshotReport } = await import("../plugins/status.js");
105118
const { applySlotSelectionForPlugin, logSlotWarnings } =
106119
await import("./plugins-command-helpers.js");
107120
const { refreshPluginRegistryAfterConfigMutation } =
108121
await import("./plugins-registry-refresh.js");
109122
const snapshot = await readConfigFileSnapshot();
110123
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
124+
const report = buildPluginRegistrySnapshotReport({ config: cfg });
125+
id = normalizePluginId(id);
126+
if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) {
127+
return reportMissingPlugin(id);
128+
}
111129
const enableResult = enablePluginInConfig(cfg, id);
112130
let next: OpenClawConfig = enableResult.config;
113131
const slotResult = applySlotSelectionForPlugin(next, id);
@@ -141,11 +159,18 @@ export function registerPluginsCli(program: Command) {
141159
.description("Disable a plugin in config")
142160
.argument("<id>", "Plugin id")
143161
.action(async (id: string) => {
162+
const { normalizePluginId } = await import("../plugins/config-state.js");
163+
const { buildPluginRegistrySnapshotReport } = await import("../plugins/status.js");
144164
const { setPluginEnabledInConfig } = await import("./plugins-config.js");
145165
const { refreshPluginRegistryAfterConfigMutation } =
146166
await import("./plugins-registry-refresh.js");
147167
const snapshot = await readConfigFileSnapshot();
148168
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
169+
const report = buildPluginRegistrySnapshotReport({ config: cfg });
170+
id = normalizePluginId(id);
171+
if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) {
172+
return reportMissingPlugin(id);
173+
}
149174
const next = setPluginEnabledInConfig(cfg, id, false);
150175
await replaceConfigFile({
151176
nextConfig: next,

0 commit comments

Comments
 (0)