fix(cli): prevent blocked plugin enable side effects (#114490)

This commit is contained in:
Peter Steinberger
2026-07-27 05:58:33 -04:00
committed by GitHub
parent d92a8f5812
commit d16c2dedd2
2 changed files with 49 additions and 10 deletions
+35
View File
@@ -8,6 +8,7 @@ import {
refreshPluginRegistry,
resetPluginsCliTestState,
runtimeErrors,
runtimeLogs,
runPluginsCommand,
writeConfigFile,
} from "./plugins-cli-test-helpers.js";
@@ -93,6 +94,40 @@ describe("plugins cli policy mutations", () => {
});
});
it.each([
{
policy: "globally disabled plugins",
plugins: { enabled: false },
reason: "plugins disabled",
},
{
policy: "a plugin denylist",
plugins: { deny: ["alpha"] },
reason: "blocked by denylist",
},
{
policy: "a restrictive plugin allowlist",
plugins: { allow: ["other-plugin"] },
reason: "blocked by allowlist",
},
])("does not mutate plugin state when $policy blocks enablement", async ({ plugins, reason }) => {
const sourceConfig = { plugins } as OpenClawConfig;
loadConfig.mockReturnValue(sourceConfig);
enablePluginInConfig.mockReturnValue({
config: sourceConfig,
enabled: false,
pluginId: "alpha",
reason,
});
mockPluginRegistry(["alpha"]);
await runPluginsCommand(["plugins", "enable", "alpha"]);
expect(writeConfigFile).not.toHaveBeenCalled();
expect(refreshPluginRegistry).not.toHaveBeenCalled();
expect(runtimeLogs).toContain(`Plugin "alpha" could not be enabled (${reason}).`);
});
it("refuses plugin enablement in Nix mode before config mutation", async () => {
const previous = process.env.OPENCLAW_NIX_MODE;
process.env.OPENCLAW_NIX_MODE = "1";
+14 -10
View File
@@ -203,9 +203,6 @@ async function runPluginsEnableCommandUnlocked(idInput: string): Promise<void> {
const { enableExplicitlySelectedPluginInConfig } = await import("../plugins/enable.js");
const { normalizePluginId } = await loadPluginsConfigState();
const { buildPluginRegistrySnapshotReport } = await loadPluginsStatus();
const { applySlotSelectionForPlugin } = await loadPluginSlotSelection();
const { logSlotWarnings } = await loadPluginsCommandHelpers();
const { refreshPluginRegistryAfterConfigMutation } = await loadPluginsRegistryRefresh();
const snapshot = await readConfigFileSnapshot();
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
const report = buildPluginRegistrySnapshotReport({ config: cfg });
@@ -216,6 +213,19 @@ async function runPluginsEnableCommandUnlocked(idInput: string): Promise<void> {
const enableResult = enableExplicitlySelectedPluginInConfig(cfg, id, {
updateChannelConfig: false,
});
// A blocked request must not displace the active slot or rewrite persisted state.
if (!enableResult.enabled) {
defaultRuntime.log(
theme.warn(
`Plugin "${id}" could not be enabled (${enableResult.reason ?? "unknown reason"}).`,
),
);
return;
}
const { applySlotSelectionForPlugin } = await loadPluginSlotSelection();
const { logSlotWarnings } = await loadPluginsCommandHelpers();
const { refreshPluginRegistryAfterConfigMutation } = await loadPluginsRegistryRefresh();
let next: OpenClawConfig = enableResult.config;
const slotResult = applySlotSelectionForPlugin(next, id);
next = slotResult.config;
@@ -233,13 +243,7 @@ async function runPluginsEnableCommandUnlocked(idInput: string): Promise<void> {
},
});
logSlotWarnings(slotResult.warnings);
if (enableResult.enabled) {
defaultRuntime.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
return;
}
defaultRuntime.log(
theme.warn(`Plugin "${id}" could not be enabled (${enableResult.reason ?? "unknown reason"}).`),
);
defaultRuntime.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
}
/** Disable a plugin in config and refresh the registry snapshot for the changed policy. */