diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 468e498f2aa..2fb4c54b7aa 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -527,9 +527,12 @@ actual behavior such as hooks, tools, commands, or provider flows. Optional manifest `activation` and `setup` blocks stay on the control plane. They are metadata-only descriptors for activation planning and setup discovery; they do not replace runtime registration, `register(...)`, or `setupEntry`. -The first activation consumer now uses manifest command hints to narrow CLI -plugin loading when a primary command is known, instead of always loading every -CLI-capable plugin up front. +The first live activation consumers now use manifest command and provider hints +to narrow plugin loading before broader registry materialization: + +- CLI loading narrows to plugins that own the requested primary command +- explicit provider setup/runtime resolution narrows to plugins that own the + requested provider id Setup discovery now prefers descriptor-owned ids such as `setup.providers` and `setup.cliBackends` to narrow candidate plugins before it falls back to diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 86fa25a102c..1642db8f9de 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -222,8 +222,8 @@ should activate it later. This block is metadata only. It does not register runtime behavior, and it does not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints. Current consumers use it as a narrowing hint before broader plugin loading, so -missing activation metadata only costs performance; it should not change -correctness. +missing activation metadata usually only costs performance; it should not +change correctness while legacy manifest ownership fallbacks still exist. ```json { @@ -245,9 +245,13 @@ correctness. | `onRoutes` | No | `string[]` | Route kinds that should activate this plugin. | | `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. | -For command-triggered planning specifically, OpenClaw still falls back to -legacy `commandAliases[].cliCommand` or `commandAliases[].name` when a plugin -has not added explicit `activation.onCommands` metadata yet. +Current live consumers: + +- command-triggered CLI planning falls back to legacy + `commandAliases[].cliCommand` or `commandAliases[].name` +- provider-triggered setup/runtime planning falls back to legacy + `providers[]` and top-level `cliBackends[]` ownership when explicit provider + activation metadata is missing ## setup reference diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 660c2421758..9f46233b815 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -50,6 +50,8 @@ export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & { export function withActivatedPluginIds(params: { config?: OpenClawConfig; pluginIds: readonly string[]; + overrideGlobalDisable?: boolean; + overrideExplicitDisable?: boolean; }): OpenClawConfig | undefined { if (params.pluginIds.length === 0) { return params.config; @@ -64,12 +66,14 @@ export function withActivatedPluginIds(params: { continue; } allow.add(normalized); + const existingEntry = entries[normalized]; entries[normalized] = { - ...entries[normalized], - enabled: true, + ...existingEntry, + enabled: existingEntry?.enabled !== false || params.overrideExplicitDisable === true, }; } - const forcePluginsEnabled = params.config?.plugins?.enabled === false; + const forcePluginsEnabled = + params.overrideGlobalDisable === true && params.config?.plugins?.enabled === false; return { ...params.config, plugins: { diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index b96da79b9f1..e2f9909277d 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -1,5 +1,6 @@ import { withActivatedPluginIds } from "./activation-context.js"; import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; +import { resolveManifestActivationPluginIds } from "./activation-planner.js"; import { isPluginRegistryLoadInFlight, loadOpenClawPlugins, @@ -7,6 +8,8 @@ import { type PluginLoadOptions, } from "./loader.js"; import { + resolveActivatableProviderOwnerPluginIds, + resolveDiscoverableProviderOwnerPluginIds, resolveDiscoveredProviderPluginIds, resolveEnabledProviderPluginIds, resolveBundledProviderCompatPluginIds, @@ -21,6 +24,54 @@ import { } from "./runtime/load-context.js"; import type { ProviderPlugin } from "./types.js"; +function dedupeSortedPluginIds(values: Iterable): string[] { + return [...new Set(values)].toSorted((left, right) => left.localeCompare(right)); +} + +function resolveExplicitProviderOwnerPluginIds(params: { + providerRefs: readonly string[]; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; +}): string[] { + return dedupeSortedPluginIds( + params.providerRefs.flatMap((provider) => { + const plannedPluginIds = resolveManifestActivationPluginIds({ + trigger: { + kind: "provider", + provider, + }, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + if (plannedPluginIds.length > 0) { + return plannedPluginIds; + } + // Keep legacy provider/CLI-backend ownership working until every owner is + // expressible through activation descriptors. + return ( + resolveOwningPluginIdsForProvider({ + provider, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }) ?? [] + ); + }), + ); +} + +function mergeExplicitOwnerPluginIds( + providerPluginIds: readonly string[], + explicitOwnerPluginIds: readonly string[], +): string[] { + if (explicitOwnerPluginIds.length === 0) { + return [...providerPluginIds]; + } + return dedupeSortedPluginIds([...providerPluginIds, ...explicitOwnerPluginIds]); +} + function resolvePluginProviderLoadBase(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; @@ -32,19 +83,12 @@ function resolvePluginProviderLoadBase(params: { const env = params.env ?? process.env; const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDir(); const providerOwnedPluginIds = params.providerRefs?.length - ? [ - ...new Set( - params.providerRefs.flatMap( - (provider) => - resolveOwningPluginIdsForProvider({ - provider, - config: params.config, - workspaceDir, - env, - }) ?? [], - ), - ), - ] + ? resolveExplicitProviderOwnerPluginIds({ + providerRefs: params.providerRefs, + config: params.config, + workspaceDir, + env, + }) : []; const modelOwnedPluginIds = params.modelRefs?.length ? resolveOwningPluginIdsForModelRefs({ @@ -68,15 +112,16 @@ function resolvePluginProviderLoadBase(params: { ]), ].toSorted((left, right) => left.localeCompare(right)) : undefined; - const runtimeConfig = withActivatedPluginIds({ - config: params.config, - pluginIds: [...providerOwnedPluginIds, ...modelOwnedPluginIds], - }); + const explicitOwnerPluginIds = dedupeSortedPluginIds([ + ...providerOwnedPluginIds, + ...modelOwnedPluginIds, + ]); return { env, workspaceDir, requestedPluginIds, - runtimeConfig, + explicitOwnerPluginIds, + rawConfig: params.config, }; } @@ -85,29 +130,38 @@ function resolveSetupProviderPluginLoadState( base: ReturnType, ) { const providerPluginIds = resolveDiscoveredProviderPluginIds({ - config: base.runtimeConfig, + config: params.config, workspaceDir: base.workspaceDir, env: base.env, onlyPluginIds: base.requestedPluginIds, includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, }); - if (providerPluginIds.length === 0) { + const explicitOwnerPluginIds = resolveDiscoverableProviderOwnerPluginIds({ + pluginIds: base.explicitOwnerPluginIds, + config: params.config, + workspaceDir: base.workspaceDir, + env: base.env, + includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, + }); + const setupPluginIds = mergeExplicitOwnerPluginIds(providerPluginIds, explicitOwnerPluginIds); + if (setupPluginIds.length === 0) { return undefined; } + const setupConfig = withActivatedPluginIds({ + config: base.rawConfig, + pluginIds: setupPluginIds, + }); const loadOptions = buildPluginRuntimeLoadOptionsFromValues( { - config: withActivatedPluginIds({ - config: base.runtimeConfig, - pluginIds: providerPluginIds, - }), - activationSourceConfig: base.runtimeConfig, + config: setupConfig, + activationSourceConfig: setupConfig, autoEnabledReasons: {}, workspaceDir: base.workspaceDir, env: base.env, logger: createPluginRuntimeLoaderLogger(), }, { - onlyPluginIds: providerPluginIds, + onlyPluginIds: setupPluginIds, pluginSdkResolution: params.pluginSdkResolution, cache: params.cache ?? false, activate: params.activate ?? false, @@ -120,11 +174,26 @@ function resolveRuntimeProviderPluginLoadState( params: Parameters[0], base: ReturnType, ) { + const explicitOwnerPluginIds = resolveActivatableProviderOwnerPluginIds({ + pluginIds: base.explicitOwnerPluginIds, + config: base.rawConfig, + workspaceDir: base.workspaceDir, + env: base.env, + includeUntrustedWorkspacePlugins: params.includeUntrustedWorkspacePlugins, + }); + const runtimeRequestedPluginIds = + base.requestedPluginIds !== undefined + ? dedupeSortedPluginIds([...(params.onlyPluginIds ?? []), ...explicitOwnerPluginIds]) + : undefined; + const requestConfig = withActivatedPluginIds({ + config: base.rawConfig, + pluginIds: explicitOwnerPluginIds, + }); const activation = resolveBundledPluginCompatibleActivationInputs({ - rawConfig: base.runtimeConfig, + rawConfig: requestConfig, env: base.env, workspaceDir: base.workspaceDir, - onlyPluginIds: base.requestedPluginIds, + onlyPluginIds: runtimeRequestedPluginIds, applyAutoEnable: true, compatMode: { allowlist: params.bundledProviderAllowlistCompat, @@ -140,12 +209,15 @@ function resolveRuntimeProviderPluginLoadState( env: base.env, }) : activation.config; - const providerPluginIds = resolveEnabledProviderPluginIds({ - config, - workspaceDir: base.workspaceDir, - env: base.env, - onlyPluginIds: base.requestedPluginIds, - }); + const providerPluginIds = mergeExplicitOwnerPluginIds( + resolveEnabledProviderPluginIds({ + config, + workspaceDir: base.workspaceDir, + env: base.env, + onlyPluginIds: runtimeRequestedPluginIds, + }), + explicitOwnerPluginIds, + ); const loadOptions = buildPluginRuntimeLoadOptionsFromValues( { config, diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index e2724dfa2f9..fa74dabf0f3 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -21,7 +21,9 @@ const applyPluginAutoEnableMock = vi.fn(); let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider; let resolveOwningPluginIdsForModelRef: typeof import("./providers.js").resolveOwningPluginIdsForModelRef; +let resolveActivatableProviderOwnerPluginIds: typeof import("./providers.js").resolveActivatableProviderOwnerPluginIds; let resolveEnabledProviderPluginIds: typeof import("./providers.js").resolveEnabledProviderPluginIds; +let resolveDiscoverableProviderOwnerPluginIds: typeof import("./providers.js").resolveDiscoverableProviderOwnerPluginIds; let resolvePluginProviders: typeof import("./providers.runtime.js").resolvePluginProviders; let setActivePluginRegistry: SetActivePluginRegistry; @@ -32,6 +34,8 @@ function createManifestProviderPlugin(params: { origin?: "bundled" | "workspace"; enabledByDefault?: boolean; modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] }; + activation?: PluginManifestRecord["activation"]; + setup?: PluginManifestRecord["setup"]; }): PluginManifestRecord { return { id: params.id, @@ -40,6 +44,8 @@ function createManifestProviderPlugin(params: { providers: params.providerIds, cliBackends: params.cliBackends ?? [], modelSupport: params.modelSupport, + activation: params.activation, + setup: params.setup, skills: [], hooks: [], origin: params.origin ?? "bundled", @@ -280,9 +286,11 @@ describe("resolvePluginProviders", () => { loadPluginManifestRegistryMock(...args), })); ({ + resolveActivatableProviderOwnerPluginIds, resolveOwningPluginIdsForProvider, resolveOwningPluginIdsForModelRef, resolveEnabledProviderPluginIds, + resolveDiscoverableProviderOwnerPluginIds, } = await import("./providers.js")); ({ resolvePluginProviders } = await import("./providers.runtime.js")); ({ setActivePluginRegistry } = await import("./runtime.js")); @@ -538,7 +546,7 @@ describe("resolvePluginProviders", () => { "workspace-provider", ]), entries: expect.objectContaining({ - google: { enabled: true }, + google: { enabled: false }, kilocode: { enabled: true }, moonshot: { enabled: true }, "workspace-provider": { enabled: true }, @@ -709,6 +717,414 @@ describe("resolvePluginProviders", () => { }), ); }); + + it("uses activation.onProviders to keep explicit provider owners on the runtime path", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "activation-owned-provider", + providerIds: [], + activation: { + onProviders: ["activation-owned"], + }, + }), + ]); + + resolvePluginProviders({ + config: {}, + providerRefs: ["activation-owned"], + activate: true, + }); + + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["activation-owned-provider"], + activate: true, + config: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: ["activation-owned-provider"], + entries: { + "activation-owned-provider": { enabled: true }, + }, + }), + }), + }), + ); + }); + + it("does not activate explicit runtime owners when plugins are globally disabled", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "activation-owned-provider", + providerIds: [], + activation: { + onProviders: ["activation-owned"], + }, + }), + ]); + + expect( + resolveActivatableProviderOwnerPluginIds({ + pluginIds: ["activation-owned-provider"], + config: { + plugins: { + enabled: false, + }, + }, + }), + ).toEqual([]); + }); + + it("does not activate explicit runtime owners disabled in config", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "activation-owned-provider", + providerIds: [], + activation: { + onProviders: ["activation-owned"], + }, + }), + ]); + + expect( + resolveActivatableProviderOwnerPluginIds({ + pluginIds: ["activation-owned-provider"], + config: { + plugins: { + entries: { + "activation-owned-provider": { enabled: false }, + }, + }, + }, + }), + ).toEqual([]); + }); + + it("does not activate explicit runtime owners outside the allowlist", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "activation-owned-provider", + providerIds: [], + activation: { + onProviders: ["activation-owned"], + }, + }), + ]); + + expect( + resolveActivatableProviderOwnerPluginIds({ + pluginIds: ["activation-owned-provider"], + config: { + plugins: { + allow: ["other-plugin"], + }, + }, + }), + ).toEqual([]); + }); + + it("uses setup.providers to keep explicit provider owners on the setup path", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "setup-owned-provider", + providerIds: [], + setup: { + providers: [{ id: "setup-owned" }], + }, + }), + ]); + + resolvePluginProviders({ + config: {}, + providerRefs: ["setup-owned"], + activate: true, + mode: "setup", + }); + + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["setup-owned-provider"], + activate: true, + config: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: ["setup-owned-provider"], + entries: { + "setup-owned-provider": { enabled: true }, + }, + }), + }), + }), + ); + }); + + it("does not override global plugin disable during setup owner loading", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "setup-owned-provider", + providerIds: [], + setup: { + providers: [{ id: "setup-owned" }], + }, + }), + ]); + + resolvePluginProviders({ + config: { + plugins: { + enabled: false, + }, + }, + providerRefs: ["setup-owned"], + activate: true, + mode: "setup", + }); + + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + plugins: expect.objectContaining({ + enabled: false, + allow: ["setup-owned-provider"], + }), + }), + }), + ); + }); + + it("does not override explicitly disabled setup owners", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "setup-owned-provider", + providerIds: [], + setup: { + providers: [{ id: "setup-owned" }], + }, + }), + ]); + + resolvePluginProviders({ + config: { + plugins: { + entries: { + "setup-owned-provider": { enabled: false }, + }, + }, + }, + providerRefs: ["setup-owned"], + activate: true, + mode: "setup", + }); + + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: ["setup-owned-provider"], + entries: { + "setup-owned-provider": { enabled: false }, + }, + }), + }), + }), + ); + }); + + it("filters explicit setup owners through the untrusted workspace discovery gate", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "workspace-activation-owner", + providerIds: [], + origin: "workspace", + activation: { + onProviders: ["workspace-activation"], + }, + }), + ]); + + const providers = resolvePluginProviders({ + config: {}, + providerRefs: ["workspace-activation"], + activate: true, + mode: "setup", + includeUntrustedWorkspacePlugins: false, + }); + + expect(providers).toEqual([]); + expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + }); + + it("does not auto-activate untrusted workspace runtime owners when requested", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "workspace-activation-owner", + providerIds: [], + origin: "workspace", + activation: { + onProviders: ["workspace-activation"], + }, + }), + ]); + resolveRuntimePluginRegistryMock.mockReturnValue(createEmptyPluginRegistry()); + + const providers = resolvePluginProviders({ + config: {}, + providerRefs: ["workspace-activation"], + activate: true, + includeUntrustedWorkspacePlugins: false, + }); + + expect(providers).toEqual([]); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: {}, + onlyPluginIds: [], + }), + ); + }); + + it("does not auto-activate workspace runtime owners by default", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "workspace-activation-owner", + providerIds: [], + origin: "workspace", + activation: { + onProviders: ["workspace-activation"], + }, + }), + ]); + resolveRuntimePluginRegistryMock.mockReturnValue(createEmptyPluginRegistry()); + + const providers = resolvePluginProviders({ + config: {}, + providerRefs: ["workspace-activation"], + activate: true, + }); + + expect(providers).toEqual([]); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: {}, + onlyPluginIds: [], + }), + ); + }); + + it("keeps explicit provider requests scoped when runtime owner activation resolves nothing", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "activation-owned-provider", + providerIds: [], + activation: { + onProviders: ["activation-owned"], + }, + }), + ]); + resolveRuntimePluginRegistryMock.mockReturnValue(createEmptyPluginRegistry()); + + const providers = resolvePluginProviders({ + config: { + plugins: { + allow: ["other-plugin"], + }, + }, + providerRefs: ["activation-owned"], + activate: true, + }); + + expect(providers).toEqual([]); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: { + plugins: { + allow: ["other-plugin"], + }, + }, + onlyPluginIds: [], + }), + ); + }); + + it("keeps explicitly trusted disabled workspace setup owners discoverable", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "workspace-activation-owner", + providerIds: [], + origin: "workspace", + activation: { + onProviders: ["workspace-activation"], + }, + }), + ]); + + expect( + resolveDiscoverableProviderOwnerPluginIds({ + pluginIds: ["workspace-activation-owner"], + config: { + plugins: { + enabled: true, + allow: ["workspace-activation-owner"], + entries: { + "workspace-activation-owner": { enabled: false }, + }, + }, + }, + includeUntrustedWorkspacePlugins: false, + }), + ).toEqual(["workspace-activation-owner"]); + }); + + it("does not auto-activate explicitly disabled trusted workspace runtime owners", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "workspace-activation-owner", + providerIds: [], + origin: "workspace", + activation: { + onProviders: ["workspace-activation"], + }, + }), + ]); + + expect( + resolveActivatableProviderOwnerPluginIds({ + pluginIds: ["workspace-activation-owner"], + config: { + plugins: { + allow: ["workspace-activation-owner"], + entries: { + "workspace-activation-owner": { enabled: false }, + }, + }, + }, + includeUntrustedWorkspacePlugins: false, + }), + ).toEqual([]); + }); + + it("keeps legacy CLI backend ownership as the explicit provider fallback", () => { + setOwningProviderManifestPlugins(); + + resolvePluginProviders({ + config: {}, + providerRefs: ["claude-cli"], + activate: true, + }); + + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["anthropic"], + activate: true, + config: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: ["anthropic"], + entries: { + anthropic: { enabled: true }, + }, + }), + }), + }), + ); + }); it.each([ { provider: "minimax-portal", diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 19958a315ad..7ad3e9ea75b 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -89,33 +89,145 @@ export function resolveDiscoveredProviderPluginIds(params: { if (!(plugin.providers.length > 0 && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)))) { return false; } - if (!shouldFilterUntrustedWorkspacePlugins || plugin.origin !== "workspace") { - return true; - } - const activation = resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedConfig, + return isProviderPluginEligibleForSetupDiscovery({ + plugin, + shouldFilterUntrustedWorkspacePlugins, + normalizedConfig, rootConfig: params.config, - enabledByDefault: plugin.enabledByDefault, }); - if (activation.activated) { - return true; - } - const explicitlyTrustedButDisabled = - normalizedConfig.enabled && - !normalizedConfig.deny.includes(plugin.id) && - normalizedConfig.allow.includes(plugin.id) && - normalizedConfig.entries[plugin.id]?.enabled === false; - return explicitlyTrustedButDisabled; }) .map((plugin) => plugin.id) .toSorted((left, right) => left.localeCompare(right)); } +function isProviderPluginEligibleForSetupDiscovery(params: { + plugin: PluginManifestRecord; + shouldFilterUntrustedWorkspacePlugins: boolean; + normalizedConfig: ReturnType; + rootConfig?: PluginLoadOptions["config"]; +}): boolean { + if (!params.shouldFilterUntrustedWorkspacePlugins || params.plugin.origin !== "workspace") { + return true; + } + const activation = resolveEffectivePluginActivationState({ + id: params.plugin.id, + origin: params.plugin.origin, + config: params.normalizedConfig, + rootConfig: params.rootConfig, + enabledByDefault: params.plugin.enabledByDefault, + }); + if (activation.activated) { + return true; + } + const explicitlyTrustedButDisabled = + params.normalizedConfig.enabled && + !params.normalizedConfig.deny.includes(params.plugin.id) && + params.normalizedConfig.allow.includes(params.plugin.id) && + params.normalizedConfig.entries[params.plugin.id]?.enabled === false; + return explicitlyTrustedButDisabled; +} + +export function resolveDiscoverableProviderOwnerPluginIds(params: { + pluginIds: readonly string[]; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + includeUntrustedWorkspacePlugins?: boolean; +}): string[] { + if (params.pluginIds.length === 0) { + return []; + } + const pluginIdSet = new Set(params.pluginIds); + const registry = loadPluginManifestRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false; + const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + return registry.plugins + .filter( + (plugin) => + pluginIdSet.has(plugin.id) && + isProviderPluginEligibleForSetupDiscovery({ + plugin, + shouldFilterUntrustedWorkspacePlugins, + normalizedConfig, + rootConfig: params.config, + }), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +function isProviderPluginEligibleForRuntimeOwnerActivation(params: { + plugin: PluginManifestRecord; + normalizedConfig: ReturnType; + rootConfig?: PluginLoadOptions["config"]; +}): boolean { + if (!params.normalizedConfig.enabled) { + return false; + } + if (params.normalizedConfig.deny.includes(params.plugin.id)) { + return false; + } + if (params.normalizedConfig.entries[params.plugin.id]?.enabled === false) { + return false; + } + if ( + params.normalizedConfig.allow.length > 0 && + !params.normalizedConfig.allow.includes(params.plugin.id) + ) { + return false; + } + if (params.plugin.origin !== "workspace") { + return true; + } + return resolveEffectivePluginActivationState({ + id: params.plugin.id, + origin: params.plugin.origin, + config: params.normalizedConfig, + rootConfig: params.rootConfig, + enabledByDefault: params.plugin.enabledByDefault, + }).activated; +} + +export function resolveActivatableProviderOwnerPluginIds(params: { + pluginIds: readonly string[]; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + includeUntrustedWorkspacePlugins?: boolean; +}): string[] { + if (params.pluginIds.length === 0) { + return []; + } + const pluginIdSet = new Set(params.pluginIds); + const registry = loadPluginManifestRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + return registry.plugins + .filter( + (plugin) => + pluginIdSet.has(plugin.id) && + isProviderPluginEligibleForRuntimeOwnerActivation({ + plugin, + normalizedConfig, + rootConfig: params.config, + }), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + export const __testing = { + resolveActivatableProviderOwnerPluginIds, resolveEnabledProviderPluginIds, resolveDiscoveredProviderPluginIds, + resolveDiscoverableProviderOwnerPluginIds, resolveBundledProviderCompatPluginIds, withBundledProviderVitestCompat, } as const;