From 41ce3269f48b2acc6a964bc971858aeabea820b8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 00:42:01 +0900 Subject: [PATCH] refactor(plugins): split activation snapshot and compat flow --- src/config/plugin-auto-enable.test.ts | 33 +++++- src/config/plugin-auto-enable.ts | 119 ++++++++++++++----- src/gateway/server-plugin-bootstrap.ts | 11 +- src/plugins/activation-context.ts | 128 +++++++++++++++++++-- src/plugins/providers.runtime.ts | 38 ++---- src/plugins/providers.ts | 4 +- src/plugins/web-fetch-providers.shared.ts | 25 ++-- src/plugins/web-search-providers.shared.ts | 25 ++-- 8 files changed, 275 insertions(+), 108 deletions(-) diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts index d546cffab6c..00e54a92bdd 100644 --- a/src/config/plugin-auto-enable.test.ts +++ b/src/config/plugin-auto-enable.test.ts @@ -11,7 +11,11 @@ import { makeTrackedTempDir, mkdirSafeDir, } from "../plugins/test-helpers/fs-fixtures.js"; -import { applyPluginAutoEnable } from "./plugin-auto-enable.js"; +import { + applyPluginAutoEnable, + detectPluginAutoEnableCandidates, + resolvePluginAutoEnableCandidateReason, +} from "./plugin-auto-enable.js"; import { validateConfigObject } from "./validation.js"; const tempDirs: string[] = []; @@ -121,6 +125,33 @@ afterEach(() => { }); describe("applyPluginAutoEnable", () => { + it("detects typed channel-configured candidates", () => { + const candidates = detectPluginAutoEnableCandidates({ + config: { + channels: { slack: { botToken: "x" } }, + }, + env: {}, + }); + + expect(candidates).toEqual([ + { + pluginId: "slack", + kind: "channel-configured", + channelId: "slack", + }, + ]); + }); + + it("formats typed provider-auth candidates into stable reasons", () => { + expect( + resolvePluginAutoEnableCandidateReason({ + pluginId: "google", + kind: "provider-auth-configured", + providerId: "google", + }), + ).toBe("google auth configured"); + }); + it("treats an undefined config as empty", () => { const result = applyPluginAutoEnable({ config: undefined, diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index a0e432e007b..ba97deda9a3 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -21,12 +21,43 @@ import type { OpenClawConfig } from "./config.js"; import { ensurePluginAllowlisted } from "./plugins-allowlist.js"; import { isBlockedObjectKey } from "./prototype-keys.js"; -type PluginEnableChange = { - pluginId: string; - reason: string; -}; - -export type PluginAutoEnableCandidate = PluginEnableChange; +export type PluginAutoEnableCandidate = + | { + pluginId: string; + kind: "channel-configured"; + channelId: string; + } + | { + pluginId: "browser"; + kind: "browser-configured"; + source: "browser-configured" | "browser-plugin-configured" | "browser-tool-referenced"; + } + | { + pluginId: string; + kind: "provider-auth-configured"; + providerId: string; + } + | { + pluginId: string; + kind: "web-fetch-provider-selected"; + providerId: string; + } + | { + pluginId: string; + kind: "plugin-web-search-configured"; + } + | { + pluginId: string; + kind: "plugin-web-fetch-configured"; + } + | { + pluginId: string; + kind: "plugin-tool-configured"; + } + | { + pluginId: "acpx"; + kind: "acp-runtime-configured"; + }; export type PluginAutoEnableResult = { config: OpenClawConfig; @@ -370,7 +401,7 @@ function configMayNeedPluginAutoEnable(cfg: OpenClawConfig, env: NodeJS.ProcessE if (hasPotentialConfiguredChannels(cfg, env)) { return true; } - if (resolveBrowserAutoEnableReason(cfg)) { + if (resolveBrowserAutoEnableSource(cfg)) { return true; } if (cfg.acp?.enabled === true || cfg.acp?.dispatch?.enabled === true) { @@ -435,26 +466,59 @@ function hasExplicitBrowserPluginEntry(cfg: OpenClawConfig): boolean { ); } -function resolveBrowserAutoEnableReason(cfg: OpenClawConfig): string | null { +function resolveBrowserAutoEnableSource( + cfg: OpenClawConfig, +): Extract["source"] | null { if (cfg.browser?.enabled === false || cfg.plugins?.entries?.browser?.enabled === false) { return null; } if (Object.prototype.hasOwnProperty.call(cfg, "browser")) { - return "browser configured"; + return "browser-configured"; } if (hasExplicitBrowserPluginEntry(cfg)) { - return "browser plugin configured"; + return "browser-plugin-configured"; } if (hasBrowserToolReference(cfg)) { - return "browser tool referenced"; + return "browser-tool-referenced"; } return null; } +export function resolvePluginAutoEnableCandidateReason( + candidate: PluginAutoEnableCandidate, +): string { + switch (candidate.kind) { + case "channel-configured": + return `${candidate.channelId} configured`; + case "browser-configured": + switch (candidate.source) { + case "browser-configured": + return "browser configured"; + case "browser-plugin-configured": + return "browser plugin configured"; + case "browser-tool-referenced": + return "browser tool referenced"; + } + break; + case "provider-auth-configured": + return `${candidate.providerId} auth configured`; + case "web-fetch-provider-selected": + return `${candidate.providerId} web fetch provider selected`; + case "plugin-web-search-configured": + return `${candidate.pluginId} web search configured`; + case "plugin-web-fetch-configured": + return `${candidate.pluginId} web fetch configured`; + case "plugin-tool-configured": + return `${candidate.pluginId} tool configured`; + case "acp-runtime-configured": + return "ACP runtime configured"; + } +} + function resolveConfiguredPlugins( cfg: OpenClawConfig, env: NodeJS.ProcessEnv, @@ -466,13 +530,13 @@ function resolveConfiguredPlugins( for (const channelId of collectCandidateChannelIds(cfg, env)) { const pluginId = resolvePluginIdForChannel(channelId, channelToPluginId); if (isChannelConfigured(cfg, channelId, env)) { - changes.push({ pluginId, reason: `${channelId} configured` }); + changes.push({ pluginId, kind: "channel-configured", channelId }); } } - const browserReason = resolveBrowserAutoEnableReason(cfg); - if (browserReason) { - changes.push({ pluginId: "browser", reason: browserReason }); + const browserSource = resolveBrowserAutoEnableSource(cfg); + if (browserSource) { + changes.push({ pluginId: "browser", kind: "browser-configured", source: browserSource }); } for (const [providerId, pluginId] of Object.entries( @@ -481,7 +545,8 @@ function resolveConfiguredPlugins( if (isProviderConfigured(cfg, providerId)) { changes.push({ pluginId, - reason: `${providerId} auth configured`, + kind: "provider-auth-configured", + providerId, }); } } @@ -491,14 +556,15 @@ function resolveConfiguredPlugins( if (webFetchPluginId) { changes.push({ pluginId: webFetchPluginId, - reason: `${String(webFetchProvider).trim().toLowerCase()} web fetch provider selected`, + kind: "web-fetch-provider-selected", + providerId: String(webFetchProvider).trim().toLowerCase(), }); } for (const pluginId of resolveProviderPluginsWithOwnedWebSearch(registry)) { if (hasPluginOwnedWebSearchConfig(cfg, pluginId)) { changes.push({ pluginId, - reason: `${pluginId} web search configured`, + kind: "plugin-web-search-configured", }); } } @@ -506,7 +572,7 @@ function resolveConfiguredPlugins( if (hasPluginOwnedWebFetchConfig(cfg, pluginId)) { changes.push({ pluginId, - reason: `${pluginId} web fetch configured`, + kind: "plugin-web-fetch-configured", }); } } @@ -514,7 +580,7 @@ function resolveConfiguredPlugins( if (hasPluginOwnedToolConfig(cfg, pluginId)) { changes.push({ pluginId, - reason: `${pluginId} tool configured`, + kind: "plugin-tool-configured", }); } } @@ -525,7 +591,7 @@ function resolveConfiguredPlugins( if (acpConfigured && (!backendRaw || backendRaw === "acpx")) { changes.push({ pluginId: "acpx", - reason: "ACP runtime configured", + kind: "acp-runtime-configured", }); } return changes; @@ -577,8 +643,8 @@ function resolvePreferredOverIds( function shouldSkipPreferredPluginAutoEnable( cfg: OpenClawConfig, - entry: PluginEnableChange, - configured: PluginEnableChange[], + entry: PluginAutoEnableCandidate, + configured: PluginAutoEnableCandidate[], env: NodeJS.ProcessEnv, registry: PluginManifestRegistry, ): boolean { @@ -636,8 +702,8 @@ function registerPluginEntry(cfg: OpenClawConfig, pluginId: string): OpenClawCon }; } -function formatAutoEnableChange(entry: PluginEnableChange): string { - let reason = entry.reason.trim(); +function formatAutoEnableChange(entry: PluginAutoEnableCandidate): string { + let reason = resolvePluginAutoEnableCandidateReason(entry).trim(); const channelId = normalizeChatChannelId(entry.pluginId); if (channelId) { const label = getChatChannelMeta(channelId).label; @@ -720,9 +786,10 @@ export function materializePluginAutoEnableCandidates(params: { if (!builtInChannelId) { next = ensurePluginAllowlisted(next, entry.pluginId); } + const reason = resolvePluginAutoEnableCandidateReason(entry); autoEnabledReasons.set(entry.pluginId, [ ...(autoEnabledReasons.get(entry.pluginId) ?? []), - entry.reason, + reason, ]); changes.push(formatAutoEnableChange(entry)); } diff --git a/src/gateway/server-plugin-bootstrap.ts b/src/gateway/server-plugin-bootstrap.ts index b3f75912325..3384fe7e519 100644 --- a/src/gateway/server-plugin-bootstrap.ts +++ b/src/gateway/server-plugin-bootstrap.ts @@ -1,6 +1,6 @@ import { primeConfiguredBindingRegistry } from "../channels/plugins/binding-registry.js"; import type { loadConfig } from "../config/config.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; +import { resolvePluginActivationSnapshot } from "../plugins/activation-context.js"; import type { PluginRegistry } from "../plugins/registry.js"; import { pinActivePluginChannelRegistry } from "../plugins/runtime.js"; import { setGatewaySubagentRuntime } from "../plugins/runtime/index.js"; @@ -59,16 +59,17 @@ function logGatewayPluginDiagnostics(params: { } export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) { - const autoEnabled = applyPluginAutoEnable({ - config: params.activationSourceConfig ?? params.cfg, + const activation = resolvePluginActivationSnapshot({ + rawConfig: params.activationSourceConfig ?? params.cfg, env: process.env, + applyAutoEnable: true, }); - const resolvedConfig = autoEnabled.config; + const resolvedConfig = activation.config ?? params.cfg; installGatewayPluginRuntimeEnvironment(resolvedConfig); const loaded = loadGatewayPlugins({ cfg: resolvedConfig, activationSourceConfig: params.activationSourceConfig ?? params.cfg, - autoEnabledReasons: autoEnabled.autoEnabledReasons, + autoEnabledReasons: activation.autoEnabledReasons, workspaceDir: params.workspaceDir, log: params.log, coreGatewayHandlers: params.coreGatewayHandlers, diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 6cfd624a281..94fc4958849 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -18,6 +18,12 @@ export type PluginActivationCompatConfig = { vitestPluginIds?: readonly string[]; }; +export type PluginActivationBundledCompatMode = { + allowlist?: boolean; + enablement?: "always" | "allowlist"; + vitest?: boolean; +}; + export type PluginActivationInputs = { rawConfig?: OpenClawConfig; config?: OpenClawConfig; @@ -27,7 +33,21 @@ export type PluginActivationInputs = { autoEnabledReasons: Record; }; -function applyPluginActivationCompat(params: { +export type PluginActivationSnapshot = Pick< + PluginActivationInputs, + | "rawConfig" + | "config" + | "normalized" + | "activationSourceConfig" + | "activationSource" + | "autoEnabledReasons" +>; + +export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & { + compatPluginIds: string[]; +}; + +export function applyPluginCompatibilityOverrides(params: { config?: OpenClawConfig; compat?: PluginActivationCompatConfig; env: NodeJS.ProcessEnv; @@ -54,14 +74,13 @@ function applyPluginActivationCompat(params: { return vitestCompat; } -export function resolvePluginActivationInputs(params: { +export function resolvePluginActivationSnapshot(params: { rawConfig?: OpenClawConfig; resolvedConfig?: OpenClawConfig; autoEnabledReasons?: Record; env?: NodeJS.ProcessEnv; - compat?: PluginActivationCompatConfig; applyAutoEnable?: boolean; -}): PluginActivationInputs { +}): PluginActivationSnapshot { const env = params.env ?? process.env; const rawConfig = params.rawConfig ?? params.resolvedConfig; let resolvedConfig = params.resolvedConfig ?? params.rawConfig; @@ -76,16 +95,10 @@ export function resolvePluginActivationInputs(params: { autoEnabledReasons = autoEnabled.autoEnabledReasons; } - const config = applyPluginActivationCompat({ - config: resolvedConfig, - compat: params.compat, - env, - }); - return { rawConfig, - config, - normalized: normalizePluginsConfig(config?.plugins), + config: resolvedConfig, + normalized: normalizePluginsConfig(resolvedConfig?.plugins), activationSourceConfig: rawConfig, activationSource: createPluginActivationSource({ config: rawConfig, @@ -93,3 +106,94 @@ export function resolvePluginActivationInputs(params: { autoEnabledReasons: autoEnabledReasons ?? {}, }; } + +export function resolvePluginActivationInputs(params: { + rawConfig?: OpenClawConfig; + resolvedConfig?: OpenClawConfig; + autoEnabledReasons?: Record; + env?: NodeJS.ProcessEnv; + compat?: PluginActivationCompatConfig; + applyAutoEnable?: boolean; +}): PluginActivationInputs { + const env = params.env ?? process.env; + const snapshot = resolvePluginActivationSnapshot({ + rawConfig: params.rawConfig, + resolvedConfig: params.resolvedConfig, + autoEnabledReasons: params.autoEnabledReasons, + env, + applyAutoEnable: params.applyAutoEnable, + }); + const config = applyPluginCompatibilityOverrides({ + config: snapshot.config, + compat: params.compat, + env, + }); + + return { + rawConfig: snapshot.rawConfig, + config, + normalized: normalizePluginsConfig(config?.plugins), + activationSourceConfig: snapshot.activationSourceConfig, + activationSource: snapshot.activationSource, + autoEnabledReasons: snapshot.autoEnabledReasons, + }; +} + +export function resolveBundledPluginCompatibleActivationInputs(params: { + rawConfig?: OpenClawConfig; + resolvedConfig?: OpenClawConfig; + autoEnabledReasons?: Record; + env?: NodeJS.ProcessEnv; + workspaceDir?: string; + onlyPluginIds?: readonly string[]; + applyAutoEnable?: boolean; + compatMode: PluginActivationBundledCompatMode; + resolveCompatPluginIds: (params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + onlyPluginIds?: readonly string[]; + }) => string[]; +}): BundledPluginCompatibleActivationInputs { + const snapshot = resolvePluginActivationSnapshot({ + rawConfig: params.rawConfig, + resolvedConfig: params.resolvedConfig, + autoEnabledReasons: params.autoEnabledReasons, + env: params.env, + applyAutoEnable: params.applyAutoEnable, + }); + const allowlistCompatEnabled = params.compatMode.allowlist === true; + const shouldResolveCompatPluginIds = + allowlistCompatEnabled || + params.compatMode.enablement === "always" || + (params.compatMode.enablement === "allowlist" && allowlistCompatEnabled) || + params.compatMode.vitest === true; + const compatPluginIds = shouldResolveCompatPluginIds + ? params.resolveCompatPluginIds({ + config: snapshot.config, + workspaceDir: params.workspaceDir, + env: params.env, + onlyPluginIds: params.onlyPluginIds, + }) + : []; + const activation = resolvePluginActivationInputs({ + rawConfig: snapshot.rawConfig, + resolvedConfig: snapshot.config, + autoEnabledReasons: snapshot.autoEnabledReasons, + env: params.env, + compat: { + allowlistPluginIds: allowlistCompatEnabled ? compatPluginIds : undefined, + enablementPluginIds: + params.compatMode.enablement === "always" || + (params.compatMode.enablement === "allowlist" && allowlistCompatEnabled) + ? compatPluginIds + : undefined, + vitestPluginIds: params.compatMode.vitest ? compatPluginIds : undefined, + }, + }); + + return { + ...activation, + compatPluginIds, + }; +} diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index 29f1f12e980..4b4786edc1a 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -1,5 +1,5 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolvePluginActivationInputs } from "./activation-context.js"; +import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; import { @@ -24,41 +24,23 @@ export function resolvePluginProviders(params: { pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"]; }): ProviderPlugin[] { const env = params.env ?? process.env; - const autoEnabled = resolvePluginActivationInputs({ + const activation = resolveBundledPluginCompatibleActivationInputs({ rawConfig: params.config, env, + workspaceDir: params.workspaceDir, + onlyPluginIds: params.onlyPluginIds, applyAutoEnable: true, - }); - const bundledProviderCompatPluginIds = - params.bundledProviderAllowlistCompat || params.bundledProviderVitestCompat - ? resolveBundledProviderCompatPluginIds({ - config: autoEnabled.config, - workspaceDir: params.workspaceDir, - env, - onlyPluginIds: params.onlyPluginIds, - }) - : []; - const activation = resolvePluginActivationInputs({ - rawConfig: params.config, - resolvedConfig: autoEnabled.config, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - env, - compat: { - allowlistPluginIds: params.bundledProviderAllowlistCompat - ? bundledProviderCompatPluginIds - : undefined, - enablementPluginIds: params.bundledProviderAllowlistCompat - ? bundledProviderCompatPluginIds - : undefined, - vitestPluginIds: params.bundledProviderVitestCompat - ? bundledProviderCompatPluginIds - : undefined, + compatMode: { + allowlist: params.bundledProviderAllowlistCompat, + enablement: "allowlist", + vitest: params.bundledProviderVitestCompat, }, + resolveCompatPluginIds: resolveBundledProviderCompatPluginIds, }); const config = params.bundledProviderVitestCompat ? withBundledProviderVitestCompat({ config: activation.config, - pluginIds: bundledProviderCompatPluginIds, + pluginIds: activation.compatPluginIds, env, }) : activation.config; diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 06832b1679b..88671fcb598 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -16,7 +16,7 @@ export function resolveBundledProviderCompatPluginIds(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; env?: PluginLoadOptions["env"]; - onlyPluginIds?: string[]; + onlyPluginIds?: readonly string[]; }): string[] { const onlyPluginIdSet = params.onlyPluginIds ? new Set(params.onlyPluginIds) : null; const registry = loadPluginManifestRegistry({ @@ -39,7 +39,7 @@ export function resolveEnabledProviderPluginIds(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; env?: PluginLoadOptions["env"]; - onlyPluginIds?: string[]; + onlyPluginIds?: readonly string[]; }): string[] { const onlyPluginIdSet = params.onlyPluginIds ? new Set(params.onlyPluginIds) : null; const registry = loadPluginManifestRegistry({ diff --git a/src/plugins/web-fetch-providers.shared.ts b/src/plugins/web-fetch-providers.shared.ts index ef9e9967372..4fad7f6c061 100644 --- a/src/plugins/web-fetch-providers.shared.ts +++ b/src/plugins/web-fetch-providers.shared.ts @@ -1,4 +1,4 @@ -import { resolvePluginActivationInputs } from "./activation-context.js"; +import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; import { resolveBundledWebFetchPluginIds } from "./bundled-web-fetch.js"; import { type NormalizedPluginsConfig } from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; @@ -53,26 +53,17 @@ export function resolveBundledWebFetchResolutionConfig(params: { activationSourceConfig?: PluginLoadOptions["config"]; autoEnabledReasons: Record; } { - const autoEnabled = resolvePluginActivationInputs({ + const activation = resolveBundledPluginCompatibleActivationInputs({ rawConfig: params.config, env: params.env, - applyAutoEnable: true, - }); - const bundledCompatPluginIds = resolveBundledWebFetchCompatPluginIds({ - config: autoEnabled.config, workspaceDir: params.workspaceDir, - env: params.env, - }); - const activation = resolvePluginActivationInputs({ - rawConfig: params.config, - resolvedConfig: autoEnabled.config, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - env: params.env, - compat: { - allowlistPluginIds: params.bundledAllowlistCompat ? bundledCompatPluginIds : undefined, - enablementPluginIds: bundledCompatPluginIds, - vitestPluginIds: bundledCompatPluginIds, + applyAutoEnable: true, + compatMode: { + allowlist: params.bundledAllowlistCompat, + enablement: "always", + vitest: true, }, + resolveCompatPluginIds: resolveBundledWebFetchCompatPluginIds, }); return { diff --git a/src/plugins/web-search-providers.shared.ts b/src/plugins/web-search-providers.shared.ts index 9fd2aae88b8..e2c310ba48a 100644 --- a/src/plugins/web-search-providers.shared.ts +++ b/src/plugins/web-search-providers.shared.ts @@ -1,4 +1,4 @@ -import { resolvePluginActivationInputs } from "./activation-context.js"; +import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; import { resolveBundledWebSearchPluginIds } from "./bundled-web-search.js"; import { type NormalizedPluginsConfig } from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; @@ -53,26 +53,17 @@ export function resolveBundledWebSearchResolutionConfig(params: { activationSourceConfig?: PluginLoadOptions["config"]; autoEnabledReasons: Record; } { - const autoEnabled = resolvePluginActivationInputs({ + const activation = resolveBundledPluginCompatibleActivationInputs({ rawConfig: params.config, env: params.env, - applyAutoEnable: true, - }); - const bundledCompatPluginIds = resolveBundledWebSearchCompatPluginIds({ - config: autoEnabled.config, workspaceDir: params.workspaceDir, - env: params.env, - }); - const activation = resolvePluginActivationInputs({ - rawConfig: params.config, - resolvedConfig: autoEnabled.config, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - env: params.env, - compat: { - allowlistPluginIds: params.bundledAllowlistCompat ? bundledCompatPluginIds : undefined, - enablementPluginIds: bundledCompatPluginIds, - vitestPluginIds: bundledCompatPluginIds, + applyAutoEnable: true, + compatMode: { + allowlist: params.bundledAllowlistCompat, + enablement: "always", + vitest: true, }, + resolveCompatPluginIds: resolveBundledWebSearchCompatPluginIds, }); return {