High severity7.1NVD Advisory· Published Apr 23, 2026· Updated Apr 29, 2026
CVE-2026-41359
CVE-2026-41359
Description
OpenClaw before 2026.3.28 contains a privilege escalation vulnerability allowing authenticated operators with write permissions to access admin-class Telegram configuration and cron persistence settings via the send endpoint. Attackers with operator.write credentials can exploit insufficient access controls to reach sensitive administrative functionality and modify persistence mechanisms.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.28 | 2026.3.28 |
Affected products
1Patches
1b7d70ade3b99Fix/telegram writeback admin scope gate (#54561)
18 files changed · +808 −73
docs/.generated/plugin-sdk-api-baseline.json+33 −33 modified@@ -127,7 +127,7 @@ "exportName": "ChannelConfiguredBindingConversationRef", "kind": "type", "source": { - "line": 553, + "line": 554, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -136,7 +136,7 @@ "exportName": "ChannelConfiguredBindingMatch", "kind": "type", "source": { - "line": 558, + "line": 559, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -145,7 +145,7 @@ "exportName": "ChannelConfiguredBindingProvider", "kind": "type", "source": { - "line": 562, + "line": 563, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -154,7 +154,7 @@ "exportName": "ChannelGatewayContext", "kind": "type", "source": { - "line": 238, + "line": 239, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1044,7 +1044,7 @@ "exportName": "BaseProbeResult", "kind": "type", "source": { - "line": 558, + "line": 559, "path": "src/channels/plugins/types.core.ts" } }, @@ -1053,7 +1053,7 @@ "exportName": "BaseTokenResolution", "kind": "type", "source": { - "line": 564, + "line": 565, "path": "src/channels/plugins/types.core.ts" } }, @@ -1518,7 +1518,7 @@ "exportName": "BaseProbeResult", "kind": "type", "source": { - "line": 558, + "line": 559, "path": "src/channels/plugins/types.core.ts" } }, @@ -1527,7 +1527,7 @@ "exportName": "BaseTokenResolution", "kind": "type", "source": { - "line": 564, + "line": 565, "path": "src/channels/plugins/types.core.ts" } }, @@ -1581,7 +1581,7 @@ "exportName": "ChannelAllowlistAdapter", "kind": "type", "source": { - "line": 497, + "line": 498, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1590,7 +1590,7 @@ "exportName": "ChannelAuthAdapter", "kind": "type", "source": { - "line": 362, + "line": 363, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1635,7 +1635,7 @@ "exportName": "ChannelCommandAdapter", "kind": "type", "source": { - "line": 444, + "line": 445, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1653,7 +1653,7 @@ "exportName": "ChannelConfiguredBindingConversationRef", "kind": "type", "source": { - "line": 553, + "line": 554, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1662,7 +1662,7 @@ "exportName": "ChannelConfiguredBindingMatch", "kind": "type", "source": { - "line": 558, + "line": 559, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1671,7 +1671,7 @@ "exportName": "ChannelConfiguredBindingProvider", "kind": "type", "source": { - "line": 562, + "line": 563, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1680,7 +1680,7 @@ "exportName": "ChannelDirectoryAdapter", "kind": "type", "source": { - "line": 406, + "line": 407, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1707,7 +1707,7 @@ "exportName": "ChannelElevatedAdapter", "kind": "type", "source": { - "line": 437, + "line": 438, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1716,7 +1716,7 @@ "exportName": "ChannelExecApprovalAdapter", "kind": "type", "source": { - "line": 463, + "line": 464, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1743,7 +1743,7 @@ "exportName": "ChannelGatewayAdapter", "kind": "type", "source": { - "line": 346, + "line": 347, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1752,7 +1752,7 @@ "exportName": "ChannelGatewayContext", "kind": "type", "source": { - "line": 238, + "line": 239, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1779,7 +1779,7 @@ "exportName": "ChannelHeartbeatAdapter", "kind": "type", "source": { - "line": 372, + "line": 373, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1806,7 +1806,7 @@ "exportName": "ChannelLifecycleAdapter", "kind": "type", "source": { - "line": 449, + "line": 450, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1815,7 +1815,7 @@ "exportName": "ChannelLoginWithQrStartResult", "kind": "type", "source": { - "line": 317, + "line": 318, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1824,7 +1824,7 @@ "exportName": "ChannelLoginWithQrWaitResult", "kind": "type", "source": { - "line": 322, + "line": 323, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1833,7 +1833,7 @@ "exportName": "ChannelLogoutContext", "kind": "type", "source": { - "line": 327, + "line": 328, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1842,7 +1842,7 @@ "exportName": "ChannelLogoutResult", "kind": "type", "source": { - "line": 311, + "line": 312, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1950,7 +1950,7 @@ "exportName": "ChannelOutboundAdapter", "kind": "type", "source": { - "line": 154, + "line": 155, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -1977,7 +1977,7 @@ "exportName": "ChannelPairingAdapter", "kind": "type", "source": { - "line": 335, + "line": 336, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -2013,7 +2013,7 @@ "exportName": "ChannelResolveKind", "kind": "type", "source": { - "line": 417, + "line": 418, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -2022,7 +2022,7 @@ "exportName": "ChannelResolverAdapter", "kind": "type", "source": { - "line": 427, + "line": 428, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -2031,7 +2031,7 @@ "exportName": "ChannelResolveResult", "kind": "type", "source": { - "line": 419, + "line": 420, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -2040,7 +2040,7 @@ "exportName": "ChannelSecurityAdapter", "kind": "type", "source": { - "line": 575, + "line": 576, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -2085,7 +2085,7 @@ "exportName": "ChannelStatusAdapter", "kind": "type", "source": { - "line": 184, + "line": 185, "path": "src/channels/plugins/types.adapters.ts" } }, @@ -4884,7 +4884,7 @@ "exportName": "ChannelGatewayContext", "kind": "type", "source": { - "line": 238, + "line": 239, "path": "src/channels/plugins/types.adapters.ts" } },
docs/.generated/plugin-sdk-api-baseline.jsonl+33 −33 modified@@ -12,10 +12,10 @@ {"declaration":"export type ChannelAgentToolFactory = ChannelAgentToolFactory;","entrypoint":"index","exportName":"ChannelAgentToolFactory","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":23,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelCapabilities = ChannelCapabilities;","entrypoint":"index","exportName":"ChannelCapabilities","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":230,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelConfigSchema = ChannelConfigSchema;","entrypoint":"index","exportName":"ChannelConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":48,"sourcePath":"src/channels/plugins/types.plugin.ts"} -{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"index","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":553,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"index","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":562,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"index","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"index","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"index","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":563,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"index","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelId = ChannelId;","entrypoint":"index","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"index","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"index","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"} @@ -113,8 +113,8 @@ {"declaration":"export const MarkdownConfigSchema: z.ZodOptional<z.ZodObject<{ tables: z.ZodOptional<z.ZodEnum<{ off: \"off\"; bullets: \"bullets\"; code: \"code\"; }>>; }, z.core.$strict>>;","entrypoint":"channel-config-schema","exportName":"MarkdownConfigSchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"const","recordType":"export","sourceLine":371,"sourcePath":"src/config/zod-schema.core.ts"} {"declaration":"export const ToolPolicySchema: z.ZodOptional<z.ZodObject<{ allow: z.ZodOptional<z.ZodArray<z.ZodString>>; alsoAllow: z.ZodOptional<z.ZodArray<z.ZodString>>; deny: z.ZodOptional<z.ZodArray<z.ZodString>>; }, z.core.$strict>>;","entrypoint":"channel-config-schema","exportName":"ToolPolicySchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"const","recordType":"export","sourceLine":253,"sourcePath":"src/config/zod-schema.agent-runtime.ts"} {"category":"channel","entrypoint":"channel-contract","importSpecifier":"openclaw/plugin-sdk/channel-contract","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/channel-contract.ts"} -{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-contract","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-contract","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/channels/plugins/types.core.ts"} +{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-contract","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.core.ts"} +{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-contract","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"channel-contract","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAgentTool = ChannelAgentTool;","entrypoint":"channel-contract","exportName":"ChannelAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":18,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelGroupContext = ChannelGroupContext;","entrypoint":"channel-contract","exportName":"ChannelGroupContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":216,"sourcePath":"src/channels/plugins/types.core.ts"} @@ -165,43 +165,43 @@ {"declaration":"export function waitUntilAbort(signal?: AbortSignal | undefined, onAbort?: (() => void | Promise<void>) | undefined): Promise<void>;","entrypoint":"channel-runtime","exportName":"waitUntilAbort","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"function","recordType":"export","sourceLine":38,"sourcePath":"src/plugin-sdk/channel-lifecycle.ts"} {"declaration":"export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_ACTION_NAMES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-action-names.ts"} {"declaration":"export const CHANNEL_MESSAGE_CAPABILITIES: readonly [\"interactive\", \"buttons\", \"cards\", \"components\", \"blocks\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_CAPABILITIES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-capabilities.ts"} -{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-runtime","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-runtime","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/channels/plugins/types.core.ts"} +{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-runtime","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.core.ts"} +{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-runtime","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"channel-runtime","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAccountState = ChannelAccountState;","entrypoint":"channel-runtime","exportName":"ChannelAccountState","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":108,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAgentPromptAdapter = ChannelAgentPromptAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAgentPromptAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":463,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAgentTool = ChannelAgentTool;","entrypoint":"channel-runtime","exportName":"ChannelAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":18,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelAgentToolFactory = ChannelAgentToolFactory;","entrypoint":"channel-runtime","exportName":"ChannelAgentToolFactory","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":23,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelAllowlistAdapter = ChannelAllowlistAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAllowlistAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":497,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelAuthAdapter = ChannelAuthAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAuthAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":362,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelAllowlistAdapter = ChannelAllowlistAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAllowlistAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelAuthAdapter = ChannelAuthAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAuthAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":363,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelCapabilities = ChannelCapabilities;","entrypoint":"channel-runtime","exportName":"ChannelCapabilities","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":230,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelCapabilitiesDiagnostics = ChannelCapabilitiesDiagnostics;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDiagnostics","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelCapabilitiesDisplayLine = ChannelCapabilitiesDisplayLine;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayLine","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":42,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelCapabilitiesDisplayTone = ChannelCapabilitiesDisplayTone;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayTone","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":40,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelCommandAdapter = ChannelCommandAdapter;","entrypoint":"channel-runtime","exportName":"ChannelCommandAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":444,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelCommandAdapter = ChannelCommandAdapter;","entrypoint":"channel-runtime","exportName":"ChannelCommandAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":445,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelConfigAdapter = ChannelConfigAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelConfigAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":91,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":553,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":562,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelDirectoryAdapter = ChannelDirectoryAdapter;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":406,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":563,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelDirectoryAdapter = ChannelDirectoryAdapter;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":407,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelDirectoryEntry = ChannelDirectoryEntry;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntry","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":469,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelDirectoryEntryKind = ChannelDirectoryEntryKind;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntryKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":467,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelElevatedAdapter = ChannelElevatedAdapter;","entrypoint":"channel-runtime","exportName":"ChannelElevatedAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":437,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelExecApprovalAdapter = ChannelExecApprovalAdapter;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":463,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelElevatedAdapter = ChannelElevatedAdapter;","entrypoint":"channel-runtime","exportName":"ChannelElevatedAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":438,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelExecApprovalAdapter = ChannelExecApprovalAdapter;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":464,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelExecApprovalForwardTarget = ChannelExecApprovalForwardTarget;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalForwardTarget","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelExecApprovalInitiatingSurfaceState = ChannelExecApprovalInitiatingSurfaceState;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalInitiatingSurfaceState","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":27,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelGatewayAdapter = ChannelGatewayAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":346,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelGatewayAdapter = ChannelGatewayAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":347,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelGroupAdapter = ChannelGroupAdapter;","entrypoint":"channel-runtime","exportName":"ChannelGroupAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":122,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelGroupContext = ChannelGroupContext;","entrypoint":"channel-runtime","exportName":"ChannelGroupContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":216,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelHeartbeatAdapter = ChannelHeartbeatAdapter;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":372,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelHeartbeatAdapter = ChannelHeartbeatAdapter;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":373,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelHeartbeatDeps = ChannelHeartbeatDeps;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatDeps","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":116,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelId = ChannelId;","entrypoint":"channel-runtime","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelLifecycleAdapter = ChannelLifecycleAdapter;","entrypoint":"channel-runtime","exportName":"ChannelLifecycleAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":449,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelLoginWithQrStartResult = ChannelLoginWithQrStartResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrStartResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":317,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelLoginWithQrWaitResult = ChannelLoginWithQrWaitResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrWaitResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":322,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelLogoutContext = ChannelLogoutContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelLogoutContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":327,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelLogoutResult = ChannelLogoutResult;","entrypoint":"channel-runtime","exportName":"ChannelLogoutResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":311,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelLifecycleAdapter = ChannelLifecycleAdapter;","entrypoint":"channel-runtime","exportName":"ChannelLifecycleAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":450,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelLoginWithQrStartResult = ChannelLoginWithQrStartResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrStartResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":318,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelLoginWithQrWaitResult = ChannelLoginWithQrWaitResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrWaitResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":323,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelLogoutContext = ChannelLogoutContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelLogoutContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":328,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelLogoutResult = ChannelLogoutResult;","entrypoint":"channel-runtime","exportName":"ChannelLogoutResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelLogSink = ChannelLogSink;","entrypoint":"channel-runtime","exportName":"ChannelLogSink","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":209,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMentionAdapter = ChannelMentionAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMentionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":260,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"} @@ -213,22 +213,22 @@ {"declaration":"export type ChannelMessageToolSchemaContribution = ChannelMessageToolSchemaContribution;","entrypoint":"channel-runtime","exportName":"ChannelMessageToolSchemaContribution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":51,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMessagingAdapter = ChannelMessagingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMessagingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelMeta = ChannelMeta;","entrypoint":"channel-runtime","exportName":"ChannelMeta","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":122,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelOutboundAdapter = ChannelOutboundAdapter;","entrypoint":"channel-runtime","exportName":"ChannelOutboundAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":154,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelOutboundAdapter = ChannelOutboundAdapter;","entrypoint":"channel-runtime","exportName":"ChannelOutboundAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelOutboundContext = ChannelOutboundContext;","entrypoint":"channel-runtime","exportName":"ChannelOutboundContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":128,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelOutboundTargetMode = ChannelOutboundTargetMode;","entrypoint":"channel-runtime","exportName":"ChannelOutboundTargetMode","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelPairingAdapter = ChannelPairingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelPairingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":335,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelPairingAdapter = ChannelPairingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelPairingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":336,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"} {"declaration":"export type ChannelPollContext = ChannelPollContext;","entrypoint":"channel-runtime","exportName":"ChannelPollContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelPollResult = ChannelPollResult;","entrypoint":"channel-runtime","exportName":"ChannelPollResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":538,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelResolveKind = ChannelResolveKind;","entrypoint":"channel-runtime","exportName":"ChannelResolveKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":417,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelResolverAdapter = ChannelResolverAdapter;","entrypoint":"channel-runtime","exportName":"ChannelResolverAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelResolveResult = ChannelResolveResult;","entrypoint":"channel-runtime","exportName":"ChannelResolveResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":419,"sourcePath":"src/channels/plugins/types.adapters.ts"} -{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":575,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelResolveKind = ChannelResolveKind;","entrypoint":"channel-runtime","exportName":"ChannelResolveKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":418,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelResolverAdapter = ChannelResolverAdapter;","entrypoint":"channel-runtime","exportName":"ChannelResolverAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":428,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelResolveResult = ChannelResolveResult;","entrypoint":"channel-runtime","exportName":"ChannelResolveResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":420,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":576,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelSecurityContext = ChannelSecurityContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelSecurityDmPolicy = ChannelSecurityDmPolicy;","entrypoint":"channel-runtime","exportName":"ChannelSecurityDmPolicy","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":245,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelSetupAdapter = ChannelSetupAdapter;","entrypoint":"channel-runtime","exportName":"ChannelSetupAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":56,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelSetupInput = ChannelSetupInput;","entrypoint":"channel-runtime","exportName":"ChannelSetupInput","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":63,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelStatusAdapter = ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelStatusAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":184,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelStatusAdapter = ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelStatusAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":185,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"channel-runtime","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelStreamingAdapter = ChannelStreamingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelStreamingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":279,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type ChannelStructuredComponents = ChannelStructuredComponents;","entrypoint":"channel-runtime","exportName":"ChannelStructuredComponents","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":288,"sourcePath":"src/channels/plugins/types.core.ts"} @@ -537,7 +537,7 @@ {"declaration":"export function removeAckReactionAfterReply(params: { removeAfterReply: boolean; ackReactionPromise: Promise<boolean> | null; ackReactionValue: string | null; remove: () => Promise<void>; onError?: ((err: unknown) => void) | undefined; }): void;","entrypoint":"testing","exportName":"removeAckReactionAfterReply","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":81,"sourcePath":"src/channels/ack-reactions.ts"} {"declaration":"export function shouldAckReaction(params: AckReactionGateParams): boolean;","entrypoint":"testing","exportName":"shouldAckReaction","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/channels/ack-reactions.ts"} {"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"testing","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"} -{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"testing","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"} +{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"testing","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"} {"declaration":"export type MockFn = MockFn<T>;","entrypoint":"testing","exportName":"MockFn","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":5,"sourcePath":"src/test-utils/vitest-mock-fn.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"testing","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} {"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"testing","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
extensions/telegram/src/channel.ts+33 −2 modified@@ -88,6 +88,7 @@ function buildTelegramSendOptions(params: { threadId?: string | number | null; silent?: boolean | null; forceDocument?: boolean | null; + gatewayClientScopes?: readonly string[] | null; }): TelegramSendOptions { return { verbose: false, @@ -99,6 +100,9 @@ function buildTelegramSendOptions(params: { accountId: params.accountId ?? undefined, silent: params.silent ?? undefined, forceDocument: params.forceDocument ?? undefined, + ...(Array.isArray(params.gatewayClientScopes) + ? { gatewayClientScopes: [...params.gatewayClientScopes] } + : {}), }; } @@ -113,6 +117,7 @@ async function sendTelegramOutbound(params: { replyToId?: string | null; threadId?: string | number | null; silent?: boolean | null; + gatewayClientScopes?: readonly string[] | null; }) { const send = resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ?? @@ -128,6 +133,7 @@ async function sendTelegramOutbound(params: { replyToId: params.replyToId, threadId: params.threadId, silent: params.silent, + gatewayClientScopes: params.gatewayClientScopes, }), ); } @@ -710,6 +716,7 @@ export const telegramPlugin = createChatChannelPlugin({ threadId, silent, forceDocument, + gatewayClientScopes, }) => { const send = resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ?? @@ -726,14 +733,25 @@ export const telegramPlugin = createChatChannelPlugin({ threadId, silent, forceDocument, + gatewayClientScopes, }), }); return attachChannelToResult("telegram", result); }, }, attachedResults: { channel: "telegram", - sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) => + sendText: async ({ + cfg, + to, + text, + accountId, + deps, + replyToId, + threadId, + silent, + gatewayClientScopes, + }) => await sendTelegramOutbound({ cfg, to, @@ -743,6 +761,7 @@ export const telegramPlugin = createChatChannelPlugin({ replyToId, threadId, silent, + gatewayClientScopes, }), sendMedia: async ({ cfg, @@ -755,6 +774,7 @@ export const telegramPlugin = createChatChannelPlugin({ replyToId, threadId, silent, + gatewayClientScopes, }) => await sendTelegramOutbound({ cfg, @@ -767,14 +787,25 @@ export const telegramPlugin = createChatChannelPlugin({ replyToId, threadId, silent, + gatewayClientScopes, }), - sendPoll: async ({ cfg, to, poll, accountId, threadId, silent, isAnonymous }) => + sendPoll: async ({ + cfg, + to, + poll, + accountId, + threadId, + silent, + isAnonymous, + gatewayClientScopes, + }) => await getTelegramRuntime().channel.telegram.sendPollTelegram(to, poll, { cfg, accountId: accountId ?? undefined, messageThreadId: parseTelegramThreadId(threadId), silent: silent ?? undefined, isAnonymous: isAnonymous ?? undefined, + gatewayClientScopes, }), }, },
extensions/telegram/src/outbound-adapter.ts+18 −1 modified@@ -30,6 +30,7 @@ function resolveTelegramSendContext(params: { accountId?: string | null; replyToId?: string | null; threadId?: string | number | null; + gatewayClientScopes?: readonly string[]; }): { send: TelegramSendFn; baseOpts: { @@ -39,6 +40,7 @@ function resolveTelegramSendContext(params: { messageThreadId?: number; replyToMessageId?: number; accountId?: string; + gatewayClientScopes?: readonly string[]; }; } { const send = @@ -52,6 +54,7 @@ function resolveTelegramSendContext(params: { messageThreadId: parseTelegramThreadId(params.threadId), replyToMessageId: parseTelegramReplyToMessageId(params.replyToId), accountId: params.accountId ?? undefined, + gatewayClientScopes: params.gatewayClientScopes, }, }; } @@ -111,13 +114,23 @@ export const telegramOutbound: ChannelOutboundAdapter = { typeof fallbackLimit === "number" ? Math.min(fallbackLimit, 4096) : 4096, ...createAttachedChannelResultAdapter({ channel: "telegram", - sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId }) => { + sendText: async ({ + cfg, + to, + text, + accountId, + deps, + replyToId, + threadId, + gatewayClientScopes, + }) => { const { send, baseOpts } = resolveTelegramSendContext({ cfg, deps, accountId, replyToId, threadId, + gatewayClientScopes, }); return await send(to, text, { ...baseOpts, @@ -134,13 +147,15 @@ export const telegramOutbound: ChannelOutboundAdapter = { replyToId, threadId, forceDocument, + gatewayClientScopes, }) => { const { send, baseOpts } = resolveTelegramSendContext({ cfg, deps, accountId, replyToId, threadId, + gatewayClientScopes, }); return await send(to, text, { ...baseOpts, @@ -160,13 +175,15 @@ export const telegramOutbound: ChannelOutboundAdapter = { replyToId, threadId, forceDocument, + gatewayClientScopes, }) => { const { send, baseOpts } = resolveTelegramSendContext({ cfg, deps, accountId, replyToId, threadId, + gatewayClientScopes, }); const result = await sendTelegramPayloadMessages({ send,
extensions/telegram/src/send.test.ts+28 −0 modified@@ -596,6 +596,7 @@ describe("sendMessageTelegram", () => { await sendMessageTelegram("https://t.me/mychannel", "hi", { token: "tok", api, + gatewayClientScopes: ["operator.write"], }); expect(getChat).toHaveBeenCalledWith("@mychannel"); @@ -606,6 +607,7 @@ describe("sendMessageTelegram", () => { expect.objectContaining({ rawTarget: "https://t.me/mychannel", resolvedChatId: "-100123", + gatewayClientScopes: ["operator.write"], }), ); }); @@ -2117,6 +2119,32 @@ describe("editMessageTelegram", () => { }); describe("sendPollTelegram", () => { + it("propagates gateway client scopes when resolving legacy poll targets", async () => { + const api = { + getChat: vi.fn(async () => ({ id: -100321 })), + sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })), + }; + + await sendPollTelegram( + "https://t.me/mychannel", + { question: " Q ", options: [" A ", "B "] }, + { + token: "t", + api: api as unknown as Bot["api"], + gatewayClientScopes: ["operator.admin"], + }, + ); + + expect(api.getChat).toHaveBeenCalledWith("@mychannel"); + expect(maybePersistResolvedTelegramTarget).toHaveBeenCalledWith( + expect.objectContaining({ + rawTarget: "https://t.me/mychannel", + resolvedChatId: "-100321", + gatewayClientScopes: ["operator.admin"], + }), + ); + }); + it("maps durationSeconds to open_period", async () => { const api = { sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })),
extensions/telegram/src/send.ts+6 −0 modified@@ -65,6 +65,7 @@ type TelegramSendOpts = { verbose?: boolean; mediaUrl?: string; mediaLocalRoots?: readonly string[]; + gatewayClientScopes?: readonly string[]; maxBytes?: number; api?: TelegramApiOverride; retry?: RetryConfig; @@ -315,6 +316,7 @@ async function resolveAndPersistChatId(params: { lookupTarget: string; persistTarget: string; verbose?: boolean; + gatewayClientScopes?: readonly string[]; }): Promise<string> { const chatId = await resolveChatId(params.lookupTarget, { api: params.api, @@ -325,6 +327,7 @@ async function resolveAndPersistChatId(params: { rawTarget: params.persistTarget, resolvedChatId: chatId, verbose: params.verbose, + gatewayClientScopes: params.gatewayClientScopes, }); return chatId; } @@ -632,6 +635,7 @@ export async function sendMessageTelegram( lookupTarget: target.chatId, persistTarget: to, verbose: opts.verbose, + gatewayClientScopes: opts.gatewayClientScopes, }); const mediaUrl = opts.mediaUrl?.trim(); const mediaMaxBytes = @@ -1555,6 +1559,7 @@ type TelegramPollOpts = { verbose?: boolean; api?: TelegramApiOverride; retry?: RetryConfig; + gatewayClientScopes?: readonly string[]; /** Message ID to reply to (for threading) */ replyToMessageId?: number; /** Forum topic thread ID (for forum supergroups) */ @@ -1584,6 +1589,7 @@ export async function sendPollTelegram( lookupTarget: target.chatId, persistTarget: to, verbose: opts.verbose, + gatewayClientScopes: opts.gatewayClientScopes, }); // Normalize the poll input (validates question, options, maxSelections)
extensions/telegram/src/target-writeback.test.ts+216 −0 added@@ -0,0 +1,216 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../src/config/config.js"; + +const readConfigFileSnapshotForWrite = vi.fn(); +const writeConfigFile = vi.fn(); +const loadCronStore = vi.fn(); +const resolveCronStorePath = vi.fn(); +const saveCronStore = vi.fn(); + +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); + return { + ...actual, + readConfigFileSnapshotForWrite, + writeConfigFile, + loadCronStore, + resolveCronStorePath, + saveCronStore, + }; +}); + +describe("maybePersistResolvedTelegramTarget", () => { + let maybePersistResolvedTelegramTarget: typeof import("./target-writeback.js").maybePersistResolvedTelegramTarget; + + beforeEach(async () => { + vi.resetModules(); + ({ maybePersistResolvedTelegramTarget } = await import("./target-writeback.js")); + readConfigFileSnapshotForWrite.mockReset(); + writeConfigFile.mockReset(); + loadCronStore.mockReset(); + resolveCronStorePath.mockReset(); + saveCronStore.mockReset(); + resolveCronStorePath.mockReturnValue("/tmp/cron/jobs.json"); + }); + + it("skips writeback when target is already numeric", async () => { + await maybePersistResolvedTelegramTarget({ + cfg: {} as OpenClawConfig, + rawTarget: "-100123", + resolvedChatId: "-100123", + }); + + expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled(); + expect(loadCronStore).not.toHaveBeenCalled(); + }); + + it("skips config and cron writeback for gateway callers missing operator.admin", async () => { + await maybePersistResolvedTelegramTarget({ + cfg: { + cron: { store: "/tmp/cron/jobs.json" }, + } as OpenClawConfig, + rawTarget: "t.me/mychannel", + resolvedChatId: "-100123", + gatewayClientScopes: ["operator.write"], + }); + + expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled(); + expect(writeConfigFile).not.toHaveBeenCalled(); + expect(loadCronStore).not.toHaveBeenCalled(); + expect(saveCronStore).not.toHaveBeenCalled(); + }); + + it("skips config and cron writeback for gateway callers with an empty scope set", async () => { + await maybePersistResolvedTelegramTarget({ + cfg: { + cron: { store: "/tmp/cron/jobs.json" }, + } as OpenClawConfig, + rawTarget: "t.me/mychannel", + resolvedChatId: "-100123", + gatewayClientScopes: [], + }); + + expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled(); + expect(writeConfigFile).not.toHaveBeenCalled(); + expect(loadCronStore).not.toHaveBeenCalled(); + expect(saveCronStore).not.toHaveBeenCalled(); + }); + + it("writes back matching config and cron targets for gateway callers with operator.admin", async () => { + readConfigFileSnapshotForWrite.mockResolvedValue({ + snapshot: { + config: { + channels: { + telegram: { + defaultTo: "t.me/mychannel", + accounts: { + alerts: { + defaultTo: "@mychannel", + }, + }, + }, + }, + }, + }, + writeOptions: { expectedConfigPath: "/tmp/openclaw.json" }, + }); + loadCronStore.mockResolvedValue({ + version: 1, + jobs: [ + { id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } }, + { id: "b", delivery: { channel: "slack", to: "C123" } }, + ], + }); + + await maybePersistResolvedTelegramTarget({ + cfg: { + cron: { store: "/tmp/cron/jobs.json" }, + } as OpenClawConfig, + rawTarget: "t.me/mychannel", + resolvedChatId: "-100123", + gatewayClientScopes: ["operator.admin"], + }); + + expect(writeConfigFile).toHaveBeenCalledTimes(1); + expect(writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + channels: { + telegram: { + defaultTo: "-100123", + accounts: { + alerts: { + defaultTo: "-100123", + }, + }, + }, + }, + }), + expect.objectContaining({ expectedConfigPath: "/tmp/openclaw.json" }), + ); + expect(saveCronStore).toHaveBeenCalledTimes(1); + expect(saveCronStore).toHaveBeenCalledWith( + "/tmp/cron/jobs.json", + expect.objectContaining({ + jobs: [ + { id: "a", delivery: { channel: "telegram", to: "-100123" } }, + { id: "b", delivery: { channel: "slack", to: "C123" } }, + ], + }), + ); + }); + + it("preserves topic suffix style in writeback target", async () => { + readConfigFileSnapshotForWrite.mockResolvedValue({ + snapshot: { + config: { + channels: { + telegram: { + defaultTo: "t.me/mychannel:topic:9", + }, + }, + }, + }, + writeOptions: {}, + }); + loadCronStore.mockResolvedValue({ version: 1, jobs: [] }); + + await maybePersistResolvedTelegramTarget({ + cfg: {} as OpenClawConfig, + rawTarget: "t.me/mychannel:topic:9", + resolvedChatId: "-100123", + }); + + expect(writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + channels: { + telegram: { + defaultTo: "-100123:topic:9", + }, + }, + }), + expect.any(Object), + ); + }); + + it("matches username targets case-insensitively", async () => { + readConfigFileSnapshotForWrite.mockResolvedValue({ + snapshot: { + config: { + channels: { + telegram: { + defaultTo: "https://t.me/mychannel", + }, + }, + }, + }, + writeOptions: {}, + }); + loadCronStore.mockResolvedValue({ + version: 1, + jobs: [{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } }], + }); + + await maybePersistResolvedTelegramTarget({ + cfg: {} as OpenClawConfig, + rawTarget: "@MyChannel", + resolvedChatId: "-100123", + }); + + expect(writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + channels: { + telegram: { + defaultTo: "-100123", + }, + }, + }), + expect.any(Object), + ); + expect(saveCronStore).toHaveBeenCalledWith( + "/tmp/cron/jobs.json", + expect.objectContaining({ + jobs: [{ id: "a", delivery: { channel: "telegram", to: "-100123" } }], + }), + ); + }); +});
extensions/telegram/src/target-writeback.ts+11 −0 modified@@ -16,6 +16,7 @@ import { } from "./targets.js"; const writebackLogger = createSubsystemLogger("telegram/target-writeback"); +const TELEGRAM_ADMIN_SCOPE = "operator.admin"; function asObjectRecord(value: unknown): Record<string, unknown> | null { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -141,6 +142,7 @@ export async function maybePersistResolvedTelegramTarget(params: { rawTarget: string; resolvedChatId: string; verbose?: boolean; + gatewayClientScopes?: readonly string[]; }): Promise<void> { const raw = params.rawTarget.trim(); if (!raw) { @@ -154,6 +156,15 @@ export async function maybePersistResolvedTelegramTarget(params: { return; } const { matchKey, resolvedTarget } = rewrite; + if ( + Array.isArray(params.gatewayClientScopes) && + !params.gatewayClientScopes.includes(TELEGRAM_ADMIN_SCOPE) + ) { + writebackLogger.warn( + `skipping Telegram target writeback for ${raw} because gateway caller is missing ${TELEGRAM_ADMIN_SCOPE}`, + ); + return; + } try { const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
src/channels/plugins/types.adapters.ts+1 −0 modified@@ -141,6 +141,7 @@ export type ChannelOutboundContext = { identity?: OutboundIdentity; deps?: OutboundSendDeps; silent?: boolean; + gatewayClientScopes?: readonly string[]; }; export type ChannelOutboundPayloadContext = ChannelOutboundContext & {
src/channels/plugins/types.core.ts+1 −0 modified@@ -552,6 +552,7 @@ export type ChannelPollContext = { threadId?: string | null; silent?: boolean; isAnonymous?: boolean; + gatewayClientScopes?: readonly string[]; }; /** Minimal base for all channel probe results. Channel-specific probes extend this. */
src/gateway/server-methods/send.test.ts+100 −2 modified@@ -101,26 +101,40 @@ const makeContext = (): GatewayRequestContext => }) as unknown as GatewayRequestContext; async function runSend(params: Record<string, unknown>) { + return await runSendWithClient(params); +} + +async function runSendWithClient( + params: Record<string, unknown>, + client?: { connect?: { scopes?: string[] } } | null, +) { const respond = vi.fn(); await sendHandlers.send({ params: params as never, respond, context: makeContext(), req: { type: "req", id: "1", method: "send" }, - client: null, + client: (client ?? null) as never, isWebchatConnect: () => false, }); return { respond }; } async function runPoll(params: Record<string, unknown>) { + return await runPollWithClient(params); +} + +async function runPollWithClient( + params: Record<string, unknown>, + client?: { connect?: { scopes?: string[] } } | null, +) { const respond = vi.fn(); await sendHandlers.poll({ params: params as never, respond, context: makeContext(), req: { type: "req", id: "1", method: "poll" }, - client: null, + client: (client ?? null) as never, isWebchatConnect: () => false, }); return { respond }; @@ -185,6 +199,48 @@ describe("gateway send mirroring", () => { ); }); + it("forwards gateway client scopes into outbound delivery", async () => { + mockDeliverySuccess("m-telegram-scope"); + + await runSendWithClient( + { + to: "https://t.me/mychannel", + message: "hi", + channel: "telegram", + idempotencyKey: "idem-telegram-scope", + }, + { connect: { scopes: ["operator.write"] } }, + ); + + expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + gatewayClientScopes: ["operator.write"], + }), + ); + }); + + it("forwards an empty gateway scope array into outbound delivery", async () => { + mockDeliverySuccess("m-telegram-empty-scope"); + + await runSendWithClient( + { + to: "https://t.me/mychannel", + message: "hi", + channel: "telegram", + idempotencyKey: "idem-telegram-empty-scope", + }, + { connect: { scopes: [] } }, + ); + + expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + gatewayClientScopes: [], + }), + ); + }); + it("rejects empty sends when neither text nor media is present", async () => { const { respond } = await runSend({ to: "channel:C1", @@ -268,6 +324,48 @@ describe("gateway send mirroring", () => { ); }); + it("forwards gateway client scopes into outbound poll delivery", async () => { + await runPollWithClient( + { + to: "https://t.me/mychannel", + question: "Q?", + options: ["A", "B"], + channel: "telegram", + idempotencyKey: "idem-poll-scope", + }, + { connect: { scopes: ["operator.admin"] } }, + ); + + expect(mocks.sendPoll).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: expect.any(Object), + to: "resolved", + gatewayClientScopes: ["operator.admin"], + }), + ); + }); + + it("forwards an empty gateway scope array into outbound poll delivery", async () => { + await runPollWithClient( + { + to: "https://t.me/mychannel", + question: "Q?", + options: ["A", "B"], + channel: "telegram", + idempotencyKey: "idem-poll-empty-scope", + }, + { connect: { scopes: [] } }, + ); + + expect(mocks.sendPoll).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: expect.any(Object), + to: "resolved", + gatewayClientScopes: [], + }), + ); + }); + it("auto-picks the single configured channel for poll", async () => { const { respond } = await runPoll({ to: "x",
src/gateway/server-methods/send.ts+4 −2 modified@@ -89,7 +89,7 @@ async function resolveRequestedChannel(params: { } export const sendHandlers: GatewayRequestHandlers = { - send: async ({ params, respond, context }) => { + send: async ({ params, respond, context, client }) => { const p = params; if (!validateSendParams(p)) { respond( @@ -263,6 +263,7 @@ export const sendHandlers: GatewayRequestHandlers = { gifPlayback: request.gifPlayback, threadId: threadId ?? null, deps: outboundDeps, + gatewayClientScopes: client?.connect?.scopes ?? [], mirror: providedSessionKey ? { sessionKey: providedSessionKey, @@ -332,7 +333,7 @@ export const sendHandlers: GatewayRequestHandlers = { inflightMap.delete(dedupeKey); } }, - poll: async ({ params, respond, context }) => { + poll: async ({ params, respond, context, client }) => { const p = params; if (!validatePollParams(p)) { respond( @@ -444,6 +445,7 @@ export const sendHandlers: GatewayRequestHandlers = { threadId, silent: request.silent, isAnonymous: request.isAnonymous, + gatewayClientScopes: client?.connect?.scopes ?? [], }); const payload: Record<string, unknown> = { runId: idem,
src/gateway/server.send-telegram-target-writeback-scope.test.ts+296 −0 added@@ -0,0 +1,296 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { + sendMessageTelegram, + sendPollTelegram, + type TelegramApiOverride, +} from "../../extensions/telegram/src/send.js"; +import { + clearConfigCache, + loadConfig, + writeConfigFile, + type OpenClawConfig, +} from "../config/config.js"; +import { loadCronStore, saveCronStore } from "../cron/store.js"; +import type { CronStoreFile } from "../cron/types.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { + getActivePluginRegistry, + releasePinnedPluginChannelRegistry, + setActivePluginRegistry, +} from "../plugins/runtime.js"; +import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; +import { connectOk, installGatewayTestHooks, rpcReq } from "./test-helpers.js"; +import { withServer } from "./test-with-server.js"; + +installGatewayTestHooks({ scope: "suite" }); + +type TelegramGetChat = NonNullable<TelegramApiOverride["getChat"]>; +type TelegramSendMessage = NonNullable<TelegramApiOverride["sendMessage"]>; +type TelegramSendPoll = NonNullable<TelegramApiOverride["sendPoll"]>; + +function createCronStore(): CronStoreFile { + const now = Date.now(); + return { + version: 1, + jobs: [ + { + id: "telegram-writeback-job", + name: "Telegram writeback job", + enabled: true, + createdAtMs: now, + updatedAtMs: now, + schedule: { kind: "every", everyMs: 60_000 }, + sessionTarget: "main", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "tick" }, + state: {}, + delivery: { + mode: "announce", + channel: "telegram", + to: "@mychannel", + }, + }, + ], + }; +} + +async function withTelegramGatewayWritebackFixture( + run: (params: { + cronStorePath: string; + getChatMock: ReturnType<typeof vi.fn>; + sendMessageMock: ReturnType<typeof vi.fn>; + sendPollMock: ReturnType<typeof vi.fn>; + installTelegramTestPlugin: () => void; + }) => Promise<void>, +): Promise<void> { + const previousRegistry = getActivePluginRegistry() ?? createEmptyPluginRegistry(); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-telegram-writeback-")); + const cronStorePath = path.join(tempDir, "cron", "jobs.json"); + const getChatMock = vi.fn(); + const sendMessageMock = vi.fn(); + const sendPollMock = vi.fn(); + const getChat: TelegramGetChat = async (...args) => { + getChatMock(...args); + return { id: -100321 } as unknown as Awaited<ReturnType<TelegramGetChat>>; + }; + const sendMessage: TelegramSendMessage = async (...args) => { + sendMessageMock(...args); + return { + message_id: 17, + date: 1, + chat: { id: "-100321" }, + } as unknown as Awaited<ReturnType<TelegramSendMessage>>; + }; + const sendPoll: TelegramSendPoll = async (...args) => { + sendPollMock(...args); + return { + message_id: 19, + date: 1, + chat: { id: "-100321" }, + poll: { id: "poll-1" }, + } as unknown as Awaited<ReturnType<TelegramSendPoll>>; + }; + + const installTelegramTestPlugin = () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: createOutboundTestPlugin({ + id: "telegram", + label: "Telegram", + outbound: { + deliveryMode: "direct", + sendText: async ({ cfg, to, text, accountId, gatewayClientScopes }) => + ({ + channel: "telegram", + ...(await sendMessageTelegram(to, text, { + cfg, + accountId: accountId ?? undefined, + gatewayClientScopes, + token: "123:abc", + api: { + getChat, + sendMessage, + }, + })), + }), + sendPoll: async ({ cfg, to, poll, accountId, gatewayClientScopes, threadId }) => + ({ + channel: "telegram", + ...(await sendPollTelegram(to, poll, { + cfg, + accountId: accountId ?? undefined, + gatewayClientScopes, + messageThreadId: + typeof threadId === "number" && Number.isFinite(threadId) + ? Math.trunc(threadId) + : undefined, + token: "123:abc", + api: { + getChat, + sendPoll, + }, + })), + }), + }, + }), + }, + ]), + "telegram-target-writeback-scope", + ); + }; + + installTelegramTestPlugin(); + + try { + await saveCronStore(cronStorePath, createCronStore()); + clearConfigCache(); + await writeConfigFile({ + agents: { + defaults: { + model: "gpt-5.4", + workspace: path.join(process.env.HOME ?? ".", "openclaw"), + }, + }, + channels: { + telegram: { + botToken: "123:abc", + defaultTo: "https://t.me/mychannel", + }, + }, + cron: { + store: cronStorePath, + }, + } satisfies OpenClawConfig); + clearConfigCache(); + + await run({ + cronStorePath, + getChatMock, + sendMessageMock, + sendPollMock, + installTelegramTestPlugin, + }); + } finally { + setActivePluginRegistry(previousRegistry); + clearConfigCache(); + await fs.rm(tempDir, { recursive: true, force: true }); + } +} + +describe("gateway Telegram target writeback scope enforcement", () => { + it("allows operator.write delivery but skips config and cron persistence", async () => { + await withTelegramGatewayWritebackFixture(async (params) => { + const { cronStorePath, getChatMock, sendMessageMock } = params; + await withServer(async (ws) => { + await connectOk(ws, { token: "secret", scopes: ["operator.write"] }); + + const current = await rpcReq<{ hash?: string }>(ws, "config.get", {}); + expect(current.ok).toBe(true); + expect(typeof current.payload?.hash).toBe("string"); + + const directPatch = await rpcReq(ws, "config.patch", { + raw: JSON.stringify({ + channels: { + telegram: { + defaultTo: "-100321", + }, + }, + }), + baseHash: current.payload?.hash, + }); + expect(directPatch.ok).toBe(false); + expect(directPatch.error?.message).toBe("missing scope: operator.admin"); + + const viaSend = await rpcReq(ws, "send", { + to: "https://t.me/mychannel", + message: "hello from send scope test", + channel: "telegram", + sessionKey: "main", + idempotencyKey: "idem-send-telegram-target-writeback-operator-write", + }); + expect(viaSend.ok).toBe(true); + + clearConfigCache(); + const stored = loadConfig(); + const cronStore = await loadCronStore(cronStorePath); + + expect(stored.channels?.telegram?.defaultTo).toBe("https://t.me/mychannel"); + expect(cronStore.jobs[0]?.delivery?.to).toBe("@mychannel"); + expect(getChatMock).toHaveBeenCalledWith("@mychannel"); + expect(sendMessageMock).toHaveBeenCalledWith("-100321", "hello from send scope test", { + parse_mode: "HTML", + }); + }); + }); + }); + + it("persists config and cron rewrites for operator.admin delivery", async () => { + await withTelegramGatewayWritebackFixture(async (params) => { + const { cronStorePath, getChatMock, sendMessageMock } = params; + await withServer(async (ws) => { + await connectOk(ws, { token: "secret", scopes: ["operator.write", "operator.admin"] }); + + const viaSend = await rpcReq(ws, "send", { + to: "https://t.me/mychannel", + message: "hello from admin scope test", + channel: "telegram", + sessionKey: "main", + idempotencyKey: "idem-send-telegram-target-writeback-operator-admin", + }); + expect(viaSend.ok).toBe(true); + + clearConfigCache(); + const stored = loadConfig(); + const cronStore = await loadCronStore(cronStorePath); + + expect(stored.channels?.telegram?.defaultTo).toBe("-100321"); + expect(cronStore.jobs[0]?.delivery?.to).toBe("-100321"); + expect(getChatMock).toHaveBeenCalledWith("@mychannel"); + expect(sendMessageMock).toHaveBeenCalledWith("-100321", "hello from admin scope test", { + parse_mode: "HTML", + }); + }); + }); + }); + + it("allows operator.write poll delivery but skips config and cron persistence", async () => { + await withTelegramGatewayWritebackFixture(async (params) => { + const { cronStorePath, getChatMock, sendPollMock, installTelegramTestPlugin } = params; + await withServer(async (ws) => { + releasePinnedPluginChannelRegistry(); + installTelegramTestPlugin(); + await connectOk(ws, { token: "secret", scopes: ["operator.write"] }); + + const viaPoll = await rpcReq(ws, "poll", { + to: "https://t.me/mychannel", + question: "Which one?", + options: ["A", "B"], + channel: "telegram", + idempotencyKey: "idem-poll-telegram-target-writeback-operator-write", + }); + if (!viaPoll.ok) { + throw new Error(`poll failed: ${viaPoll.error?.message ?? "unknown error"}`); + } + expect(viaPoll.ok).toBe(true); + + clearConfigCache(); + const stored = loadConfig(); + const cronStore = await loadCronStore(cronStorePath); + + expect(stored.channels?.telegram?.defaultTo).toBe("https://t.me/mychannel"); + expect(cronStore.jobs[0]?.delivery?.to).toBe("@mychannel"); + expect(getChatMock).toHaveBeenCalledWith("@mychannel"); + expect(sendPollMock).toHaveBeenCalledWith("-100321", "Which one?", ["A", "B"], { + allows_multiple_answers: false, + is_anonymous: true, + }); + }); + }); + }); +});
src/infra/outbound/deliver.ts+5 −0 modified@@ -130,6 +130,7 @@ type ChannelHandlerParams = { forceDocument?: boolean; silent?: boolean; mediaLocalRoots?: readonly string[]; + gatewayClientScopes?: readonly string[]; }; // Channel docking: outbound delivery delegates to plugin.outbound adapters. @@ -250,6 +251,7 @@ function createChannelOutboundContextBase( deps: params.deps, silent: params.silent, mediaLocalRoots: params.mediaLocalRoots, + gatewayClientScopes: params.gatewayClientScopes, }; } @@ -275,6 +277,7 @@ type DeliverOutboundPayloadsCoreParams = { session?: OutboundSessionContext; mirror?: DeliveryMirror; silent?: boolean; + gatewayClientScopes?: readonly string[]; }; function collectPayloadMediaSources(payloads: ReplyPayload[]): string[] { @@ -508,6 +511,7 @@ export async function deliverOutboundPayloads( forceDocument: params.forceDocument, silent: params.silent, mirror: params.mirror, + gatewayClientScopes: params.gatewayClientScopes, }).catch(() => null); // Best-effort — don't block delivery if queue write fails. // Wrap onError to detect partial failures under bestEffort mode. @@ -576,6 +580,7 @@ async function deliverOutboundPayloadsCore( forceDocument: params.forceDocument, silent: params.silent, mediaLocalRoots, + gatewayClientScopes: params.gatewayClientScopes, }); const configuredTextLimit = handler.chunker ? resolveTextChunkLimit(cfg, channel, accountId, {
src/infra/outbound/delivery-queue.recovery.test.ts+2 −0 modified@@ -125,6 +125,7 @@ describe("delivery-queue recovery", () => { bestEffort: true, gifPlayback: true, silent: true, + gatewayClientScopes: ["operator.write"], mirror: { sessionKey: "agent:main:main", text: "a", @@ -142,6 +143,7 @@ describe("delivery-queue recovery", () => { bestEffort: true, gifPlayback: true, silent: true, + gatewayClientScopes: ["operator.write"], mirror: { sessionKey: "agent:main:main", text: "a",
src/infra/outbound/delivery-queue-recovery.ts+1 −0 modified@@ -75,6 +75,7 @@ function buildRecoveryDeliverParams(entry: QueuedDelivery, cfg: OpenClawConfig) forceDocument: entry.forceDocument, silent: entry.silent, mirror: entry.mirror, + gatewayClientScopes: entry.gatewayClientScopes, skipQueue: true, // Prevent re-enqueueing during recovery. } satisfies Parameters<DeliverFn>[0]; }
src/infra/outbound/delivery-queue.storage.test.ts+17 −0 modified@@ -23,6 +23,7 @@ describe("delivery-queue storage", () => { bestEffort: true, gifPlayback: true, silent: true, + gatewayClientScopes: ["operator.write"], mirror: { sessionKey: "agent:main:main", text: "hello", @@ -45,6 +46,7 @@ describe("delivery-queue storage", () => { bestEffort: true, gifPlayback: true, silent: true, + gatewayClientScopes: ["operator.write"], mirror: { sessionKey: "agent:main:main", text: "hello", @@ -157,6 +159,21 @@ describe("delivery-queue storage", () => { expect(await loadPendingDeliveries(tmpDir())).toHaveLength(2); }); + it("persists gateway caller scopes for replay", async () => { + const id = await enqueueDelivery( + { + channel: "telegram", + to: "2", + payloads: [{ text: "b" }], + gatewayClientScopes: ["operator.write"], + }, + tmpDir(), + ); + + const entry = readQueuedEntry(tmpDir(), id); + expect(entry.gatewayClientScopes).toEqual(["operator.write"]); + }); + it("backfills lastAttemptAt for legacy retry entries during load", async () => { const id = await enqueueDelivery( { channel: "whatsapp", to: "+1", payloads: [{ text: "legacy" }] },
src/infra/outbound/delivery-queue-storage.ts+3 −0 modified@@ -26,6 +26,8 @@ export type QueuedDeliveryPayload = { forceDocument?: boolean; silent?: boolean; mirror?: OutboundMirror; + /** Gateway caller scopes at enqueue time, preserved for recovery replay. */ + gatewayClientScopes?: readonly string[]; }; export interface QueuedDelivery extends QueuedDeliveryPayload { @@ -142,6 +144,7 @@ export async function enqueueDelivery( forceDocument: params.forceDocument, silent: params.silent, mirror: params.mirror, + gatewayClientScopes: params.gatewayClientScopes, retryCount: 0, }); return id;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/openclaw/openclaw/commit/b7d70ade3b9900dbe97bd73be9c02e924ff3c986nvdPatchWEB
- github.com/advisories/GHSA-767m-xrhc-fxm7ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-767m-xrhc-fxm7nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41359ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-privilege-escalation-via-operator-write-to-admin-class-telegram-config-and-cron-persistencenvdThird Party AdvisoryWEB
News mentions
0No linked articles in our index yet.