From cd38eba316792a1f4c2e245e14f028cebf886d02 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 23:36:52 +0900 Subject: [PATCH] refactor: unify plugin activation source plumbing --- src/plugin-sdk/facade-runtime.ts | 17 +++------ src/plugins/channel-plugin-ids.ts | 18 ++++++--- src/plugins/config-state.test.ts | 24 +++++------- src/plugins/config-state.ts | 34 +++++++++++++---- src/plugins/loader.ts | 61 +++++++++++++------------------ 5 files changed, 78 insertions(+), 76 deletions(-) diff --git a/src/plugin-sdk/facade-runtime.ts b/src/plugin-sdk/facade-runtime.ts index d02c7e0badd..4a4e424866b 100644 --- a/src/plugin-sdk/facade-runtime.ts +++ b/src/plugin-sdk/facade-runtime.ts @@ -8,6 +8,7 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { resolveBundledPluginPublicSurfacePath } from "../plugins/bundled-plugin-metadata.js"; import { + createPluginActivationSource, normalizePluginsConfig, resolveEffectivePluginActivationState, } from "../plugins/config-state.js"; @@ -44,7 +45,7 @@ let cachedBoundaryResolvedConfig: rawConfig: OpenClawConfig; config: OpenClawConfig; normalizedPluginsConfig: ReturnType; - sourceNormalizedPluginsConfig: ReturnType; + activationSource: ReturnType; autoEnabledReasons: Record; } | undefined; @@ -157,7 +158,7 @@ function getFacadeBoundaryResolvedConfig() { rawConfig, config, normalizedPluginsConfig: normalizePluginsConfig(config?.plugins), - sourceNormalizedPluginsConfig: normalizePluginsConfig(rawConfig?.plugins), + activationSource: createPluginActivationSource({ config: rawConfig }), autoEnabledReasons: autoEnabled.autoEnabledReasons, }; cachedBoundaryRawConfig = rawConfig; @@ -202,21 +203,15 @@ function resolveBundledPluginPublicSurfaceAccess(params: { reason: `no bundled plugin manifest found for ${params.dirName}`, }; } - const { - rawConfig, - config, - normalizedPluginsConfig, - sourceNormalizedPluginsConfig, - autoEnabledReasons, - } = getFacadeBoundaryResolvedConfig(); + const { config, normalizedPluginsConfig, activationSource, autoEnabledReasons } = + getFacadeBoundaryResolvedConfig(); const activationState = resolveEffectivePluginActivationState({ id: manifestRecord.id, origin: manifestRecord.origin, config: normalizedPluginsConfig, rootConfig: config, enabledByDefault: manifestRecord.enabledByDefault, - sourceConfig: sourceNormalizedPluginsConfig, - sourceRootConfig: rawConfig, + activationSource, autoEnabledReason: autoEnabledReasons[manifestRecord.id]?.[0], }); if (activationState.enabled) { diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index 814906976e2..b8e50cc7cab 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -1,6 +1,10 @@ import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/config.js"; -import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js"; +import { + createPluginActivationSource, + normalizePluginsConfig, + resolveEffectivePluginActivationState, +} from "./config-state.js"; import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js"; import { hasKind } from "./slots.js"; @@ -83,9 +87,12 @@ export function resolveGatewayStartupPluginIds(params: { listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()), ); const pluginsConfig = normalizePluginsConfig(params.config.plugins); - const sourcePluginsConfig = normalizePluginsConfig( - (params.activationSourceConfig ?? params.config).plugins, - ); + // Startup must classify allowlist exceptions against the raw config snapshot, + // not the auto-enabled effective snapshot, or configured-only channels can be + // misclassified as explicit enablement. + const activationSource = createPluginActivationSource({ + config: params.activationSourceConfig ?? params.config, + }); return loadPluginManifestRegistry({ config: params.config, workspaceDir: params.workspaceDir, @@ -104,8 +111,7 @@ export function resolveGatewayStartupPluginIds(params: { config: pluginsConfig, rootConfig: params.config, enabledByDefault: plugin.enabledByDefault, - sourceConfig: sourcePluginsConfig, - sourceRootConfig: params.activationSourceConfig ?? params.config, + activationSource, }); if (!activationState.enabled) { return false; diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index 60218aa639e..2ff9ff9632b 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + createPluginActivationSource, normalizePluginsConfig, resolveEffectiveEnableState, resolveEnableState, @@ -206,7 +207,7 @@ describe("resolveEffectiveEnableState", () => { describe("resolveEffectivePluginActivationState", () => { it("distinguishes explicit enablement from auto activation", () => { const rawConfig: NonNullable< - Parameters[0]["sourceRootConfig"] + Parameters[0]["rootConfig"] > = { channels: { telegram: { @@ -231,8 +232,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(effectiveConfig.plugins), rootConfig: effectiveConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), autoEnabledReason: "telegram configured", }), ).toEqual({ @@ -262,8 +262,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(rawConfig.plugins), rootConfig: rawConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), }), ).toEqual({ enabled: false, @@ -309,8 +308,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(rawConfig.plugins), rootConfig: rawConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), }), ).toEqual({ enabled: false, @@ -339,8 +337,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(rawConfig.plugins), rootConfig: rawConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), }), ).toEqual({ enabled: true, @@ -369,8 +366,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(rawConfig.plugins), rootConfig: rawConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), }), ).toEqual({ enabled: false, @@ -394,8 +390,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(rawConfig.plugins), rootConfig: rawConfig, - sourceConfig: normalizePluginsConfig(rawConfig.plugins), - sourceRootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), autoEnabledReason: "telegram configured", }), ).toEqual({ @@ -427,8 +422,7 @@ describe("resolveEffectivePluginActivationState", () => { origin: "bundled", config: normalizePluginsConfig(effectiveConfig.plugins), rootConfig: effectiveConfig, - sourceConfig: normalizePluginsConfig(sourceConfig.plugins), - sourceRootConfig: sourceConfig, + activationSource: createPluginActivationSource({ config: sourceConfig }), }), ).toEqual({ enabled: true, diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index 0d0274817dc..c26f602574b 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -17,6 +17,11 @@ export type PluginActivationState = { reason?: string; }; +export type PluginActivationConfigSource = { + plugins: NormalizedPluginsConfig; + rootConfig?: OpenClawConfig; +}; + export type NormalizedPluginsConfig = { enabled: boolean; allow: string[]; @@ -162,6 +167,16 @@ export const normalizePluginsConfig = ( }; }; +export function createPluginActivationSource(params: { + config?: OpenClawConfig; + plugins?: NormalizedPluginsConfig; +}): PluginActivationConfigSource { + return { + plugins: params.plugins ?? normalizePluginsConfig(params.config?.plugins), + rootConfig: params.config, + }; +} + const hasExplicitMemorySlot = (plugins?: OpenClawConfig["plugins"]) => Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory")); @@ -275,15 +290,20 @@ export function resolvePluginActivationState(params: { config: NormalizedPluginsConfig; rootConfig?: OpenClawConfig; enabledByDefault?: boolean; - sourceConfig?: NormalizedPluginsConfig; - sourceRootConfig?: OpenClawConfig; + activationSource?: PluginActivationConfigSource; autoEnabledReason?: string; }): PluginActivationState { + const activationSource = + params.activationSource ?? + createPluginActivationSource({ + config: params.rootConfig, + plugins: params.config, + }); const explicitSelection = resolveExplicitPluginSelection({ id: params.id, origin: params.origin, - config: params.sourceConfig ?? params.config, - rootConfig: params.sourceRootConfig ?? params.rootConfig, + config: activationSource.plugins, + rootConfig: activationSource.rootConfig, }); const explicitlyConfiguredBundledChannel = params.origin === "bundled" && @@ -451,8 +471,7 @@ export function resolveEffectiveEnableState(params: { config: NormalizedPluginsConfig; rootConfig?: OpenClawConfig; enabledByDefault?: boolean; - sourceConfig?: NormalizedPluginsConfig; - sourceRootConfig?: OpenClawConfig; + activationSource?: PluginActivationConfigSource; }): { enabled: boolean; reason?: string } { const state = resolveEffectivePluginActivationState(params); return state.enabled ? { enabled: true } : { enabled: false, reason: state.reason }; @@ -464,8 +483,7 @@ export function resolveEffectivePluginActivationState(params: { config: NormalizedPluginsConfig; rootConfig?: OpenClawConfig; enabledByDefault?: boolean; - sourceConfig?: NormalizedPluginsConfig; - sourceRootConfig?: OpenClawConfig; + activationSource?: PluginActivationConfigSource; autoEnabledReason?: string; }): PluginActivationState { return resolvePluginActivationState(params); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index dc2a761652d..44219bff95e 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -15,10 +15,12 @@ import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; import { clearPluginCommands } from "./command-registry-state.js"; import { applyTestPluginDefaults, + createPluginActivationSource, normalizePluginsConfig, resolveEffectiveEnableState, resolveEffectivePluginActivationState, resolveMemorySlotDecision, + type PluginActivationConfigSource, type NormalizedPluginsConfig, type PluginActivationState, } from "./config-state.js"; @@ -315,12 +317,11 @@ function resolveRuntimeSubagentMode( } function buildActivationMetadataHash(params: { - sourcePlugins: NormalizedPluginsConfig; - sourceConfig?: OpenClawConfig; + activationSource: PluginActivationConfigSource; autoEnabledReasons: Readonly>; }): string { const enabledSourceChannels = Object.entries( - (params.sourceConfig?.channels as Record) ?? {}, + (params.activationSource.rootConfig?.channels as Record) ?? {}, ) .filter(([, value]) => { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -330,7 +331,7 @@ function buildActivationMetadataHash(params: { }) .map(([channelId]) => channelId) .toSorted((left, right) => left.localeCompare(right)); - const pluginEntryStates = Object.entries(params.sourcePlugins.entries) + const pluginEntryStates = Object.entries(params.activationSource.plugins.entries) .map(([pluginId, entry]) => [pluginId, entry?.enabled ?? null] as const) .toSorted(([left], [right]) => left.localeCompare(right)); const autoEnableReasonEntries = Object.entries(params.autoEnabledReasons) @@ -340,10 +341,10 @@ function buildActivationMetadataHash(params: { return createHash("sha256") .update( JSON.stringify({ - enabled: params.sourcePlugins.enabled, - allow: params.sourcePlugins.allow, - deny: params.sourcePlugins.deny, - memorySlot: params.sourcePlugins.slots.memory, + enabled: params.activationSource.plugins.enabled, + allow: params.activationSource.plugins.allow, + deny: params.activationSource.plugins.deny, + memorySlot: params.activationSource.plugins.slots.memory, entries: pluginEntryStates, enabledChannels: enabledSourceChannels, autoEnabledReasons: autoEnableReasonEntries, @@ -374,7 +375,9 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { const cfg = applyTestPluginDefaults(options.config ?? {}, env); const activationSourceConfig = options.activationSourceConfig ?? options.config ?? {}; const normalized = normalizePluginsConfig(cfg.plugins); - const activationSourceNormalized = normalizePluginsConfig(activationSourceConfig.plugins); + const activationSource = createPluginActivationSource({ + config: activationSourceConfig, + }); const onlyPluginIds = normalizeScopedPluginIds(options.onlyPluginIds); const includeSetupOnlyChannelPlugins = options.includeSetupOnlyChannelPlugins === true; const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true; @@ -383,8 +386,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { workspaceDir: options.workspaceDir, plugins: normalized, activationMetadataKey: buildActivationMetadataHash({ - sourcePlugins: activationSourceNormalized, - sourceConfig: activationSourceConfig, + activationSource, autoEnabledReasons: options.autoEnabledReasons ?? {}, }), installs: cfg.plugins?.installs, @@ -402,7 +404,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { cfg, normalized, activationSourceConfig, - activationSourceNormalized, + activationSource, autoEnabledReasons: options.autoEnabledReasons ?? {}, onlyPluginIds, includeSetupOnlyChannelPlugins, @@ -950,8 +952,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi env, cfg, normalized, - activationSourceConfig, - activationSourceNormalized, + activationSource, autoEnabledReasons, onlyPluginIds, includeSetupOnlyChannelPlugins, @@ -1148,8 +1149,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi config: normalized, rootConfig: cfg, enabledByDefault: manifestRecord.enabledByDefault, - sourceConfig: activationSourceNormalized, - sourceRootConfig: activationSourceConfig, + activationSource, autoEnabledReason: formatAutoEnabledActivationReason(autoEnabledReasons[pluginId]), }); const existingOrigin = seenIds.get(pluginId); @@ -1183,8 +1183,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi config: normalized, rootConfig: cfg, enabledByDefault: manifestRecord.enabledByDefault, - sourceConfig: activationSourceNormalized, - sourceRootConfig: activationSourceConfig, + activationSource, }); const entry = normalized.entries[pluginId]; const record = createPluginRecord({ @@ -1622,20 +1621,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi export async function loadOpenClawPluginCliRegistry( options: PluginLoadOptions = {}, ): Promise { - const { - env, - cfg, - normalized, - activationSourceConfig, - activationSourceNormalized, - autoEnabledReasons, - onlyPluginIds, - cacheKey, - } = resolvePluginLoadCacheContext({ - ...options, - activate: false, - cache: false, - }); + const { env, cfg, normalized, activationSource, autoEnabledReasons, onlyPluginIds, cacheKey } = + resolvePluginLoadCacheContext({ + ...options, + activate: false, + cache: false, + }); const logger = options.logger ?? defaultLogger(); const onlyPluginIdSet = onlyPluginIds ? new Set(onlyPluginIds) : null; const getJiti = createPluginJitiLoader(options); @@ -1716,8 +1707,7 @@ export async function loadOpenClawPluginCliRegistry( config: normalized, rootConfig: cfg, enabledByDefault: manifestRecord.enabledByDefault, - sourceConfig: activationSourceNormalized, - sourceRootConfig: activationSourceConfig, + activationSource, autoEnabledReason: formatAutoEnabledActivationReason(autoEnabledReasons[pluginId]), }); const existingOrigin = seenIds.get(pluginId); @@ -1751,8 +1741,7 @@ export async function loadOpenClawPluginCliRegistry( config: normalized, rootConfig: cfg, enabledByDefault: manifestRecord.enabledByDefault, - sourceConfig: activationSourceNormalized, - sourceRootConfig: activationSourceConfig, + activationSource, }); const entry = normalized.entries[pluginId]; const record = createPluginRecord({