/** Computes which manifest-owned plugins need activation for commands, routes, providers, or capabilities. */ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig } from "../config/types.js"; import { normalizePluginsConfig } from "./config-state.js"; import { hasExplicitManifestOwnerTrust, isBundledManifestOwner, passesManifestOwnerBasePolicy, } from "./manifest-owner-policy.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; import type { PluginManifestActivationCapability } from "./manifest.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions.js"; import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js"; /** Runtime surface that can request a lazily activated plugin owner. */ export type PluginActivationPlannerTrigger = | { kind: "command"; command: string } | { kind: "provider"; provider: string } | { kind: "agentHarness"; runtime: string } | { kind: "channel"; channel: string } | { kind: "route"; route: string } | { kind: "capability"; capability: PluginManifestActivationCapability }; export type PluginActivationPlannerHintReason = | "activation-agent-harness-hint" | "activation-capability-hint" | "activation-channel-hint" | "activation-command-hint" | "activation-provider-hint" | "activation-route-hint"; export type PluginActivationPlannerManifestReason = | "manifest-channel-owner" | "manifest-command-alias" | "manifest-hook-owner" | "manifest-provider-owner" | "manifest-setup-provider-owner" | "manifest-tool-contract"; export type PluginActivationPlannerReason = | PluginActivationPlannerHintReason | PluginActivationPlannerManifestReason; export type PluginActivationPlanEntry = { pluginId: string; origin: PluginOrigin; reasons: readonly PluginActivationPlannerReason[]; }; type PluginActivationPlan = { trigger: PluginActivationPlannerTrigger; pluginIds: readonly string[]; entries: readonly PluginActivationPlanEntry[]; diagnostics: readonly PluginDiagnostic[]; }; type ResolveManifestActivationPlanParams = { trigger: PluginActivationPlannerTrigger; config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; origin?: PluginOrigin; onlyPluginIds?: readonly string[]; manifestRecords?: readonly PluginManifestRecord[]; allowRestrictiveAllowlistBypass?: boolean; requireExplicitManifestOwnerTrust?: boolean; }; /** Returns a deterministic activation plan without importing plugin runtime modules. */ export function resolveManifestActivationPlan( params: ResolveManifestActivationPlanParams, ): PluginActivationPlan { const onlyPluginIdSet = createPluginIdScopeSet(normalizePluginIdScope(params.onlyPluginIds)); const registry = params.manifestRecords ? { plugins: params.manifestRecords, diagnostics: [] } : loadPluginManifestRegistryForPluginRegistry({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, includeDisabled: true, }); const normalizedConfig = normalizePluginsConfig(params.config?.plugins); const entries = registry.plugins .flatMap((plugin) => { if (params.origin && plugin.origin !== params.origin) { return []; } if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) { return []; } if ( !passesManifestOwnerBasePolicy({ plugin, normalizedConfig, allowRestrictiveAllowlistBypass: params.allowRestrictiveAllowlistBypass, }) ) { return []; } if ( params.requireExplicitManifestOwnerTrust && !hasExplicitActivationPlannerManifestOwnerTrust({ plugin, normalizedConfig, }) ) { return []; } const reasons = listManifestActivationTriggerReasons(plugin, params.trigger); if (reasons.length === 0) { return []; } return [ { pluginId: plugin.id, origin: plugin.origin, reasons, } satisfies PluginActivationPlanEntry, ]; }) .toSorted((left, right) => left.pluginId.localeCompare(right.pluginId)); return { trigger: params.trigger, pluginIds: uniqueStrings(entries.map((entry) => entry.pluginId)), entries, diagnostics: registry.diagnostics, }; } /** Convenience wrapper for callers that only need plugin ids from the activation plan. */ export function resolveManifestActivationPluginIds( params: ResolveManifestActivationPlanParams, ): string[] { return [...resolveManifestActivationPlan(params).pluginIds]; } function hasExplicitActivationPlannerManifestOwnerTrust(params: { plugin: Pick; normalizedConfig: ReturnType; }): boolean { // plugins.load.paths is already an operator-selected trust boundary. Keep // the trust local to planner callers so setup-only channel imports retain // their stricter scoped-import policy. return ( isBundledManifestOwner(params.plugin) || params.plugin.origin === "config" || hasExplicitManifestOwnerTrust({ plugin: params.plugin, normalizedConfig: params.normalizedConfig, }) ); } function listManifestActivationTriggerReasons( plugin: PluginManifestRecord, trigger: PluginActivationPlannerTrigger, ): PluginActivationPlannerReason[] { switch (trigger.kind) { case "command": return listCommandTriggerReasons(plugin, normalizeCommandId(trigger.command)); case "provider": return listProviderTriggerReasons(plugin, normalizeProviderId(trigger.provider)); case "agentHarness": return listAgentHarnessTriggerReasons(plugin, normalizeCommandId(trigger.runtime)); case "channel": return listChannelTriggerReasons(plugin, normalizeCommandId(trigger.channel)); case "route": return listRouteTriggerReasons(plugin, normalizeCommandId(trigger.route)); case "capability": return listCapabilityTriggerReasons(plugin, trigger.capability); } const unreachableTrigger: never = trigger; return unreachableTrigger; } function listAgentHarnessTriggerReasons( plugin: PluginManifestRecord, runtime: string, ): PluginActivationPlannerReason[] { return listHasNormalizedValue(plugin.activation?.onAgentHarnesses, runtime, normalizeCommandId) ? ["activation-agent-harness-hint"] : []; } function listCommandTriggerReasons( plugin: PluginManifestRecord, command: string, ): PluginActivationPlannerReason[] { return dedupeReasons([ listHasNormalizedValue(plugin.activation?.onCommands, command, normalizeCommandId) ? "activation-command-hint" : null, listHasNormalizedValue( (plugin.commandAliases ?? []).flatMap((alias) => alias.cliCommand ?? alias.name), command, normalizeCommandId, ) ? "manifest-command-alias" : null, ]); } function listProviderTriggerReasons( plugin: PluginManifestRecord, provider: string, ): PluginActivationPlannerReason[] { return dedupeReasons([ listHasNormalizedValue(plugin.activation?.onProviders, provider, normalizeProviderId) ? "activation-provider-hint" : null, listHasNormalizedValue(plugin.providers, provider, normalizeProviderId) ? "manifest-provider-owner" : null, listHasNormalizedValue( plugin.setup?.providers?.map((setupProvider) => setupProvider.id), provider, normalizeProviderId, ) ? "manifest-setup-provider-owner" : null, ]); } function listChannelTriggerReasons( plugin: PluginManifestRecord, channel: string, ): PluginActivationPlannerReason[] { return dedupeReasons([ listHasNormalizedValue(plugin.activation?.onChannels, channel, normalizeCommandId) ? "activation-channel-hint" : null, listHasNormalizedValue(plugin.channels, channel, normalizeCommandId) ? "manifest-channel-owner" : null, ]); } function listRouteTriggerReasons( plugin: PluginManifestRecord, route: string, ): PluginActivationPlannerReason[] { return listHasNormalizedValue(plugin.activation?.onRoutes, route, normalizeCommandId) ? ["activation-route-hint"] : []; } function listCapabilityTriggerReasons( plugin: PluginManifestRecord, capability: PluginManifestActivationCapability, ): PluginActivationPlannerReason[] { switch (capability) { case "provider": return dedupeReasons([ plugin.activation?.onCapabilities?.includes(capability) ? "activation-capability-hint" : null, hasValues(plugin.activation?.onProviders) ? "activation-provider-hint" : null, hasValues(plugin.providers) ? "manifest-provider-owner" : null, hasValues(plugin.setup?.providers) ? "manifest-setup-provider-owner" : null, ]); case "channel": return dedupeReasons([ plugin.activation?.onCapabilities?.includes(capability) ? "activation-capability-hint" : null, hasValues(plugin.activation?.onChannels) ? "activation-channel-hint" : null, hasValues(plugin.channels) ? "manifest-channel-owner" : null, ]); case "tool": return dedupeReasons([ plugin.activation?.onCapabilities?.includes(capability) ? "activation-capability-hint" : null, hasValues(plugin.contracts?.tools) ? "manifest-tool-contract" : null, ]); case "hook": return dedupeReasons([ plugin.activation?.onCapabilities?.includes(capability) ? "activation-capability-hint" : null, hasValues(plugin.hooks) ? "manifest-hook-owner" : null, ]); } const unreachableCapability: never = capability; return unreachableCapability; } function listHasNormalizedValue( values: readonly string[] | undefined, expected: string, normalize: (value: string) => string, ): boolean { return values?.some((value) => normalize(value) === expected) ?? false; } function hasValues(values: readonly unknown[] | undefined): boolean { return (values?.length ?? 0) > 0; } function dedupeReasons( reasons: readonly (PluginActivationPlannerReason | null)[], ): PluginActivationPlannerReason[] { return [ ...new Set( reasons.filter((reason): reason is PluginActivationPlannerReason => Boolean(reason)), ), ]; } function normalizeCommandId(value: string | undefined): string { return normalizeOptionalLowercaseString(value) ?? ""; }