fix: gate configured channel startup plugins

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 20:10:50 -04:00
parent e1745cd621
commit dca770ae01
2 changed files with 159 additions and 4 deletions

View File

@@ -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"],
});
});

View File

@@ -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<string>;
}): boolean {
return params.plugin.channels.some((channelId) => params.configuredChannelIds.has(channelId));
}
function canStartConfiguredChannelPlugin(params: {
plugin: PluginManifestRecord;
config: OpenClawConfig;
pluginsConfig: ReturnType<typeof normalizePluginsConfig>;
activationSource: ReturnType<typeof createPluginActivationSource>;
}): 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({