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>
This commit is contained in:
Devin Robison
2026-03-25 11:12:09 -07:00
committed by GitHub
parent 89c4c674d1
commit b7d70ade3b
18 changed files with 808 additions and 73 deletions

View File

@@ -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"
}
},

View File

@@ -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"}

View File

@@ -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,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,
}),
},
},

View File

@@ -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,

View File

@@ -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" } })),

View File

@@ -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)

View File

@@ -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" } }],
}),
);
});
});

View File

@@ -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();

View File

@@ -141,6 +141,7 @@ export type ChannelOutboundContext = {
identity?: OutboundIdentity;
deps?: OutboundSendDeps;
silent?: boolean;
gatewayClientScopes?: readonly string[];
};
export type ChannelOutboundPayloadContext = ChannelOutboundContext & {

View File

@@ -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. */

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,
});
});
});
});
});

View File

@@ -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, {

View File

@@ -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];
}

View File

@@ -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;

View File

@@ -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",

View File

@@ -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" }] },