refactor(plugin-sdk): prune eligible deprecated exports (#106010)

* refactor(plugin-sdk): remove unshipped compatibility exports

* refactor(zalo): use typed outbound delivery results

* chore(plugin-sdk): ratchet deprecated surface budgets

* chore(changelog): defer release note to release automation

* fix(zalo): reject failed message adapter sends

* refactor(zalo): share typed send boundary

* chore(plugin-sdk): account for main deprecation drift
This commit is contained in:
Peter Steinberger
2026-07-13 01:39:38 -07:00
committed by GitHub
parent 0d177c9b4d
commit c2da10585c
11 changed files with 132 additions and 116 deletions
@@ -1,2 +1,2 @@
8bb175b965c6fab4512f110cd584ba601074885fb49375f2dd18e832882d61a5 plugin-sdk-api-baseline.json
5a1548ecb0cf8dffb67591b41b9e1a3c04ee7fe0cb22ac2f3bcfa24e319f4f47 plugin-sdk-api-baseline.jsonl
598c5f1bb39362de6b11b4cd3403551f99fb7c3bbf85d5e3dcf1b1408a8cec24 plugin-sdk-api-baseline.json
d87a296d6d449c2e01b63eeb1e1743d1f11dcdf4091520328c54798ac18f149f plugin-sdk-api-baseline.jsonl
+12
View File
@@ -791,6 +791,18 @@ major release. Every entry maps the old API to its canonical replacement.
</Accordion>
<Accordion title="Raw channel send results -> OutboundDeliveryResult">
**Old**: return `{ ok, messageId, error }` through
`ChannelSendRawResult` and normalize it with
`createRawChannelSendResultAdapter(...)`.
**New**: return `OutboundDeliveryResult` fields and attach the channel with
`createAttachedChannelResultAdapter(...)`. Failed sends should throw instead
of returning an error string. The raw result type remains available until
the next plugin-SDK major release.
</Accordion>
<Accordion title="Subagent session messages types renamed">
Two legacy type aliases still exported from `src/plugins/runtime/types.ts`:
@@ -1,5 +1,5 @@
// Telegram tests cover inline buttons plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import { buildTelegramInteractiveButtons } from "./button-types.js";
import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js";
+35 -44
View File
@@ -14,15 +14,18 @@ import {
createChatChannelPlugin,
type ChannelPlugin,
} from "openclaw/plugin-sdk/channel-core";
import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
import {
defineChannelMessageAdapter,
type MessageReceipt,
} from "openclaw/plugin-sdk/channel-outbound";
import {
buildOpenGroupPolicyRestrictSendersWarning,
buildOpenGroupPolicyWarning,
createOpenProviderGroupPolicyWarningCollector,
} from "openclaw/plugin-sdk/channel-policy";
import {
createAttachedChannelResultAdapter,
createEmptyChannelResult,
createRawChannelSendResultAdapter,
} from "openclaw/plugin-sdk/channel-send-result";
import { buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/channel-status";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -82,27 +85,32 @@ const zaloSetupWizard = createZaloSetupWizardProxy(
);
const zaloTextChunkLimit = 2000;
const zaloRawSendResultAdapter = createRawChannelSendResultAdapter({
async function sendZaloDelivery(ctx: {
cfg: OpenClawConfig;
to: string;
text: string;
accountId?: string | null;
mediaUrl?: string;
}): Promise<{ messageId: string; receipt: MessageReceipt }> {
const result = await (
await loadZaloChannelRuntime()
).sendZaloText({
to: ctx.to,
text: ctx.text,
accountId: ctx.accountId ?? undefined,
mediaUrl: ctx.mediaUrl,
cfg: ctx.cfg,
});
if (!result.ok) {
throw new Error(result.error ?? `Failed to send Zalo ${ctx.mediaUrl ? "media" : "message"}`);
}
return { messageId: result.messageId ?? "", receipt: result.receipt };
}
const zaloSendResultAdapter = createAttachedChannelResultAdapter({
channel: "zalo",
sendText: async ({ to, text, accountId, cfg }) =>
await (
await loadZaloChannelRuntime()
).sendZaloText({
to,
text,
accountId: accountId ?? undefined,
cfg,
}),
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) =>
await (
await loadZaloChannelRuntime()
).sendZaloText({
to,
text,
accountId: accountId ?? undefined,
mediaUrl,
cfg,
}),
sendText: sendZaloDelivery,
sendMedia: sendZaloDelivery,
});
export const zaloMessageAdapter = defineChannelMessageAdapter({
@@ -115,25 +123,8 @@ export const zaloMessageAdapter = defineChannelMessageAdapter({
},
},
send: {
text: async ({ to, text, accountId, cfg }) =>
await (
await loadZaloChannelRuntime()
).sendZaloText({
to,
text,
accountId: accountId ?? undefined,
cfg,
}),
media: async ({ to, text, mediaUrl, accountId, cfg }) =>
await (
await loadZaloChannelRuntime()
).sendZaloText({
to,
text,
accountId: accountId ?? undefined,
mediaUrl,
cfg,
}),
text: sendZaloDelivery,
media: sendZaloDelivery,
},
});
@@ -303,11 +294,11 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount, ZaloProbeResult> =
ctx,
textChunkLimit: zaloTextChunkLimit,
chunker: chunkTextForOutbound,
sendText: (nextCtx) => zaloRawSendResultAdapter.sendText!(nextCtx),
sendMedia: (nextCtx) => zaloRawSendResultAdapter.sendMedia!(nextCtx),
sendText: (nextCtx) => zaloSendResultAdapter.sendText!(nextCtx),
sendMedia: (nextCtx) => zaloSendResultAdapter.sendMedia!(nextCtx),
emptyResult: createEmptyChannelResult("zalo"),
onResult: ctx.onDeliveryResult,
}),
...zaloRawSendResultAdapter,
...zaloSendResultAdapter,
},
});
@@ -6,9 +6,16 @@ import {
} from "openclaw/plugin-sdk/channel-contract-testing";
import {
createMessageReceiptFromOutboundResults,
sendDurableMessageBatch,
verifyChannelMessageAdapterCapabilityProofs,
} from "openclaw/plugin-sdk/channel-outbound";
import { describe, expect, it, vi } from "vitest";
import {
createEmptyPluginRegistry,
createTestRegistry,
resetPluginRuntimeStateForTest,
setActivePluginRegistry,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { zaloMessageAdapter, zaloPlugin } from "./channel.js";
const { sendZaloTextMock } = vi.hoisted(() => ({
@@ -47,9 +54,21 @@ function requireZaloMediaSender(): NonNullable<ZaloMessageSender["media"]> {
return media;
}
function requireZaloOutboundTextSender(): NonNullable<ZaloOutbound["sendText"]> {
const sendText = zaloPlugin.outbound?.sendText;
if (!sendText) {
throw new Error("Expected Zalo outbound text sender");
}
return sendText;
}
function createZaloHarness(params: OutboundPayloadHarnessParams) {
const sendZalo = vi.fn();
primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults);
primeChannelOutboundSendMock(
sendZalo,
{ ok: true, messageId: "zl-1" },
params.sendResults?.map((result) => ({ ok: true, ...result })),
);
sendZaloTextMock.mockReset().mockImplementation(
async (nextCtx: { to: string; text: string; mediaUrl?: string }) =>
await sendZalo(nextCtx.to, nextCtx.text, {
@@ -70,6 +89,11 @@ function createZaloHarness(params: OutboundPayloadHarnessParams) {
};
}
afterEach(() => {
resetPluginRuntimeStateForTest();
setActivePluginRegistry(createEmptyPluginRegistry());
});
describe("Zalo outbound payload contract", () => {
installChannelOutboundPayloadContractSuite({
channel: "zalo",
@@ -77,6 +101,52 @@ describe("Zalo outbound payload contract", () => {
createHarness: createZaloHarness,
});
it("throws legacy raw send failures at the typed delivery boundary", async () => {
sendZaloTextMock.mockReset().mockResolvedValue({
ok: false,
error: "Zalo rejected the message",
receipt: createMessageReceiptFromOutboundResults({ results: [], kind: "text" }),
});
await expect(
requireZaloOutboundTextSender()({ cfg: {}, to: "123456789", text: "hello" }),
).rejects.toThrow("Zalo rejected the message");
});
it.each([
{ kind: "text", payload: { text: "hello" } },
{
kind: "media",
payload: { text: "image", mediaUrl: "https://example.com/image.png" },
},
] as const)(
"reports $kind message-adapter failures to durable delivery",
async ({ kind, payload }) => {
setActivePluginRegistry(
createTestRegistry([{ pluginId: "zalo", source: "test", plugin: zaloPlugin }]),
);
sendZaloTextMock.mockReset().mockResolvedValue({
ok: false,
error: `Zalo rejected the ${kind}`,
receipt: createMessageReceiptFromOutboundResults({ results: [], kind }),
});
const result = await sendDurableMessageBatch({
cfg: {},
channel: "zalo",
to: "123456789",
payloads: [payload],
skipQueue: true,
});
expect(result.status).toBe("failed");
if (result.status !== "failed") {
throw new Error(`Expected failed Zalo ${kind} delivery`);
}
expect(result.error).toMatchObject({ message: `Zalo rejected the ${kind}` });
},
);
it("declares message adapter durable text and media with receipt proofs", async () => {
sendZaloTextMock.mockReset().mockImplementation(async (ctx: { mediaUrl?: string }) =>
ctx.mediaUrl
-8
View File
@@ -55,14 +55,6 @@ function findViolations(content, filePath) {
reason: "readChannelAllowFromStore call must pass explicit accountId as 3rd arg",
});
}
} else if (
callName === "readLegacyChannelAllowFromStore" ||
callName === "readLegacyChannelAllowFromStoreSync"
) {
violations.push({
line: toLine(sourceFile, node),
reason: `${callName} is legacy-only; use account-scoped readChannelAllowFromStore* APIs`,
});
} else if (callName === "upsertChannelPairingRequest") {
const firstArg = node.arguments[0];
if (!firstArg || !hasRequiredAccountIdProperty(firstArg)) {
+5 -7
View File
@@ -104,7 +104,6 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"channel-streaming": 49,
"approval-gateway-runtime": 1,
"approval-handler-runtime": 1,
"approval-reaction-runtime": 1,
"approval-reply-runtime": 3,
"approval-runtime": 1,
"config-runtime": 123,
@@ -125,6 +124,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"ssrf-runtime": 1,
"media-runtime": 2,
"text-runtime": 191,
"agent-core": 1,
"agent-runtime": 7,
"plugin-runtime": 13,
"channel-secret-runtime": 23,
@@ -158,6 +158,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
// landed on main (#105802) without entrypoint pins; not touched by this PR.
"channel-pairing": 1,
"conversation-runtime": 4,
"channel-send-result": 1,
"channel-policy": 8,
"channel-route": 5,
"session-store-runtime": 4,
@@ -203,20 +204,17 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
),
publicExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
10644,
10640,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
5358,
5354,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
// 3279 + 5 deprecated pairing/conversation exports added on main by the
// SQLite pairing migration (#105802) without a pin bump (its changed-path
// set skipped this lane); sources are byte-identical to main here.
3284,
3282,
env,
),
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
-19
View File
@@ -210,14 +210,6 @@ async function updateAllowFromStoreEntry(params: {
}, sqliteOptionsForEnv(env));
}
/** @deprecated Reads the canonical default-account SQLite rows. */
export async function readLegacyChannelAllowFromStore(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): Promise<string[]> {
return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID);
}
export async function readChannelAllowFromStore(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
@@ -226,14 +218,6 @@ export async function readChannelAllowFromStore(
return readAllowFromState(channel, env, accountId);
}
/** @deprecated Reads the canonical default-account SQLite rows. */
export function readLegacyChannelAllowFromStoreSync(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
): string[] {
return readAllowFromState(channel, env, DEFAULT_ACCOUNT_ID);
}
export function readChannelAllowFromStoreSync(
channel: PairingChannel,
env: NodeJS.ProcessEnv = process.env,
@@ -242,9 +226,6 @@ export function readChannelAllowFromStoreSync(
return readAllowFromState(channel, env, accountId);
}
/** @deprecated SQLite reads are uncached; retained for the shipped SDK test surface. */
export function clearPairingAllowFromReadCacheForTest(): void {}
type AllowFromStoreEntryUpdateParams = {
channel: PairingChannel;
entry: string | number;
@@ -14,7 +14,6 @@ import {
listApprovalReactionBindings,
normalizeApprovalReactionEmoji,
resolveApprovalReactionDecision,
resolveApprovalReactionTarget,
resolveTypedApprovalReactionTarget,
shouldSuppressLocalNativeExecApprovalPrompt,
} from "./approval-reaction-runtime.js";
@@ -145,23 +144,6 @@ describe("plugin-sdk/approval-reaction-runtime", () => {
});
});
it("preserves deprecated id-based kind inference", () => {
expect(
resolveApprovalReactionTarget({
target: {
approvalId: "plugin:legacy-id",
allowedDecisions: ["deny"],
},
reactionKey: "👎",
}),
).toEqual({
approvalId: "plugin:legacy-id",
approvalKind: "plugin",
decision: "deny",
normalizedEmoji: "👎",
});
});
it("builds canonical exec reaction prompts without presentation controls", () => {
const payload = buildApprovalReactionPromptPayloadForRequest({
request: execRequest,
@@ -242,20 +242,6 @@ function resolveApprovalReactionTargetInternal<TRoute>(params: {
};
}
/**
* Resolve a stored target while retaining shipped id-based ownership inference.
* @deprecated Use resolveTypedApprovalReactionTarget with an explicit approvalKind.
*/
export function resolveApprovalReactionTarget<TRoute = unknown>(params: {
target: ApprovalReactionTargetRecord<TRoute> | null | undefined;
reactionKey: string;
}): ApprovalReactionTargetResolution<TRoute> | null {
return resolveApprovalReactionTargetInternal({
...params,
allowLegacyKindInference: true,
});
}
/** Resolve an explicitly typed target without deriving ownership from its id. */
export function resolveTypedApprovalReactionTarget<TRoute = unknown>(params: {
target:
+5 -1
View File
@@ -6,7 +6,11 @@ import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
export type { ChannelOutboundAdapter } from "../channels/plugins/outbound.types.js";
export type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
/** Legacy raw send result shape accepted from channel SDK adapters. */
/**
* Legacy raw send result shape accepted from channel SDK adapters.
* @deprecated Return `OutboundDeliveryResult` and use
* `createAttachedChannelResultAdapter`. Removal with the next plugin-SDK major.
*/
export type ChannelSendRawResult = {
/** Whether the channel send operation succeeded. */
ok: boolean;