mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
feat(plugins): expose activation plan reasons (#70943)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) ?? "";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user