diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d853b4c8b..c4e0d42afac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/docs.json b/docs/docs.json index 487f357e6a9..0df3e09b929 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1165,6 +1165,7 @@ "plugins/hooks", "plugins/sdk-channel-plugins", "plugins/sdk-provider-plugins", + "plugins/compatibility", "plugins/sdk-migration" ] }, diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md new file mode 100644 index 00000000000..ac9e92f536f --- /dev/null +++ b/docs/plugins/compatibility.md @@ -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`. diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts new file mode 100644 index 00000000000..f90737161fc --- /dev/null +++ b/src/plugins/compat/registry.test.ts @@ -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); + } + }); +}); diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts new file mode 100644 index 00000000000..d7473508ad2 --- /dev/null +++ b/src/plugins/compat/registry.ts @@ -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/` 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..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; + +const pluginCompatRecordByCode = new Map( + 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), + ); +} diff --git a/src/plugins/compat/types.ts b/src/plugins/compat/types.ts new file mode 100644 index 00000000000..67b3a76de4c --- /dev/null +++ b/src/plugins/compat/types.ts @@ -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: 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; +}; diff --git a/src/plugins/status.test-helpers.ts b/src/plugins/status.test-helpers.ts index d51746e7d77..d063049f53e 100644 --- a/src/plugins/status.test-helpers.ts +++ b/src/plugins/status.test-helpers.ts @@ -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, }; diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 46c136a454e..af2d522357c 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -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.", diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index 8d2526ded16..088aedee456 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -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.",