From d2ce3e9accc6dbdcc2ad7c215bc7aae8f12a1e0c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 3 Apr 2026 00:28:15 +0900 Subject: [PATCH] perf(plugins): keep gateway startup channel-only (#59754) * perf(plugins): keep gateway startup channel-only * fix(gateway): preserve startup sidecars in plugin scope --- src/gateway/server-plugins.test.ts | 16 ++ src/gateway/server-plugins.ts | 10 + src/plugins/channel-plugin-ids.test.ts | 77 +++++-- src/plugins/channel-plugin-ids.ts | 283 +++---------------------- 4 files changed, 116 insertions(+), 270 deletions(-) diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index ae59a61d225..eb785105b13 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -277,6 +277,22 @@ describe("loadGatewayPlugins", () => { ); }); + test("treats an empty startup scope as no plugin load instead of an unscoped load", async () => { + resolveGatewayStartupPluginIds.mockReturnValue([]); + + const result = serverPluginsModule.loadGatewayPlugins({ + cfg: {}, + workspaceDir: "/tmp", + log: createTestLog(), + coreGatewayHandlers: {}, + baseMethods: ["sessions.get"], + }); + + expect(loadOpenClawPlugins).not.toHaveBeenCalled(); + expect(result.pluginRegistry.plugins).toEqual([]); + expect(result.gatewayMethods).toEqual(["sessions.get"]); + }); + test("loads gateway plugins from the auto-enabled config snapshot", async () => { const autoEnabledConfig = { channels: { slack: { enabled: true } }, autoEnabled: true }; applyPluginAutoEnable.mockReturnValue({ diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 03d7d53f06b..f0cf3cdc0fd 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -5,6 +5,8 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { resolveGatewayStartupPluginIds } from "../plugins/channel-plugin-ids.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { loadOpenClawPlugins } from "../plugins/loader.js"; +import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; @@ -424,6 +426,14 @@ export function loadGatewayPlugins(params: { workspaceDir: params.workspaceDir, env: process.env, }); + if (pluginIds.length === 0) { + const pluginRegistry = createEmptyPluginRegistry(); + setActivePluginRegistry(pluginRegistry, undefined, "gateway-bindable"); + return { + pluginRegistry, + gatewayMethods: [...params.baseMethods], + }; + } const pluginRegistry = loadOpenClawPlugins({ config: resolvedConfig, activationSourceConfig: params.activationSourceConfig ?? params.cfg, diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index fa7fcfed613..c39afd48466 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -26,7 +26,15 @@ function createManifestRegistryFixture() { cliBackends: [], }, { - id: "demo-default-on-sidecar", + id: "demo-other-channel", + channels: ["demo-other-channel"], + origin: "bundled", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, + { + id: "browser", channels: [], origin: "bundled", enabledByDefault: true, @@ -42,7 +50,7 @@ function createManifestRegistryFixture() { cliBackends: ["demo-cli"], }, { - id: "demo-bundled-sidecar", + id: "voice-call", channels: [], origin: "bundled", enabledByDefault: undefined, @@ -84,17 +92,38 @@ function createStartupConfig(params: { enabledPluginIds?: string[]; providerIds?: string[]; modelId?: string; + channelIds?: string[]; + allowPluginIds?: string[]; + noConfiguredChannels?: boolean; }) { return { + ...(params.noConfiguredChannels + ? { + channels: {}, + } + : params.channelIds?.length + ? { + channels: Object.fromEntries( + params.channelIds.map((channelId) => [channelId, { enabled: true }]), + ), + } + : {}), ...(params.enabledPluginIds?.length ? { plugins: { + ...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}), entries: Object.fromEntries( params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]), ), }, } - : {}), + : params.allowPluginIds?.length + ? { + plugins: { + allow: params.allowPluginIds, + }, + } + : {}), ...(params.providerIds?.length ? { models: { @@ -127,37 +156,57 @@ function createStartupConfig(params: { describe("resolveGatewayStartupPluginIds", () => { beforeEach(() => { - listPotentialConfiguredChannelIds.mockReset().mockReturnValue(["demo-channel"]); + listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => { + if (Object.prototype.hasOwnProperty.call(config, "channels")) { + return Object.keys(config.channels ?? {}); + } + return ["demo-channel"]; + }); loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture()); }); it.each([ [ - "includes configured channels and explicitly enabled bundled sidecars", + "includes only configured channel plugins at idle startup", createStartupConfig({ - enabledPluginIds: ["demo-bundled-sidecar"], + enabledPluginIds: ["voice-call"], modelId: "demo-cli/demo-model", }), - ["demo-channel", "demo-provider-plugin", "demo-bundled-sidecar"], + ["demo-channel", "browser", "voice-call"], ], [ - "skips bundled plugins with enabledByDefault: true until something references them", + "keeps bundled startup sidecars with enabledByDefault at idle startup", {} as OpenClawConfig, - ["demo-channel"], + ["demo-channel", "browser"], ], [ - "auto-loads bundled plugins referenced by configured provider ids", + "keeps provider plugins out of idle startup when only provider config references them", createStartupConfig({ providerIds: ["demo-provider"], }), - ["demo-channel", "demo-provider-plugin"], + ["demo-channel", "browser"], ], [ - "keeps non-bundled sidecars out of startup unless explicitly enabled", + "includes explicitly enabled non-channel sidecars in startup scope", createStartupConfig({ - enabledPluginIds: ["demo-global-sidecar"], + enabledPluginIds: ["demo-global-sidecar", "voice-call"], }), - ["demo-channel", "demo-global-sidecar"], + ["demo-channel", "browser", "voice-call", "demo-global-sidecar"], + ], + [ + "keeps default-enabled startup sidecars when a restrictive allowlist permits them", + createStartupConfig({ + allowPluginIds: ["browser"], + noConfiguredChannels: true, + }), + ["browser"], + ], + [ + "includes every configured channel plugin and excludes other channels", + createStartupConfig({ + channelIds: ["demo-channel", "demo-other-channel"], + }), + ["demo-channel", "demo-other-channel", "browser"], ], ] as const)("%s", (_name, config, expected) => { expectStartupPluginIdsCase({ config, expected }); diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index c5d11565209..f5d49823e88 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -1,239 +1,24 @@ -import { DEFAULT_PROVIDER } from "../agents/defaults.js"; -import { - buildModelAliasIndex, - normalizeProviderId, - resolveModelRefFromString, -} from "../agents/model-selection.js"; import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/config.js"; -import { resolvePluginWebSearchConfig } from "../config/legacy-web-search.js"; -import { - resolveAgentModelFallbackValues, - resolveAgentModelPrimaryValue, -} from "../config/model-input.js"; -import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js"; -import { loadPluginManifestRegistry } from "./manifest-registry.js"; +import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js"; +import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js"; +import { hasKind } from "./slots.js"; -type ModelListLike = string | { primary?: string; fallbacks?: string[] } | undefined; - -function addResolvedActivationId(params: { - raw: string | undefined; - activationIds: Set; - aliasIndex: ReturnType; -}): void { - const raw = params.raw?.trim(); - if (!raw) { - return; - } - const resolved = resolveModelRefFromString({ - raw, - defaultProvider: DEFAULT_PROVIDER, - aliasIndex: params.aliasIndex, - }); - if (!resolved) { - return; - } - params.activationIds.add(normalizeProviderId(resolved.ref.provider)); +function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean { + return Boolean( + plugin.providers.length > 0 || + plugin.cliBackends.length > 0 || + plugin.contracts?.speechProviders?.length || + plugin.contracts?.mediaUnderstandingProviders?.length || + plugin.contracts?.imageGenerationProviders?.length || + plugin.contracts?.webFetchProviders?.length || + plugin.contracts?.webSearchProviders?.length || + hasKind(plugin.kind, "memory"), + ); } -function addModelListActivationIds(params: { - value: ModelListLike; - activationIds: Set; - aliasIndex: ReturnType; -}): void { - addResolvedActivationId({ - raw: resolveAgentModelPrimaryValue(params.value), - activationIds: params.activationIds, - aliasIndex: params.aliasIndex, - }); - for (const fallback of resolveAgentModelFallbackValues(params.value)) { - addResolvedActivationId({ - raw: fallback, - activationIds: params.activationIds, - aliasIndex: params.aliasIndex, - }); - } -} - -function addProviderModelPairActivationId(params: { - provider: string | undefined; - model: string | undefined; - activationIds: Set; -}): void { - const provider = normalizeProviderId(params.provider ?? ""); - const model = params.model?.trim(); - if (!provider || !model) { - return; - } - params.activationIds.add(provider); -} - -function collectConfiguredActivationIds(config: OpenClawConfig): Set { - const activationIds = new Set(); - const aliasIndex = buildModelAliasIndex({ - cfg: config, - defaultProvider: DEFAULT_PROVIDER, - }); - - addModelListActivationIds({ value: config.agents?.defaults?.model, activationIds, aliasIndex }); - addModelListActivationIds({ - value: config.agents?.defaults?.imageModel, - activationIds, - aliasIndex, - }); - addModelListActivationIds({ - value: config.agents?.defaults?.imageGenerationModel, - activationIds, - aliasIndex, - }); - addModelListActivationIds({ - value: config.agents?.defaults?.pdfModel, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: config.agents?.defaults?.compaction?.model, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: config.agents?.defaults?.heartbeat?.model, - activationIds, - aliasIndex, - }); - addModelListActivationIds({ - value: config.agents?.defaults?.subagents?.model, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: config.messages?.tts?.summaryModel, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: config.hooks?.gmail?.model, - activationIds, - aliasIndex, - }); - - for (const modelRef of Object.keys(config.agents?.defaults?.models ?? {})) { - addResolvedActivationId({ - raw: modelRef, - activationIds, - aliasIndex, - }); - } - - for (const providerId of Object.keys(config.agents?.defaults?.cliBackends ?? {})) { - const normalized = normalizeProviderId(providerId); - if (normalized) { - activationIds.add(normalized); - } - } - - for (const providerId of Object.keys(config.models?.providers ?? {})) { - const normalized = normalizeProviderId(providerId); - if (normalized) { - activationIds.add(normalized); - } - } - - for (const agent of config.agents?.list ?? []) { - addModelListActivationIds({ value: agent.model, activationIds, aliasIndex }); - addModelListActivationIds({ value: agent.subagents?.model, activationIds, aliasIndex }); - addResolvedActivationId({ - raw: agent.heartbeat?.model, - activationIds, - aliasIndex, - }); - } - - for (const mapping of config.hooks?.mappings ?? []) { - addResolvedActivationId({ - raw: mapping.model, - activationIds, - aliasIndex, - }); - } - - for (const channelMap of Object.values(config.channels?.modelByChannel ?? {})) { - if (!channelMap || typeof channelMap !== "object") { - continue; - } - for (const raw of Object.values(channelMap)) { - addResolvedActivationId({ - raw: typeof raw === "string" ? raw : undefined, - activationIds, - aliasIndex, - }); - } - } - - addResolvedActivationId({ - raw: config.tools?.subagents?.model - ? resolveAgentModelPrimaryValue(config.tools?.subagents?.model) - : undefined, - activationIds, - aliasIndex, - }); - if (config.tools?.subagents?.model) { - for (const fallback of resolveAgentModelFallbackValues(config.tools.subagents.model)) { - addResolvedActivationId({ raw: fallback, activationIds, aliasIndex }); - } - } - - addResolvedActivationId({ - raw: resolvePluginWebSearchConfig(config, "google")?.model as string | undefined, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: resolvePluginWebSearchConfig(config, "xai")?.model as string | undefined, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: resolvePluginWebSearchConfig(config, "moonshot")?.model as string | undefined, - activationIds, - aliasIndex, - }); - addResolvedActivationId({ - raw: resolvePluginWebSearchConfig(config, "perplexity")?.model as string | undefined, - activationIds, - aliasIndex, - }); - - for (const entry of config.tools?.media?.models ?? []) { - addProviderModelPairActivationId({ - provider: entry.provider, - model: entry.model, - activationIds, - }); - } - for (const entry of config.tools?.media?.image?.models ?? []) { - addProviderModelPairActivationId({ - provider: entry.provider, - model: entry.model, - activationIds, - }); - } - for (const entry of config.tools?.media?.audio?.models ?? []) { - addProviderModelPairActivationId({ - provider: entry.provider, - model: entry.model, - activationIds, - }); - } - for (const entry of config.tools?.media?.video?.models ?? []) { - addProviderModelPairActivationId({ - provider: entry.provider, - model: entry.model, - activationIds, - }); - } - - return activationIds; +function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean { + return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin); } export function resolveChannelPluginIds(params: { @@ -297,46 +82,32 @@ export function resolveGatewayStartupPluginIds(params: { listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()), ); const pluginsConfig = normalizePluginsConfig(params.config.plugins); - const manifestRegistry = loadPluginManifestRegistry({ + return loadPluginManifestRegistry({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - }); - const configuredActivationIds = collectConfiguredActivationIds(params.config); - return manifestRegistry.plugins - .filter((plugin) => { + }) + .plugins.filter((plugin) => { if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) { return true; } - if (plugin.channels.length > 0) { + if (!isGatewayStartupSidecar(plugin)) { return false; } - if ( - plugin.origin === "bundled" && - (plugin.providers.some((providerId) => - configuredActivationIds.has(normalizeProviderId(providerId)), - ) || - plugin.cliBackends.some((backendId) => - configuredActivationIds.has(normalizeProviderId(backendId)), - )) - ) { - return true; - } - const enabled = resolveEffectiveEnableState({ + const activationState = resolveEffectivePluginActivationState({ id: plugin.id, origin: plugin.origin, config: pluginsConfig, rootConfig: params.config, enabledByDefault: plugin.enabledByDefault, - }).enabled; - if (!enabled) { + }); + if (!activationState.enabled) { return false; } - return ( - pluginsConfig.allow.includes(plugin.id) || - pluginsConfig.entries[plugin.id]?.enabled === true || - pluginsConfig.slots.memory === plugin.id - ); + if (plugin.origin !== "bundled") { + return activationState.explicitlyEnabled; + } + return activationState.source === "explicit" || activationState.source === "default"; }) .map((plugin) => plugin.id); }