From a964dcbddb771fb88ac33a10487c5fc8c4c8f622 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 13:18:43 +0100 Subject: [PATCH] fix: honor source plugin activation at startup --- src/config/plugin-auto-enable.core.test.ts | 28 +++++++++++ src/config/plugin-auto-enable.shared.ts | 20 ++++++-- src/gateway/server-plugin-bootstrap.ts | 23 ++++++++- src/gateway/server-startup-plugins.test.ts | 54 ++++++++++++++++++++++ src/gateway/server-startup-plugins.ts | 32 ++++++++++--- src/gateway/server.impl.ts | 3 ++ src/plugins/channel-plugin-ids.test.ts | 36 +++++++++++++++ 7 files changed, 185 insertions(+), 11 deletions(-) diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index f75d0c5b86e..8670fe30c53 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -188,6 +188,34 @@ describe("applyPluginAutoEnable core", () => { ).toBe(false); }); + it("does not load disabled setup plugin manifests when another setup signal exists", () => { + const readFileSync = vi.spyOn(fs, "readFileSync"); + + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["telegram"], + entries: { + browser: { enabled: false }, + }, + }, + tools: { + allow: ["browser"], + }, + }, + env, + }); + + expect(result.config.plugins?.allow).toEqual(["telegram"]); + expect(result.config.plugins?.entries?.browser?.enabled).toBe(false); + expect(result.changes).toEqual([]); + expect( + readFileSync.mock.calls.some( + ([filePath]) => typeof filePath === "string" && filePath.endsWith("openclaw.plugin.json"), + ), + ).toBe(false); + }); + it("still treats a non-disabled browser plugin entry as setup auto-enable input", () => { const result = applyPluginAutoEnable({ config: { diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 42a12e5d4ce..19b46bb74cf 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -351,7 +351,7 @@ function collectConfiguredPluginEntryIds(cfg: OpenClawConfig): string[] { } return Object.keys(entries) .map((pluginId) => pluginId.trim()) - .filter(Boolean); + .filter((pluginId) => pluginId && !isPluginEntryExplicitlyDisabled(cfg, pluginId)); } function hasOwnPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean { @@ -359,16 +359,22 @@ function hasOwnPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean { return !!entries && typeof entries === "object" && Object.hasOwn(entries, pluginId); } +function isPluginEntryExplicitlyDisabled(cfg: OpenClawConfig, pluginId: string): boolean { + return cfg.plugins?.entries?.[pluginId]?.enabled === false; +} + function hasNonDisabledPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean { if (!hasOwnPluginEntry(cfg, pluginId)) { return false; } - const entry = cfg.plugins?.entries?.[pluginId]; - return !isRecord(entry) || entry.enabled !== false; + return !isPluginEntryExplicitlyDisabled(cfg, pluginId); } function hasBrowserSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean { - if (isRecord(cfg.browser) && cfg.browser.enabled !== false) { + if (cfg.browser?.enabled === false || isPluginEntryExplicitlyDisabled(cfg, "browser")) { + return false; + } + if (isRecord(cfg.browser)) { return true; } if (hasNonDisabledPluginEntry(cfg, "browser")) { @@ -378,6 +384,9 @@ function hasBrowserSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean { } function hasAcpxSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean { + if (isPluginEntryExplicitlyDisabled(cfg, "acpx")) { + return false; + } if (!isRecord(cfg.acp)) { return false; } @@ -390,6 +399,9 @@ function hasAcpxSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean { } function hasXaiSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean { + if (isPluginEntryExplicitlyDisabled(cfg, "xai")) { + return false; + } const pluginConfig = cfg.plugins?.entries?.xai?.config; return ( (isRecord(pluginConfig) && diff --git a/src/gateway/server-plugin-bootstrap.ts b/src/gateway/server-plugin-bootstrap.ts index ffb354cf54a..01f648be127 100644 --- a/src/gateway/server-plugin-bootstrap.ts +++ b/src/gateway/server-plugin-bootstrap.ts @@ -47,6 +47,21 @@ function installGatewayPluginRuntimeEnvironment(cfg: OpenClawConfig) { setGatewayNodesRuntime(createGatewayNodesRuntime()); } +function applyActivationSectionsToRuntimeConfig(params: { + runtimeConfig: OpenClawConfig; + activationConfig: OpenClawConfig; +}): OpenClawConfig { + return { + ...params.runtimeConfig, + ...(params.activationConfig.channels !== undefined + ? { channels: params.activationConfig.channels } + : {}), + ...(params.activationConfig.plugins !== undefined + ? { plugins: params.activationConfig.plugins } + : {}), + }; +} + function logGatewayPluginDiagnostics(params: { diagnostics: PluginRegistry["diagnostics"]; log: Pick; @@ -78,7 +93,13 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) { ? { manifestRegistry: params.pluginLookUpTable.manifestRegistry } : {}), }); - const resolvedConfig = autoEnabled.config; + const resolvedConfig = + activationSourceConfig === params.cfg + ? autoEnabled.config + : applyActivationSectionsToRuntimeConfig({ + runtimeConfig: params.cfg, + activationConfig: autoEnabled.config, + }); installGatewayPluginRuntimeEnvironment(resolvedConfig); const loaded = loadGatewayPlugins({ cfg: resolvedConfig, diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index 3f4dda1e2f2..60adec919c9 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; const applyPluginAutoEnable = vi.hoisted(() => vi.fn((params: { config: unknown }) => ({ @@ -193,6 +194,59 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { ); }); + it("derives startup activation from source config instead of runtime plugin defaults", async () => { + const sourceConfig = { + plugins: { + allow: ["bench-plugin"], + }, + } as OpenClawConfig; + const runtimeConfig = { + plugins: { + allow: ["bench-plugin"], + entries: { + "memory-core": { + config: { + dreaming: { + enabled: false, + }, + }, + }, + }, + }, + } as OpenClawConfig; + const log = createLog(); + const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js"); + + await prepareGatewayPluginBootstrap({ + cfgAtStart: runtimeConfig, + activationSourceConfig: sourceConfig, + startupRuntimeConfig: runtimeConfig, + minimalTestGateway: false, + log, + }); + + expect(applyPluginAutoEnable).toHaveBeenCalledWith({ + config: sourceConfig, + env: process.env, + }); + expect(loadPluginLookUpTable).toHaveBeenCalledWith( + expect.objectContaining({ + activationSourceConfig: sourceConfig, + config: expect.objectContaining({ + plugins: sourceConfig.plugins, + }), + }), + ); + expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + activationSourceConfig: sourceConfig, + cfg: expect.objectContaining({ + plugins: sourceConfig.plugins, + }), + }), + ); + }); + it("falls back to per-plugin runtime-deps installs after failed pre-start scan", async () => { scanBundledPluginRuntimeDeps.mockImplementationOnce(() => { throw new Error("unsupported runtime dependency spec"); diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 32ffbfdb08b..9fd67521746 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -23,6 +23,21 @@ type GatewayPluginBootstrapLog = { debug: (message: string) => void; }; +function applyActivationSectionsToRuntimeConfig(params: { + runtimeConfig: OpenClawConfig; + activationConfig: OpenClawConfig; +}): OpenClawConfig { + return { + ...params.runtimeConfig, + ...(params.activationConfig.channels !== undefined + ? { channels: params.activationConfig.channels } + : {}), + ...(params.activationConfig.plugins !== undefined + ? { plugins: params.activationConfig.plugins } + : {}), + }; +} + async function prestageGatewayBundledRuntimeDeps(params: { cfg: OpenClawConfig; pluginIds: readonly string[]; @@ -92,10 +107,12 @@ async function prestageGatewayBundledRuntimeDeps(params: { export async function prepareGatewayPluginBootstrap(params: { cfgAtStart: OpenClawConfig; + activationSourceConfig?: OpenClawConfig; startupRuntimeConfig: OpenClawConfig; minimalTestGateway: boolean; log: GatewayPluginBootstrapLog; }) { + const activationSourceConfig = params.activationSourceConfig ?? params.cfgAtStart; const startupMaintenanceConfig = params.cfgAtStart.channels === undefined && params.startupRuntimeConfig.channels !== undefined ? { @@ -130,10 +147,13 @@ export async function prepareGatewayPluginBootstrap(params: { const gatewayPluginConfig = params.minimalTestGateway ? params.cfgAtStart - : applyPluginAutoEnable({ - config: params.cfgAtStart, - env: process.env, - }).config; + : applyActivationSectionsToRuntimeConfig({ + runtimeConfig: params.cfgAtStart, + activationConfig: applyPluginAutoEnable({ + config: activationSourceConfig, + env: process.env, + }).config, + }); const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfig); const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfig, defaultAgentId); const pluginLookUpTable = params.minimalTestGateway @@ -142,7 +162,7 @@ export async function prepareGatewayPluginBootstrap(params: { config: gatewayPluginConfig, workspaceDir: defaultWorkspaceDir, env: process.env, - activationSourceConfig: params.cfgAtStart, + activationSourceConfig, }); const deferredConfiguredChannelPluginIds = [ ...(pluginLookUpTable?.startup.configuredDeferredChannelPluginIds ?? []), @@ -162,7 +182,7 @@ export async function prepareGatewayPluginBootstrap(params: { }); ({ pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayStartupPlugins({ cfg: gatewayPluginConfig, - activationSourceConfig: params.cfgAtStart, + activationSourceConfig, workspaceDir: defaultWorkspaceDir, log: params.log, coreGatewayMethodNames: baseMethods, diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 0078ade5790..cf25183dc79 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -336,6 +336,7 @@ export async function startGatewayServer( let cfgAtStart: OpenClawConfig; let startupInternalWriteHash: string | null = null; let startupLastGoodSnapshot = configSnapshot; + const startupActivationSourceConfig = configSnapshot.sourceConfig; const startupRuntimeConfig = applyConfigOverrides(configSnapshot.config); const authBootstrap = await startupTrace.measure("config.auth", () => prepareGatewayStartupConfig({ @@ -408,6 +409,7 @@ export async function startGatewayServer( const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () => prepareGatewayPluginBootstrap({ cfgAtStart, + activationSourceConfig: startupActivationSourceConfig, startupRuntimeConfig, minimalTestGateway, log, @@ -856,6 +858,7 @@ export async function startGatewayServer( const { reloadDeferredGatewayPlugins } = await import("./server-plugin-bootstrap.js"); ({ pluginRegistry, gatewayMethods: baseGatewayMethods } = reloadDeferredGatewayPlugins({ cfg: gatewayPluginConfigAtStart, + activationSourceConfig: startupActivationSourceConfig, workspaceDir: defaultWorkspaceDir, log, coreGatewayMethodNames: baseMethods, diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 28c86d70466..c536550519d 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -488,6 +488,42 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); + it("does not let runtime-default plugin entries bypass the authored startup allowlist", () => { + const activationSourceConfig = { + channels: {}, + plugins: { + allow: ["bench-plugin"], + entries: { + browser: { + enabled: false, + }, + }, + }, + } as OpenClawConfig; + const runtimeConfig = { + ...activationSourceConfig, + plugins: { + ...activationSourceConfig.plugins, + entries: { + ...activationSourceConfig.plugins?.entries, + "memory-core": { + config: { + dreaming: { + enabled: false, + }, + }, + }, + }, + }, + } as OpenClawConfig; + + expectStartupPluginIdsCase({ + config: runtimeConfig, + activationSourceConfig, + expected: [], + }); + }); + it("starts bundled sidecars selected by root config activation paths", () => { const rawConfig = { browser: {