From a2456ce58aa2989168badb1184b40a4990393ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Wed, 20 May 2026 17:36:35 -0600 Subject: [PATCH] fix(plugins): load explicitly enabled non-bundled plugins at gateway startup [DOJ-4055] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2026.5.7 introduced a `shouldConsiderForGatewayStartup` filter in `resolveGatewayStartupPluginPlanFromRegistry` that builds the list of `onlyPluginIds` passed to `loadOpenClawPlugins`. The filter accepts a plugin only when it declares `activation.onStartup === true`, is the context-engine slot, or is a memory-startup plugin matching the dreaming or memory-slot configuration. For tool/hook/route plugins that the operator has explicitly enabled via `plugins.entries.{id}.enabled = true` (and/or pinned in `plugins.allow`) but that do NOT also expose a channel/provider/memory contract or declare `activation.onStartup`, every `canStart*` path returns false and `shouldConsiderForGatewayStartup` returns false. The plugin id is silently dropped from `plan.pluginIds`. Downstream, the loader's `matchesScopedPluginRequest` skips any candidate not in `onlyPluginIds`, so the plugin module is never `import()`-ed. Race B's instrumentation in the DojoOS Agent plugin confirmed this empirically: five `console.error` breadcrumbs at the top of `dist/index.js` (well before any sub-import) never fired, while the gateway emitted `http server listening (1 plugin: slack)` and never logged `DojoOS Agent Plugin registered`. Race D removed `plugins.load.paths` from the gateway config (eliminating the redundant config-source candidate and the matching `duplicate plugin id` warning), but the global candidate still failed to load — because the gateway-startup filter, not duplicate resolution, was excluding it. This commit adds a single branch to `shouldConsiderForGatewayStartup`: for any non-bundled plugin, treat an explicit `entries.{id}.enabled === true` OR explicit `plugins.allow` membership as sufficient signal to consider the plugin at gateway startup. Bundled plugin semantics are unchanged (they keep relying on `activation.onStartup`, slot configuration, and channel detection). Tests: `src/plugins/bundled-plugin-metadata.test.ts` continues to pass (30/30), including the empty-config-startup baseline and the "starts Bonjour when explicitly enabled" case (Bonjour is bundled, so it routes through the pre-existing paths; the new branch only widens non-bundled startup behavior). Full plugin/loader/registry suite passes (209/209: loader, manifest-registry, setup-registry, plugins-cli.list). Scope: 21 lines added in a single file (`src/plugins/gateway-startup-plugin-ids.ts`). No public type changes, no config-schema additions, no architectural restructuring. Co-Authored-By: Claude Opus 4.7 --- src/plugins/gateway-startup-plugin-ids.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 8beaf901f6869..5df384bf38dcf 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -137,6 +137,8 @@ function shouldConsiderForGatewayStartup(params: { startupDreamingPluginIds: ReadonlySet; memorySlotStartupPluginId?: string; contextEngineSlotStartupPluginId?: string; + pluginsConfig: NormalizedPluginsConfig; + activationSourcePlugins: NormalizedPluginsConfig; }): boolean { if (params.manifest?.activation?.onStartup === true) { return true; @@ -144,6 +146,23 @@ function shouldConsiderForGatewayStartup(params: { if (params.contextEngineSlotStartupPluginId === params.plugin.pluginId) { return true; } + // DOJ-4055 (dojo fork): a non-bundled plugin that the operator has explicitly + // enabled in config (entries.{id}.enabled === true) or pinned via plugins.allow + // is intended to load at gateway startup even when the plugin manifest does not + // declare activation.onStartup. Without this branch, tool/hook/route plugins + // that do not also expose a channel/provider/memory contract are silently + // dropped from the startup plan and never imported. + if (params.plugin.origin !== "bundled") { + const entryEnabled = + params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === true || + params.activationSourcePlugins.entries[params.plugin.pluginId]?.enabled === true; + const allowed = + params.pluginsConfig.allow.includes(params.plugin.pluginId) || + params.activationSourcePlugins.allow.includes(params.plugin.pluginId); + if (entryEnabled || allowed) { + return true; + } + } if (!isGatewayStartupMemoryPlugin(params.plugin)) { return false; } @@ -738,6 +757,8 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: { startupDreamingPluginIds, memorySlotStartupPluginId, contextEngineSlotStartupPluginId, + pluginsConfig, + activationSourcePlugins, }) ) { return false;