From d16c2dedd2b1ab508b4c2a2fa315ada27aa5d8e1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Jul 2026 05:58:33 -0400 Subject: [PATCH] fix(cli): prevent blocked plugin enable side effects (#114490) --- src/cli/plugins-cli.policy.test.ts | 35 ++++++++++++++++++++++++++++++ src/cli/plugins-cli.runtime.ts | 24 +++++++++++--------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/cli/plugins-cli.policy.test.ts b/src/cli/plugins-cli.policy.test.ts index ca2a8300272d..7d9a2b38c2c8 100644 --- a/src/cli/plugins-cli.policy.test.ts +++ b/src/cli/plugins-cli.policy.test.ts @@ -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"; diff --git a/src/cli/plugins-cli.runtime.ts b/src/cli/plugins-cli.runtime.ts index 08561d5fdad1..5c9360d42c19 100644 --- a/src/cli/plugins-cli.runtime.ts +++ b/src/cli/plugins-cli.runtime.ts @@ -203,9 +203,6 @@ async function runPluginsEnableCommandUnlocked(idInput: string): Promise { 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 { 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 { }, }); 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. */