diff --git a/src/gateway/server-plugin-bootstrap.ts b/src/gateway/server-plugin-bootstrap.ts index 0d95d23606b..30c8a03048f 100644 --- a/src/gateway/server-plugin-bootstrap.ts +++ b/src/gateway/server-plugin-bootstrap.ts @@ -24,6 +24,7 @@ type GatewayPluginBootstrapParams = { log: GatewayPluginBootstrapLog; coreGatewayHandlers: Record; baseMethods: string[]; + pluginIds?: string[]; preferSetupRuntimeForChannelPlugins?: boolean; logDiagnostics?: boolean; beforePrimeRegistry?: (pluginRegistry: PluginRegistry) => void; @@ -68,6 +69,7 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) { log: params.log, coreGatewayHandlers: params.coreGatewayHandlers, baseMethods: params.baseMethods, + pluginIds: params.pluginIds, preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins, }); params.beforePrimeRegistry?.(loaded.pluginRegistry); diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index e62b539c5a8..d25609e4e6b 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -257,6 +257,21 @@ describe("loadGatewayPlugins", () => { ); }); + test("reuses the provided startup plugin scope without recomputing it", async () => { + loadOpenClawPlugins.mockReturnValue(createRegistry([])); + + loadGatewayPluginsForTest({ + pluginIds: ["browser"], + }); + + expect(resolveGatewayStartupPluginIds).not.toHaveBeenCalled(); + expect(loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["browser"], + }), + ); + }); + test("loads gateway plugins from the auto-enabled config snapshot", async () => { const autoEnabledConfig = { channels: { slack: { enabled: true } }, autoEnabled: true }; applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); @@ -634,6 +649,28 @@ describe("loadGatewayPlugins", () => { expect(log.info).not.toHaveBeenCalled(); }); + test("reuses the initial startup plugin scope during deferred reloads", async () => { + const { reloadDeferredGatewayPlugins } = serverPluginBootstrapModule; + loadOpenClawPlugins.mockReturnValue(createRegistry([])); + + reloadDeferredGatewayPlugins({ + cfg: {}, + workspaceDir: "/tmp", + log: createTestLog(), + coreGatewayHandlers: {}, + baseMethods: [], + pluginIds: ["discord"], + logDiagnostics: false, + }); + + expect(resolveGatewayStartupPluginIds).not.toHaveBeenCalled(); + expect(loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["discord"], + }), + ); + }); + test("runs registry hook before priming configured bindings", async () => { const { prepareGatewayPluginLoad } = serverPluginBootstrapModule; const order: string[] = []; diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index e599d32c18b..c312b620e42 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -389,20 +389,24 @@ export function loadGatewayPlugins(params: { }; coreGatewayHandlers: Record; baseMethods: string[]; + pluginIds?: string[]; preferSetupRuntimeForChannelPlugins?: boolean; }) { const resolvedConfig = applyPluginAutoEnable({ config: params.cfg, env: process.env, }).config; - const pluginRegistry = loadOpenClawPlugins({ - config: resolvedConfig, - workspaceDir: params.workspaceDir, - onlyPluginIds: resolveGatewayStartupPluginIds({ + const pluginIds = + params.pluginIds ?? + resolveGatewayStartupPluginIds({ config: resolvedConfig, workspaceDir: params.workspaceDir, env: process.env, - }), + }); + const pluginRegistry = loadOpenClawPlugins({ + config: resolvedConfig, + workspaceDir: params.workspaceDir, + onlyPluginIds: pluginIds, logger: { info: (msg) => params.log.info(msg), warn: (msg) => params.log.warn(msg), diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 3fbb6b8b50b..916790d58f0 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -54,7 +54,10 @@ import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js"; import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js"; import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js"; import { resolveBundledPluginInstallCommandHint } from "../plugins/bundled-sources.js"; -import { resolveConfiguredDeferredChannelPluginIds } from "../plugins/channel-plugin-ids.js"; +import { + resolveConfiguredDeferredChannelPluginIds, + resolveGatewayStartupPluginIds, +} from "../plugins/channel-plugin-ids.js"; import { getGlobalHookRunner, runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; @@ -582,6 +585,13 @@ export async function startGatewayServer( workspaceDir: defaultWorkspaceDir, env: process.env, }); + const startupPluginIds = minimalTestGateway + ? [] + : resolveGatewayStartupPluginIds({ + config: gatewayPluginConfigAtStart, + workspaceDir: defaultWorkspaceDir, + env: process.env, + }); const baseMethods = listGatewayMethods(); const emptyPluginRegistry = createEmptyPluginRegistry(); let pluginRegistry = emptyPluginRegistry; @@ -593,6 +603,7 @@ export async function startGatewayServer( log, coreGatewayHandlers, baseMethods, + pluginIds: startupPluginIds, preferSetupRuntimeForChannelPlugins: deferredConfiguredChannelPluginIds.length > 0, })); } else { @@ -1348,6 +1359,7 @@ export async function startGatewayServer( log, coreGatewayHandlers, baseMethods, + pluginIds: startupPluginIds, logDiagnostics: false, })); } diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 43d9ba9344f..fa7fcfed613 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -133,30 +133,31 @@ describe("resolveGatewayStartupPluginIds", () => { it.each([ [ - "includes configured channels, explicit bundled sidecars, and enabled non-bundled sidecars", + "includes configured channels and explicitly enabled bundled sidecars", createStartupConfig({ enabledPluginIds: ["demo-bundled-sidecar"], modelId: "demo-cli/demo-model", }), - [ - "demo-channel", - "demo-default-on-sidecar", - "demo-provider-plugin", - "demo-bundled-sidecar", - "demo-global-sidecar", - ], + ["demo-channel", "demo-provider-plugin", "demo-bundled-sidecar"], ], [ - "includes bundled plugins with enabledByDefault: true", + "skips bundled plugins with enabledByDefault: true until something references them", {} as OpenClawConfig, - ["demo-channel", "demo-default-on-sidecar", "demo-global-sidecar"], + ["demo-channel"], ], [ "auto-loads bundled plugins referenced by configured provider ids", createStartupConfig({ providerIds: ["demo-provider"], }), - ["demo-channel", "demo-default-on-sidecar", "demo-provider-plugin", "demo-global-sidecar"], + ["demo-channel", "demo-provider-plugin"], + ], + [ + "keeps non-bundled sidecars out of startup unless explicitly enabled", + createStartupConfig({ + enabledPluginIds: ["demo-global-sidecar"], + }), + ["demo-channel", "demo-global-sidecar"], ], ] 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 a6a268bef54..54915ca21c1 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -331,14 +331,10 @@ export function resolveGatewayStartupPluginIds(params: { if (!enabled) { return false; } - if (plugin.origin !== "bundled") { - return true; - } return ( pluginsConfig.allow.includes(plugin.id) || pluginsConfig.entries[plugin.id]?.enabled === true || - pluginsConfig.slots.memory === plugin.id || - plugin.enabledByDefault === true + pluginsConfig.slots.memory === plugin.id ); }) .map((plugin) => plugin.id);