Files
openclaw/src/plugins/activation-planner.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00

281 lines
9.2 KiB
TypeScript

import { normalizeProviderId } from "../agents/provider-id.js";
import type { OpenClawConfig } from "../config/types.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { uniqueStrings } from "../shared/string-normalization.js";
import { normalizePluginsConfig } from "./config-state.js";
import { 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";
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[];
};
export 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;
};
export function resolveManifestActivationPlan(
params: ResolveManifestActivationPlanParams,
): PluginActivationPlan {
const onlyPluginIdSet = createPluginIdScopeSet(normalizePluginIdScope(params.onlyPluginIds));
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
const registry = params.manifestRecords
? { plugins: params.manifestRecords, diagnostics: [] }
: loadPluginManifestRegistryForPluginRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
includeDisabled: true,
});
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 [];
}
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,
};
}
export function resolveManifestActivationPluginIds(
params: ResolveManifestActivationPlanParams,
): string[] {
return [...resolveManifestActivationPlan(params).pluginIds];
}
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 => !!reason)),
];
}
function normalizeCommandId(value: string | undefined): string {
return normalizeOptionalLowercaseString(value) ?? "";
}