From e3faa99c6a4752e64ee1cefa82bcda4731562d90 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 29 Mar 2026 09:53:30 +0530 Subject: [PATCH] fix(plugins): preserve gateway-bindable registry reuse # Conflicts: # src/agents/runtime-plugins.test.ts # src/agents/runtime-plugins.ts # src/plugins/loader.ts # src/plugins/tools.ts --- src/plugins/loader.ts | 14 ++++++++++---- src/plugins/runtime.ts | 14 +++++++++++++- src/plugins/tools.optional.test.ts | 30 +++++++++++++++++++++++++++++- src/plugins/tools.ts | 12 ++++++++++-- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 7768510cdf6..feece504123 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -301,6 +301,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { includeSetupOnlyChannelPlugins, preferSetupRuntimeForChannelPlugins, shouldActivate: options.activate !== false, + runtimeSubagentMode: resolveRuntimeSubagentMode(options.runtimeOptions), cacheKey, }; } @@ -768,8 +769,12 @@ function warnAboutUntrackedLoadedPlugins(params: { } } -function activatePluginRegistry(registry: PluginRegistry, cacheKey: string): void { - setActivePluginRegistry(registry, cacheKey); +function activatePluginRegistry( + registry: PluginRegistry, + cacheKey: string, + runtimeSubagentMode: "default" | "explicit" | "gateway-bindable", +): void { + setActivePluginRegistry(registry, cacheKey, runtimeSubagentMode); initializeGlobalHookRunner(registry); } @@ -790,6 +795,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi preferSetupRuntimeForChannelPlugins, shouldActivate, cacheKey, + runtimeSubagentMode, } = resolvePluginLoadCacheContext(options); const logger = options.logger ?? defaultLogger(); const validateOnly = options.mode === "validate"; @@ -805,7 +811,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi runtime: cached.memoryRuntime, }); if (shouldActivate) { - activatePluginRegistry(cached.registry, cacheKey); + activatePluginRegistry(cached.registry, cacheKey, runtimeSubagentMode); } return cached.registry; } @@ -1396,7 +1402,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi }); } if (shouldActivate) { - activatePluginRegistry(registry, cacheKey); + activatePluginRegistry(registry, cacheKey, runtimeSubagentMode); } return registry; } diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index 3a6b99a8ee8..2f512b7e83d 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -15,6 +15,7 @@ type RegistryState = { httpRoute: RegistrySurfaceState; channel: RegistrySurfaceState; key: string | null; + runtimeSubagentMode: "default" | "explicit" | "gateway-bindable"; }; const state: RegistryState = (() => { @@ -36,6 +37,7 @@ const state: RegistryState = (() => { version: 0, }, key: null, + runtimeSubagentMode: "default", }; } return globalState[REGISTRY_STATE]; @@ -71,12 +73,17 @@ function syncTrackedSurface( installSurfaceRegistry(surface, registry, false); } -export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string) { +export function setActivePluginRegistry( + registry: PluginRegistry, + cacheKey?: string, + runtimeSubagentMode: "default" | "explicit" | "gateway-bindable" = "default", +) { state.activeRegistry = registry; state.activeVersion += 1; syncTrackedSurface(state.httpRoute, registry, true); syncTrackedSurface(state.channel, registry, true); state.key = cacheKey ?? null; + state.runtimeSubagentMode = runtimeSubagentMode; } export function getActivePluginRegistry(): PluginRegistry | null { @@ -175,6 +182,10 @@ export function getActivePluginRegistryKey(): string | null { return state.key; } +export function getActivePluginRuntimeSubagentMode(): "default" | "explicit" | "gateway-bindable" { + return state.runtimeSubagentMode; +} + export function getActivePluginRegistryVersion(): number { return state.activeVersion; } @@ -185,4 +196,5 @@ export function resetPluginRuntimeStateForTest(): void { installSurfaceRegistry(state.httpRoute, null, false); installSurfaceRegistry(state.channel, null, false); state.key = null; + state.runtimeSubagentMode = "default"; } diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 1a2bc83cacc..0cf0430be80 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -208,6 +208,8 @@ describe("resolvePluginTools optional tools", () => { ({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js")); resetPluginRuntimeStateForTest(); ({ resolvePluginTools } = await import("./tools.js")); + ({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js")); + resetPluginRuntimeStateForTest(); }); it("skips optional tools without explicit allowlist", () => { @@ -349,7 +351,7 @@ describe("resolvePluginTools optional tools", () => { it("reuses the active registry for gateway-bindable tool loads before reloading", () => { const activeRegistry = createOptionalDemoActiveRegistry(); - setActivePluginRegistry(activeRegistry as never, "gateway-startup"); + setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable"); resolveRuntimePluginRegistryMock.mockReturnValue(undefined); const tools = resolvePluginTools( @@ -381,4 +383,30 @@ describe("resolvePluginTools optional tools", () => { }, }); }); + + it("reloads when gateway binding would otherwise reuse a default-mode active registry", () => { + setActivePluginRegistry( + { + tools: [], + diagnostics: [], + } as never, + "default-registry", + "default", + ); + setOptionalDemoRegistry(); + + resolvePluginTools({ + context: createContext() as never, + allowGatewaySubagentBinding: true, + toolAllowlist: ["optional_tool"], + }); + + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + runtimeOptions: { + allowGatewaySubagentBinding: true, + }, + }), + ); + }); }); diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 7f6e76c7e37..8ff1052232b 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -5,7 +5,11 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; -import { getActivePluginRegistry } from "./runtime.js"; +import { + getActivePluginRegistry, + getActivePluginRegistryKey, + getActivePluginRuntimeSubagentMode, +} from "./runtime.js"; import type { OpenClawPluginToolContext } from "./types.js"; const log = createSubsystemLogger("plugins"); @@ -55,7 +59,11 @@ function resolvePluginToolRegistry(params: { loadOptions: PluginLoadOptions; allowGatewaySubagentBinding?: boolean; }) { - if (params.allowGatewaySubagentBinding) { + if ( + params.allowGatewaySubagentBinding && + getActivePluginRegistryKey() && + getActivePluginRuntimeSubagentMode() === "gateway-bindable" + ) { return getActivePluginRegistry() ?? resolveRuntimePluginRegistry(params.loadOptions); } return resolveRuntimePluginRegistry(params.loadOptions);