From a5689accc4a47f18865992008e18afbe546d308b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Apr 2026 11:04:28 +0100 Subject: [PATCH] fix(plugins): preserve empty provider scopes --- src/plugins/loader.runtime-registry.test.ts | 25 +++++++++++++++++ src/plugins/loader.test.ts | 28 +++++++++++++++++++ src/plugins/loader.ts | 11 ++++---- .../runtime/metadata-registry-loader.ts | 2 +- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/plugins/loader.runtime-registry.test.ts b/src/plugins/loader.runtime-registry.test.ts index 4e6be841509..8b44a825e84 100644 --- a/src/plugins/loader.runtime-registry.test.ts +++ b/src/plugins/loader.runtime-registry.test.ts @@ -55,6 +55,12 @@ describe("getCompatibleActivePluginRegistry", () => { onlyPluginIds: ["demo"], }), ).toBeUndefined(); + expect( + __testing.getCompatibleActivePluginRegistry({ + ...loadOptions, + onlyPluginIds: [], + }), + ).toBeUndefined(); expect( __testing.getCompatibleActivePluginRegistry({ ...loadOptions, @@ -183,6 +189,25 @@ describe("resolveRuntimePluginRegistry", () => { expect(resolveRuntimePluginRegistry()).toBe(registry); }); + + it("does not treat an explicit empty plugin scope as the active runtime", () => { + const registry = createEmptyPluginRegistry(); + const loadOptions = { + config: { + plugins: { + allow: ["demo"], + load: { paths: ["/tmp/demo.js"] }, + }, + }, + workspaceDir: "/tmp/workspace-a", + }; + const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions); + setActivePluginRegistry(registry, cacheKey); + + const scopedEmpty = resolveRuntimePluginRegistry({ ...loadOptions, onlyPluginIds: [] }); + expect(scopedEmpty).not.toBe(registry); + expect(scopedEmpty?.plugins).toEqual([]); + }); }); describe("clearPluginLoaderCache", () => { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 3006ab7f3f9..c6f2fb43a7d 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1431,6 +1431,34 @@ module.exports = { id: "throws-after-import", register() {} };`, run(); }); + it("treats an explicit empty plugin scope as scoped-empty instead of unscoped", () => { + useNoBundledPlugins(); + const allowed = writePlugin({ + id: "allowed-empty-scope", + filename: "allowed-empty-scope.cjs", + body: `module.exports = { id: "allowed-empty-scope", register() {} };`, + }); + const extra = writePlugin({ + id: "extra-empty-scope", + filename: "extra-empty-scope.cjs", + body: `module.exports = { id: "extra-empty-scope", register() {} };`, + }); + + const registry = loadOpenClawPlugins({ + cache: false, + activate: false, + config: { + plugins: { + load: { paths: [allowed.file, extra.file] }, + allow: ["allowed-empty-scope", "extra-empty-scope"], + }, + }, + onlyPluginIds: [], + }); + + expect(registry.plugins).toEqual([]); + }); + it("only publishes plugin commands to the global registry during activating loads", async () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index fd60f4b1c9d..5f8f089824d 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -358,7 +358,8 @@ function buildCacheKey(params: { }, ]), ); - const scopeKey = JSON.stringify(params.onlyPluginIds ?? []); + const scopeKey = + params.onlyPluginIds === undefined ? "__unscoped__" : JSON.stringify(params.onlyPluginIds); const setupOnlyKey = params.includeSetupOnlyChannelPlugins === true ? "setup-only" : "runtime"; const startupChannelMode = params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full"; @@ -378,7 +379,7 @@ function normalizeScopedPluginIds(ids?: string[]): string[] | undefined { return undefined; } const normalized = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean))).toSorted(); - return normalized.length > 0 ? normalized : undefined; + return normalized; } function matchesScopedPluginRequest(params: { @@ -442,19 +443,19 @@ function buildActivationMetadataHash(params: { } function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean { - return Boolean( + return ( options.config !== undefined || options.activationSourceConfig !== undefined || options.autoEnabledReasons !== undefined || options.workspaceDir !== undefined || options.env !== undefined || - options.onlyPluginIds?.length || + options.onlyPluginIds !== undefined || options.runtimeOptions !== undefined || options.pluginSdkResolution !== undefined || options.coreGatewayHandlers !== undefined || options.includeSetupOnlyChannelPlugins === true || options.preferSetupRuntimeForChannelPlugins === true || - options.loadModules === false, + options.loadModules === false ); } diff --git a/src/plugins/runtime/metadata-registry-loader.ts b/src/plugins/runtime/metadata-registry-loader.ts index beb8f06fdb3..a88f915b47e 100644 --- a/src/plugins/runtime/metadata-registry-loader.ts +++ b/src/plugins/runtime/metadata-registry-loader.ts @@ -20,7 +20,7 @@ export function loadPluginMetadataRegistrySnapshot(options?: { activate: false, mode: "validate", loadModules: options?.loadModules, - ...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}), + ...(options?.onlyPluginIds !== undefined ? { onlyPluginIds: options.onlyPluginIds } : {}), }), ); }