refactor: unify plugin activation source plumbing

This commit is contained in:
Peter Steinberger
2026-04-03 23:36:52 +09:00
parent 975d2ddce2
commit cd38eba316
5 changed files with 78 additions and 76 deletions

View File

@@ -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<typeof normalizePluginsConfig>;
sourceNormalizedPluginsConfig: ReturnType<typeof normalizePluginsConfig>;
activationSource: ReturnType<typeof createPluginActivationSource>;
autoEnabledReasons: Record<string, string[]>;
}
| 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) {

View File

@@ -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;

View File

@@ -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<typeof resolveEffectivePluginActivationState>[0]["sourceRootConfig"]
Parameters<typeof resolveEffectivePluginActivationState>[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,

View File

@@ -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);

View File

@@ -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<Record<string, string[]>>;
}): string {
const enabledSourceChannels = Object.entries(
(params.sourceConfig?.channels as Record<string, unknown>) ?? {},
(params.activationSource.rootConfig?.channels as Record<string, unknown>) ?? {},
)
.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<PluginRegistry> {
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({