feat(plugins): add compatibility registry

This commit is contained in:
Vincent Koc
2026-04-24 22:15:24 -07:00
parent f0ceb4b68f
commit 86dc820560
9 changed files with 392 additions and 0 deletions

View File

@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
- Gateway/VoiceClaw: add a realtime brain WebSocket endpoint backed by Gemini Live, with owner-auth gating and async OpenClaw tool handoff. (#70938) Thanks @yagudaev.
- Providers/DeepSeek: add DeepSeek V4 Flash and V4 Pro to the bundled catalog and make V4 Flash the onboarding default. Thanks @lsdsjy.
- CLI/Gateway: make `gateway status` start faster by skipping plugin loading on the read-only status path. (#71364) Thanks @andyylin.
- Plugins/compatibility: add a central plugin compatibility registry and docs for SDK/config/setup/runtime deprecation records, including dated migration metadata for legacy harness naming and other plugin-facing aliases. Thanks @vincentkoc.
### Fixes

View File

@@ -1165,6 +1165,7 @@
"plugins/hooks",
"plugins/sdk-channel-plugins",
"plugins/sdk-provider-plugins",
"plugins/compatibility",
"plugins/sdk-migration"
]
},

View File

@@ -0,0 +1,74 @@
---
summary: "Plugin compatibility contracts, deprecation metadata, and migration expectations"
title: "Plugin compatibility"
read_when:
- You maintain an OpenClaw plugin
- You see a plugin compatibility warning
- You are planning a plugin SDK or manifest migration
---
OpenClaw keeps older plugin contracts wired through named compatibility
adapters before removing them. This protects existing bundled and external
plugins while the SDK, manifest, setup, config, and agent runtime contracts
evolve.
## Compatibility registry
Plugin compatibility contracts are tracked in the core registry at
`src/plugins/compat/registry.ts`.
Each record has:
- a stable compatibility code
- status: `active`, `deprecated`, `removal-pending`, or `removed`
- owner: SDK, config, setup, channel, provider, plugin execution, agent runtime,
or core
- introduction and deprecation dates when applicable
- replacement guidance
- docs, diagnostics, and tests that cover the old and new behavior
The registry is the source for maintainer planning and future plugin inspector
checks. If a plugin-facing behavior changes, add or update the compatibility
record in the same change that adds the adapter.
## Deprecation policy
OpenClaw should not remove a documented plugin contract in the same release
that introduces its replacement.
The migration sequence is:
1. Add the new contract.
2. Keep the old behavior wired through a named compatibility adapter.
3. Emit diagnostics or warnings when plugin authors can act.
4. Document the replacement and timeline.
5. Test both old and new paths.
6. Wait through the announced migration window.
7. Remove only with explicit breaking-release approval.
Deprecated records must include a warning start date, replacement, docs link,
and target removal date when known.
## Current compatibility areas
Current compatibility records include:
- legacy broad SDK imports such as `openclaw/plugin-sdk/compat`
- legacy hook-only plugin shapes and `before_agent_start`
- bundled plugin allowlist and enablement behavior
- legacy provider/channel env-var manifest metadata
- activation hints that are being replaced by manifest contribution ownership
- `embeddedHarness` and `agent-harness` naming aliases while public naming moves
toward `agentRuntime`
- generated bundled channel config metadata fallback while registry-first
`channelConfigs` metadata lands
New plugin code should prefer the replacement listed in the registry and in the
specific migration guide. Existing plugins can keep using a compatibility path
until the docs, diagnostics, and release notes announce a removal window.
## Release notes
Release notes should include upcoming plugin deprecations with target dates and
links to migration docs. That warning needs to happen before a compatibility
path moves to `removal-pending` or `removed`.

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import {
getPluginCompatRecord,
isPluginCompatCode,
listDeprecatedPluginCompatRecords,
listPluginCompatRecords,
} from "./registry.js";
const datePattern = /^\d{4}-\d{2}-\d{2}$/u;
describe("plugin compatibility registry", () => {
it("keeps compatibility codes unique and lookup-safe", () => {
const records = listPluginCompatRecords();
const codes = records.map((record) => record.code);
expect(new Set(codes).size).toBe(codes.length);
expect(isPluginCompatCode("legacy-root-sdk-import")).toBe(true);
expect(isPluginCompatCode("missing-code")).toBe(false);
expect(getPluginCompatRecord("legacy-root-sdk-import").owner).toBe("sdk");
});
it("requires dated deprecation metadata for deprecated records", () => {
for (const record of listDeprecatedPluginCompatRecords()) {
expect(record.deprecated, record.code).toMatch(datePattern);
expect(record.warningStarts, record.code).toMatch(datePattern);
expect(record.replacement, record.code).toBeTruthy();
expect(record.docsPath, record.code).toMatch(/^\//u);
}
});
it("keeps every record actionable", () => {
for (const record of listPluginCompatRecords()) {
expect(record.introduced, record.code).toMatch(datePattern);
expect(record.docsPath, record.code).toMatch(/^\//u);
expect(record.surfaces.length, record.code).toBeGreaterThan(0);
expect(record.diagnostics.length, record.code).toBeGreaterThan(0);
expect(record.tests.length, record.code).toBeGreaterThan(0);
}
});
});

View File

@@ -0,0 +1,242 @@
import type { PluginCompatRecord } from "./types.js";
export const PLUGIN_COMPAT_RECORDS = [
{
code: "legacy-before-agent-start",
status: "deprecated",
owner: "sdk",
introduced: "2026-04-24",
deprecated: "2026-04-24",
warningStarts: "2026-04-24",
replacement: "`before_model_resolve` and `before_prompt_build` hooks",
docsPath: "/plugins/sdk-migration",
surfaces: ["plugin hooks", "plugins inspect", "status diagnostics"],
diagnostics: ["plugin compatibility notice"],
tests: ["src/plugins/status.test.ts", "src/plugins/contracts/shape.contract.test.ts"],
releaseNote:
"Legacy `before_agent_start` hook compatibility remains wired while plugins migrate to modern hook stages.",
},
{
code: "hook-only-plugin-shape",
status: "active",
owner: "sdk",
introduced: "2026-04-24",
replacement: "explicit capability registration",
docsPath: "/plugins/sdk-migration",
surfaces: ["plugin shape inspection", "plugins inspect", "status diagnostics"],
diagnostics: ["plugin compatibility notice"],
tests: ["src/plugins/status.test.ts", "src/plugins/contracts/shape.contract.test.ts"],
},
{
code: "legacy-root-sdk-import",
status: "deprecated",
owner: "sdk",
introduced: "2026-04-24",
deprecated: "2026-04-24",
warningStarts: "2026-04-24",
replacement: "focused `openclaw/plugin-sdk/<subpath>` imports",
docsPath: "/plugins/sdk-migration",
surfaces: ["openclaw/plugin-sdk", "openclaw/plugin-sdk/compat"],
diagnostics: ["OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED"],
tests: [
"src/plugins/contracts/plugin-sdk-index.test.ts",
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
],
},
{
code: "bundled-plugin-allowlist",
status: "active",
owner: "config",
introduced: "2026-04-24",
replacement: "manifest-owned plugin enablement and scoped load plans",
docsPath: "/plugins/architecture",
surfaces: ["plugins.allow", "bundled provider startup", "plugins status"],
diagnostics: ["plugin status report"],
tests: ["src/plugins/status.test.ts", "src/plugins/config-state.test.ts"],
},
{
code: "bundled-plugin-enablement",
status: "active",
owner: "config",
introduced: "2026-04-24",
replacement: "manifest-owned plugin defaults and scoped load plans",
docsPath: "/plugins/architecture",
surfaces: ["plugins.entries", "bundled provider startup", "plugins status"],
diagnostics: ["plugin status report"],
tests: ["src/plugins/status.test.ts", "src/plugins/config-state.test.ts"],
},
{
code: "bundled-plugin-vitest-defaults",
status: "active",
owner: "config",
introduced: "2026-04-24",
replacement: "explicit test plugin config fixtures",
docsPath: "/plugins/architecture",
surfaces: ["Vitest plugin defaults", "bundled provider tests"],
diagnostics: ["test-only compatibility path"],
tests: ["src/plugins/config-state.test.ts"],
},
{
code: "provider-auth-env-vars",
status: "deprecated",
owner: "setup",
introduced: "2026-04-24",
deprecated: "2026-04-24",
warningStarts: "2026-04-24",
replacement: "`setup.providers[].envVars` and `providerAuthChoices`",
docsPath: "/plugins/manifest",
surfaces: ["openclaw.plugin.json providerAuthEnvVars", "provider setup"],
diagnostics: ["manifest compatibility diagnostic"],
tests: ["src/plugins/setup-registry.test.ts", "src/plugins/provider-auth-choices.test.ts"],
},
{
code: "channel-env-vars",
status: "deprecated",
owner: "channel",
introduced: "2026-04-24",
deprecated: "2026-04-24",
warningStarts: "2026-04-24",
replacement: "`channelConfigs.<id>.schema` and setup descriptors",
docsPath: "/plugins/manifest",
surfaces: ["openclaw.plugin.json channelEnvVars", "channel setup"],
diagnostics: ["manifest compatibility diagnostic"],
tests: [
"src/plugins/setup-registry.test.ts",
"src/channels/plugins/setup-group-access.test.ts",
],
},
{
code: "activation-provider-hint",
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
replacement: "`providers[]` manifest ownership",
docsPath: "/plugins/manifest",
surfaces: ["activation.onProviders", "activation planner"],
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"],
},
{
code: "activation-channel-hint",
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
replacement: "`channels[]` manifest ownership",
docsPath: "/plugins/manifest",
surfaces: ["activation.onChannels", "activation planner"],
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"],
},
{
code: "activation-command-hint",
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
replacement: "`commandAliases` or command contribution metadata",
docsPath: "/plugins/manifest",
surfaces: ["activation.onCommands", "activation planner"],
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"],
},
{
code: "activation-route-hint",
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
replacement: "HTTP route contribution metadata",
docsPath: "/plugins/manifest",
surfaces: ["activation.onRoutes", "activation planner"],
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"],
},
{
code: "activation-capability-hint",
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
replacement: "manifest contribution ownership",
docsPath: "/plugins/manifest",
surfaces: ["activation.onCapabilities", "activation planner"],
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"],
},
{
code: "embedded-harness-config-alias",
status: "deprecated",
owner: "agent-runtime",
introduced: "2026-04-24",
deprecated: "2026-04-25",
warningStarts: "2026-04-25",
replacement: "`agentRuntime` config naming",
docsPath: "/plugins/sdk-agent-harness",
surfaces: ["agents.defaults.embeddedHarness", "model/provider runtime selection"],
diagnostics: ["agent runtime config compatibility"],
tests: ["src/agents/config.test.ts", "src/agents/runtime-selection.test.ts"],
},
{
code: "agent-harness-sdk-alias",
status: "deprecated",
owner: "agent-runtime",
introduced: "2026-04-24",
deprecated: "2026-04-25",
warningStarts: "2026-04-25",
replacement: "`openclaw/plugin-sdk/agent-runtime`",
docsPath: "/plugins/sdk-agent-harness",
surfaces: ["openclaw/plugin-sdk/agent-harness", "openclaw/plugin-sdk/agent-harness-runtime"],
diagnostics: ["plugin SDK compatibility warning"],
tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"],
},
{
code: "agent-harness-id-alias",
status: "deprecated",
owner: "agent-runtime",
introduced: "2026-04-24",
deprecated: "2026-04-25",
warningStarts: "2026-04-25",
replacement: "`agentRuntime` ids and policy metadata",
docsPath: "/plugins/sdk-agent-harness",
surfaces: ["manifest/catalog execution policy", "runtime selection"],
diagnostics: ["agent runtime compatibility warning"],
tests: ["src/plugins/provider-runtime.test.ts", "src/web/provider-runtime-shared.test.ts"],
},
{
code: "generated-bundled-channel-config-fallback",
status: "active",
owner: "channel",
introduced: "2026-04-24",
replacement: "manifest registry `channelConfigs` metadata",
docsPath: "/plugins/manifest",
surfaces: ["generated bundled channel config metadata", "channel config validation"],
diagnostics: ["channel config metadata fallback"],
tests: ["src/plugins/contracts/config-footprint-guardrails.test.ts"],
},
] as const satisfies readonly PluginCompatRecord[];
export type PluginCompatCode = (typeof PLUGIN_COMPAT_RECORDS)[number]["code"];
export type KnownPluginCompatRecord = PluginCompatRecord<PluginCompatCode>;
const pluginCompatRecordByCode = new Map<PluginCompatCode, KnownPluginCompatRecord>(
PLUGIN_COMPAT_RECORDS.map((record) => [record.code, record]),
);
export function listPluginCompatRecords(): readonly KnownPluginCompatRecord[] {
return PLUGIN_COMPAT_RECORDS;
}
export function getPluginCompatRecord(code: PluginCompatCode): KnownPluginCompatRecord {
const record = pluginCompatRecordByCode.get(code);
if (!record) {
throw new Error(`Unknown plugin compatibility code: ${code}`);
}
return record;
}
export function isPluginCompatCode(code: string): code is PluginCompatCode {
return pluginCompatRecordByCode.has(code as PluginCompatCode);
}
export function listDeprecatedPluginCompatRecords(): readonly KnownPluginCompatRecord[] {
return PLUGIN_COMPAT_RECORDS.filter((record) =>
(["deprecated", "removal-pending"] as readonly string[]).includes(record.status),
);
}

View File

@@ -0,0 +1,27 @@
export type PluginCompatStatus = "active" | "deprecated" | "removal-pending" | "removed";
export type PluginCompatOwner =
| "agent-runtime"
| "channel"
| "config"
| "core"
| "plugin-execution"
| "provider"
| "sdk"
| "setup";
export type PluginCompatRecord<Code extends string = string> = {
code: Code;
status: PluginCompatStatus;
owner: PluginCompatOwner;
introduced: string;
deprecated?: string;
warningStarts?: string;
removeAfter?: string;
replacement?: string;
docsPath: string;
surfaces: readonly string[];
diagnostics: readonly string[];
tests: readonly string[];
releaseNote?: string;
};

View File

@@ -15,6 +15,7 @@ export function createCompatibilityNotice(
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "legacy-before-agent-start",
severity: "warn",
message: LEGACY_BEFORE_AGENT_START_MESSAGE,
};
@@ -23,6 +24,7 @@ export function createCompatibilityNotice(
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "hook-only-plugin-shape",
severity: "info",
message: HOOK_ONLY_MESSAGE,
};

View File

@@ -10,6 +10,7 @@ import {
withBundledPluginAllowlistCompat,
withBundledPluginEnablementCompat,
} from "./bundled-compat.js";
import type { PluginCompatCode } from "./compat/registry.js";
import { normalizePluginsConfig } from "./config-state.js";
import {
buildPluginShapeSummary,
@@ -37,6 +38,7 @@ export type { PluginCapabilityKind, PluginInspectShape } from "./inspect-shape.j
export type PluginCompatibilityNotice = {
pluginId: string;
code: "legacy-before-agent-start" | "hook-only";
compatCode: PluginCompatCode;
severity: "warn" | "info";
message: string;
};
@@ -100,6 +102,7 @@ function buildCompatibilityNoticesForInspect(
warnings.push({
pluginId: inspect.plugin.id,
code: "legacy-before-agent-start",
compatCode: "legacy-before-agent-start",
severity: "warn",
message:
"still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work.",
@@ -109,6 +112,7 @@ function buildCompatibilityNoticesForInspect(
warnings.push({
pluginId: inspect.plugin.id,
code: "hook-only",
compatCode: "hook-only-plugin-shape",
severity: "info",
message:
"is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet.",

View File

@@ -748,6 +748,7 @@ describe("runSetupWizard", () => {
{
pluginId: "legacy-plugin",
code: "legacy-before-agent-start",
compatCode: "legacy-before-agent-start",
severity: "warn",
message:
"still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work.",