refactor(plugin-sdk)!: retire unused command-auth, discord, and telegram-account facades (#163366)

Retire the public plugin-sdk/command-auth, plugin-sdk/discord, and plugin-sdk/telegram-account subpaths early, with the Plugin SDK owner's approval (2026-10-02). No bundled runtime imports them. No official external plugin at latest or extended-stable imports them either, and the only older @openclaw/discord releases that did (2026.3.7-3.13, 2026.5.2) already import subpaths main removed earlier.

Remove the modules, package exports, entrypoint inventory, compat registrations, and obsolete tests. Move the three surviving contract tests to their focused SDK subpaths. Drop 15 internal exports orphaned by the retirement and the dead isCommandMessage and resolveDualTextControlCommandGate wrappers. Shrink the SDK surface budgets by exactly 3 entrypoints, 128 exports, 79 callable exports, and 128 deprecated exports.

Breaking change for third-party plugins that import these paths: use channel-ingress-runtime, command-auth-native, command-status, or models-provider-runtime, as described in docs/plugins/sdk-migration/import-paths.md and the removal timeline.

Landed over inherited main reds only (Codex model/list sequence tests from #163320, the memory-host-sdk boundary test fixed by 2516be90f1).
This commit is contained in:
Peter Steinberger 2026-10-02 04:09:07 -05:00 • committed by GitHub
parent 745421393a
commit 0d530ea5b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 205 additions and 1368 deletions

View file

@ -27,7 +27,7 @@ barrel when authoring new plugins. Core subpaths:
Channel plugins pick from a family of narrow seams — `channel-setup`,
`setup-runtime`, `setup-tools`, `channel-pairing`,
`channel-contract`, `channel-feedback`, `channel-inbound`, `channel-outbound`,
`command-auth`, `secret-input`, `webhook-ingress`,
`command-auth-native`, `secret-input`, `webhook-ingress`,
`channel-targets`, and `channel-actions`. Approval behavior should consolidate
on one `approvalCapability` contract rather than mixing across unrelated
plugin fields. See [Channel plugins](/plugins/sdk-channel-plugins).

View file

@ -26,10 +26,9 @@ The mappings on this page are a migration subset, not the full SDK surface.
Check both the public subpath and its actual named exports before replacing an
import.
Reserved bundled-plugin helper seams have been retired from the public SDK
export map except for explicitly documented compatibility facades such as the
deprecated `plugin-sdk/discord` shim retained for external plugins that still
import the published `@openclaw/discord` package directly. Owner-specific
Reserved bundled-plugin helper seams, including the `plugin-sdk/discord` and
`plugin-sdk/telegram-account` compatibility facades, have been retired from the
public SDK export map. Owner-specific
helpers live inside the owning plugin package; shared host behavior moves
through generic SDK contracts such as `plugin-sdk/gateway-runtime`,
`plugin-sdk/security-runtime`, and the injected plugin API.
@ -38,6 +37,31 @@ Use the narrowest import that matches the job. If you cannot find an export,
check the source at `src/plugin-sdk/` or ask maintainers which generic
contract should own it.
### Removed command and channel facades
The `command-auth`, `discord`, and `telegram-account` subpaths were retired on
October 2, 2026 with explicit SDK-owner approval. Use these replacements:
| Removed surface | Replacement |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command-auth` sender authorization helpers and runtime parameter types | `resolveChannelMessageIngress` from `channel-ingress-runtime`; adapt the caller to the ingress contract rather than recreating legacy allowlist policy. |
| `command-auth` native command parsing, specs, authorization, and session-target helpers | The matching named exports from `command-auth-native`. |
| `command-auth` help builders | `buildCommandsMessage`, `buildCommandsMessagePaginated`, and `buildHelpMessage` from `command-status`. |
| `command-auth` model-provider data helpers | The matching named exports from `models-provider-runtime`. |
| `command-auth` access-group helpers | Delegate sender authorization to `channel-ingress-runtime`; `access-groups` is private-local and is not a third-party replacement. |
| `command-auth` direct-DM access helpers | The matching named exports from `channel-inbound`. |
| `discord` generic channel types and helpers | The matching named exports from `channel-contract`, `channel-core`, `channel-plugin-common`, `channel-status`, or `config-contracts`. |
| `discord` channel-owned helpers and types | Repository consumers use the Discord plugin's `api.ts` / `runtime-api.ts`; external plugins use generic channel contracts and the injected runtime. |
| `telegram-account` account resolution and types | Repository consumers use the Telegram plugin's `api.ts`; external plugins use generic channel contracts and injected runtime helpers. |
Check each named export before changing an import. The legacy sender-authorization
types and Discord facade's permissive component/thread-binding types are removed;
the owning APIs have their own contracts. Some legacy exports have no typed-public
replacement and require caller changes. Do not replace them with private-local
host exports or another plugin's private `src/*` files. Older published packages
that import these facades must be upgraded before loading them on a host containing
this removal; SDK-owner approval is not evidence of external migration.
<a id="retained-channel-facade-mappings" />
### Removed channel facade mappings

View file

@ -61,6 +61,13 @@ and [config and infrastructure migration](/plugins/sdk-migration/how-to-migrate)
for replacements and behavioral differences. System-event snapshot inspection
and consumption now use `openclaw/plugin-sdk/system-event-runtime`.
The compatibility subpaths `command-auth`, `discord`, and `telegram-account`
were retired early by explicit SDK-owner approval on October 2, 2026. That
decision closes their compatibility window; it does not certify that every
external plugin has migrated. Their public exports are removed. Upgrade affected
plugins before loading them on a host containing this removal. See the
[command and channel facade replacements](/plugins/sdk-migration/import-paths#removed-command-and-channel-facades).
Bundled-plugin migration does not prove that every external caller can use a
path-only replacement. Migrate the functions with verified typed-public mappings;
adapt the caller where a removed named type or required behavior lacks a

View file

@ -31,6 +31,21 @@ Some legacy helpers and named types require caller changes rather than an
import-path substitution. See the [channel mappings](/plugins/sdk-migration/import-paths#retained-channel-facade-mappings)
and [config and infrastructure migration steps](/plugins/sdk-migration/how-to-migrate).
### Command, Discord, and Telegram account facades
`command-auth`, `discord`, and `telegram-account` were removed with explicit
SDK-owner approval on October 2, 2026. Move sender authorization to
`channel-ingress-runtime`, native command helpers to `command-auth-native`, and
help builders to `command-status`. Discord and Telegram behavior remains owned
by their plugins; use generic channel contracts and injected runtime helpers
from external plugins, and the owning plugin's `api.ts` / `runtime-api.ts`
barrels for repository consumers.
These are breaking removals for third-party plugins that still import the old
subpaths, including older published `@openclaw/discord` packages. Upgrade affected
plugins before upgrading the host. Not every export has a path-only replacement;
see the [per-surface mappings](/plugins/sdk-migration/import-paths#removed-command-and-channel-facades).
### Process-global API-provider publication
`registerApiProvider(...)` and `unregisterApiProviders(...)` were removed from

View file

@ -52,12 +52,11 @@ map when they have tracked owner usage. They exist for bundled-plugin
maintenance only and are not recommended import paths for new third-party
plugins.
`openclaw/plugin-sdk/discord` and `openclaw/plugin-sdk/telegram-account` are
also kept as deprecated compatibility facades for tracked owner usage. They have
no published removal date; run `pnpm plugins:boundary-report` and see the
[removal timeline](/plugins/sdk-migration/removal-timeline) for the surfaces that
do. Do not copy those import paths into new plugins; use injected runtime helpers
and generic channel SDK subpaths instead.
`openclaw/plugin-sdk/discord` and `openclaw/plugin-sdk/telegram-account` were
removed with explicit SDK-owner approval on October 2, 2026. Use injected runtime
helpers and generic channel SDK subpaths; see the
[migration mappings](/plugins/sdk-migration/import-paths#removed-command-and-channel-facades)
before upgrading plugins that still import these facades.
</Warning>
For provider discovery that only needs credential values, use

View file

@ -238,8 +238,6 @@ new code; see the per-row notes below.
| `plugin-sdk/channel-plugin-common` | Shared channel plugin prelude exports |
| `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers |
| `plugin-sdk/direct-dm-guard-policy` | Private-local after July 2026; Narrow direct-DM pre-crypto guard policy helpers |
| `plugin-sdk/discord` | Deprecated Discord compatibility facade for published `@openclaw/discord@2026.3.13` and tracked owner compatibility; new plugins should use generic channel SDK subpaths |
| `plugin-sdk/telegram-account` | Deprecated Telegram account-resolution compatibility facade for tracked owner compatibility; new plugins should use injected runtime helpers or generic channel SDK subpaths |
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) |
| `plugin-sdk/question-gateway-runtime` | Resolve runtime-authored `ask_user` choices through the Gateway from channel interaction handlers |
| `plugin-sdk/channel-inbound` | Shared inbound helpers for event classification, context building, formatting, roots, debounce, mention matching, mention-policy, and inbound logging |
@ -314,7 +312,6 @@ usage endpoint failed or returned no usable usage data.
<Accordion title="Auth and security subpaths">
| Subpath | Key exports |
| --- | --- |
| `plugin-sdk/command-auth` | Deprecated broad command authorization surface (`resolveControlCommandGate`, command registry helpers including dynamic argument menu formatting, sender-authorization helpers); use channel ingress/runtime authorization or command-status helpers |
| `plugin-sdk/command-status` | Command/help message builders such as `buildCommandsMessagePaginated` and `buildHelpMessage` |
| `plugin-sdk/approval-auth-runtime` | Approver resolution and same-chat action-auth helpers |
| `plugin-sdk/approval-client-runtime` | Native exec approval profile/filter helpers |

View file

@ -1038,10 +1038,6 @@
"./plugin-sdk/dangerous-name-runtime": {
"default": "./dist/plugin-sdk/dangerous-name-runtime.js"
},
"./plugin-sdk/command-auth": {
"types": "./dist/plugin-sdk/command-auth.d.ts",
"default": "./dist/plugin-sdk/command-auth.js"
},
"./plugin-sdk/command-auth-native": {
"types": "./dist/plugin-sdk/command-auth-native.d.ts",
"default": "./dist/plugin-sdk/command-auth-native.js"
@ -1071,10 +1067,6 @@
"./plugin-sdk/direct-dm-guard-policy": {
"default": "./dist/plugin-sdk/direct-dm-guard-policy.js"
},
"./plugin-sdk/discord": {
"types": "./dist/plugin-sdk/discord.d.ts",
"default": "./dist/plugin-sdk/discord.js"
},
"./plugin-sdk/device-bootstrap": {
"types": "./dist/plugin-sdk/device-bootstrap.d.ts",
"default": "./dist/plugin-sdk/device-bootstrap.js"
@ -1585,10 +1577,6 @@
"./plugin-sdk/target-resolver-runtime": {
"default": "./dist/plugin-sdk/target-resolver-runtime.js"
},
"./plugin-sdk/telegram-account": {
"types": "./dist/plugin-sdk/telegram-account.d.ts",
"default": "./dist/plugin-sdk/telegram-account.js"
},
"./plugin-sdk/text-autolink-runtime": {
"default": "./dist/plugin-sdk/text-autolink-runtime.js"
},

View file

@ -105,7 +105,6 @@ import { prepareHostChannelContextAdmissionEvidence } from "openclaw/plugin-sdk/
import { registerChannelAdmissionEvidenceOwner } from "openclaw/plugin-sdk/channel-ingress-runtime";
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
import type { buildModelsProviderData, buildPreparedModelsProviderData, ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
import type { buildModelsProviderData as buildCommandAuthModelsProviderData } from "openclaw/plugin-sdk/command-auth";
import type { ClientRequestArgs } from "node:http";
import type { ClientOptions as PublishedClientOptions, WebSocket as PublishedWebSocket } from "ws";
import { WebSocket, type ClientOptions } from "openclaw/plugin-sdk/websocket-runtime";
@ -180,9 +179,8 @@ const legacyModelsData = {
};
const modelsData: ModelsProviderData = legacyModelsData;
const modelsAdapter: typeof buildModelsProviderData = async () => legacyModelsData;
const commandAuthModelsAdapter: typeof buildCommandAuthModelsProviderData = modelsAdapter;
void modelsData;
void commandAuthModelsAdapter;
void modelsAdapter;
declare const preparedModelsData: Awaited<ReturnType<typeof buildPreparedModelsProviderData>>;
const preparedCatalog: { id: string; provider: string; contextWindow?: number }[] = preparedModelsData.modelCatalog;
void preparedCatalog;

View file

@ -1,7 +1,4 @@
[
"agent-media-payload",
"command-auth",
"discord",
"inbound-reply-dispatch",
"telegram-account"
"inbound-reply-dispatch"
]

View file

@ -69,9 +69,6 @@ export const pluginSdkDocMetadata = {
"channel-dm-policy": {
category: "channel",
},
"command-auth": {
category: "channel",
},
"command-status": {
category: "channel",
},

View file

@ -108,7 +108,7 @@ export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((en
);
/** Supported SDK facades backed by bundled plugins until generic contracts replace them. */
export const supportedBundledFacadeSdkEntrypoints = ["discord", "telegram-account"] as const;
export const supportedBundledFacadeSdkEntrypoints = [] as const;
/** Plugin-owned surfaces intentionally public and documented for third-party plugins. */
export const publicPluginOwnedSdkEntrypoints = ["memory-core-host-engine-foundation"] as const;

View file

@ -185,7 +185,6 @@
"channel-contract-testing",
"channel-dm-policy",
"dangerous-name-runtime",
"command-auth",
"command-auth-native",
"command-primitives-runtime",
"command-status",
@ -194,7 +193,6 @@
"command-surface",
"collection-runtime",
"direct-dm-guard-policy",
"discord",
"device-bootstrap",
"diagnostic-flags",
"diagnostic-runtime",
@ -349,7 +347,6 @@
"string-normalization-runtime",
"state-paths",
"target-resolver-runtime",
"telegram-account",
"text-autolink-runtime",
"text-grapheme",
"text-utility-runtime",

View file

@ -155,8 +155,6 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
// +1: bounded structured-input compiler/executor for native harness protocol adapters.
"agent-harness": 2,
"agent-harness-runtime": 10,
"command-auth": 78,
discord: 47,
// +4: deprecated media projection type, builder, and turn aliases.
"channel-inbound": 18,
// +2: Slack progress-draft render bridge (function + mode type).
@ -173,33 +171,32 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"setup-runtime": 2,
"reply-history": 6,
"provider-auth": 19,
"telegram-account": 3,
} satisfies Record<string, number>);
export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env) {
const budgets = {
publicEntrypoints: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS",
154,
151,
env,
),
publicExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
// +1: createChannelSecretContract consolidates seven channel secret contracts (approved by Peter, 2026-10-01).
// +1: createSessionHeaderLink shares plugin-owned conversation navigation (PR #158742).
3759,
3631,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
// +1: createChannelSecretContract consolidates seven channel secret contracts (approved by Peter, 2026-10-01).
// +1: createSessionHeaderLink shares plugin-owned conversation navigation (PR #158742).
2188,
2109,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
269,
141,
env,
),
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(

View file

@ -156,7 +156,7 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
}
/** Returns cached exact and regex detectors for the current command registry instance. */
export function getCommandDetection(_cfg?: OpenClawConfig): CommandDetection {
function getCommandDetection(_cfg?: OpenClawConfig): CommandDetection {
return getCommandRegistryLookup().detection;
}

View file

@ -5,11 +5,9 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plug
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { createCommandTurnContext } from "./command-turn-context.js";
import {
buildCommandText,
buildCommandTextFromArgs,
findCommandByNativeName,
formatCommandArgMenuTitle,
getCommandDetection,
isActiveRunSafeCommandTurn,
listChatCommands,
listChatCommandsForConfig,
@ -184,8 +182,10 @@ function requireCommandArgMenu(
describe("commands registry", () => {
it("builds command text with args", () => {
expect(buildCommandText("status")).toBe("/status");
expect(buildCommandText("model", "gpt-5")).toBe("/model gpt-5");
expect(buildCommandTextFromArgs(requireChatCommand("status"))).toBe("/status");
expect(buildCommandTextFromArgs(requireChatCommand("model"), { raw: "gpt-5" })).toBe(
"/model gpt-5",
);
});
it("registers /login natively for Discord, Slack, and Telegram", () => {
@ -542,23 +542,21 @@ describe("commands registry", () => {
});
it("detects known text commands", () => {
const detection = getCommandDetection();
for (const command of listChatCommands()) {
for (const alias of command.textAliases) {
expect(detection.exact.has(alias.toLowerCase())).toBe(true);
expect(detection.regex.test(alias)).toBe(true);
expect(detection.regex.test(`${alias}:`)).toBe(true);
expect(resolveTextCommand(alias)?.command.key).toBe(command.key);
expect(resolveTextCommand(`${alias}:`)?.command.key).toBe(command.key);
if (command.acceptsArgs) {
expect(detection.regex.test(`${alias} list`)).toBe(true);
expect(detection.regex.test(`${alias}: list`)).toBe(true);
expect(resolveTextCommand(`${alias} list`)?.command.key).toBe(command.key);
expect(resolveTextCommand(`${alias}: list`)?.command.key).toBe(command.key);
} else {
expect(detection.regex.test(`${alias} list`)).toBe(false);
expect(detection.regex.test(`${alias}: list`)).toBe(false);
expect(resolveTextCommand(`${alias} list`)).toBeNull();
expect(resolveTextCommand(`${alias}: list`)).toBeNull();
}
}
}
expect(detection.regex.test("try /status")).toBe(false);
expect(resolveTextCommand("try /status")).toBeNull();
});
it("respects text command gating", () => {

View file

@ -14,7 +14,7 @@ import type { OpenClawConfig } from "../config/types.js";
import type { SkillCommandSpec } from "../skills/types.js";
import type { CommandTurnContext } from "./command-turn-context.js";
import { listChatCommands, listChatCommandsForConfig } from "./commands-registry-list.js";
import { normalizeCommandBody, resolveTextCommand } from "./commands-registry-normalize.js";
import { resolveTextCommand } from "./commands-registry-normalize.js";
import { getChatCommands } from "./commands-registry.data.js";
import type {
ChatCommandDefinition,
@ -25,33 +25,24 @@ import type {
NativeCommandSpec,
} from "./commands-registry.types.js";
export {
isCommandEnabled,
listChatCommands,
listChatCommandsForConfig,
} from "./commands-registry-list.js";
export { listChatCommands, listChatCommandsForConfig } from "./commands-registry-list.js";
export {
getCommandDetection,
maybeResolveTextAlias,
normalizeCommandBody,
resolveTextCommand,
} from "./commands-registry-normalize.js";
export { isNativeCommandSurface, shouldHandleTextCommands } from "./commands-text-routing.js";
export { shouldHandleTextCommands } from "./commands-text-routing.js";
export type {
ChatCommandDefinition,
CommandArgChoiceContext,
CommandArgDefinition,
CommandArgMenuSpec,
CommandArgValues,
CommandArgs,
CommandDetection,
CommandNormalizeOptions,
CommandScope,
NativeCommandSpec,
ShouldHandleTextCommandsParams,
} from "./commands-registry.types.js";
type NativeCommandProviderLookupOptions = {
@ -239,7 +230,7 @@ export function isActiveRunSafeCommandTurn(params: {
}
/** Formats a command and optional raw argument string as slash-command text. */
export function buildCommandText(commandName: string, args?: string): string {
function buildCommandText(commandName: string, args?: string): string {
const trimmedArgs = args?.trim();
return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`;
}
@ -487,9 +478,3 @@ export function formatCommandArgMenuTitle(params: {
}
return `Choose ${menu.arg.description || menu.arg.name} for /${commandLabel}.`;
}
/** Returns true for normalized slash-command text. */
export function isCommandMessage(raw: string): boolean {
const trimmed = normalizeCommandBody(raw);
return trimmed.startsWith("/");
}

View file

@ -6,7 +6,7 @@ import type { ThinkingCatalogEntry } from "./thinking.shared.js";
export type { CommandArgValues, CommandArgs } from "./commands-args.types.js";
/** Where a command may be invoked. */
export type CommandScope = "text" | "native" | "both";
type CommandScope = "text" | "native" | "both";
/**
* Controls progressive disclosure of commands in the UI.
@ -56,7 +56,7 @@ export type CommandArgDefinition = {
};
/** Menu metadata for commands that should prompt for a missing argument. */
export type CommandArgMenuSpec = {
type CommandArgMenuSpec = {
arg: string;
title?: string;
};

View file

@ -4,7 +4,7 @@ import { getLoadedChannelPluginById } from "../channels/plugins/registry-loaded.
import type { ShouldHandleTextCommandsParams } from "./commands-registry.types.js";
/** Returns whether a surface can receive provider-native slash commands. */
export function isNativeCommandSurface(surface?: string): boolean {
function isNativeCommandSurface(surface?: string): boolean {
const normalized = normalizeOptionalLowercaseString(surface);
if (!normalized) {
return false;

View file

@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
import {
resolveCommandAuthorizedFromAuthorizers,
resolveControlCommandGate,
resolveDualTextControlCommandGate,
} from "./command-gating.js";
describe("resolveCommandAuthorizedFromAuthorizers", () => {
@ -97,13 +96,14 @@ describe("resolveControlCommandGate", () => {
expect(result.shouldBlock).toBe(false);
});
it("supports the dual-authorizer text gate helper", () => {
const result = resolveDualTextControlCommandGate({
it("accepts a secondary authorizer when the primary denies", () => {
const result = resolveControlCommandGate({
useAccessGroups: true,
primaryConfigured: true,
primaryAllowed: false,
secondaryConfigured: true,
secondaryAllowed: true,
authorizers: [
{ configured: true, allowed: false },
{ configured: true, allowed: true },
],
allowTextCommands: true,
hasControlCommand: true,
});
expect(result.commandAuthorized).toBe(true);

View file

@ -57,34 +57,3 @@ export function resolveControlCommandGate(
const shouldBlock = params.allowTextCommands && params.hasControlCommand && !commandAuthorized;
return { commandAuthorized, shouldBlock };
}
/** Convenience gate for channels that check primary and secondary text command identities. */
export function resolveDualTextControlCommandGate(params: {
/** Global access-group switch for the channel/runtime. */
useAccessGroups: boolean;
/** Whether the primary identity has an access-group rule. */
primaryConfigured: boolean;
/** Whether the primary configured rule permits the command. */
primaryAllowed: boolean;
/** Whether the secondary identity has an access-group rule. */
secondaryConfigured: boolean;
/** Whether the secondary configured rule permits the command. */
secondaryAllowed: boolean;
/** True when the current inbound message parsed as a control command. */
hasControlCommand: boolean;
/** Policy used only when `useAccessGroups` is false. Defaults to open. */
modeWhenAccessGroupsOff?: CommandGatingModeWhenAccessGroupsOff;
}): { commandAuthorized: boolean; shouldBlock: boolean } {
// Treat primary and secondary identities as independent authorization sources; channels use
// this when a text command can come from either a sender id or a platform-specific actor id.
return resolveControlCommandGate({
useAccessGroups: params.useAccessGroups,
authorizers: [
{ configured: params.primaryConfigured, allowed: params.primaryAllowed },
{ configured: params.secondaryConfigured, allowed: params.secondaryAllowed },
],
allowTextCommands: true,
hasControlCommand: params.hasControlCommand,
modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff,
});
}

View file

@ -0,0 +1,94 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import type { ChannelId } from "../channels/plugins/types.public.js";
import {
resolveCommandAuthorization,
resolveStoredModelOverride,
type CommandAuthorization,
} from "./command-auth-native.js";
import type { GatewayRequestHandlerOptions } from "./gateway-runtime.js";
import { resolveSessionModelRef } from "./model-session-runtime.js";
describe("plugin-sdk/command-auth-native", () => {
it("keeps prepared host metadata outside public model resolver inputs", () => {
type PublishedCatalog = NonNullable<
Awaited<
ReturnType<
NonNullable<GatewayRequestHandlerOptions["context"]["readPreparedGatewayModelCatalog"]>
>
>
>;
expectTypeOf<keyof PublishedCatalog>().toEqualTypeOf<
"entries" | "pluginRegistry" | "routeVariants"
>();
type StoredModelInput = Parameters<typeof resolveStoredModelOverride>[0];
expectTypeOf<keyof StoredModelInput>().toEqualTypeOf<
| "loadSessionEntry"
| "sessionEntry"
| "sessionStore"
| "sessionKey"
| "parentSessionKey"
| "defaultProvider"
| "allowPluginNormalization"
>();
expectTypeOf<NonNullable<Parameters<typeof resolveSessionModelRef>[3]>>().toEqualTypeOf<{
allowPluginNormalization?: boolean;
}>();
const options = {
allowPluginNormalization: false,
manifestPlugins: [
{
modelIdNormalization: {
providers: { example: { aliases: { raw: "injected" } } },
},
},
],
};
const params = {
defaultProvider: "example",
sessionEntry: { sessionId: "sdk-model", updatedAt: 1, modelOverride: "raw" },
...options,
};
expect(resolveStoredModelOverride(params)?.model).toBe("raw");
expect(
resolveSessionModelRef(
{ agents: { defaults: { model: "example/raw" } } },
undefined,
undefined,
options,
),
).toEqual({ provider: "example", model: "raw" });
});
it("preserves the native authorization result contract", () => {
type PublishedAuthorization = {
providerId?: ChannelId;
ownerList: string[];
senderId?: string;
senderIsOwner: boolean;
isAuthorizedSender: boolean;
assertOwnerCurrent?: () => void;
from?: string;
to?: string;
};
expectTypeOf<CommandAuthorization>().toEqualTypeOf<PublishedAuthorization>();
expectTypeOf<
ReturnType<typeof resolveCommandAuthorization>
>().toEqualTypeOf<PublishedAuthorization>();
const expected: PublishedAuthorization = {
providerId: undefined,
ownerList: ["owner"],
senderId: "guest",
senderIsOwner: false,
isAuthorizedSender: false,
from: undefined,
to: undefined,
};
const actual = resolveCommandAuthorization({
ctx: { Provider: "webchat", Surface: "webchat", SenderId: "guest" },
cfg: { commands: { ownerAllowFrom: ["owner"] } },
commandAuthorized: true,
});
expect(actual).toStrictEqual(expected);
expect(new Set(Reflect.ownKeys(actual))).toEqual(new Set(Reflect.ownKeys(expected)));
});
});

View file

@ -1,251 +0,0 @@
/**
* Tests command authorization helpers and native command gating.
*/
import { describe, expect, expectTypeOf, it } from "vitest";
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveCommandAuthorization as resolveNativeCommandAuthorization,
resolveStoredModelOverride as resolveNativeStoredModelOverride,
type CommandAuthorization as NativeCommandAuthorization,
} from "./command-auth-native.js";
import {
resolveCommandAuthorization,
resolveSenderCommandAuthorization,
resolveStoredModelOverride,
type CommandAuthorization,
} from "./command-auth.js";
import {
buildCommandsMessage,
buildCommandsMessagePaginated,
buildHelpMessage,
} from "./command-status.js";
import type { GatewayRequestHandlerOptions } from "./gateway-runtime.js";
import { resolveSessionModelRef } from "./model-session-runtime.js";
const baseCfg = {
commands: { useAccessGroups: true },
} as unknown as OpenClawConfig;
async function resolveAuthorization(params: {
senderId: string;
configuredAllowFrom?: string[];
configuredGroupAllowFrom?: string[];
cfg?: OpenClawConfig;
}) {
return resolveSenderCommandAuthorization({
cfg: params.cfg ?? baseCfg,
rawBody: "/status",
isGroup: true,
dmPolicy: "pairing",
configuredAllowFrom: params.configuredAllowFrom ?? ["dm-owner"],
configuredGroupAllowFrom: params.configuredGroupAllowFrom ?? ["group-owner"],
senderId: params.senderId,
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
channel: "zalouser",
accountId: "default",
readAllowFromStore: async () => ["paired-user"],
shouldComputeCommandAuthorized: () => true,
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
});
}
describe("plugin-sdk/command-auth", () => {
it("keeps prepared host metadata outside public model resolver inputs", () => {
type PublishedCatalog = NonNullable<
Awaited<
ReturnType<
NonNullable<GatewayRequestHandlerOptions["context"]["readPreparedGatewayModelCatalog"]>
>
>
>;
expectTypeOf<keyof PublishedCatalog>().toEqualTypeOf<
"entries" | "pluginRegistry" | "routeVariants"
>();
type StoredModelInput = Parameters<typeof resolveStoredModelOverride>[0];
expectTypeOf<keyof StoredModelInput>().toEqualTypeOf<
| "loadSessionEntry"
| "sessionEntry"
| "sessionStore"
| "sessionKey"
| "parentSessionKey"
| "defaultProvider"
| "allowPluginNormalization"
>();
expectTypeOf<
Parameters<typeof resolveNativeStoredModelOverride>[0]
>().toEqualTypeOf<StoredModelInput>();
expectTypeOf<NonNullable<Parameters<typeof resolveSessionModelRef>[3]>>().toEqualTypeOf<{
allowPluginNormalization?: boolean;
}>();
const options = {
allowPluginNormalization: false,
manifestPlugins: [
{
modelIdNormalization: {
providers: { example: { aliases: { raw: "injected" } } },
},
},
],
};
const params = {
defaultProvider: "example",
sessionEntry: { sessionId: "sdk-model", updatedAt: 1, modelOverride: "raw" },
...options,
};
for (const resolve of [resolveStoredModelOverride, resolveNativeStoredModelOverride]) {
expect(resolve(params)?.model).toBe("raw");
}
expect(
resolveSessionModelRef(
{ agents: { defaults: { model: "example/raw" } } },
undefined,
undefined,
options,
),
).toEqual({ provider: "example", model: "raw" });
});
it("keeps SDK and native authorization result contracts aligned", () => {
type PublishedAuthorization = {
providerId?: ChannelId;
ownerList: string[];
senderId?: string;
senderIsOwner: boolean;
isAuthorizedSender: boolean;
assertOwnerCurrent?: () => void;
from?: string;
to?: string;
};
expectTypeOf<CommandAuthorization>().toEqualTypeOf<PublishedAuthorization>();
expectTypeOf<NativeCommandAuthorization>().toEqualTypeOf<PublishedAuthorization>();
expectTypeOf<
ReturnType<typeof resolveCommandAuthorization>
>().toEqualTypeOf<PublishedAuthorization>();
expectTypeOf<
ReturnType<typeof resolveNativeCommandAuthorization>
>().toEqualTypeOf<PublishedAuthorization>();
const expected: PublishedAuthorization = {
providerId: undefined,
ownerList: ["owner"],
senderId: "guest",
senderIsOwner: false,
isAuthorizedSender: false,
from: undefined,
to: undefined,
};
for (const resolve of [resolveCommandAuthorization, resolveNativeCommandAuthorization]) {
const actual = resolve({
ctx: { Provider: "webchat", Surface: "webchat", SenderId: "guest" },
cfg: { commands: { ownerAllowFrom: ["owner"] } },
commandAuthorized: true,
});
expect(actual).toStrictEqual(expected);
expect(new Set(Reflect.ownKeys(actual))).toEqual(new Set(Reflect.ownKeys(expected)));
}
});
it("keeps command status builders on their focused subpath", () => {
const cfg = { commands: { config: false, debug: false } } as unknown as OpenClawConfig;
expect(buildHelpMessage(cfg)).toContain("/commands for full list");
expect(buildCommandsMessage(cfg)).toContain("More: /tools for available capabilities");
expect(buildCommandsMessage(cfg)).toContain("/models - List model providers/models.");
const commandsPage = buildCommandsMessagePaginated(cfg);
expect(commandsPage.currentPage).toBe(1);
expect(typeof commandsPage.totalPages).toBe("number");
});
it("resolves command authorization across allowlist sources", async () => {
const cases = [
{
name: "authorizes group commands from explicit group allowlist",
senderId: "group-owner",
expectedAuthorized: true,
expectedSenderAllowed: true,
},
{
name: "keeps pairing-store identities DM-only for group command auth",
senderId: "paired-user",
expectedAuthorized: false,
expectedSenderAllowed: false,
},
];
for (const testCase of cases) {
const result = await resolveAuthorization({ senderId: testCase.senderId });
expect(result.commandAuthorized).toBe(testCase.expectedAuthorized);
expect(result.senderAllowedForCommands).toBe(testCase.expectedSenderAllowed);
expect(result.effectiveAllowFrom).toEqual(["dm-owner"]);
expect(result.effectiveGroupAllowFrom).toEqual(["group-owner"]);
}
});
it("does not grant command authorization to non-command DM input from pairing store", async () => {
const result = await resolveSenderCommandAuthorization({
cfg: baseCfg,
rawBody: "hello",
isGroup: false,
dmPolicy: "pairing",
configuredAllowFrom: [],
configuredGroupAllowFrom: [],
senderId: "paired-user",
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
readAllowFromStore: async () => ["paired-user"],
shouldComputeCommandAuthorized: (rawBody) => rawBody.startsWith("/"),
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
});
expect(result.shouldComputeAuth).toBe(false);
expect(result.effectiveAllowFrom).toEqual(["paired-user"]);
expect(result.senderAllowedForCommands).toBe(true);
expect(result.commandAuthorized).toBeUndefined();
});
it("resolves generic message sender access groups for group command authorization", async () => {
const result = await resolveAuthorization({
senderId: "group-admin",
configuredAllowFrom: [],
configuredGroupAllowFrom: ["accessGroup:admins"],
cfg: {
...baseCfg,
accessGroups: {
admins: {
type: "message.senders",
members: {
zalouser: ["group-admin"],
telegram: ["12345"],
},
},
},
} as OpenClawConfig,
});
expect(result.effectiveGroupAllowFrom).toEqual(["accessGroup:admins", "group-admin"]);
expect(result.senderAllowedForCommands).toBe(true);
expect(result.commandAuthorized).toBe(true);
});
it("does not treat open DM policy as an allowlist bypass", async () => {
const result = await resolveSenderCommandAuthorization({
cfg: baseCfg,
rawBody: "hello",
isGroup: false,
dmPolicy: "open",
configuredAllowFrom: [],
configuredGroupAllowFrom: [],
senderId: "paired-user",
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
readAllowFromStore: async () => ["paired-user"],
shouldComputeCommandAuthorized: (rawBody) => rawBody.startsWith("/"),
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
});
expect(result.effectiveAllowFrom).toStrictEqual([]);
expect(result.senderAllowedForCommands).toBe(false);
expect(result.commandAuthorized).toBeUndefined();
});
});

View file

@ -1,276 +0,0 @@
/**
* @deprecated Public SDK subpath has no bundled extension production imports.
* Use channel ingress/runtime authorization helpers or command-status helpers
* instead of this broad compatibility surface.
*/
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
expandAllowFromWithAccessGroups,
type AccessGroupMembershipResolver,
} from "./access-groups.js";
import { resolveDmGroupAccessWithLists } from "./channel-access-compat.js";
export {
ACCESS_GROUP_ALLOW_FROM_PREFIX,
expandAllowFromWithAccessGroups,
parseAccessGroupAllowFromEntry,
resolveAccessGroupAllowFromMatches,
resolveAccessGroupAllowFromState,
type AccessGroupMembershipResolver,
type AccessGroupMembershipLookup,
type ResolvedAccessGroupAllowFromState,
} from "./access-groups.js";
export { buildCommandsPaginationKeyboard } from "./telegram-command-ui.js";
export {
createPreCryptoDirectDmAuthorizer,
resolveInboundDirectDmAccessWithRuntime,
type DirectDmCommandAuthorizationRuntime,
type ResolvedInboundDirectDmAccess,
} from "../channels/direct-dm-access.js";
export {
hasControlCommand,
hasInlineCommandTokens,
isControlCommandMessage,
shouldComputeCommandAuthorized,
} from "../auto-reply/command-detection.js";
export {
buildCommandText,
buildCommandTextFromArgs,
findCommandByNativeName,
formatCommandArgMenuTitle,
getCommandDetection,
isCommandEnabled,
isCommandMessage,
isNativeCommandSurface,
listChatCommands,
listChatCommandsForConfig,
listNativeCommandSpecs,
listNativeCommandSpecsForConfig,
maybeResolveTextAlias,
normalizeCommandBody,
parseCommandArgs,
resolveCommandArgChoices,
resolveCommandArgMenu,
resolveTextCommand,
serializeCommandArgs,
shouldHandleTextCommands,
} from "../auto-reply/commands-registry.js";
export type {
ChatCommandDefinition,
CommandArgChoiceContext,
CommandArgDefinition,
CommandArgMenuSpec,
CommandArgValues,
CommandArgs,
CommandDetection,
CommandNormalizeOptions,
CommandScope,
NativeCommandSpec,
ResolvedCommandArgChoice,
ShouldHandleTextCommandsParams,
} from "../auto-reply/commands-registry.js";
export type { CommandArgsParsing } from "../auto-reply/commands-registry.types.js";
export {
resolveCommandAuthorizedFromAuthorizers,
resolveControlCommandGate,
resolveDualTextControlCommandGate,
type CommandAuthorizer,
type CommandGatingModeWhenAccessGroupsOff,
} from "../channels/command-gating.js";
export {
resolveNativeCommandSessionTargets,
type ResolveNativeCommandSessionTargetsParams,
} from "../channels/native-command-session-targets.js";
export {
resolveCommandAuthorization,
type CommandAuthorization,
} from "../auto-reply/command-auth.js";
export {
listReservedChatSlashCommandNames,
listSkillCommandsForAgents,
listSkillCommandsForWorkspace,
resolveSkillCommandInvocation,
} from "../skills/discovery/chat-commands.js";
export { getPluginCommandSpecs, listProviderPluginCommandSpecs } from "../plugins/command-specs.js";
export type { SkillCommandSpec } from "../skills/types.js";
export {
buildModelsProviderData,
formatModelsAvailableHeader,
resolveModelsCommandReply,
} from "./models-provider-runtime.js";
export type { ModelsProviderData } from "../auto-reply/reply/commands-models-catalog.js";
export { resolveStoredModelOverride } from "../sessions/stored-model-overrides.js";
export type { StoredModelOverride } from "../sessions/stored-model-overrides.js";
/**
* Inputs for legacy sender command authorization.
* Kept for plugins that still compose command auth from DM/group allowlists instead of channel ingress.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export type ResolveSenderCommandAuthorizationParams = {
cfg: OpenClawConfig;
rawBody: string;
isGroup: boolean;
dmPolicy: string;
configuredAllowFrom: string[];
configuredGroupAllowFrom?: string[];
senderId: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
channel?: ChannelId;
accountId?: string;
resolveAccessGroupMembership?: AccessGroupMembershipResolver;
readAllowFromStore: () => Promise<string[]>;
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
/** @deprecated Command authorization is resolved by channel ingress. Kept for runtime injection compatibility. */
resolveCommandAuthorizedFromAuthorizers?: (params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
}) => boolean;
};
/**
* Injectable runtime hooks for legacy command authorization tests and channel adapters.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export type CommandAuthorizationRuntime = {
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
resolveCommandAuthorizedFromAuthorizers: (params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
}) => boolean;
};
/**
* Legacy command authorization params with runtime hooks grouped for dependency injection.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit<
ResolveSenderCommandAuthorizationParams,
"shouldComputeCommandAuthorized" | "resolveCommandAuthorizedFromAuthorizers"
> & {
runtime: CommandAuthorizationRuntime;
};
/**
* Classify direct-DM command handling after sender authorization has been computed.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export function resolveDirectDmAuthorizationOutcome(params: {
isGroup: boolean;
dmPolicy: string;
senderAllowedForCommands: boolean;
}): "disabled" | "unauthorized" | "allowed" {
if (params.isGroup) {
return "allowed";
}
if (params.dmPolicy === "disabled") {
return "disabled";
}
if (!params.senderAllowedForCommands) {
return "unauthorized";
}
return "allowed";
}
/**
* Resolve legacy command authorization using an injected runtime object.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export async function resolveSenderCommandAuthorizationWithRuntime(
params: ResolveSenderCommandAuthorizationWithRuntimeParams,
): ReturnType<typeof resolveSenderCommandAuthorization> {
return resolveSenderCommandAuthorization({
...params,
shouldComputeCommandAuthorized: params.runtime.shouldComputeCommandAuthorized,
resolveCommandAuthorizedFromAuthorizers: params.runtime.resolveCommandAuthorizedFromAuthorizers,
});
}
/**
* Resolve whether a sender may run slash/control commands under legacy DM/group policy.
* Returns effective allowlists so callers can report the exact source set used for authorization.
*
* @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`.
*/
export async function resolveSenderCommandAuthorization(
params: ResolveSenderCommandAuthorizationParams,
): Promise<{
shouldComputeAuth: boolean;
effectiveAllowFrom: string[];
effectiveGroupAllowFrom: string[];
senderAllowedForCommands: boolean;
commandAuthorized: boolean | undefined;
}> {
const shouldComputeAuth = params.shouldComputeCommandAuthorized(params.rawBody, params.cfg);
// Pairing-store allowlists apply to DM sender authorization only; group commands
// must rely on configured group allowlists or access-group expansion.
const storeAllowFrom =
!params.isGroup && params.dmPolicy !== "allowlist" && params.dmPolicy !== "open"
? await params.readAllowFromStore().catch(() => [])
: [];
const channel = params.channel;
const accountId = params.accountId ?? "default";
let configuredAllowFrom = params.configuredAllowFrom;
let configuredGroupAllowFrom = params.configuredGroupAllowFrom ?? [];
let dmStoreAllowFrom = storeAllowFrom;
if (channel) {
const expand = (allowFrom: string[]) =>
expandAllowFromWithAccessGroups({
cfg: params.cfg,
allowFrom,
channel,
accountId,
senderId: params.senderId,
isSenderAllowed: params.isSenderAllowed,
resolveMembership: params.resolveAccessGroupMembership,
});
[configuredAllowFrom, configuredGroupAllowFrom] = await Promise.all([
expand(params.configuredAllowFrom),
expand(params.configuredGroupAllowFrom ?? []),
]);
if (!params.isGroup) {
dmStoreAllowFrom = await expand(storeAllowFrom);
}
}
const access = resolveDmGroupAccessWithLists({
isGroup: params.isGroup,
dmPolicy: params.dmPolicy,
groupPolicy: "allowlist",
allowFrom: configuredAllowFrom,
groupAllowFrom: configuredGroupAllowFrom,
storeAllowFrom: dmStoreAllowFrom,
isSenderAllowed: (allowFrom) => params.isSenderAllowed(params.senderId, allowFrom),
});
const effectiveAllowFrom = access.effectiveAllowFrom;
const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
const senderAllowedForCommands = params.isSenderAllowed(
params.senderId,
params.isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
);
const ownerAllowedForCommands = params.isSenderAllowed(params.senderId, effectiveAllowFrom);
const groupAllowedForCommands = params.isSenderAllowed(params.senderId, effectiveGroupAllowFrom);
const commandAuthorized = shouldComputeAuth
? (params.resolveCommandAuthorizedFromAuthorizers?.({
useAccessGroups: true,
authorizers: [
{ configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
],
}) ?? senderAllowedForCommands)
: undefined;
return {
shouldComputeAuth,
effectiveAllowFrom,
effectiveGroupAllowFrom,
senderAllowedForCommands,
commandAuthorized,
};
}

View file

@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import {
buildCommandsMessage,
buildCommandsMessagePaginated,
buildHelpMessage,
} from "./command-status.js";
import type { OpenClawConfig } from "./config-contracts.js";
describe("plugin-sdk/command-status", () => {
it("keeps command status builders on their focused subpath", () => {
const cfg: OpenClawConfig = { commands: { config: false, debug: false } };
expect(buildHelpMessage(cfg)).toContain("/commands for full list");
expect(buildCommandsMessage(cfg)).toContain("More: /tools for available capabilities");
expect(buildCommandsMessage(cfg)).toContain("/models - List model providers/models.");
const commandsPage = buildCommandsMessagePaginated(cfg);
expect(commandsPage.currentPage).toBe(1);
expect(typeof commandsPage.totalPages).toBe("number");
});
});

View file

@ -1,218 +0,0 @@
import { describe, expect, expectTypeOf, it, vi } from "vitest";
import type { MessageReceipt } from "./channel-outbound.js";
/**
* Tests Discord SDK helpers and Discord-facing compatibility behavior.
*/
import type {
DiscordComponentSendOpts,
DiscordComponentSendResult,
OpenClawConfig,
} from "./discord.js";
const mocks = vi.hoisted(() => {
const runtimeConfig = { channels: { discord: { token: "token" } } };
const componentEditResult = {
channelId: "channel",
messageId: "message",
receipt: {
parts: [
{
index: 0,
kind: "card",
platformMessageId: "message",
raw: { channel: "discord", channelId: "channel", messageId: "message" },
},
],
platformMessageIds: ["message"],
primaryPlatformMessageId: "message",
raw: [{ channel: "discord", channelId: "channel", messageId: "message" }],
sentAt: 0,
},
};
const apiModule = {
buildDiscordComponentMessage: vi.fn((params: { spec: { text?: string } }) => ({
components: [],
text: params.spec.text ?? "",
})),
collectDiscordStatusIssues: vi.fn(() => []),
discordOnboardingAdapter: { kind: "discord-onboarding" },
inspectDiscordAccount: vi.fn(() => ({ accountId: "default" })),
listDiscordAccountIds: vi.fn(() => ["default"]),
listDiscordDirectoryGroupsFromConfig: vi.fn(() => []),
listDiscordDirectoryPeersFromConfig: vi.fn(() => []),
looksLikeDiscordTargetId: vi.fn(() => true),
normalizeDiscordMessagingTarget: vi.fn(() => "channel:123"),
normalizeDiscordOutboundTarget: vi.fn(() => ({ ok: true, to: "channel:123" })),
resolveDefaultDiscordAccountId: vi.fn(() => "default"),
resolveDiscordAccount: vi.fn(() => ({
accountId: "default",
config: {},
enabled: true,
token: "token",
tokenSource: "config",
})),
resolveDiscordGroupRequireMention: vi.fn(() => true),
resolveDiscordGroupToolPolicy: vi.fn(() => undefined),
};
const runtimeModule = {
autoBindSpawnedDiscordSubagent: vi.fn(async (params) => ({
accountId: params.accountId ?? "default",
channelId: "123",
targetKind: "subagent",
targetSessionKey: params.childSessionKey,
threadId: "456",
cfg: params.cfg,
})),
collectDiscordAuditChannelIds: vi.fn(() => ({ channelIds: [], unresolvedChannels: [] })),
editDiscordComponentMessage: vi.fn(async () => componentEditResult),
listThreadBindingsBySessionKey: vi.fn(() => []),
registerBuiltDiscordComponentMessage: vi.fn().mockResolvedValue(undefined),
unbindThreadBindingsBySessionKey: vi.fn(() => []),
};
return {
apiModule,
componentEditResult,
runtimeModule,
runtimeConfig,
loadBundledPluginPublicSurfaceModuleSyncCore: vi.fn((params: { artifactBasename: string }) => {
if (params.artifactBasename === "runtime-api.js") {
return runtimeModule;
}
return apiModule;
}),
};
});
vi.mock("./facade-loader.js", () => ({
createLazyFacadeObjectValue: (load: () => object) =>
new Proxy(
{},
{
get(_target, property) {
return Reflect.get(load(), property);
},
},
),
loadBundledPluginPublicSurfaceModuleSyncCore: mocks.loadBundledPluginPublicSurfaceModuleSyncCore,
}));
vi.mock("./runtime-config-snapshot.js", () => ({
getRuntimeConfig: () => mocks.runtimeConfig,
getRuntimeConfigSnapshot: () => mocks.runtimeConfig,
}));
describe("discord plugin-sdk facade", () => {
it("exports the @openclaw/discord 2026.3.13 import surface", async () => {
const discordSdk = await import("./discord.js");
for (const exportName of [
"DEFAULT_ACCOUNT_ID",
"PAIRING_APPROVED_MESSAGE",
"applyAccountNameToChannelSection",
"autoBindSpawnedDiscordSubagent",
"buildDiscordComponentMessage",
"buildChannelConfigSchema",
"buildComputedAccountStatusSnapshot",
"buildTokenChannelStatusSummary",
"collectDiscordAuditChannelIds",
"collectDiscordStatusIssues",
"discordOnboardingAdapter",
"emptyPluginConfigSchema",
"getChatChannelMeta",
"inspectDiscordAccount",
"listDiscordAccountIds",
"listDiscordDirectoryGroupsFromConfig",
"listDiscordDirectoryPeersFromConfig",
"listThreadBindingsBySessionKey",
"looksLikeDiscordTargetId",
"migrateBaseNameToDefaultAccount",
"normalizeAccountId",
"normalizeDiscordMessagingTarget",
"normalizeDiscordOutboundTarget",
"projectCredentialSnapshotFields",
"editDiscordComponentMessage",
"registerBuiltDiscordComponentMessage",
"resolveConfiguredFromCredentialStatuses",
"resolveDefaultDiscordAccountId",
"resolveDiscordAccount",
"resolveDiscordGroupRequireMention",
"resolveDiscordGroupToolPolicy",
"unbindThreadBindingsBySessionKey",
]) {
expect(discordSdk).toHaveProperty(exportName);
}
});
it("forwards Discord component helpers through the facade", async () => {
const {
buildDiscordComponentMessage,
editDiscordComponentMessage,
registerBuiltDiscordComponentMessage,
} = await import("./discord.js");
const built = buildDiscordComponentMessage({ spec: { text: "hello" } });
const editResult = await editDiscordComponentMessage(
"channel",
"message",
{ text: "edited" },
{ cfg: mocks.runtimeConfig },
);
await registerBuiltDiscordComponentMessage({
buildResult: built,
messageId: "message",
});
expect(mocks.apiModule.buildDiscordComponentMessage).toHaveBeenCalledWith({
spec: { text: "hello" },
});
expect(mocks.runtimeModule.editDiscordComponentMessage).toHaveBeenCalledWith(
"channel",
"message",
{ text: "edited" },
{ cfg: mocks.runtimeConfig },
);
expect(editResult).toEqual(mocks.componentEditResult);
expect(mocks.runtimeModule.registerBuiltDiscordComponentMessage).toHaveBeenCalledWith({
buildResult: built,
messageId: "message",
});
});
it("types Discord component edit options and normalized result", () => {
type IsCfgOptional = object extends Pick<DiscordComponentSendOpts, "cfg"> ? true : false;
expectTypeOf<IsCfgOptional>().toEqualTypeOf<false>();
expectTypeOf<DiscordComponentSendOpts["cfg"]>().toEqualTypeOf<OpenClawConfig>();
expectTypeOf<DiscordComponentSendResult>().toEqualTypeOf<{
messageId: string;
channelId: string;
receipt: MessageReceipt;
}>();
expectTypeOf<keyof DiscordComponentSendResult>().toEqualTypeOf<
"messageId" | "channelId" | "receipt"
>();
});
it("fills runtime config for Discord subagent auto-bind calls without cfg", async () => {
const { autoBindSpawnedDiscordSubagent } = await import("./discord.js");
const binding = await autoBindSpawnedDiscordSubagent({
agentId: "agent",
channel: "discord",
childSessionKey: "child",
});
expect(mocks.runtimeModule.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
const callParams = mocks.runtimeModule.autoBindSpawnedDiscordSubagent.mock.calls[0]?.[0];
expect(callParams.agentId).toBe("agent");
expect(callParams.cfg).toBe(mocks.runtimeConfig);
expect(callParams.childSessionKey).toBe("child");
if (!binding) {
throw new Error("expected Discord subagent binding");
}
expect(binding.cfg).toBe(mocks.runtimeConfig);
expect(binding.targetKind).toBe("subagent");
expect(binding.targetSessionKey).toBe("child");
});
});

View file

@ -1,354 +0,0 @@
/**
* @deprecated Public SDK subpath has no bundled extension production imports.
* Use generic channel SDK subpaths or plugin-local API barrels instead.
*/
import type {
ChannelAccountSnapshot,
ChannelGroupContext,
ChannelStatusIssue,
} from "./channel-contract.js";
import type { ChannelPlugin } from "./channel-core.js";
import type { MessageReceipt } from "./channel-outbound.js";
import type { OpenClawConfig } from "./config-contracts.js";
import {
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSyncCore,
} from "./facade-loader.js";
import { getRuntimeConfig, getRuntimeConfigSnapshot } from "./runtime-config-snapshot.js";
/**
* @deprecated Compatibility facade for the `openclaw/plugin-sdk/discord` subpath.
* New channel plugins should use generic channel SDK subpaths.
*/
export type { ChannelMessageActionAdapter, ChannelMessageActionName } from "./channel-contract.js";
export type { ChannelPlugin } from "./channel-core.js";
export type { OpenClawConfig } from "./config-contracts.js";
export type { OpenClawPluginApi, PluginRuntime } from "./channel-plugin-common.js";
export {
DEFAULT_ACCOUNT_ID,
applyAccountNameToChannelSection,
buildChannelConfigSchema,
emptyPluginConfigSchema,
getChatChannelMeta,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
} from "./channel-plugin-common.js";
export {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
} from "./channel-status.js";
/** Discord channel config shape for one account in OpenClaw config. */
export type DiscordAccountConfig = NonNullable<NonNullable<OpenClawConfig["channels"]>["discord"]>;
/** Component-message request accepted by the deprecated Discord SDK facade. */
export type DiscordComponentMessageSpec = {
text?: string;
reusable?: boolean;
container?: {
accentColor?: string | number;
spoiler?: boolean;
};
blocks?: unknown[];
modal?: unknown;
};
/** Built Discord component payload plus registration metadata. */
export type DiscordComponentBuildResult = {
components: unknown[];
entries: unknown[];
modals: unknown[];
};
/** Send/edit options for Discord component messages. */
export type DiscordComponentSendOpts = {
cfg: OpenClawConfig;
accountId?: string;
replyTo?: string;
files?: unknown;
mediaReadFile?: (filePath: string) => Promise<Buffer>;
filename?: string;
textLimit?: number;
maxLinesPerMessage?: number;
tableMode?: unknown;
chunkMode?: unknown;
[key: string]: unknown;
};
/** Normalized Discord message result returned by component send/edit helpers. */
export type DiscordComponentSendResult = {
messageId: string;
channelId: string;
receipt: MessageReceipt;
};
/** Resolved Discord account with token source metadata for status and runtime checks. */
export type ResolvedDiscordAccount = {
accountId: string;
enabled: boolean;
name?: string;
token: string;
tokenSource: "env" | "config" | "none";
config: DiscordAccountConfig;
};
/** Normalized outbound target result for Discord channel ids and DM targets. */
export type DiscordOutboundTargetResolution =
| { ok: true; to: string }
| { ok: false; error: Error };
/** Supported thread binding owners for Discord session routing. */
export type ThreadBindingTargetKind = "subagent" | "acp";
/** Persisted Discord thread-to-session binding record. */
export type ThreadBindingRecord = {
accountId: string;
threadId: string;
channelId?: string;
targetKind: ThreadBindingTargetKind;
targetSessionKey: string;
[key: string]: unknown;
};
type DirectoryConfigParams = {
cfg: OpenClawConfig;
accountId?: string | null;
};
type BuildDiscordComponentMessage = (params: {
spec: DiscordComponentMessageSpec;
fallbackText?: string;
sessionKey?: string;
agentId?: string;
accountId?: string;
}) => DiscordComponentBuildResult;
type EditDiscordComponentMessage = (
to: string,
messageId: string,
spec: DiscordComponentMessageSpec,
opts: DiscordComponentSendOpts,
) => Promise<DiscordComponentSendResult>;
type RegisterBuiltDiscordComponentMessage = (params: {
buildResult: DiscordComponentBuildResult;
messageId: string;
}) => Promise<void>;
type DiscordApiFacadeModule = {
collectDiscordStatusIssues: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
buildDiscordComponentMessage: BuildDiscordComponentMessage;
discordOnboardingAdapter?: NonNullable<ChannelPlugin<ResolvedDiscordAccount>["setup"]>;
inspectDiscordAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => unknown;
listDiscordAccountIds: (cfg: OpenClawConfig) => string[];
listDiscordDirectoryGroupsFromConfig: (
params: DirectoryConfigParams,
) => unknown[] | Promise<unknown[]>;
listDiscordDirectoryPeersFromConfig: (
params: DirectoryConfigParams,
) => unknown[] | Promise<unknown[]>;
looksLikeDiscordTargetId: (raw: string) => boolean;
normalizeDiscordMessagingTarget: (raw: string) => string | undefined;
normalizeDiscordOutboundTarget: (to?: string) => DiscordOutboundTargetResolution;
resolveDefaultDiscordAccountId: (cfg: OpenClawConfig) => string;
resolveDiscordAccount: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => ResolvedDiscordAccount;
resolveDiscordGroupRequireMention: (params: ChannelGroupContext) => boolean | undefined;
resolveDiscordGroupToolPolicy: (params: ChannelGroupContext) => unknown;
};
type DiscordRuntimeFacadeModule = {
editDiscordComponentMessage: EditDiscordComponentMessage;
registerBuiltDiscordComponentMessage: RegisterBuiltDiscordComponentMessage;
autoBindSpawnedDiscordSubagent: (params: {
cfg: OpenClawConfig;
accountId?: string;
channel?: string;
to?: string;
threadId?: string | number;
childSessionKey: string;
agentId: string;
label?: string;
boundBy?: string;
}) => Promise<ThreadBindingRecord | null>;
collectDiscordAuditChannelIds: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => unknown;
listThreadBindingsBySessionKey: (params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}) => ThreadBindingRecord[];
unbindThreadBindingsBySessionKey: (params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
reason?: string;
sendFarewell?: boolean;
farewellText?: string;
}) => ThreadBindingRecord[];
};
function loadDiscordApiFacadeModule(): DiscordApiFacadeModule {
return loadBundledPluginPublicSurfaceModuleSyncCore<DiscordApiFacadeModule>({
dirName: "discord",
artifactBasename: "api.js",
});
}
function loadDiscordRuntimeFacadeModule(): DiscordRuntimeFacadeModule {
return loadBundledPluginPublicSurfaceModuleSyncCore<DiscordRuntimeFacadeModule>({
dirName: "discord",
artifactBasename: "runtime-api.js",
});
}
function resolveCompatRuntimeConfig(params: { cfg?: OpenClawConfig }): OpenClawConfig {
return params.cfg ?? getRuntimeConfigSnapshot() ?? getRuntimeConfig();
}
/** Lazy Discord setup adapter retained for deprecated subpath compatibility. */
export const discordOnboardingAdapter = createLazyFacadeObjectValue(
() => loadDiscordApiFacadeModule().discordOnboardingAdapter ?? {},
);
/** Collect Discord account status issues from account snapshots. */
export function collectDiscordStatusIssues(
accounts: ChannelAccountSnapshot[],
): ChannelStatusIssue[] {
return loadDiscordApiFacadeModule().collectDiscordStatusIssues(accounts);
}
/** Build Discord component payloads without sending them. */
export const buildDiscordComponentMessage: DiscordApiFacadeModule["buildDiscordComponentMessage"] =
(...args) => loadDiscordApiFacadeModule().buildDiscordComponentMessage(...args);
/** Inspect one configured Discord account for setup/status output. */
export function inspectDiscordAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): unknown {
return loadDiscordApiFacadeModule().inspectDiscordAccount(params);
}
/** List configured Discord account ids from OpenClaw config. */
export function listDiscordAccountIds(cfg: OpenClawConfig): string[] {
return loadDiscordApiFacadeModule().listDiscordAccountIds(cfg);
}
/** List Discord directory group records from static config. */
export function listDiscordDirectoryGroupsFromConfig(
params: DirectoryConfigParams,
): unknown[] | Promise<unknown[]> {
return loadDiscordApiFacadeModule().listDiscordDirectoryGroupsFromConfig(params);
}
/** List Discord directory peer records from static config. */
export function listDiscordDirectoryPeersFromConfig(
params: DirectoryConfigParams,
): unknown[] | Promise<unknown[]> {
return loadDiscordApiFacadeModule().listDiscordDirectoryPeersFromConfig(params);
}
/** Check whether a raw value has Discord target-id shape. */
export function looksLikeDiscordTargetId(raw: string): boolean {
return loadDiscordApiFacadeModule().looksLikeDiscordTargetId(raw);
}
/** Normalize a Discord messaging target for send helpers. */
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
return loadDiscordApiFacadeModule().normalizeDiscordMessagingTarget(raw);
}
/** Normalize a Discord outbound target and return a typed error on failure. */
export function normalizeDiscordOutboundTarget(to?: string): DiscordOutboundTargetResolution {
return loadDiscordApiFacadeModule().normalizeDiscordOutboundTarget(to);
}
/** Resolve the default Discord account id from config. */
export function resolveDefaultDiscordAccountId(cfg: OpenClawConfig): string {
return loadDiscordApiFacadeModule().resolveDefaultDiscordAccountId(cfg);
}
/** Resolve a Discord account config plus token source for runtime use. */
export function resolveDiscordAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): ResolvedDiscordAccount {
return loadDiscordApiFacadeModule().resolveDiscordAccount(params);
}
/** Resolve group mention policy for a Discord channel context. */
export function resolveDiscordGroupRequireMention(
params: ChannelGroupContext,
): boolean | undefined {
return loadDiscordApiFacadeModule().resolveDiscordGroupRequireMention(params);
}
/** Resolve group tool policy for a Discord channel context. */
export function resolveDiscordGroupToolPolicy(params: ChannelGroupContext): unknown {
return loadDiscordApiFacadeModule().resolveDiscordGroupToolPolicy(params);
}
/** Collect configured Discord audit channel ids for runtime status checks. */
export function collectDiscordAuditChannelIds(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): unknown {
return loadDiscordRuntimeFacadeModule().collectDiscordAuditChannelIds(params);
}
/** Edit an already-sent Discord component message. */
export const editDiscordComponentMessage: DiscordRuntimeFacadeModule["editDiscordComponentMessage"] =
(...args) => loadDiscordRuntimeFacadeModule().editDiscordComponentMessage(...args);
/** Await callback registration after Discord assigns the built component message its id. */
export const registerBuiltDiscordComponentMessage: DiscordRuntimeFacadeModule["registerBuiltDiscordComponentMessage"] =
(...args) => loadDiscordRuntimeFacadeModule().registerBuiltDiscordComponentMessage(...args);
/** Bind a spawned subagent session to the current Discord thread when possible. */
export async function autoBindSpawnedDiscordSubagent(params: {
cfg?: OpenClawConfig;
accountId?: string;
channel?: string;
to?: string;
threadId?: string | number;
childSessionKey: string;
agentId: string;
label?: string;
boundBy?: string;
}): Promise<ThreadBindingRecord | null> {
return await loadDiscordRuntimeFacadeModule().autoBindSpawnedDiscordSubagent({
...params,
cfg: resolveCompatRuntimeConfig(params),
});
}
/** List Discord thread bindings for a target session key. */
export function listThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
}): ThreadBindingRecord[] {
return loadDiscordRuntimeFacadeModule().listThreadBindingsBySessionKey(params);
}
/** Remove Discord thread bindings for a target session key. */
export function unbindThreadBindingsBySessionKey(params: {
targetSessionKey: string;
accountId?: string;
targetKind?: ThreadBindingTargetKind;
reason?: string;
sendFarewell?: boolean;
farewellText?: string;
}): ThreadBindingRecord[] {
return loadDiscordRuntimeFacadeModule().unbindThreadBindingsBySessionKey(params);
}

View file

@ -1,45 +0,0 @@
/**
* Tests Telegram account helper behavior.
*/
import { describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const apiModule = {
resolveTelegramAccount: vi.fn(() => ({
accountId: "default",
config: {},
enabled: true,
token: "token",
tokenSource: "config",
})),
};
return {
apiModule,
loadBundledPluginPublicSurfaceModuleSyncCore: vi.fn(() => apiModule),
};
});
vi.mock("./facade-loader.js", () => ({
loadBundledPluginPublicSurfaceModuleSyncCore: mocks.loadBundledPluginPublicSurfaceModuleSyncCore,
}));
describe("telegram account plugin-sdk compatibility facade", () => {
it("forwards account resolution through Telegram's public surface", async () => {
const { resolveTelegramAccount } = await import("./telegram-account.js");
const cfg = { channels: { telegram: { botToken: "token" } } };
const account = resolveTelegramAccount({ cfg, accountId: "default" });
expect(mocks.loadBundledPluginPublicSurfaceModuleSyncCore).toHaveBeenCalledWith({
dirName: "telegram",
artifactBasename: "api.js",
});
expect(mocks.apiModule.resolveTelegramAccount).toHaveBeenCalledWith({
cfg,
accountId: "default",
});
expect(account.accountId).toBe("default");
expect(account.token).toBe("token");
});
});

View file

@ -1,48 +0,0 @@
import type { OpenClawConfig } from "./config-contracts.js";
import { loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader.js";
/**
* @deprecated Compatibility type for the `openclaw/plugin-sdk/telegram-account` facade.
* New channel plugins should prefer injected runtime helpers and generic SDK subpaths.
*/
export type TelegramAccountConfig = NonNullable<
NonNullable<OpenClawConfig["channels"]>["telegram"]
>;
/**
* @deprecated Compatibility type for the `openclaw/plugin-sdk/telegram-account` facade.
* New channel plugins should prefer injected runtime helpers and generic SDK subpaths.
*/
export type ResolvedTelegramAccount = {
accountId: string;
enabled: boolean;
name?: string;
token: string;
tokenSource: "env" | "tokenFile" | "config" | "none";
config: TelegramAccountConfig;
};
type TelegramAccountFacadeModule = {
resolveTelegramAccount: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => ResolvedTelegramAccount;
};
function loadTelegramAccountFacadeModule(): TelegramAccountFacadeModule {
return loadBundledPluginPublicSurfaceModuleSyncCore<TelegramAccountFacadeModule>({
dirName: "telegram",
artifactBasename: "api.js",
});
}
/**
* @deprecated Compatibility facade for plugin code that needs Telegram account resolution.
* New channel plugins should prefer injected runtime helpers and generic SDK subpaths.
*/
export function resolveTelegramAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): ResolvedTelegramAccount {
return loadTelegramAccountFacadeModule().resolveTelegramAccount(params);
}

View file

@ -189,14 +189,6 @@ export const DEPRECATION_MARKING_COMPAT_RECORDS = [
"openclaw/plugin-sdk/channel-inbound finalizeChannelInboundContext",
"openclaw/plugin-sdk/channel-inbound filterChannelTurnSupplementalContext",
"openclaw/plugin-sdk/channel-send-result ChannelSendRawResult",
"openclaw/plugin-sdk/command-auth",
"openclaw/plugin-sdk/command-auth ResolveSenderCommandAuthorizationParams",
"openclaw/plugin-sdk/command-auth resolveCommandAuthorizedFromAuthorizers",
"openclaw/plugin-sdk/command-auth CommandAuthorizationRuntime",
"openclaw/plugin-sdk/command-auth ResolveSenderCommandAuthorizationWithRuntimeParams",
"openclaw/plugin-sdk/command-auth resolveDirectDmAuthorizationOutcome",
"openclaw/plugin-sdk/command-auth resolveSenderCommandAuthorizationWithRuntime",
"openclaw/plugin-sdk/command-auth resolveSenderCommandAuthorization",
"openclaw/plugin-sdk/keyed-async-queue KeyedAsyncQueue.getTailMapForTesting",
"openclaw/plugin-sdk/persistent-dedupe PersistentDedupeLegacyPathOptions.lockOptions",
"openclaw/plugin-sdk/retry-runtime createTelegramRetryRunner",
@ -211,7 +203,6 @@ export const DEPRECATION_MARKING_COMPAT_RECORDS = [
],
tests: [
"src/plugin-sdk/channel-inbound.test.ts",
"src/plugin-sdk/command-auth.test.ts",
"src/plugin-sdk/ssrf-policy.test.ts",
"src/plugins/compat/registry.test.ts",
],

View file

@ -468,25 +468,6 @@ export const PLUGIN_COMPAT_RECORDS = [
releaseNote:
"Untrusted-named prompt-context SDK identifiers remain wired as deprecated aliases of the channel-named fields while plugins migrate.",
},
{
code: "bundled-channel-sdk-compat-facades",
status: "active",
owner: "sdk",
introduced: "2026-04-28",
replacement:
"generic channel SDK subpaths or plugin-local `api.ts` / `runtime-api.ts` barrels for new plugins",
docsPath: "/plugins/sdk-overview",
surfaces: [
"openclaw/plugin-sdk/discord component message helpers",
"openclaw/plugin-sdk/telegram-account resolveTelegramAccount",
],
diagnostics: ["plugin SDK compatibility registry"],
tests: [
"src/plugin-sdk/discord.test.ts",
"src/plugin-sdk/telegram-account.test.ts",
"src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts",
],
},
{
code: "channel-explicit-target-parser",
status: "removed",

View file

@ -1127,31 +1127,11 @@ describe("plugin-sdk subpath exports", () => {
"isRecord",
"resolveEnabledConfiguredAccountId",
]);
expectSourceMentions("command-auth", [
"buildCommandTextFromArgs",
"buildModelsProviderData",
"hasControlCommand",
"listNativeCommandSpecsForConfig",
"listSkillCommandsForAgents",
"normalizeCommandBody",
"createPreCryptoDirectDmAuthorizer",
"resolveCommandAuthorization",
"resolveCommandAuthorizedFromAuthorizers",
"resolveInboundDirectDmAccessWithRuntime",
"resolveControlCommandGate",
"resolveDualTextControlCommandGate",
"resolveNativeCommandSessionTargets",
"resolveStoredModelOverride",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
]);
expectSourceMentions("command-status", [
"buildCommandsMessage",
"buildCommandsMessagePaginated",
"buildHelpMessage",
]);
expectSourceOmitsImportPattern("command-auth", "../auto-reply/status.js");
expectSourceOmitsSnippet("command-auth", "../../extensions/");
expectSourceMentions("channel-send-result", [
"attachChannelToResult",
"buildChannelSendResult",

View file

@ -47,10 +47,6 @@ const CORE_SECRET_SURFACE_GUARDS = [
/\bformatWhatsAppConfigAllowFromEntries\b/,
],
},
{
path: "src/plugin-sdk/command-auth.ts",
forbiddenPatterns: [/\bpluginId:\s*"telegram"/],
},
{
path: "src/gateway/channel-health-policy.ts",
forbiddenPatterns: [/\btelegram\b/],

View file

@ -30,7 +30,6 @@ import {
export {
expandExplicitSkillReferences,
hasSkillReferenceCandidate,
listReservedChatSlashCommandNames,
resolveSkillCommandInvocation,
} from "./chat-command-invocation.js";

View file

@ -118,7 +118,7 @@ describe("scripts/check-deprecated-api-usage", () => {
"extensions/probe/src/a.ts":
'export { x } from "openclaw/plugin-sdk/inbound-reply-dispatch";\ndeliverOutboundPayloads();',
"packages/a.ts":
'import { x } from "openclaw/plugin-sdk/command-auth";\ndeliverOutboundPayloads();',
'import { x } from "openclaw/plugin-sdk/agent-media-payload";\ndeliverOutboundPayloads();',
"src/infra/outbound/deliver.ts": "deliverOutboundPayloads();",
"src/a.test.ts": "deliverOutboundPayloads();",
},
@ -137,7 +137,7 @@ describe("scripts/check-deprecated-api-usage", () => {
"Deprecated API usage guard failed:",
"- plugin-sdk-compat-subpaths: src/a.ts:1: openclaw/plugin-sdk/inbound-reply-dispatch (use focused non-deprecated plugin SDK subpaths)",
"- plugin-sdk-compat-subpaths: src/a.ts:2: @openclaw/plugin-sdk/inbound-reply-dispatch (use focused non-deprecated plugin SDK subpaths)",
"- plugin-sdk-compat-subpaths: packages/a.ts:1: openclaw/plugin-sdk/command-auth (use focused non-deprecated plugin SDK subpaths)",
"- plugin-sdk-compat-subpaths: packages/a.ts:1: openclaw/plugin-sdk/agent-media-payload (use focused non-deprecated plugin SDK subpaths)",
"- extension-plugin-sdk-compat-subpaths: extensions/probe/src/a.ts:1: openclaw/plugin-sdk/inbound-reply-dispatch (extensions must use focused non-deprecated plugin SDK subpaths)",
"- facade-internal-imports: src/a.ts:1: openclaw/plugin-sdk/inbound-reply-dispatch (use openclaw/plugin-sdk/channel-inbound)",
"- facade-internal-imports: src/a.ts:2: @openclaw/plugin-sdk/inbound-reply-dispatch (use openclaw/plugin-sdk/channel-inbound)",