From b7d70ade3b9900dbe97bd73be9c02e924ff3c986 Mon Sep 17 00:00:00 2001 From: Devin Robison Date: Wed, 25 Mar 2026 11:12:09 -0700 Subject: [PATCH] Fix/telegram writeback admin scope gate (#54561) * fix(telegram): require operator.admin for legacy target writeback persistence * Address claude feedback * Update extensions/telegram/src/target-writeback.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Remove stray brace * Add updated docs * Add missing test file, address codex concerns * Fix test formatting error * Address comments, fix tests --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- docs/.generated/plugin-sdk-api-baseline.json | 66 ++-- docs/.generated/plugin-sdk-api-baseline.jsonl | 66 ++-- extensions/telegram/src/channel.ts | 35 ++- extensions/telegram/src/outbound-adapter.ts | 19 +- extensions/telegram/src/send.test.ts | 28 ++ extensions/telegram/src/send.ts | 6 + .../telegram/src/target-writeback.test.ts | 216 +++++++++++++ extensions/telegram/src/target-writeback.ts | 11 + src/channels/plugins/types.adapters.ts | 1 + src/channels/plugins/types.core.ts | 1 + src/gateway/server-methods/send.test.ts | 102 +++++- src/gateway/server-methods/send.ts | 6 +- ...nd-telegram-target-writeback-scope.test.ts | 296 ++++++++++++++++++ src/infra/outbound/deliver.ts | 5 + src/infra/outbound/delivery-queue-recovery.ts | 1 + src/infra/outbound/delivery-queue-storage.ts | 3 + .../outbound/delivery-queue.recovery.test.ts | 2 + .../outbound/delivery-queue.storage.test.ts | 17 + 18 files changed, 808 insertions(+), 73 deletions(-) create mode 100644 extensions/telegram/src/target-writeback.test.ts create mode 100644 src/gateway/server.send-telegram-target-writeback-scope.test.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.json b/docs/.generated/plugin-sdk-api-baseline.json index e73c4afb7be..702b8fcc59f 100644 --- a/docs/.generated/plugin-sdk-api-baseline.json +++ b/docs/.generated/plugin-sdk-api-baseline.json @@ -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" } }, diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index 6b1fc85b388..c0fed7fd019 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -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;","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;","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.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>; alsoAllow: z.ZodOptional>; deny: z.ZodOptional>; }, 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;","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;","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) | undefined): Promise;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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;","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 | null; ackReactionValue: string | null; remove: () => Promise; 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;","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;","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;","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"} diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 8aec3e4d148..c9be246070a 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -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(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(deps, "telegram") ?? @@ -726,6 +733,7 @@ export const telegramPlugin = createChatChannelPlugin({ threadId, silent, forceDocument, + gatewayClientScopes, }), }); return attachChannelToResult("telegram", result); @@ -733,7 +741,17 @@ export const telegramPlugin = createChatChannelPlugin({ }, 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, }), }, }, diff --git a/extensions/telegram/src/outbound-adapter.ts b/extensions/telegram/src/outbound-adapter.ts index c7fe00b9600..5139cdc20b8 100644 --- a/extensions/telegram/src/outbound-adapter.ts +++ b/extensions/telegram/src/outbound-adapter.ts @@ -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,6 +147,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { replyToId, threadId, forceDocument, + gatewayClientScopes, }) => { const { send, baseOpts } = resolveTelegramSendContext({ cfg, @@ -141,6 +155,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { accountId, replyToId, threadId, + gatewayClientScopes, }); return await send(to, text, { ...baseOpts, @@ -160,6 +175,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { replyToId, threadId, forceDocument, + gatewayClientScopes, }) => { const { send, baseOpts } = resolveTelegramSendContext({ cfg, @@ -167,6 +183,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { accountId, replyToId, threadId, + gatewayClientScopes, }); const result = await sendTelegramPayloadMessages({ send, diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index c8514f48d87..4d3b90eb891 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -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" } })), diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index 652ddacbad0..17d243ed0c0 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -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 { 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) diff --git a/extensions/telegram/src/target-writeback.test.ts b/extensions/telegram/src/target-writeback.test.ts new file mode 100644 index 00000000000..6eaf765030f --- /dev/null +++ b/extensions/telegram/src/target-writeback.test.ts @@ -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(); + 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" } }], + }), + ); + }); +}); diff --git a/extensions/telegram/src/target-writeback.ts b/extensions/telegram/src/target-writeback.ts index 8e5bf197a23..0bdc3d160f2 100644 --- a/extensions/telegram/src/target-writeback.ts +++ b/extensions/telegram/src/target-writeback.ts @@ -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 | 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 { 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(); diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index a5adf62c280..2499f38be15 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -141,6 +141,7 @@ export type ChannelOutboundContext = { identity?: OutboundIdentity; deps?: OutboundSendDeps; silent?: boolean; + gatewayClientScopes?: readonly string[]; }; export type ChannelOutboundPayloadContext = ChannelOutboundContext & { diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 5b6332ac1da..e007ffd9443 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -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. */ diff --git a/src/gateway/server-methods/send.test.ts b/src/gateway/server-methods/send.test.ts index 408f789cb9d..4dc9bb1eafe 100644 --- a/src/gateway/server-methods/send.test.ts +++ b/src/gateway/server-methods/send.test.ts @@ -101,26 +101,40 @@ const makeContext = (): GatewayRequestContext => }) as unknown as GatewayRequestContext; async function runSend(params: Record) { + return await runSendWithClient(params); +} + +async function runSendWithClient( + params: Record, + 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) { + return await runPollWithClient(params); +} + +async function runPollWithClient( + params: Record, + 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", diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index a118002dc45..ead034991aa 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -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 = { runId: idem, diff --git a/src/gateway/server.send-telegram-target-writeback-scope.test.ts b/src/gateway/server.send-telegram-target-writeback-scope.test.ts new file mode 100644 index 00000000000..a1ceef813c5 --- /dev/null +++ b/src/gateway/server.send-telegram-target-writeback-scope.test.ts @@ -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; +type TelegramSendMessage = NonNullable; +type TelegramSendPoll = NonNullable; + +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; + sendMessageMock: ReturnType; + sendPollMock: ReturnType; + installTelegramTestPlugin: () => void; + }) => Promise, +): Promise { + 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>; + }; + const sendMessage: TelegramSendMessage = async (...args) => { + sendMessageMock(...args); + return { + message_id: 17, + date: 1, + chat: { id: "-100321" }, + } as unknown as Awaited>; + }; + const sendPoll: TelegramSendPoll = async (...args) => { + sendPollMock(...args); + return { + message_id: 19, + date: 1, + chat: { id: "-100321" }, + poll: { id: "poll-1" }, + } as unknown as Awaited>; + }; + + 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, + }); + }); + }); + }); +}); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index fb206470c37..34f4223020e 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -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, { diff --git a/src/infra/outbound/delivery-queue-recovery.ts b/src/infra/outbound/delivery-queue-recovery.ts index 8532404506f..6649cd8684d 100644 --- a/src/infra/outbound/delivery-queue-recovery.ts +++ b/src/infra/outbound/delivery-queue-recovery.ts @@ -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[0]; } diff --git a/src/infra/outbound/delivery-queue-storage.ts b/src/infra/outbound/delivery-queue-storage.ts index ce8eba05b7b..601fc46beda 100644 --- a/src/infra/outbound/delivery-queue-storage.ts +++ b/src/infra/outbound/delivery-queue-storage.ts @@ -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; diff --git a/src/infra/outbound/delivery-queue.recovery.test.ts b/src/infra/outbound/delivery-queue.recovery.test.ts index c22b4cf5da4..423ddd86ccf 100644 --- a/src/infra/outbound/delivery-queue.recovery.test.ts +++ b/src/infra/outbound/delivery-queue.recovery.test.ts @@ -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", diff --git a/src/infra/outbound/delivery-queue.storage.test.ts b/src/infra/outbound/delivery-queue.storage.test.ts index a0719aa240f..19da937c4a4 100644 --- a/src/infra/outbound/delivery-queue.storage.test.ts +++ b/src/infra/outbound/delivery-queue.storage.test.ts @@ -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" }] },