From da2a8bd6bbc52f7e2e26484f5cc581ea659d3e75 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 15:54:37 +0100 Subject: [PATCH] fix: scope runtime plugin preload to effective plugins --- CHANGELOG.md | 1 + docs/plugins/architecture-internals.md | 6 + src/cli/plugin-registry.test.ts | 10 ++ .../channel-setup/plugin-install.test.ts | 14 +- src/commands/channel-setup/plugin-install.ts | 24 +-- src/plugins/effective-plugin-ids.test.ts | 148 ++++++++++++++++++ src/plugins/effective-plugin-ids.ts | 22 +++ .../runtime/runtime-registry-loader.test.ts | 64 ++++++++ .../runtime/runtime-registry-loader.ts | 65 +++++--- 9 files changed, 314 insertions(+), 40 deletions(-) create mode 100644 src/plugins/effective-plugin-ids.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b202d0bade..f7f0a75cf7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY. - Plugins/onboarding: carry ClawHub install metadata through channel setup catalogs so missing channel plugins can install from ClawHub before npm/local fallback. Thanks @vincentkoc. +- Plugins/runtime: scope broad runtime preloads to the effective plugin ids derived from config, startup planning, configured channels, slots, and auto-enable rules instead of importing every discoverable plugin. ### Fixes diff --git a/docs/plugins/architecture-internals.md b/docs/plugins/architecture-internals.md index 31f93f99ace..abd63cfd8ff 100644 --- a/docs/plugins/architecture-internals.md +++ b/docs/plugins/architecture-internals.md @@ -64,6 +64,12 @@ to narrow plugin loading before broader registry materialization: imports and startup opt-outs; plugins without startup metadata load only through narrower activation triggers +Request-time runtime preloads that ask for the broad `all` scope still derive an +explicit effective plugin id set from config, startup planning, configured +channels, slots, and auto-enable rules. If that derived set is empty, OpenClaw +loads an empty runtime registry instead of widening to every discoverable +plugin. + The activation planner exposes both an ids-only API for existing callers and a plan API for new diagnostics. Plan entries report why a plugin was selected, separating explicit `activation.*` planner hints from manifest ownership diff --git a/src/cli/plugin-registry.test.ts b/src/cli/plugin-registry.test.ts index c25d5a9c8b2..c8ade757fbc 100644 --- a/src/cli/plugin-registry.test.ts +++ b/src/cli/plugin-registry.test.ts @@ -42,6 +42,8 @@ const mocks = vi.hoisted(() => ({ >(), resolveChannelPluginIds: vi.fn(), + resolveEffectivePluginIds: + vi.fn(), resolvePluginRuntimeLoadContext: vi.fn(), })); @@ -75,6 +77,11 @@ vi.mock("../plugins/channel-plugin-ids.js", () => ({ mocks.resolveChannelPluginIds(...args), })); +vi.mock("../plugins/effective-plugin-ids.js", () => ({ + resolveEffectivePluginIds: (...args: Parameters) => + mocks.resolveEffectivePluginIds(...args), +})); + vi.mock("../plugins/runtime/load-context.js", () => ({ resolvePluginRuntimeLoadContext: ( ...args: Parameters @@ -134,6 +141,7 @@ describe("ensurePluginRegistryLoaded", () => { mocks.resolveConfiguredChannelPluginIds.mockReset(); mocks.resolveDiscoverableScopedChannelPluginIds.mockReset(); mocks.resolveChannelPluginIds.mockReset(); + mocks.resolveEffectivePluginIds.mockReset(); mocks.resolvePluginRuntimeLoadContext.mockReset(); resetPluginRegistryLoadedForTests(); @@ -141,6 +149,7 @@ describe("ensurePluginRegistryLoaded", () => { mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined); mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined); mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]); + mocks.resolveEffectivePluginIds.mockReturnValue(["demo"]); mocks.resolvePluginRuntimeLoadContext.mockImplementation((options) => { const rawConfig = (options?.config ?? {}) as Record; return { @@ -270,6 +279,7 @@ describe("ensurePluginRegistryLoaded", () => { expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( expect.objectContaining({ config, + onlyPluginIds: ["demo"], throwOnLoadError: true, workspaceDir: "/tmp/workspace", }), diff --git a/src/commands/channel-setup/plugin-install.test.ts b/src/commands/channel-setup/plugin-install.test.ts index 66cecc9c7c6..0103ea230ec 100644 --- a/src/commands/channel-setup/plugin-install.test.ts +++ b/src/commands/channel-setup/plugin-install.test.ts @@ -215,7 +215,7 @@ function expectSetupSnapshotDoesNotScopeToPlugin(params: { const firstLoadCall = vi.mocked(loadOpenClawPlugins).mock.calls[0]?.[0] as | { onlyPluginIds?: string[] } | undefined; - expect(firstLoadCall?.onlyPluginIds).toBeUndefined(); + expect(firstLoadCall?.onlyPluginIds).toEqual([]); } beforeEach(() => { @@ -729,7 +729,7 @@ describe("ensureChannelSetupPluginInstalled", () => { }); }); - it("keeps full reloads when the active plugin registry is already populated", () => { + it("does not widen channel reloads when the active plugin registry is already populated", () => { const runtime = makeRuntime(); const cfg: OpenClawConfig = {}; const registry = createEmptyPluginRegistry(); @@ -752,8 +752,8 @@ describe("ensureChannelSetupPluginInstalled", () => { }); expect(loadOpenClawPlugins).toHaveBeenCalledWith( - expect.not.objectContaining({ - onlyPluginIds: expect.anything(), + expect.objectContaining({ + onlyPluginIds: [], }), ); }); @@ -880,7 +880,7 @@ describe("ensureChannelSetupPluginInstalled", () => { expect(getChannelPluginCatalogEntry).toHaveBeenCalledTimes(1); }); - it("does not scope by raw channel id when no trusted plugin mapping exists", () => { + it("does not widen setup snapshots when no trusted plugin mapping exists", () => { const runtime = makeRuntime(); const cfg: OpenClawConfig = {}; @@ -892,8 +892,8 @@ describe("ensureChannelSetupPluginInstalled", () => { }); expect(loadOpenClawPlugins).toHaveBeenCalledWith( - expect.not.objectContaining({ - onlyPluginIds: expect.anything(), + expect.objectContaining({ + onlyPluginIds: [], }), ); }); diff --git a/src/commands/channel-setup/plugin-install.ts b/src/commands/channel-setup/plugin-install.ts index 8ea2569626e..6e1051a1d53 100644 --- a/src/commands/channel-setup/plugin-install.ts +++ b/src/commands/channel-setup/plugin-install.ts @@ -3,11 +3,13 @@ import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.j import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { resolveDiscoverableScopedChannelPluginIds } from "../../plugins/channel-plugin-ids.js"; +import { + resolveConfiguredChannelPluginIds, + resolveDiscoverableScopedChannelPluginIds, +} from "../../plugins/channel-plugin-ids.js"; import { loadOpenClawPlugins } from "../../plugins/loader.js"; import { createPluginLoaderLogger } from "../../plugins/logger.js"; import type { PluginRegistry } from "../../plugins/registry.js"; -import { getActivePluginChannelRegistry } from "../../plugins/runtime.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import { @@ -83,6 +85,14 @@ function loadChannelSetupPluginRegistry(params: { const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(resolvedConfig, resolveDefaultAgentId(resolvedConfig)); + const onlyPluginIds = + params.onlyPluginIds ?? + resolveConfiguredChannelPluginIds({ + config: resolvedConfig, + activationSourceConfig: params.cfg, + workspaceDir, + env: process.env, + }); const log = createSubsystemLogger("plugins"); return loadOpenClawPlugins({ config: resolvedConfig, @@ -91,7 +101,7 @@ function loadChannelSetupPluginRegistry(params: { workspaceDir, cache: false, logger: createPluginLoaderLogger(log), - onlyPluginIds: params.onlyPluginIds, + onlyPluginIds, includeSetupOnlyChannelPlugins: true, forceSetupOnlyChannelPlugins: params.forceSetupOnlyChannelPlugins, activate: params.activate, @@ -137,21 +147,15 @@ export function reloadChannelSetupPluginRegistryForChannel(params: { pluginId?: string; workspaceDir?: string; }): void { - const activeRegistry = getActivePluginChannelRegistry(); const scopedPluginId = resolveScopedChannelPluginId({ cfg: params.cfg, channel: params.channel, pluginId: params.pluginId, workspaceDir: params.workspaceDir, }); - // On low-memory hosts, the empty-registry fallback should only recover the selected - // plugin when we have a trusted channel -> plugin mapping. Otherwise fall back - // to an unscoped reload instead of trusting manifest-declared channel ids. - const onlyPluginIds = - activeRegistry?.plugins.length || !scopedPluginId ? undefined : [scopedPluginId]; loadChannelSetupPluginRegistry({ ...params, - onlyPluginIds, + ...(scopedPluginId ? { onlyPluginIds: [scopedPluginId] } : {}), }); } diff --git a/src/plugins/effective-plugin-ids.test.ts b/src/plugins/effective-plugin-ids.test.ts new file mode 100644 index 00000000000..cd22a238a3f --- /dev/null +++ b/src/plugins/effective-plugin-ids.test.ts @@ -0,0 +1,148 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; + +const mocks = vi.hoisted(() => ({ + applyPluginAutoEnable: + vi.fn(), + listExplicitlyDisabledChannelIdsForConfig: vi.fn(), + listPotentialConfiguredChannelIds: vi.fn(), + listExplicitConfiguredChannelIdsForConfig: vi.fn(), + loadGatewayStartupPluginPlan: + vi.fn(), + resolveConfiguredChannelPluginIds: + vi.fn(), + loadManifestMetadataSnapshot: + vi.fn(), + passesManifestOwnerBasePolicy: + vi.fn(), +})); + +vi.mock("../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: (...args: Parameters) => + mocks.applyPluginAutoEnable(...args), +})); + +vi.mock("../channels/config-presence.js", () => ({ + listExplicitlyDisabledChannelIdsForConfig: ( + ...args: Parameters + ) => mocks.listExplicitlyDisabledChannelIdsForConfig(...args), + listPotentialConfiguredChannelIds: ( + ...args: Parameters + ) => mocks.listPotentialConfiguredChannelIds(...args), +})); + +vi.mock("./channel-plugin-ids.js", () => ({ + listExplicitConfiguredChannelIdsForConfig: ( + ...args: Parameters + ) => mocks.listExplicitConfiguredChannelIdsForConfig(...args), + loadGatewayStartupPluginPlan: (...args: Parameters) => + mocks.loadGatewayStartupPluginPlan(...args), + resolveConfiguredChannelPluginIds: ( + ...args: Parameters + ) => mocks.resolveConfiguredChannelPluginIds(...args), +})); + +vi.mock("./manifest-contract-eligibility.js", () => ({ + loadManifestMetadataSnapshot: (...args: Parameters) => + mocks.loadManifestMetadataSnapshot(...args), +})); + +vi.mock("./manifest-owner-policy.js", () => ({ + passesManifestOwnerBasePolicy: ( + ...args: Parameters + ) => mocks.passesManifestOwnerBasePolicy(...args), +})); + +import { resolveEffectivePluginIds } from "./effective-plugin-ids.js"; + +function resolve(config: OpenClawConfig): string[] { + return resolveEffectivePluginIds({ + config, + env: {}, + workspaceDir: "/workspace", + }); +} + +describe("resolveEffectivePluginIds", () => { + beforeEach(() => { + mocks.applyPluginAutoEnable.mockReset(); + mocks.listExplicitlyDisabledChannelIdsForConfig.mockReset(); + mocks.listPotentialConfiguredChannelIds.mockReset(); + mocks.listExplicitConfiguredChannelIdsForConfig.mockReset(); + mocks.loadGatewayStartupPluginPlan.mockReset(); + mocks.resolveConfiguredChannelPluginIds.mockReset(); + mocks.loadManifestMetadataSnapshot.mockReset(); + mocks.passesManifestOwnerBasePolicy.mockReset(); + + mocks.applyPluginAutoEnable.mockImplementation((params) => ({ + config: params.config ?? {}, + changes: [], + autoEnabledReasons: {}, + })); + mocks.listExplicitlyDisabledChannelIdsForConfig.mockReturnValue([]); + mocks.listPotentialConfiguredChannelIds.mockReturnValue([]); + mocks.listExplicitConfiguredChannelIdsForConfig.mockReturnValue([]); + mocks.loadGatewayStartupPluginPlan.mockReturnValue({ + channelPluginIds: [], + configuredDeferredChannelPluginIds: [], + pluginIds: [], + }); + mocks.resolveConfiguredChannelPluginIds.mockReturnValue([]); + mocks.loadManifestMetadataSnapshot.mockReturnValue({ + plugins: [], + } as unknown as PluginMetadataSnapshot); + mocks.passesManifestOwnerBasePolicy.mockReturnValue(true); + }); + + it("includes a selected context-engine slot even when omitted from explicit allow and entries", () => { + expect( + resolve({ + plugins: { + slots: { contextEngine: "lossless-claw" }, + }, + }), + ).toEqual(["lossless-claw"]); + }); + + it("keeps the built-in legacy context engine out of plugin preload ids", () => { + expect( + resolve({ + plugins: { + slots: { contextEngine: "legacy" }, + }, + }), + ).toEqual([]); + }); + + it.each([ + { + name: "plugins disabled", + plugins: { + enabled: false, + slots: { contextEngine: "lossless-claw" }, + }, + }, + { + name: "denylisted", + plugins: { + deny: ["lossless-claw"], + slots: { contextEngine: "lossless-claw" }, + }, + }, + { + name: "entry disabled", + plugins: { + entries: { + "lossless-claw": { enabled: false }, + }, + slots: { contextEngine: "lossless-claw" }, + }, + }, + ] satisfies Array<{ name: string; plugins: NonNullable }>)( + "does not preload a selected context-engine slot when $name", + ({ plugins }) => { + expect(resolve({ plugins })).toEqual([]); + }, + ); +}); diff --git a/src/plugins/effective-plugin-ids.ts b/src/plugins/effective-plugin-ids.ts index 83a5dba1748..d40b9117bde 100644 --- a/src/plugins/effective-plugin-ids.ts +++ b/src/plugins/effective-plugin-ids.ts @@ -13,6 +13,7 @@ import { import { normalizePluginsConfig } from "./config-state.js"; import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js"; import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js"; +import { defaultSlotIdForKey } from "./slots.js"; function collectConfiguredChannelIds( config: OpenClawConfig, @@ -120,6 +121,24 @@ function collectExplicitEffectivePluginIds(config: OpenClawConfig): string[] { return [...ids].toSorted((left, right) => left.localeCompare(right)); } +function collectSelectedContextEnginePluginIds(config: OpenClawConfig): string[] { + const plugins = normalizePluginsConfig(config.plugins); + if (!plugins.enabled) { + return []; + } + const pluginId = plugins.slots.contextEngine; + if (!pluginId || pluginId === defaultSlotIdForKey("contextEngine")) { + return []; + } + if (plugins.deny.includes(pluginId)) { + return []; + } + if (plugins.entries[pluginId]?.enabled === false) { + return []; + } + return [pluginId]; +} + export function resolveEffectivePluginIds(params: { config: OpenClawConfig; env: NodeJS.ProcessEnv; @@ -132,6 +151,9 @@ export function resolveEffectivePluginIds(params: { }); const effectiveConfig = autoEnabled.config; const ids = new Set(collectExplicitEffectivePluginIds(effectiveConfig)); + for (const pluginId of collectSelectedContextEnginePluginIds(effectiveConfig)) { + ids.add(pluginId); + } const configuredChannelIds = collectConfiguredChannelIds( effectiveConfig, params.config, diff --git a/src/plugins/runtime/runtime-registry-loader.test.ts b/src/plugins/runtime/runtime-registry-loader.test.ts index f28d67d9d4c..aad8235e16c 100644 --- a/src/plugins/runtime/runtime-registry-loader.test.ts +++ b/src/plugins/runtime/runtime-registry-loader.test.ts @@ -13,6 +13,8 @@ const mocks = vi.hoisted(() => ({ vi.fn(), resolveChannelPluginIds: vi.fn(), + resolveEffectivePluginIds: + vi.fn(), applyPluginAutoEnable: vi.fn(), resolveAgentWorkspaceDir: vi.fn< @@ -55,6 +57,11 @@ vi.mock("../channel-plugin-ids.js", () => ({ mocks.resolveChannelPluginIds(...args), })); +vi.mock("../effective-plugin-ids.js", () => ({ + resolveEffectivePluginIds: (...args: Parameters) => + mocks.resolveEffectivePluginIds(...args), +})); + vi.mock("../../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: (...args: Parameters) => mocks.applyPluginAutoEnable(...args), @@ -82,6 +89,7 @@ describe("ensurePluginRegistryLoaded", () => { mocks.resolveConfiguredChannelPluginIds.mockReset(); mocks.resolveDiscoverableScopedChannelPluginIds.mockReset(); mocks.resolveChannelPluginIds.mockReset(); + mocks.resolveEffectivePluginIds.mockReset(); mocks.applyPluginAutoEnable.mockReset(); mocks.resolveAgentWorkspaceDir.mockClear(); mocks.resolveDefaultAgentId.mockClear(); @@ -111,6 +119,7 @@ describe("ensurePluginRegistryLoaded", () => { }, })); mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]); + mocks.resolveEffectivePluginIds.mockReturnValue(["demo"]); }); it("uses the shared runtime load context for configured-channel loads", () => { @@ -328,8 +337,63 @@ describe("ensurePluginRegistryLoaded", () => { ).toBeUndefined(); }); + it("derives all-scope runtime loads from effective plugin ids", () => { + const config = { + plugins: { enabled: true }, + channels: { "demo-channel-a": { enabled: true } }, + }; + const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv; + + mocks.resolveEffectivePluginIds.mockReturnValue(["demo-effective", "demo-hook"]); + + ensurePluginRegistryLoaded({ scope: "all", config: config as never, env }); + + expect(mocks.resolveEffectivePluginIds).toHaveBeenCalledWith({ + config, + env, + workspaceDir: "/resolved-workspace", + }); + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + ...config, + plugins: expect.objectContaining({ + entries: expect.objectContaining({ + demo: { enabled: true }, + }), + }), + }), + onlyPluginIds: ["demo-effective", "demo-hook"], + throwOnLoadError: true, + workspaceDir: "/resolved-workspace", + }), + ); + }); + + it("preserves empty all-scope loads instead of widening to all discovered plugins", () => { + mocks.resolveEffectivePluginIds.mockReturnValue([]); + + ensurePluginRegistryLoaded({ + scope: "all", + config: { plugins: { enabled: true } } as never, + }); + + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: [], + }), + ); + }); + it("reuses a compatible active registry instead of forcing a broad reload", () => { const activeRegistry = createEmptyPluginRegistry(); + activeRegistry.plugins.push({ + id: "demo", + source: "/tmp/demo.js", + origin: "workspace", + enabled: true, + status: "loaded", + } as never); mocks.getActivePluginRegistry.mockReturnValue(activeRegistry); mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(activeRegistry); diff --git a/src/plugins/runtime/runtime-registry-loader.ts b/src/plugins/runtime/runtime-registry-loader.ts index 4354d008762..7456be6ed14 100644 --- a/src/plugins/runtime/runtime-registry-loader.ts +++ b/src/plugins/runtime/runtime-registry-loader.ts @@ -6,6 +6,7 @@ import { resolveConfiguredChannelPluginIds, resolveDiscoverableScopedChannelPluginIds, } from "../channel-plugin-ids.js"; +import { resolveEffectivePluginIds } from "../effective-plugin-ids.js"; import { loadOpenClawPlugins } from "../loader.js"; import { hasExplicitPluginIdScope, @@ -75,6 +76,35 @@ function shouldForwardChannelScope(params: { return !params.scopedLoad && params.scope === "configured-channels"; } +function resolveScopePluginIds(params: { + scope: PluginRegistryScope; + context: ReturnType; +}): string[] { + switch (params.scope) { + case "configured-channels": + return resolveConfiguredChannelPluginIds({ + config: params.context.config, + activationSourceConfig: params.context.activationSourceConfig, + workspaceDir: params.context.workspaceDir, + env: params.context.env, + }); + case "channels": + return resolveChannelPluginIds({ + config: params.context.config, + workspaceDir: params.context.workspaceDir, + env: params.context.env, + }); + case "all": + return resolveEffectivePluginIds({ + config: params.context.rawConfig, + workspaceDir: params.context.workspaceDir, + env: params.context.env, + }); + } + const unreachableScope: never = params.scope; + return unreachableScope; +} + function resolveOrLoadRuntimePluginRegistry( loadOptions: NonNullable[0]>, ): void { @@ -121,33 +151,21 @@ export function ensurePluginRegistryLoaded(options?: { ...requestedChannelOwnerPluginIds, ]); const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds); - const expectedChannelPluginIds = scopedLoad + const expectedPluginIds = scopedLoad ? (requestedPluginIds ?? []) - : scope === "configured-channels" - ? resolveConfiguredChannelPluginIds({ - config: context.config, - activationSourceConfig: context.activationSourceConfig, - workspaceDir: context.workspaceDir, - env: context.env, - }) - : scope === "channels" - ? resolveChannelPluginIds({ - config: context.config, - workspaceDir: context.workspaceDir, - env: context.env, - }) - : []; + : resolveScopePluginIds({ scope, context }); const active = getActivePluginRegistry(); + const requestedPluginIdsForScope = scope === "all" ? expectedPluginIds : undefined; if ( !scopedLoad && scopeRank(pluginRegistryLoaded) >= scopeRank(scope) && - activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds, undefined) + activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIdsForScope) ) { return; } if ( (pluginRegistryLoaded === "none" || scopedLoad) && - activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds, requestedPluginIds) + activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIds) ) { if (!scopedLoad) { pluginRegistryLoaded = scope; @@ -156,20 +174,20 @@ export function ensurePluginRegistryLoaded(options?: { } const scopedConfig = scope === "configured-channels" && - expectedChannelPluginIds.length > 0 && + expectedPluginIds.length > 0 && (!scopedLoad || requestedChannelOwnerPluginIds !== undefined) ? (withActivatedPluginIds({ config: context.config, - pluginIds: expectedChannelPluginIds, + pluginIds: expectedPluginIds, }) ?? context.config) : context.config; const scopedActivationSourceConfig = scope === "configured-channels" && - expectedChannelPluginIds.length > 0 && + expectedPluginIds.length > 0 && (!scopedLoad || requestedChannelOwnerPluginIds !== undefined) ? (withActivatedPluginIds({ config: context.activationSourceConfig, - pluginIds: expectedChannelPluginIds, + pluginIds: expectedPluginIds, }) ?? context.activationSourceConfig) : context.activationSourceConfig; const loadOptions = buildPluginRuntimeLoadOptionsFromValues( @@ -182,8 +200,9 @@ export function ensurePluginRegistryLoaded(options?: { throwOnLoadError: true, ...(hasExplicitPluginIdScope(requestedPluginIds) || shouldForwardChannelScope({ scope, scopedLoad }) || - hasNonEmptyPluginIdScope(expectedChannelPluginIds) - ? { onlyPluginIds: expectedChannelPluginIds } + hasNonEmptyPluginIdScope(expectedPluginIds) || + scope === "all" + ? { onlyPluginIds: expectedPluginIds } : {}), }, );