From dca770ae0199f714eef2082b1654eb8baa2acbda Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 21 Apr 2026 20:10:50 -0400 Subject: [PATCH] fix: gate configured channel startup plugins --- src/plugins/channel-plugin-ids.test.ts | 88 +++++++++++++++++++++++ src/plugins/gateway-startup-plugin-ids.ts | 75 +++++++++++++++++-- 2 files changed, 159 insertions(+), 4 deletions(-) diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 69b2ca18a45..3c4c71a5b02 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -37,6 +37,7 @@ import { listConfiguredChannelIdsForReadOnlyScope, listExplicitConfiguredChannelIdsForConfig, resolveConfiguredChannelPresencePolicy, + resolveConfiguredDeferredChannelPluginIds, resolveConfiguredChannelPluginIds, resolveGatewayStartupPluginIds, } from "./channel-plugin-ids.js"; @@ -181,6 +182,25 @@ function createManifestRegistryFixture() { }; } +function createManifestRegistryFixtureWithWorkspaceDemoChannel() { + const fixture = createManifestRegistryFixture(); + return { + ...fixture, + plugins: [ + ...fixture.plugins, + { + id: "workspace-demo-channel-plugin", + channels: ["demo-channel"], + startupDeferConfiguredChannelFullLoadUntilAfterListen: true, + origin: "workspace", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, + ], + }; +} + function expectStartupPluginIds(params: { config: OpenClawConfig; activationSourceConfig?: OpenClawConfig; @@ -418,6 +438,74 @@ describe("resolveGatewayStartupPluginIds", () => { expectStartupPluginIdsCase({ config: effectiveConfig, activationSourceConfig: rawConfig, + expected: ["browser"], + }); + }); + + it("does not let weak channel presence start untrusted workspace channel owners", () => { + loadPluginManifestRegistry + .mockReset() + .mockReturnValue(createManifestRegistryFixtureWithWorkspaceDemoChannel()); + listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]); + listPotentialConfiguredChannelPresenceSignals.mockReturnValue([ + { channelId: "demo-channel", source: "env" }, + ]); + + const config = {} as OpenClawConfig; + + expectStartupPluginIdsCase({ + config, + env: { + DEMO_CHANNEL_ANYTHING: "1", + } as NodeJS.ProcessEnv, + expected: ["demo-channel", "browser"], + }); + expect( + resolveConfiguredDeferredChannelPluginIds({ + config, + workspaceDir: "/tmp", + env: { + DEMO_CHANNEL_ANYTHING: "1", + } as NodeJS.ProcessEnv, + }), + ).toEqual([]); + }); + + it("keeps explicitly trusted deferred channel owners eligible at startup", () => { + loadPluginManifestRegistry + .mockReset() + .mockReturnValue(createManifestRegistryFixtureWithWorkspaceDemoChannel()); + expect( + resolveConfiguredDeferredChannelPluginIds({ + config: { + channels: { + "demo-channel": { + token: "configured", + }, + }, + plugins: { + allow: ["workspace-demo-channel-plugin"], + }, + } as OpenClawConfig, + workspaceDir: "/tmp", + env: {}, + }), + ).toEqual(["workspace-demo-channel-plugin"]); + }); + + it("preserves explicit bundled channel config under restrictive allowlists", () => { + expectStartupPluginIdsCase({ + config: { + channels: { + "demo-channel": { + token: "configured", + }, + }, + plugins: { + allow: ["browser"], + }, + } as OpenClawConfig, + env: {}, expected: ["demo-channel", "browser"], }); }); diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 59f225a7738..944197818b1 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -7,6 +7,7 @@ import { resolveMemoryDreamingPluginId, } from "../memory-host-sdk/dreaming.js"; import { resolveManifestActivationPluginIds } from "./activation-planner.js"; +import { hasExplicitChannelConfig } from "./channel-presence-policy.js"; import { createPluginActivationSource, normalizePluginId, @@ -76,6 +77,57 @@ function shouldConsiderForGatewayStartup(params: { return params.explicitMemorySlotStartupPluginId === params.plugin.id; } +function hasConfiguredStartupChannel(params: { + plugin: PluginManifestRecord; + configuredChannelIds: ReadonlySet; +}): boolean { + return params.plugin.channels.some((channelId) => params.configuredChannelIds.has(channelId)); +} + +function canStartConfiguredChannelPlugin(params: { + plugin: PluginManifestRecord; + config: OpenClawConfig; + pluginsConfig: ReturnType; + activationSource: ReturnType; +}): boolean { + if (!params.pluginsConfig.enabled) { + return false; + } + if (params.pluginsConfig.deny.includes(params.plugin.id)) { + return false; + } + if (params.pluginsConfig.entries[params.plugin.id]?.enabled === false) { + return false; + } + const explicitBundledChannelConfig = + params.plugin.origin === "bundled" && + params.plugin.channels.some((channelId) => + hasExplicitChannelConfig({ + config: params.activationSource.rootConfig ?? params.config, + channelId, + }), + ); + if ( + params.pluginsConfig.allow.length > 0 && + !params.pluginsConfig.allow.includes(params.plugin.id) && + !explicitBundledChannelConfig + ) { + return false; + } + if (params.plugin.origin === "bundled") { + return true; + } + const activationState = resolveEffectivePluginActivationState({ + id: params.plugin.id, + origin: params.plugin.origin, + config: params.pluginsConfig, + rootConfig: params.config, + enabledByDefault: params.plugin.enabledByDefault, + activationSource: params.activationSource, + }); + return activationState.enabled && activationState.explicitlyEnabled; +} + export function resolveChannelPluginIds(params: { config: OpenClawConfig; workspaceDir?: string; @@ -101,6 +153,10 @@ export function resolveConfiguredDeferredChannelPluginIds(params: { if (configuredChannelIds.size === 0) { return []; } + const pluginsConfig = normalizePluginsConfig(params.config.plugins); + const activationSource = createPluginActivationSource({ + config: params.config, + }); return loadPluginManifestRegistry({ config: params.config, workspaceDir: params.workspaceDir, @@ -108,8 +164,14 @@ export function resolveConfiguredDeferredChannelPluginIds(params: { }) .plugins.filter( (plugin) => - plugin.channels.some((channelId) => configuredChannelIds.has(channelId)) && - plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true, + hasConfiguredStartupChannel({ plugin, configuredChannelIds }) && + plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true && + canStartConfiguredChannelPlugin({ + plugin, + config: params.config, + pluginsConfig, + activationSource, + }), ) .map((plugin) => plugin.id); } @@ -157,8 +219,13 @@ export function resolveGatewayStartupPluginIds(params: { env: params.env, }) .plugins.filter((plugin) => { - if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) { - return true; + if (hasConfiguredStartupChannel({ plugin, configuredChannelIds })) { + return canStartConfiguredChannelPlugin({ + plugin, + config: params.config, + pluginsConfig, + activationSource, + }); } if (requiredAgentHarnessPluginIds.has(plugin.id)) { const activationState = resolveEffectivePluginActivationState({