feat(plugins): expose activation plan reasons (#70943)

This commit is contained in:
Vincent Koc
2026-04-23 22:06:07 -07:00
committed by GitHub
parent b2840b93c8
commit 799a42bd13
7 changed files with 481 additions and 111 deletions

View File

@@ -61,6 +61,14 @@ to narrow plugin loading before broader registry materialization:
- explicit provider setup/runtime resolution narrows to plugins that own the
requested provider id
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
fallback such as `providers`, `channels`, `commandAliases`, `setup.providers`,
`contracts.tools`, and hooks. That reason split is the compatibility boundary:
existing plugin metadata keeps working, while new code can detect broad hints
or fallback behavior without changing runtime loading semantics.
Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
`setup.cliBackends` to narrow candidate plugins before it falls back to
`setup-api` for plugins that still need setup-time runtime hooks. If more than

View File

@@ -153,6 +153,26 @@ The important design boundary:
That split lets OpenClaw validate config, explain missing/disabled plugins, and
build UI/schema hints before the full runtime is active.
### Activation planning
Activation planning is part of the control plane. Callers can ask which plugins
are relevant to a concrete command, provider, channel, route, agent harness, or
capability before loading broader runtime registries.
The planner keeps current manifest behavior compatible:
- `activation.*` fields are explicit planner hints
- `providers`, `channels`, `commandAliases`, `setup.providers`,
`contracts.tools`, and hooks remain manifest ownership fallback
- the ids-only planner API stays available for existing callers
- the plan API reports reason labels so diagnostics can distinguish explicit
hints from ownership fallback
Do not treat `activation` as a lifecycle hook or a replacement for
`register(...)`. It is metadata used to narrow loading. Prefer ownership fields
when they already describe the relationship; use `activation` only for extra
planner hints.
### Channel plugins and the shared message tool
Channel plugins do not need to register a separate send/edit/react tool for

View File

@@ -152,7 +152,7 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
| `providerAuthAliases` | No | `Record<string, string>` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. |
| `channelEnvVars` | No | `Record<string, string[]>` | Cheap channel env metadata that OpenClaw can inspect without loading plugin code. Use this for env-driven channel setup or auth surfaces that generic startup/config helpers should see. |
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
| `activation` | No | `object` | Cheap activation hints for provider, command, channel, route, and capability-triggered loading. Metadata only; plugin runtime still owns actual behavior. |
| `activation` | No | `object` | Cheap activation planner metadata for provider, command, channel, route, and capability-triggered loading. Metadata only; plugin runtime still owns actual behavior. |
| `setup` | No | `object` | Cheap setup/onboarding descriptors that discovery and setup surfaces can inspect without loading plugin runtime. |
| `qaRunners` | No | `object[]` | Cheap QA runner descriptors used by the shared `openclaw qa` host before plugin runtime loads. |
| `contracts` | No | `object` | Static bundled capability snapshot for external auth hooks, speech, realtime transcription, realtime voice, media-understanding, image-generation, music-generation, video-generation, web-fetch, web search, and tool ownership. |
@@ -215,7 +215,62 @@ uses this metadata for diagnostics without importing plugin runtime code.
## activation reference
Use `activation` when the plugin can cheaply declare which control-plane events
should activate it later.
should include it in an activation/load plan.
This block is planner metadata, not a lifecycle API. It does not register
runtime behavior, does not replace `register(...)`, and does not promise that
plugin code has already executed. The activation planner uses these fields to
narrow candidate plugins before falling back to existing manifest ownership
metadata such as `providers`, `channels`, `commandAliases`, `setup.providers`,
`contracts.tools`, and hooks.
Prefer the narrowest metadata that already describes ownership. Use
`providers`, `channels`, `commandAliases`, setup descriptors, or `contracts`
when those fields express the relationship. Use `activation` for extra planner
hints that cannot be represented by those ownership fields.
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 usually only costs performance; it should not
change correctness while legacy manifest ownership fallbacks still exist.
```json
{
"activation": {
"onProviders": ["openai"],
"onCommands": ["models"],
"onChannels": ["web"],
"onRoutes": ["gateway-webhook"],
"onCapabilities": ["provider", "tool"]
}
}
```
| Field | Required | Type | What it means |
| ---------------- | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `onProviders` | No | `string[]` | Provider ids that should include this plugin in activation/load plans. |
| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. |
| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. |
| `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. |
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. |
Current live consumers:
- command-triggered CLI planning falls back to legacy
`commandAliases[].cliCommand` or `commandAliases[].name`
- channel-triggered setup/channel planning falls back to legacy `channels[]`
ownership when explicit channel activation metadata is missing
- provider-triggered setup/runtime planning falls back to legacy
`providers[]` and top-level `cliBackends[]` ownership when explicit provider
activation metadata is missing
Planner diagnostics can distinguish explicit activation hints from manifest
ownership fallback. For example, `activation-command-hint` means
`activation.onCommands` matched, while `manifest-command-alias` means the
planner used `commandAliases` ownership instead. These reason labels are for
host diagnostics and tests; plugin authors should keep declaring the metadata
that best describes ownership.
## qaRunners reference
@@ -240,42 +295,6 @@ runtime still owns actual CLI registration through a lightweight
| `commandName` | Yes | `string` | Subcommand mounted beneath `openclaw qa`, for example `matrix`. |
| `description` | No | `string` | Fallback help text used when the shared host needs a stub command. |
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 usually only costs performance; it should not
change correctness while legacy manifest ownership fallbacks still exist.
```json
{
"activation": {
"onProviders": ["openai"],
"onCommands": ["models"],
"onChannels": ["web"],
"onRoutes": ["gateway-webhook"],
"onCapabilities": ["provider", "tool"]
}
}
```
| Field | Required | Type | What it means |
| ---------------- | -------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
| `onProviders` | No | `string[]` | Provider ids that should activate this plugin when requested. |
| `onCommands` | No | `string[]` | Command ids that should activate this plugin. |
| `onChannels` | No | `string[]` | Channel ids that should activate this plugin. |
| `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. |
Current live consumers:
- command-triggered CLI planning falls back to legacy
`commandAliases[].cliCommand` or `commandAliases[].name`
- channel-triggered setup/channel planning falls back to legacy `channels[]`
ownership when explicit channel activation metadata is missing
- provider-triggered setup/runtime planning falls back to legacy
`providers[]` and top-level `cliBackends[]` ownership when explicit provider
activation metadata is missing
## setup reference
Use `setup` when setup and onboarding surfaces need cheap plugin-owned metadata

View File

@@ -28,6 +28,12 @@ Both surfaces are now **deprecated**. They still work at runtime, but new
plugins must not use them, and existing plugins should migrate before the next
major release removes them.
OpenClaw does not remove or reinterpret documented plugin behavior in the same
change that introduces a replacement. Breaking contract changes must first go
through a compatibility adapter, diagnostics, docs, and a deprecation window.
That applies to SDK imports, manifest fields, setup APIs, hooks, and runtime
registration behavior.
<Warning>
The backwards-compatibility layer will be removed in a future major release.
Plugins that still import from these surfaces will break when that happens.
@@ -62,6 +68,22 @@ Current bundled provider examples:
- OpenRouter keeps provider builder and onboarding/config helpers in its own
`api.ts`
## Compatibility policy
For external plugins, compatibility work follows this order:
1. add the new contract
2. keep the old behavior wired through a compatibility adapter
3. emit a diagnostic or warning that names the old path and replacement
4. cover both paths in tests
5. document the deprecation and migration path
6. remove only after the announced migration window, usually in a major release
If a manifest field is still accepted, plugin authors can keep using it until
the docs and diagnostics say otherwise. New code should prefer the documented
replacement, but existing plugins should not break during ordinary minor
releases.
## How to migrate
<Steps>

View File

@@ -9,10 +9,12 @@ vi.mock("./manifest-registry.js", () => ({
}));
let resolveManifestActivationPluginIds: typeof import("./activation-planner.js").resolveManifestActivationPluginIds;
let resolveManifestActivationPlan: typeof import("./activation-planner.js").resolveManifestActivationPlan;
describe("resolveManifestActivationPluginIds", () => {
describe("activation planner", () => {
beforeAll(async () => {
({ resolveManifestActivationPluginIds } = await import("./activation-planner.js"));
({ resolveManifestActivationPlan, resolveManifestActivationPluginIds } =
await import("./activation-planner.js"));
});
beforeEach(() => {
@@ -75,7 +77,7 @@ describe("resolveManifestActivationPluginIds", () => {
});
});
it("matches command triggers from activation metadata and legacy command aliases", () => {
it("keeps ids-only command planning stable", () => {
expect(
resolveManifestActivationPluginIds({
trigger: {
@@ -104,7 +106,7 @@ describe("resolveManifestActivationPluginIds", () => {
).toEqual(["demo-channel"]);
});
it("matches provider, agent harness, channel, and route triggers from manifest-owned metadata", () => {
it("keeps ids-only provider, agent harness, channel, and route planning stable", () => {
expect(
resolveManifestActivationPluginIds({
trigger: {
@@ -151,7 +153,7 @@ describe("resolveManifestActivationPluginIds", () => {
).toEqual(["demo-channel"]);
});
it("matches capability triggers from explicit hints or existing manifest ownership", () => {
it("keeps ids-only capability planning stable", () => {
expect(
resolveManifestActivationPluginIds({
trigger: {
@@ -180,6 +182,167 @@ describe("resolveManifestActivationPluginIds", () => {
).toEqual(["demo-channel"]);
});
it("returns a richer activation plan with planner-hint reasons", () => {
expect(
resolveManifestActivationPlan({
trigger: {
kind: "command",
command: "demo-tools",
},
}),
).toMatchObject({
pluginIds: ["demo-channel"],
entries: [
{
pluginId: "demo-channel",
origin: "workspace",
reasons: ["activation-command-hint"],
},
],
diagnostics: [],
});
expect(
resolveManifestActivationPlan({
trigger: {
kind: "agentHarness",
runtime: "codex",
},
}).entries,
).toEqual([
{
pluginId: "openai",
origin: "bundled",
reasons: ["activation-agent-harness-hint"],
},
]);
expect(
resolveManifestActivationPlan({
trigger: {
kind: "route",
route: "webhook",
},
}).entries,
).toEqual([
{
pluginId: "demo-channel",
origin: "workspace",
reasons: ["activation-route-hint"],
},
]);
});
it("returns manifest-owner reasons when activation hints are absent", () => {
expect(
resolveManifestActivationPlan({
trigger: {
kind: "provider",
provider: "openai",
},
}).entries,
).toEqual([
{
pluginId: "openai",
origin: "bundled",
reasons: ["manifest-provider-owner"],
},
]);
expect(
resolveManifestActivationPlan({
trigger: {
kind: "provider",
provider: "openai-codex",
},
}).entries,
).toEqual([
{
pluginId: "openai",
origin: "bundled",
reasons: ["manifest-setup-provider-owner"],
},
]);
expect(
resolveManifestActivationPlan({
trigger: {
kind: "channel",
channel: "telegram",
},
}).entries,
).toEqual([
{
pluginId: "demo-channel",
origin: "workspace",
reasons: ["manifest-channel-owner"],
},
]);
});
it("returns capability reasons from explicit hints and manifest ownership", () => {
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "explicit-provider",
providers: [],
channels: [],
cliBackends: [],
skills: [],
hooks: [],
activation: {
onCapabilities: ["provider"],
onProviders: ["custom-provider"],
},
origin: "workspace",
},
{
id: "owned-tool",
providers: [],
channels: [],
cliBackends: [],
skills: [],
hooks: [],
contracts: {
tools: ["custom-tool"],
},
origin: "workspace",
},
],
diagnostics: [],
});
expect(
resolveManifestActivationPlan({
trigger: {
kind: "capability",
capability: "provider",
},
}).entries,
).toEqual([
{
pluginId: "explicit-provider",
origin: "workspace",
reasons: ["activation-capability-hint", "activation-provider-hint"],
},
]);
expect(
resolveManifestActivationPlan({
trigger: {
kind: "capability",
capability: "tool",
},
}).entries,
).toEqual([
{
pluginId: "owned-tool",
origin: "workspace",
reasons: ["manifest-tool-contract"],
},
]);
});
it("treats explicit empty plugin scopes as scoped-empty", () => {
expect(
resolveManifestActivationPluginIds({

View File

@@ -2,6 +2,7 @@ import { normalizeProviderId } from "../agents/provider-id.js";
import type { OpenClawConfig } from "../config/types.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { loadPluginManifestRegistry, 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 { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js";
@@ -14,7 +15,40 @@ export type PluginActivationPlannerTrigger =
| { kind: "route"; route: string }
| { kind: "capability"; capability: PluginManifestActivationCapability };
export function resolveManifestActivationPluginIds(params: {
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;
@@ -22,104 +56,208 @@ export function resolveManifestActivationPluginIds(params: {
cache?: boolean;
origin?: PluginOrigin;
onlyPluginIds?: readonly string[];
}): string[] {
const onlyPluginIdSet = createPluginIdScopeSet(normalizePluginIdScope(params.onlyPluginIds));
};
return [
...new Set(
loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
})
.plugins.filter(
(plugin) =>
(!params.origin || plugin.origin === params.origin) &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
matchesManifestActivationTrigger(plugin, params.trigger),
)
.map((plugin) => plugin.id),
),
].toSorted((left, right) => left.localeCompare(right));
export function resolveManifestActivationPlan(
params: ResolveManifestActivationPlanParams,
): PluginActivationPlan {
const onlyPluginIdSet = createPluginIdScopeSet(normalizePluginIdScope(params.onlyPluginIds));
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
const entries = registry.plugins
.flatMap((plugin) => {
if (params.origin && plugin.origin !== params.origin) {
return [];
}
if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) {
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: [...new Set(entries.map((entry) => entry.pluginId))],
entries,
diagnostics: registry.diagnostics,
};
}
function matchesManifestActivationTrigger(
export function resolveManifestActivationPluginIds(
params: ResolveManifestActivationPlanParams,
): string[] {
return [...resolveManifestActivationPlan(params).pluginIds];
}
function listManifestActivationTriggerReasons(
plugin: PluginManifestRecord,
trigger: PluginActivationPlannerTrigger,
): boolean {
): PluginActivationPlannerReason[] {
switch (trigger.kind) {
case "command":
return listActivationCommandIds(plugin).includes(normalizeCommandId(trigger.command));
return listCommandTriggerReasons(plugin, normalizeCommandId(trigger.command));
case "provider":
return listActivationProviderIds(plugin).includes(normalizeProviderId(trigger.provider));
return listProviderTriggerReasons(plugin, normalizeProviderId(trigger.provider));
case "agentHarness":
return listActivationAgentHarnessIds(plugin).includes(normalizeCommandId(trigger.runtime));
return listAgentHarnessTriggerReasons(plugin, normalizeCommandId(trigger.runtime));
case "channel":
return listActivationChannelIds(plugin).includes(normalizeCommandId(trigger.channel));
return listChannelTriggerReasons(plugin, normalizeCommandId(trigger.channel));
case "route":
return listActivationRouteIds(plugin).includes(normalizeCommandId(trigger.route));
return listRouteTriggerReasons(plugin, normalizeCommandId(trigger.route));
case "capability":
return hasActivationCapability(plugin, trigger.capability);
return listCapabilityTriggerReasons(plugin, trigger.capability);
}
const unreachableTrigger: never = trigger;
return unreachableTrigger;
}
function listActivationAgentHarnessIds(plugin: PluginManifestRecord): string[] {
return [...(plugin.activation?.onAgentHarnesses ?? [])].map(normalizeCommandId).filter(Boolean);
function listAgentHarnessTriggerReasons(
plugin: PluginManifestRecord,
runtime: string,
): PluginActivationPlannerReason[] {
return listHasNormalizedValue(plugin.activation?.onAgentHarnesses, runtime, normalizeCommandId)
? ["activation-agent-harness-hint"]
: [];
}
function listActivationCommandIds(plugin: PluginManifestRecord): string[] {
return [
...(plugin.activation?.onCommands ?? []),
...(plugin.commandAliases ?? []).flatMap((alias) => alias.cliCommand ?? alias.name),
]
.map(normalizeCommandId)
.filter(Boolean);
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 listActivationProviderIds(plugin: PluginManifestRecord): string[] {
return [
...(plugin.activation?.onProviders ?? []),
...plugin.providers,
...(plugin.setup?.providers?.map((provider) => provider.id) ?? []),
]
.map((value) => normalizeProviderId(value))
.filter(Boolean);
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 listActivationChannelIds(plugin: PluginManifestRecord): string[] {
return [...(plugin.activation?.onChannels ?? []), ...plugin.channels]
.map(normalizeCommandId)
.filter(Boolean);
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 listActivationRouteIds(plugin: PluginManifestRecord): string[] {
return (plugin.activation?.onRoutes ?? []).map(normalizeCommandId).filter(Boolean);
function listRouteTriggerReasons(
plugin: PluginManifestRecord,
route: string,
): PluginActivationPlannerReason[] {
return listHasNormalizedValue(plugin.activation?.onRoutes, route, normalizeCommandId)
? ["activation-route-hint"]
: [];
}
function hasActivationCapability(
function listCapabilityTriggerReasons(
plugin: PluginManifestRecord,
capability: PluginManifestActivationCapability,
): boolean {
if (plugin.activation?.onCapabilities?.includes(capability)) {
return true;
}
): PluginActivationPlannerReason[] {
switch (capability) {
case "provider":
return listActivationProviderIds(plugin).length > 0;
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 listActivationChannelIds(plugin).length > 0;
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 (plugin.contracts?.tools?.length ?? 0) > 0;
return dedupeReasons([
plugin.activation?.onCapabilities?.includes(capability)
? "activation-capability-hint"
: null,
hasValues(plugin.contracts?.tools) ? "manifest-tool-contract" : null,
]);
case "hook":
return plugin.hooks.length > 0;
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) ?? "";
}

View File

@@ -57,19 +57,19 @@ export type PluginManifestActivationCapability = "provider" | "channel" | "tool"
export type PluginManifestActivation = {
/**
* Provider ids that should activate this plugin when explicitly requested.
* This is metadata only; runtime loading still happens through the loader.
* Provider ids that should include this plugin in activation/load plans.
* This is planner metadata only; runtime behavior still comes from register().
*/
onProviders?: string[];
/** Agent harness runtime ids that should activate this plugin. */
/** Agent harness runtime ids that should include this plugin in activation/load plans. */
onAgentHarnesses?: string[];
/** Command ids that should activate this plugin. */
/** Command ids that should include this plugin in activation/load plans. */
onCommands?: string[];
/** Channel ids that should activate this plugin. */
/** Channel ids that should include this plugin in activation/load plans. */
onChannels?: string[];
/** Route kinds that should activate this plugin. */
/** Route kinds that should include this plugin in activation/load plans. */
onRoutes?: string[];
/** Cheap capability hints used by future activation planning. */
/** Broad capability hints for activation/load plans. Prefer narrower ownership metadata. */
onCapabilities?: PluginManifestActivationCapability[];
};
@@ -205,7 +205,7 @@ export type PluginManifest = {
* and non-runtime auth-choice routing before provider runtime loads.
*/
providerAuthChoices?: PluginManifestProviderAuthChoice[];
/** Cheap activation hints exposed before plugin runtime loads. */
/** Cheap activation planner metadata exposed before plugin runtime loads. */
activation?: PluginManifestActivation;
/** Cheap setup/onboarding metadata exposed before plugin runtime loads. */
setup?: PluginManifestSetup;