mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:40:43 +00:00
refactor(plugins): track activation compat hints
This commit is contained in:
@@ -119,22 +119,31 @@ signals also appear in `openclaw status --all` and `openclaw plugins doctor`.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
OpenClaw's plugin system has four layers:
|
||||
OpenClaw's plugin system is split into four planes:
|
||||
|
||||
1. **Manifest + discovery**
|
||||
OpenClaw finds candidate plugins from configured paths, workspace roots,
|
||||
global plugin roots, and bundled plugins. Discovery reads native
|
||||
`openclaw.plugin.json` manifests plus supported bundle manifests first.
|
||||
2. **Enablement + validation**
|
||||
Core decides whether a discovered plugin is enabled, disabled, blocked, or
|
||||
selected for an exclusive slot such as memory.
|
||||
3. **Runtime loading**
|
||||
Native OpenClaw plugins are loaded in-process via jiti and register
|
||||
capabilities into a central registry. Compatible bundles are normalized into
|
||||
registry records without importing runtime code.
|
||||
4. **Surface consumption**
|
||||
The rest of OpenClaw reads the registry to expose tools, channels, provider
|
||||
setup, hooks, HTTP routes, CLI commands, and services.
|
||||
1. **Source plane**
|
||||
OpenClaw decides where a plugin comes from and how it can be installed. This
|
||||
includes bundled catalogs, official external catalogs, ClawHub/npm specs,
|
||||
local source paths, minimum host version, expected npm integrity, and install
|
||||
policy checks.
|
||||
2. **Control plane**
|
||||
OpenClaw reads package and manifest metadata before runtime code executes.
|
||||
This includes discovery, config schemas, provider/channel ownership,
|
||||
setup/onboarding hints, contracts, auth choices, and enablement policy.
|
||||
3. **Load plane**
|
||||
OpenClaw builds deterministic plans for concrete needs such as a provider,
|
||||
channel, command, hook stage, or contract. Legacy `activation.*` fields are
|
||||
compatibility hints in this plane, not the preferred public contract.
|
||||
4. **Runtime plane**
|
||||
OpenClaw imports plugin code only for actual execution. Native plugins
|
||||
register capabilities into scoped or compatibility registries; compatible
|
||||
bundles can still normalize into registry records without importing runtime
|
||||
code.
|
||||
|
||||
The important compatibility rule: documented external plugins and existing
|
||||
bundled plugins must keep working while contracts migrate. Breaking changes need
|
||||
a replacement contract, compatibility adapter, diagnostics, tests, docs, and an
|
||||
approved deprecation window before removal.
|
||||
|
||||
For plugin CLI specifically, root command discovery is split in two phases:
|
||||
|
||||
|
||||
@@ -239,11 +239,15 @@ 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.
|
||||
This block is legacy hint metadata. It does not register runtime behavior, and
|
||||
it does not replace `register(...)`, `setupEntry`, or other runtime/plugin
|
||||
entrypoints. Existing plugins may keep these fields, but new manifests should
|
||||
prefer explicit ownership fields such as `providers`, `channels`, `contracts`,
|
||||
`commandAliases`, and `setup`.
|
||||
|
||||
Current consumers still parse `activation` through the compatibility layer so
|
||||
existing bundled and external plugins keep working. New code should treat these
|
||||
fields as fallback hints for load planning, not as the primary plugin contract.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -257,23 +261,24 @@ change correctness while legacy manifest ownership fallbacks still exist.
|
||||
}
|
||||
```
|
||||
|
||||
| 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. |
|
||||
| Field | Required | Type | What it means |
|
||||
| ---------------- | -------- | ---------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `onProviders` | No | `string[]` | Legacy provider load hint. Prefer top-level `providers`. |
|
||||
| `onCommands` | No | `string[]` | Legacy command load hint. Prefer command aliases or CLI descriptors. |
|
||||
| `onChannels` | No | `string[]` | Legacy channel load hint. Prefer top-level `channels`. |
|
||||
| `onRoutes` | No | `string[]` | Legacy route load hint. Keep only when no narrower route metadata exists yet. |
|
||||
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Legacy broad capability hint. Do not add new uses. |
|
||||
|
||||
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
|
||||
- command-triggered CLI planning prefers `commandAliases[].cliCommand` or
|
||||
`commandAliases[].name` before legacy `activation.onCommands`
|
||||
- channel-triggered setup/channel planning prefers `channels[]` before legacy
|
||||
`activation.onChannels`
|
||||
- provider-triggered setup/runtime planning prefers `providers[]` and
|
||||
`setup.providers[]` before legacy `activation.onProviders`
|
||||
- broad capability planning prefers explicit ownership metadata before legacy
|
||||
`activation.onCapabilities`
|
||||
|
||||
## setup reference
|
||||
|
||||
|
||||
@@ -25,12 +25,14 @@ anything they needed from a single entry point:
|
||||
host-side helpers like the embedded agent runner.
|
||||
|
||||
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.
|
||||
plugins must not use them, and existing plugins should migrate before an
|
||||
approved breaking release removes them.
|
||||
|
||||
<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.
|
||||
The backwards-compatibility layer remains supported during the migration
|
||||
window. Any removal must go through a documented deprecation path first:
|
||||
replacement contract, compatibility adapter, diagnostics, tests, docs, and an
|
||||
explicitly approved breaking release.
|
||||
</Warning>
|
||||
|
||||
## Why this changed
|
||||
@@ -374,13 +376,15 @@ check the source at `src/plugin-sdk/` or ask in Discord.
|
||||
|
||||
## Removal timeline
|
||||
|
||||
| When | What happens |
|
||||
| ---------------------- | ----------------------------------------------------------------------- |
|
||||
| **Now** | Deprecated surfaces emit runtime warnings |
|
||||
| **Next major release** | Deprecated surfaces will be removed; plugins still using them will fail |
|
||||
| When | What happens |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| **Now** | Deprecated surfaces emit runtime warnings and keep working through adapters. |
|
||||
| **Migration window** | Replacement contracts, diagnostics, tests, and docs stay available together. |
|
||||
| **Approved breaking release only** | Deprecated surfaces may be removed after the migration window. |
|
||||
|
||||
All core plugins have already been migrated. External plugins should migrate
|
||||
before the next major release.
|
||||
All core plugins have already been migrated. External plugins should migrate,
|
||||
but documented external plugins should not break without the compatibility path
|
||||
above.
|
||||
|
||||
## Suppressing the warnings temporarily
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ if (shouldWarnCompatImport) {
|
||||
{
|
||||
code: "OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED",
|
||||
detail:
|
||||
"Bundled plugins must use scoped plugin-sdk subpaths. External plugins may keep compat temporarily while migrating. Migration guide: https://docs.openclaw.ai/plugins/sdk-migration",
|
||||
"Bundled plugins must use scoped plugin-sdk subpaths. External plugins may keep compat during the documented migration window. Migration guide: https://docs.openclaw.ai/plugins/sdk-migration",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ vi.mock("./manifest-registry.js", () => ({
|
||||
}));
|
||||
|
||||
let resolveManifestActivationPluginIds: typeof import("./activation-planner.js").resolveManifestActivationPluginIds;
|
||||
let resolveManifestActivationPlan: typeof import("./activation-planner.js").resolveManifestActivationPlan;
|
||||
let PLUGIN_COMPAT_REASON: typeof import("./compat-reasons.js").PLUGIN_COMPAT_REASON;
|
||||
|
||||
describe("resolveManifestActivationPluginIds", () => {
|
||||
beforeAll(async () => {
|
||||
({ resolveManifestActivationPluginIds } = await import("./activation-planner.js"));
|
||||
({ resolveManifestActivationPluginIds, resolveManifestActivationPlan } =
|
||||
await import("./activation-planner.js"));
|
||||
({ PLUGIN_COMPAT_REASON } = await import("./compat-reasons.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -70,6 +74,20 @@ describe("resolveManifestActivationPluginIds", () => {
|
||||
},
|
||||
origin: "workspace",
|
||||
},
|
||||
{
|
||||
id: "legacy-activation-only",
|
||||
providers: [],
|
||||
activation: {
|
||||
onProviders: ["legacy-provider"],
|
||||
onChannels: ["legacy-channel"],
|
||||
onCapabilities: ["tool"],
|
||||
},
|
||||
channels: [],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
origin: "workspace",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
@@ -168,7 +186,7 @@ describe("resolveManifestActivationPluginIds", () => {
|
||||
capability: "tool",
|
||||
},
|
||||
}),
|
||||
).toEqual(["demo-channel"]);
|
||||
).toEqual(["demo-channel", "legacy-activation-only"]);
|
||||
|
||||
expect(
|
||||
resolveManifestActivationPluginIds({
|
||||
@@ -191,4 +209,96 @@ describe("resolveManifestActivationPluginIds", () => {
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports legacy activation field compat reasons without changing plugin-id resolution", () => {
|
||||
expect(
|
||||
resolveManifestActivationPlan({
|
||||
trigger: {
|
||||
kind: "command",
|
||||
command: "demo-tools",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["demo-channel"],
|
||||
entries: [
|
||||
{
|
||||
pluginId: "demo-channel",
|
||||
reasons: ["command:demo-tools"],
|
||||
compatReasons: [PLUGIN_COMPAT_REASON.legacyActivationField],
|
||||
},
|
||||
],
|
||||
compatReasons: {
|
||||
"demo-channel": [PLUGIN_COMPAT_REASON.legacyActivationField],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveManifestActivationPlan({
|
||||
trigger: {
|
||||
kind: "provider",
|
||||
provider: "legacy-provider",
|
||||
},
|
||||
}).compatReasons,
|
||||
).toEqual({
|
||||
"legacy-activation-only": [PLUGIN_COMPAT_REASON.legacyActivationField],
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveManifestActivationPluginIds({
|
||||
trigger: {
|
||||
kind: "provider",
|
||||
provider: "legacy-provider",
|
||||
},
|
||||
}),
|
||||
).toEqual(["legacy-activation-only"]);
|
||||
});
|
||||
|
||||
it("does not report compat reasons for stable ownership metadata", () => {
|
||||
expect(
|
||||
resolveManifestActivationPlan({
|
||||
trigger: {
|
||||
kind: "provider",
|
||||
provider: "openai",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["openai"],
|
||||
entries: [
|
||||
{
|
||||
pluginId: "openai",
|
||||
reasons: ["provider:openai"],
|
||||
compatReasons: [],
|
||||
},
|
||||
],
|
||||
compatReasons: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports legacy activation capability hints separately from stable capabilities", () => {
|
||||
expect(
|
||||
resolveManifestActivationPlan({
|
||||
trigger: {
|
||||
kind: "capability",
|
||||
capability: "tool",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["demo-channel", "legacy-activation-only"],
|
||||
entries: [
|
||||
{
|
||||
pluginId: "demo-channel",
|
||||
reasons: ["capability:tool"],
|
||||
compatReasons: [],
|
||||
},
|
||||
{
|
||||
pluginId: "legacy-activation-only",
|
||||
reasons: ["capability:tool"],
|
||||
compatReasons: [PLUGIN_COMPAT_REASON.legacyActivationField],
|
||||
},
|
||||
],
|
||||
compatReasons: {
|
||||
"legacy-activation-only": [PLUGIN_COMPAT_REASON.legacyActivationField],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { PLUGIN_COMPAT_REASON, type PluginCompatReason } from "./compat-reasons.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginManifestActivationCapability } from "./manifest.js";
|
||||
import type { PluginOrigin } from "./plugin-origin.types.js";
|
||||
@@ -14,6 +15,18 @@ export type PluginActivationPlannerTrigger =
|
||||
| { kind: "route"; route: string }
|
||||
| { kind: "capability"; capability: PluginManifestActivationCapability };
|
||||
|
||||
export type PluginActivationPlanEntry = {
|
||||
pluginId: string;
|
||||
reasons: string[];
|
||||
compatReasons: PluginCompatReason[];
|
||||
};
|
||||
|
||||
export type PluginActivationPlan = {
|
||||
pluginIds: string[];
|
||||
entries: PluginActivationPlanEntry[];
|
||||
compatReasons: Record<string, PluginCompatReason[]>;
|
||||
};
|
||||
|
||||
export function resolveManifestActivationPluginIds(params: {
|
||||
trigger: PluginActivationPlannerTrigger;
|
||||
config?: OpenClawConfig;
|
||||
@@ -23,101 +36,218 @@ export function resolveManifestActivationPluginIds(params: {
|
||||
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));
|
||||
return resolveManifestActivationPlan(params).pluginIds;
|
||||
}
|
||||
|
||||
function matchesManifestActivationTrigger(
|
||||
export function resolveManifestActivationPlan(params: {
|
||||
trigger: PluginActivationPlannerTrigger;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
origin?: PluginOrigin;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginActivationPlan {
|
||||
const onlyPluginIdSet = createPluginIdScopeSet(normalizePluginIdScope(params.onlyPluginIds));
|
||||
|
||||
const entries = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
cache: params.cache,
|
||||
})
|
||||
.plugins.flatMap((plugin): PluginActivationPlanEntry[] => {
|
||||
if (
|
||||
(params.origin && plugin.origin !== params.origin) ||
|
||||
(onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const match = matchManifestActivationTrigger(plugin, params.trigger);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
pluginId: plugin.id,
|
||||
reasons: [match.reason],
|
||||
compatReasons: match.compatReason ? [match.compatReason] : [],
|
||||
},
|
||||
];
|
||||
})
|
||||
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId));
|
||||
|
||||
return {
|
||||
pluginIds: entries.map((entry) => entry.pluginId),
|
||||
entries,
|
||||
compatReasons: Object.fromEntries(
|
||||
entries.flatMap((entry) =>
|
||||
entry.compatReasons.length > 0 ? [[entry.pluginId, entry.compatReasons]] : [],
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
type ManifestActivationMatch = {
|
||||
reason: string;
|
||||
compatReason?: PluginCompatReason;
|
||||
};
|
||||
|
||||
function matchManifestActivationTrigger(
|
||||
plugin: PluginManifestRecord,
|
||||
trigger: PluginActivationPlannerTrigger,
|
||||
): boolean {
|
||||
): ManifestActivationMatch | null {
|
||||
switch (trigger.kind) {
|
||||
case "command":
|
||||
return listActivationCommandIds(plugin).includes(normalizeCommandId(trigger.command));
|
||||
return matchActivationCommand(plugin, trigger.command);
|
||||
case "provider":
|
||||
return listActivationProviderIds(plugin).includes(normalizeProviderId(trigger.provider));
|
||||
return matchActivationProvider(plugin, trigger.provider);
|
||||
case "agentHarness":
|
||||
return listActivationAgentHarnessIds(plugin).includes(normalizeCommandId(trigger.runtime));
|
||||
return matchLegacyActivationList(
|
||||
listActivationAgentHarnessIds(plugin),
|
||||
normalizeCommandId(trigger.runtime),
|
||||
"agent-harness",
|
||||
);
|
||||
case "channel":
|
||||
return listActivationChannelIds(plugin).includes(normalizeCommandId(trigger.channel));
|
||||
return matchActivationChannel(plugin, trigger.channel);
|
||||
case "route":
|
||||
return listActivationRouteIds(plugin).includes(normalizeCommandId(trigger.route));
|
||||
return matchLegacyActivationList(
|
||||
listActivationRouteIds(plugin),
|
||||
normalizeCommandId(trigger.route),
|
||||
"route",
|
||||
);
|
||||
case "capability":
|
||||
return hasActivationCapability(plugin, trigger.capability);
|
||||
return matchActivationCapability(plugin, trigger.capability);
|
||||
}
|
||||
const unreachableTrigger: never = trigger;
|
||||
return unreachableTrigger;
|
||||
}
|
||||
|
||||
function matchLegacyActivationList(
|
||||
ids: readonly string[],
|
||||
normalizedId: string,
|
||||
reasonPrefix: string,
|
||||
): ManifestActivationMatch | null {
|
||||
if (!normalizedId || !ids.includes(normalizedId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
reason: `${reasonPrefix}:${normalizedId}`,
|
||||
compatReason: PLUGIN_COMPAT_REASON.legacyActivationField,
|
||||
};
|
||||
}
|
||||
|
||||
function listActivationAgentHarnessIds(plugin: PluginManifestRecord): string[] {
|
||||
return [...(plugin.activation?.onAgentHarnesses ?? [])].map(normalizeCommandId).filter(Boolean);
|
||||
}
|
||||
|
||||
function listActivationCommandIds(plugin: PluginManifestRecord): string[] {
|
||||
return [
|
||||
...(plugin.activation?.onCommands ?? []),
|
||||
...(plugin.commandAliases ?? []).flatMap((alias) => alias.cliCommand ?? alias.name),
|
||||
]
|
||||
function matchActivationCommand(
|
||||
plugin: PluginManifestRecord,
|
||||
command: string,
|
||||
): ManifestActivationMatch | null {
|
||||
const normalizedCommand = normalizeCommandId(command);
|
||||
if (!normalizedCommand) {
|
||||
return null;
|
||||
}
|
||||
const commandAliases = (plugin.commandAliases ?? [])
|
||||
.flatMap((alias) => alias.cliCommand ?? alias.name)
|
||||
.map(normalizeCommandId)
|
||||
.filter(Boolean);
|
||||
if (commandAliases.includes(normalizedCommand)) {
|
||||
return { reason: `command:${normalizedCommand}` };
|
||||
}
|
||||
return matchLegacyActivationList(
|
||||
(plugin.activation?.onCommands ?? []).map(normalizeCommandId).filter(Boolean),
|
||||
normalizedCommand,
|
||||
"command",
|
||||
);
|
||||
}
|
||||
|
||||
function listActivationProviderIds(plugin: PluginManifestRecord): string[] {
|
||||
return [
|
||||
...(plugin.activation?.onProviders ?? []),
|
||||
function matchActivationProvider(
|
||||
plugin: PluginManifestRecord,
|
||||
provider: string,
|
||||
): ManifestActivationMatch | null {
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
if (!normalizedProvider) {
|
||||
return null;
|
||||
}
|
||||
const stableProviderIds = [
|
||||
...plugin.providers,
|
||||
...(plugin.setup?.providers?.map((provider) => provider.id) ?? []),
|
||||
...(plugin.setup?.providers?.map((entry) => entry.id) ?? []),
|
||||
]
|
||||
.map((value) => normalizeProviderId(value))
|
||||
.filter(Boolean);
|
||||
if (stableProviderIds.includes(normalizedProvider)) {
|
||||
return { reason: `provider:${normalizedProvider}` };
|
||||
}
|
||||
return matchLegacyActivationList(
|
||||
(plugin.activation?.onProviders ?? [])
|
||||
.map((value) => normalizeProviderId(value))
|
||||
.filter(Boolean),
|
||||
normalizedProvider,
|
||||
"provider",
|
||||
);
|
||||
}
|
||||
|
||||
function listActivationChannelIds(plugin: PluginManifestRecord): string[] {
|
||||
return [...(plugin.activation?.onChannels ?? []), ...plugin.channels]
|
||||
.map(normalizeCommandId)
|
||||
.filter(Boolean);
|
||||
function matchActivationChannel(
|
||||
plugin: PluginManifestRecord,
|
||||
channel: string,
|
||||
): ManifestActivationMatch | null {
|
||||
const normalizedChannel = normalizeCommandId(channel);
|
||||
if (!normalizedChannel) {
|
||||
return null;
|
||||
}
|
||||
const stableChannelIds = plugin.channels.map(normalizeCommandId).filter(Boolean);
|
||||
if (stableChannelIds.includes(normalizedChannel)) {
|
||||
return { reason: `channel:${normalizedChannel}` };
|
||||
}
|
||||
return matchLegacyActivationList(
|
||||
(plugin.activation?.onChannels ?? []).map(normalizeCommandId).filter(Boolean),
|
||||
normalizedChannel,
|
||||
"channel",
|
||||
);
|
||||
}
|
||||
|
||||
function listActivationRouteIds(plugin: PluginManifestRecord): string[] {
|
||||
return (plugin.activation?.onRoutes ?? []).map(normalizeCommandId).filter(Boolean);
|
||||
}
|
||||
|
||||
function hasActivationCapability(
|
||||
function matchActivationCapability(
|
||||
plugin: PluginManifestRecord,
|
||||
capability: PluginManifestActivationCapability,
|
||||
): boolean {
|
||||
if (plugin.activation?.onCapabilities?.includes(capability)) {
|
||||
return true;
|
||||
}
|
||||
): ManifestActivationMatch | null {
|
||||
switch (capability) {
|
||||
case "provider":
|
||||
return listActivationProviderIds(plugin).length > 0;
|
||||
case "provider": {
|
||||
const hasProviderOwnership =
|
||||
plugin.providers.length > 0 || (plugin.setup?.providers?.length ?? 0) > 0;
|
||||
if (hasProviderOwnership) {
|
||||
return { reason: "capability:provider" };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "channel":
|
||||
return listActivationChannelIds(plugin).length > 0;
|
||||
if (plugin.channels.length > 0) {
|
||||
return { reason: "capability:channel" };
|
||||
}
|
||||
break;
|
||||
case "tool":
|
||||
return (plugin.contracts?.tools?.length ?? 0) > 0;
|
||||
if ((plugin.contracts?.tools?.length ?? 0) > 0) {
|
||||
return { reason: "capability:tool" };
|
||||
}
|
||||
break;
|
||||
case "hook":
|
||||
return plugin.hooks.length > 0;
|
||||
if (plugin.hooks.length > 0) {
|
||||
return { reason: "capability:hook" };
|
||||
}
|
||||
break;
|
||||
}
|
||||
const unreachableCapability: never = capability;
|
||||
return unreachableCapability;
|
||||
if (plugin.activation?.onCapabilities?.includes(capability)) {
|
||||
return {
|
||||
reason: `capability:${capability}`,
|
||||
compatReason: PLUGIN_COMPAT_REASON.legacyActivationField,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeCommandId(value: string | undefined): string {
|
||||
|
||||
10
src/plugins/compat-reasons.ts
Normal file
10
src/plugins/compat-reasons.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const PLUGIN_COMPAT_REASON = {
|
||||
legacyActivationField: "legacy-activation-field",
|
||||
legacySetupApi: "legacy-setup-api",
|
||||
legacyRootSdkImport: "legacy-root-sdk-import",
|
||||
legacyGlobalRegistry: "legacy-global-registry",
|
||||
legacyManifestOwnerFallback: "legacy-manifest-owner-fallback",
|
||||
legacyHookStage: "legacy-hook-stage",
|
||||
} as const;
|
||||
|
||||
export type PluginCompatReason = (typeof PLUGIN_COMPAT_REASON)[keyof typeof PLUGIN_COMPAT_REASON];
|
||||
@@ -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.
|
||||
* Legacy provider activation hints. Prefer top-level `providers` and setup
|
||||
* provider ownership for new manifests.
|
||||
*/
|
||||
onProviders?: string[];
|
||||
/** Agent harness runtime ids that should activate this plugin. */
|
||||
/** Legacy agent harness runtime activation hints. */
|
||||
onAgentHarnesses?: string[];
|
||||
/** Command ids that should activate this plugin. */
|
||||
/** Legacy command activation hints. Prefer command aliases or CLI descriptors. */
|
||||
onCommands?: string[];
|
||||
/** Channel ids that should activate this plugin. */
|
||||
/** Legacy channel activation hints. Prefer top-level `channels`. */
|
||||
onChannels?: string[];
|
||||
/** Route kinds that should activate this plugin. */
|
||||
/** Legacy route activation hints. */
|
||||
onRoutes?: string[];
|
||||
/** Cheap capability hints used by future activation planning. */
|
||||
/** Legacy broad capability hints. Do not add new uses. */
|
||||
onCapabilities?: PluginManifestActivationCapability[];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user