mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
feat(plugins): add compatibility registry
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -1165,6 +1165,7 @@
|
||||
"plugins/hooks",
|
||||
"plugins/sdk-channel-plugins",
|
||||
"plugins/sdk-provider-plugins",
|
||||
"plugins/compatibility",
|
||||
"plugins/sdk-migration"
|
||||
]
|
||||
},
|
||||
|
||||
74
docs/plugins/compatibility.md
Normal file
74
docs/plugins/compatibility.md
Normal 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`.
|
||||
40
src/plugins/compat/registry.test.ts
Normal file
40
src/plugins/compat/registry.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
242
src/plugins/compat/registry.ts
Normal file
242
src/plugins/compat/registry.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
27
src/plugins/compat/types.ts
Normal file
27
src/plugins/compat/types.ts
Normal 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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user