refactor: retire due compat-ledger surfaces (context-engine host params, deactivate alias, logging internals) (#121845)

* refactor(plugins): retire deactivate hook alias

* refactor(plugin-sdk): prune retired facade exports

* test(logging): isolate logger test controls

* refactor(logging): internalize file transport controls

* test(plugin-sdk): preserve retired facade coverage

* test(auto-reply): remove stale diagnostic imports

* refactor(logging): delete dead config-read guard

shouldSkipMutatingLoggingConfigRead had no production caller even on main;
it survived the dead-export scan only via logger's testApi re-export. The
test-isolation commit removed that mask, exposing the fossil. Delete the
guard, its test-only re-export, its mock entry, and its dedicated test file.

* refactor(plugin-sdk): retire due compatibility subpaths

* test(plugin-sdk): type group policy predicates

* refactor(plugin-sdk): split removed subpath records

* refactor(secrets): remove retired collector barrel

* test(plugin-sdk): tighten wildcard surface pin

* refactor(plugin-sdk): retire matrix facade metadata

* style(plugin-sdk): format facade metadata

* fix(ci): load channel setup contracts from source

Repair the main-owned regression from 99d662473c (Peter Steinberger): the new env-contract test could consume stale ignored dist metadata instead of the checked-in plugin declaration.

* test(plugin-sdk): refresh API baseline after rebase
This commit is contained in:
Peter Steinberger
2026-08-12 12:41:27 -07:00
committed by GitHub
parent 49092ebab3
commit dceb2c343c
116 changed files with 562 additions and 1557 deletions
@@ -1,16 +1,19 @@
import type { PluginCompatRecord } from "./types.js";
type SeedFields = "code" | "owner" | "removeAfter" | "removalGate" | "replacement";
type DeprecatedPluginSdkSubpathSeed = Pick<PluginCompatRecord, SeedFields> &
Record<"subpath", string>;
type PluginSdkSubpathSeed = Pick<PluginCompatRecord, SeedFields> &
Record<"subpath", string> &
Partial<Pick<PluginCompatRecord, "status" | "releaseNote">>;
const DEPRECATED_PLUGIN_SDK_SUBPATH_SEEDS = [
const PLUGIN_SDK_SUBPATH_SEEDS = [
{
code: "plugin-sdk-channel-streaming-subpath",
subpath: "channel-streaming",
status: "removed",
owner: "channel",
removeAfter: "2026-08-15",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote:
"The deprecated `channel-streaming` Plugin SDK subpath was removed; plugins now import channel streaming helpers from `channel-outbound`.",
},
{
code: "plugin-sdk-config-runtime-subpath",
@@ -45,39 +48,49 @@ const DEPRECATED_PLUGIN_SDK_SUBPATH_SEEDS = [
{
code: "plugin-sdk-text-runtime-subpath",
subpath: "text-runtime",
status: "removed",
owner: "sdk",
removeAfter: "2026-08-15",
replacement:
"`openclaw/plugin-sdk/logging-core`, `openclaw/plugin-sdk/text-chunking`, `openclaw/plugin-sdk/text-utility-runtime`, and `openclaw/plugin-sdk/string-coerce-runtime`",
releaseNote:
"The deprecated `text-runtime` Plugin SDK facade was removed; plugins now import logging, chunking, text utility, and string coercion helpers from their focused subpaths.",
},
{
code: "plugin-sdk-channel-secret-runtime-subpath",
subpath: "channel-secret-runtime",
status: "removed",
owner: "channel",
removeAfter: "2026-08-15",
replacement:
"`openclaw/plugin-sdk/channel-secret-basic-runtime` and `openclaw/plugin-sdk/channel-secret-tts-runtime`",
releaseNote:
"The deprecated `channel-secret-runtime` Plugin SDK subpath was removed; plugins now use the focused basic and TTS secret-runtime subpaths.",
},
{
code: "plugin-sdk-agent-config-primitives-subpath",
subpath: "agent-config-primitives",
status: "removed",
owner: "config",
removeAfter: "2026-08-15",
replacement: "`openclaw/plugin-sdk/channel-config-schema`",
releaseNote:
"The deprecated `agent-config-primitives` Plugin SDK subpath was removed; plugins now use maintained config-schema primitives.",
},
{
code: "plugin-sdk-matrix-subpath",
subpath: "matrix",
status: "removed",
owner: "channel",
removeAfter: "2026-08-15",
replacement: "`openclaw/plugin-sdk/run-command`",
releaseNote:
"The deprecated `matrix` Plugin SDK facade was removed; command execution now uses the generic `run-command` subpath.",
},
{
code: "plugin-sdk-channel-logging-subpath",
subpath: "channel-logging",
status: "removed",
owner: "channel",
removeAfter: "2026-08-15",
replacement: "`openclaw/plugin-sdk/channel-inbound` and `openclaw/plugin-sdk/channel-outbound`",
releaseNote:
"The deprecated `channel-logging` Plugin SDK subpath was removed; channel logging helpers now come from the inbound and outbound channel surfaces.",
},
{
code: "plugin-sdk-channel-lifecycle-subpath",
@@ -96,38 +109,60 @@ const DEPRECATED_PLUGIN_SDK_SUBPATH_SEEDS = [
{
code: "plugin-sdk-group-access-subpath",
subpath: "group-access",
status: "removed",
owner: "channel",
removeAfter: "2026-08-15",
replacement: "`openclaw/plugin-sdk/channel-ingress-runtime`",
releaseNote:
"The deprecated `group-access` Plugin SDK subpath was removed; plugins now resolve message admission through `channel-ingress-runtime`.",
},
{
code: "plugin-sdk-zod-subpath",
subpath: "zod",
status: "removed",
owner: "sdk",
removeAfter: "2026-08-15",
replacement: "the direct `zod` package import",
releaseNote:
"The deprecated `zod` Plugin SDK re-export was removed; plugins now import `zod` directly.",
},
] as const satisfies readonly DeprecatedPluginSdkSubpathSeed[];
] as const satisfies readonly PluginSdkSubpathSeed[];
export const DEPRECATED_PLUGIN_SDK_SUBPATH_RECORDS = DEPRECATED_PLUGIN_SDK_SUBPATH_SEEDS.map(
(seed) =>
({
function buildPluginSdkSubpathRecord(seed: (typeof PLUGIN_SDK_SUBPATH_SEEDS)[number]) {
if ("status" in seed) {
return {
code: seed.code,
status: "deprecated" as const,
status: seed.status,
owner: seed.owner,
introduced: "2026-07-06",
deprecated: "2026-07-06",
warningStarts: "2026-07-06",
removeAfter: "removeAfter" in seed ? seed.removeAfter : undefined,
removalGate: "removalGate" in seed ? seed.removalGate : undefined,
replacement: seed.replacement,
docsPath: "/plugins/sdk-migration",
surfaces: [`openclaw/plugin-sdk/${seed.subpath}`],
diagnostics: [
"repository deprecated API usage guard for core and bundled plugins; no external runtime import warning",
],
diagnostics: ["plugin SDK compatibility registry and migration guide"],
tests: ["src/plugins/compat/registry.test.ts"],
}) satisfies PluginCompatRecord,
releaseNote: seed.releaseNote,
} satisfies PluginCompatRecord;
}
return {
code: seed.code,
status: "deprecated",
owner: seed.owner,
introduced: "2026-07-06",
deprecated: "2026-07-06",
warningStarts: "2026-07-06",
removeAfter: "removeAfter" in seed ? seed.removeAfter : undefined,
removalGate: "removalGate" in seed ? seed.removalGate : undefined,
replacement: seed.replacement,
docsPath: "/plugins/sdk-migration",
surfaces: [`openclaw/plugin-sdk/${seed.subpath}`],
diagnostics: [
"repository deprecated API usage guard for core and bundled plugins; no external runtime import warning",
],
tests: ["src/plugins/compat/registry.test.ts"],
} satisfies PluginCompatRecord;
}
export const PLUGIN_SDK_SUBPATH_RECORDS = PLUGIN_SDK_SUBPATH_SEEDS.map(
buildPluginSdkSubpathRecord,
) satisfies readonly PluginCompatRecord[];
const BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_SEEDS = [
+7 -10
View File
@@ -2,12 +2,12 @@ import { DEPRECATION_MARKING_COMPAT_RECORDS } from "./deprecation-marking.js";
import { MEDIA_LEGACY_PROJECTION_COMPAT_RECORD } from "./media-legacy-projection.js";
import {
BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_RECORDS,
DEPRECATED_PLUGIN_SDK_SUBPATH_RECORDS,
PLUGIN_SDK_SUBPATH_RECORDS,
} from "./plugin-sdk-subpath-records.js";
import type { PluginCompatRecord } from "./types.js";
export const PLUGIN_COMPAT_RECORDS = [
...DEPRECATED_PLUGIN_SDK_SUBPATH_RECORDS,
...PLUGIN_SDK_SUBPATH_RECORDS,
...BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_RECORDS,
...DEPRECATION_MARKING_COMPAT_RECORDS,
MEDIA_LEGACY_PROJECTION_COMPAT_RECORD,
@@ -44,19 +44,16 @@ export const PLUGIN_COMPAT_RECORDS = [
},
{
code: "legacy-deactivate-hook-alias",
status: "deprecated",
status: "removed",
owner: "sdk",
introduced: "2026-05-16",
deprecated: "2026-05-16",
warningStarts: "2026-05-16",
removeAfter: "2026-08-16",
replacement: "`gateway_stop` hook",
docsPath: "/plugins/hooks#upcoming-deprecations",
docsPath: "/plugins/sdk-migration#deactivate-hook-alias",
surfaces: ['api.on("deactivate", ...)', "plugin typed hook registration"],
diagnostics: ["plugin runtime compatibility warning"],
tests: ["src/plugins/loader.test.ts"],
diagnostics: ["plugin compatibility registry and migration guide"],
tests: ["src/plugins/compat/registry.test.ts"],
releaseNote:
'`api.on("deactivate", ...)` remains wired as a deprecated compatibility alias while plugins migrate to `gateway_stop`.',
'The deprecated `api.on("deactivate", ...)` hook alias was removed; plugins must register cleanup with `gateway_stop`.',
},
{
code: "legacy-subagent-spawning-hook",
+34
View File
@@ -25,6 +25,16 @@ const removalDatePendingCompatCodes = new Set<PluginCompatCode>([
"plugin-sdk-tool-plugin-public-demotion",
"agent-harness-sdk-alias",
]);
const retiredPluginSdkSubpathCodes = [
"plugin-sdk-channel-streaming-subpath",
"plugin-sdk-text-runtime-subpath",
"plugin-sdk-channel-secret-runtime-subpath",
"plugin-sdk-agent-config-primitives-subpath",
"plugin-sdk-matrix-subpath",
"plugin-sdk-channel-logging-subpath",
"plugin-sdk-group-access-subpath",
"plugin-sdk-zod-subpath",
] as const satisfies readonly PluginCompatCode[];
const deprecationMarkingCodes = [
"plugin-sdk-channel-setup-input-fields",
"plugin-sdk-broad-runtime-barrels",
@@ -137,6 +147,18 @@ describe("plugin compatibility registry", () => {
]);
});
it("keeps retired Plugin SDK subpaths as migration tombstones", () => {
const records = new Map(listPluginCompatRecords().map((record) => [record.code, record]));
for (const code of retiredPluginSdkSubpathCodes) {
expect(records.get(code)).toMatchObject({
status: "removed",
releaseNote: expect.stringMatching(/\S/u),
});
expect(records.get(code)?.removeAfter, code).toBeUndefined();
}
});
it("tracks the deprecation-marking families through the approved window", () => {
const records = new Map(listPluginCompatRecords().map((record) => [record.code, record]));
@@ -189,6 +211,18 @@ describe("plugin compatibility registry", () => {
expect(record?.removeAfter).toBeUndefined();
});
it("keeps the removed deactivate hook alias as a migration tombstone", () => {
const record = listPluginCompatRecords().find(
(candidate) => candidate.code === "legacy-deactivate-hook-alias",
);
expect(record).toMatchObject({
status: "removed",
replacement: "`gateway_stop` hook",
});
expect(record?.removeAfter).toBeUndefined();
});
it("keeps deprecated explicit target parser calls inside compatibility shims", () => {
expect(deprecatedTargetParserOffenders).toEqual([]);
});
@@ -227,12 +227,6 @@ describe("opt-in extension package boundaries", () => {
expect(packageJson.exports?.["./acp-runtime"]?.types).toBe(
"./dist/src/plugin-sdk/acp-runtime.d.ts",
);
expect(packageJson.exports?.["./channel-secret-runtime"]?.types).toBe(
"./dist/src/plugin-sdk/channel-secret-runtime.d.ts",
);
expect(packageJson.exports?.["./channel-streaming"]?.types).toBe(
"./dist/src/plugin-sdk/channel-streaming.d.ts",
);
expect(packageJson.exports?.["./cli-runtime"]?.types).toBe(
"./dist/src/plugin-sdk/cli-runtime.d.ts",
);
@@ -291,10 +285,6 @@ describe("opt-in extension package boundaries", () => {
expect(packageJson.exports?.["./infra-runtime"]?.types).toBe(
"./dist/src/plugin-sdk/infra-runtime.d.ts",
);
expect(packageJson.exports?.["./text-runtime"]?.types).toBe(
"./dist/src/plugin-sdk/text-runtime.d.ts",
);
expect(packageJson.exports?.["./zod"]?.types).toBe("./dist/src/plugin-sdk/zod.d.ts");
expect(fs.existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe(
false,
);
@@ -667,7 +667,6 @@ describe("plugin-sdk subpath exports", () => {
"createChannelHistoryWindow",
"recordPendingHistoryEntryIfEnabled",
]);
expectSourceMentions("matrix", ["runPluginCommandWithTimeout"]);
expectSourceContract("reply-runtime", {
omits: [
"buildPendingHistoryContextFromMap",
@@ -694,25 +693,6 @@ describe("plugin-sdk subpath exports", () => {
],
omits: ["collectNestedChannelTtsAssignments"],
});
expectSourceContract("channel-secret-runtime", {
mentions: [
"collectSimpleChannelFieldAssignments",
"collectConditionalChannelFieldAssignments",
"collectSecretInputAssignment",
"getChannelSurface",
"pushAssignment",
"pushInactiveSurfaceWarning",
"ResolverContext",
"SecretTargetRegistryEntry",
],
omits: [
"buildChannelMetadata",
"buildUntrustedChannelMetadata",
"evaluateSupplementalContextVisibility",
"resolvePinnedMainDmOwnerFromAllowlist",
"safeMatchRegex",
],
});
expectSourceContract("channel-secret-tts-runtime", {
mentions: ["collectNestedChannelTtsAssignments"],
omits: ["collectSimpleChannelFieldAssignments", "collectConditionalChannelFieldAssignments"],
@@ -1160,8 +1140,6 @@ describe("plugin-sdk subpath exports", () => {
]);
expectRepoSourceOmitsSnippet("src/channels/ack-reactions.ts", "shouldAckReactionForWhatsApp");
expectRepoSourceOmitsSnippet("src/channels/ack-reactions.ts", "WhatsAppAckReactionMode");
expectSourceMentions("channel-streaming", ["SlackChannelStreamingConfig"]);
expectRepoSourceOmitsSnippet("src/channels/streaming.ts", "SlackChannelStreamingConfig");
expectSourceMentions("status-helpers", [
"appendMatchMetadata",
"asString",
+1 -22
View File
@@ -129,8 +129,6 @@ export type PluginHookName =
| "subagent_spawned"
| "subagent_progress"
| "subagent_ended"
/** @deprecated Use gateway_stop. */
| "deactivate"
| "gateway_start"
| "gateway_stop"
| "heartbeat_prompt_contribution"
@@ -176,7 +174,6 @@ const PLUGIN_HOOK_NAMES = [
"subagent_spawned",
"subagent_progress",
"subagent_ended",
"deactivate",
"gateway_start",
"gateway_stop",
"heartbeat_prompt_contribution",
@@ -197,7 +194,7 @@ type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? tru
const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true;
void assertAllPluginHookNamesListed;
type DeprecatedPluginHookName = "subagent_spawning" | "deactivate";
type DeprecatedPluginHookName = "subagent_spawning";
type PluginHookDeprecation = {
replacement: string;
@@ -231,11 +228,6 @@ export const DEPRECATED_PLUGIN_HOOKS = {
"Core prepares thread-bound subagent bindings through channel session-binding adapters before `subagent_spawned` fires.",
removeAfter: "2026-08-30",
},
deactivate: {
replacement: "`gateway_stop`",
reason: "`deactivate` is a legacy cleanup hook alias for `gateway_stop`.",
removeAfter: "2026-08-16",
},
} as const satisfies Record<DeprecatedPluginHookName, PluginHookDeprecation>;
const DEPRECATED_PLUGIN_HOOK_NAMES = Object.keys(
@@ -1375,19 +1367,6 @@ export type PluginHookHandlerMap = {
event: PluginHookSubagentEndedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
/**
* Deprecated compatibility alias for gateway_stop.
*
* New plugins should register gateway_stop directly; the loader normalizes
* deactivate registrations onto gateway_stop so cleanup handlers still run
* during Gateway shutdown.
*
* @deprecated Use gateway_stop.
*/
deactivate: (
event: PluginHookGatewayStopEvent,
ctx: PluginHookGatewayContext,
) => Promise<void> | void;
gateway_start: (
event: PluginHookGatewayStartEvent,
ctx: PluginHookGatewayContext,
@@ -1464,45 +1464,6 @@ ${channelPluginSource({
]);
});
it("normalizes legacy deactivate typed hooks onto gateway_stop", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "legacy-deactivate-hook",
filename: "legacy-deactivate-hook.cjs",
body: `module.exports = { id: "legacy-deactivate-hook", register(api) {
api.on("deactivate", () => undefined);
} };`,
});
const registry = loadRegistryFromSinglePlugin({
plugin,
pluginConfig: {
allow: ["legacy-deactivate-hook"],
entries: {
"legacy-deactivate-hook": {
hooks: {
timeoutMs: 250,
},
},
},
},
});
expect(registry.plugins.find((entry) => entry.id === "legacy-deactivate-hook")?.status).toBe(
"loaded",
);
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["gateway_stop"]);
expect(registry.typedHooks[0]?.timeoutMs).toBe(250);
expect(
registry.diagnostics.some(
(diag) =>
diag.pluginId === "legacy-deactivate-hook" &&
diag.message ===
'typed hook "deactivate" is deprecated (legacy-deactivate-hook-alias); use "gateway_stop". This compatibility alias will be removed after 2026-08-16.',
),
).toBe(true);
});
it("warns when plugins register deprecated subagent_spawning typed hooks", () => {
useNoBundledPlugins();
const plugin = writePlugin({
+16 -41
View File
@@ -52,7 +52,6 @@ import type {
PluginHookRegistration as TypedPluginHookRegistration,
} from "./types.js";
const LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT = getPluginCompatRecord("legacy-deactivate-hook-alias");
const LEGACY_SUBAGENT_SPAWNING_HOOK_COMPAT = getPluginCompatRecord("legacy-subagent-spawning-hook");
function normalizeEligibleTriggers(value: unknown) {
@@ -66,17 +65,8 @@ function normalizeEligibleTriggers(value: unknown) {
return uniqueValues(triggers);
}
function formatLegacyDeactivateHookAliasDiagnostic(): string {
const removeAfter =
LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.removeAfter ?? "a future breaking release";
return (
`typed hook "deactivate" is deprecated (${LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.code}); ` +
`use "gateway_stop". This compatibility alias will be removed after ${removeAfter}.`
);
}
function formatDeprecatedTypedHookDiagnostic(hookName: PluginHookName): string | undefined {
if (!isDeprecatedPluginHookName(hookName) || hookName === "deactivate") {
if (!isDeprecatedPluginHookName(hookName)) {
return undefined;
}
const deprecation = DEPRECATED_PLUGIN_HOOKS[hookName];
@@ -425,36 +415,25 @@ export function createToolHookRegistrars(state: PluginRegistryState) {
});
return;
}
const effectiveHookName = hookName === "deactivate" ? "gateway_stop" : hookName;
if (hookName === "deactivate") {
const diagnostic = formatDeprecatedTypedHookDiagnostic(hookName);
if (diagnostic) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: formatLegacyDeactivateHookAliasDiagnostic(),
message: diagnostic,
});
} else {
const diagnostic = formatDeprecatedTypedHookDiagnostic(hookName);
if (diagnostic) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: diagnostic,
});
}
}
const effectiveHandler = handler;
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(effectiveHookName)) {
if (policy?.allowPromptInjection === false && isPromptInjectionHookName(hookName)) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: `typed hook "${effectiveHookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`,
});
return;
}
if (isConversationHookName(effectiveHookName)) {
if (isConversationHookName(hookName)) {
const explicitConversationAccess = policy?.allowConversationAccess;
if (record.origin !== "bundled" && explicitConversationAccess !== true) {
pushDiagnostic({
@@ -462,7 +441,7 @@ export function createToolHookRegistrars(state: PluginRegistryState) {
pluginId: record.id,
source: record.source,
message:
`typed hook "${effectiveHookName}" blocked because non-bundled plugins must set ` +
`typed hook "${hookName}" blocked because non-bundled plugins must set ` +
`plugins.entries.${record.id}.hooks.allowConversationAccess=true`,
});
return;
@@ -472,38 +451,34 @@ export function createToolHookRegistrars(state: PluginRegistryState) {
level: "warn",
pluginId: record.id,
source: record.source,
message: `typed hook "${effectiveHookName}" blocked by plugins.entries.${record.id}.hooks.allowConversationAccess=false`,
message: `typed hook "${hookName}" blocked by plugins.entries.${record.id}.hooks.allowConversationAccess=false`,
});
return;
}
}
const timeoutMs = resolveTypedHookTimeoutMs({ hookName: effectiveHookName, opts, policy });
const timeoutMs = resolveTypedHookTimeoutMs({ hookName, opts, policy });
const eligibleTriggers =
effectiveHookName === "before_agent_reply"
hookName === "before_agent_reply"
? normalizeEligibleTriggers(opts?.eligibleTriggers)
: undefined;
const matcher =
effectiveHookName === "before_tool_call" || effectiveHookName === "after_tool_call"
hookName === "before_tool_call" || hookName === "after_tool_call"
? normalizePluginToolMatcher(opts?.matcher)
: undefined;
if (
opts?.matcher &&
effectiveHookName !== "before_tool_call" &&
effectiveHookName !== "after_tool_call"
) {
if (opts?.matcher && hookName !== "before_tool_call" && hookName !== "after_tool_call") {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: `typed hook "${effectiveHookName}" ignores tool matcher`,
message: `typed hook "${hookName}" ignores tool matcher`,
});
}
record.hookCount += 1;
registry.typedHooks.push({
pluginId: record.id,
...(opts?.registrationId ? { registrationId: opts.registrationId } : {}),
hookName: effectiveHookName,
handler: effectiveHandler,
hookName,
handler,
...(matcher ? { matcher } : {}),
priority: opts?.priority,
...(timeoutMs !== undefined ? { timeoutMs } : {}),