@poolzin/pool-bot 2026.2.21 → 2026.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/device-pair/index.ts +2 -2
  326. package/extensions/diagnostics-otel/package.json +1 -1
  327. package/extensions/discord/package.json +1 -1
  328. package/extensions/feishu/package.json +1 -1
  329. package/extensions/google-antigravity-auth/package.json +1 -1
  330. package/extensions/google-gemini-cli-auth/package.json +1 -1
  331. package/extensions/googlechat/package.json +1 -1
  332. package/extensions/imessage/package.json +1 -1
  333. package/extensions/irc/package.json +1 -1
  334. package/extensions/irc/src/accounts.ts +1 -1
  335. package/extensions/irc/src/onboarding.ts +4 -4
  336. package/extensions/line/package.json +1 -1
  337. package/extensions/llm-task/package.json +1 -1
  338. package/extensions/lobster/package.json +1 -1
  339. package/extensions/matrix/CHANGELOG.md +10 -0
  340. package/extensions/matrix/package.json +1 -1
  341. package/extensions/mattermost/package.json +1 -1
  342. package/extensions/memory-core/package.json +1 -1
  343. package/extensions/memory-lancedb/package.json +1 -1
  344. package/extensions/minimax-portal-auth/package.json +1 -1
  345. package/extensions/msteams/CHANGELOG.md +10 -0
  346. package/extensions/msteams/package.json +1 -1
  347. package/extensions/nextcloud-talk/package.json +1 -1
  348. package/extensions/nostr/CHANGELOG.md +10 -0
  349. package/extensions/nostr/package.json +1 -1
  350. package/extensions/open-prose/package.json +1 -1
  351. package/extensions/openai-codex-auth/package.json +1 -1
  352. package/extensions/signal/package.json +1 -1
  353. package/extensions/slack/package.json +1 -1
  354. package/extensions/telegram/package.json +1 -1
  355. package/extensions/tlon/package.json +1 -1
  356. package/extensions/twitch/CHANGELOG.md +10 -0
  357. package/extensions/twitch/package.json +1 -1
  358. package/extensions/voice-call/CHANGELOG.md +10 -0
  359. package/extensions/voice-call/package.json +1 -1
  360. package/extensions/whatsapp/package.json +1 -1
  361. package/extensions/zalo/CHANGELOG.md +10 -0
  362. package/extensions/zalo/package.json +1 -1
  363. package/extensions/zalouser/CHANGELOG.md +10 -0
  364. package/extensions/zalouser/package.json +1 -1
  365. package/package.json +1 -1
  366. package/skills/apple-reminders/SKILL.md +100 -49
  367. package/skills/coding-agent/SKILL.md +34 -28
  368. package/skills/github/SKILL.md +131 -16
  369. package/skills/imsg/SKILL.md +112 -15
  370. package/skills/openhue/SKILL.md +101 -19
  371. package/skills/tmux/SKILL.md +111 -79
  372. package/skills/weather/SKILL.md +88 -25
  373. package/dist/agents/openclaw-tools.js +0 -151
  374. package/dist/agents/tool-security.js +0 -96
  375. package/dist/gateway/url-validation.js +0 -94
  376. package/dist/infra/openclaw-root.js +0 -109
  377. package/dist/infra/tmp-openclaw-dir.js +0 -81
  378. package/dist/media/path-sanitization.js +0 -78
@@ -1,15 +1,221 @@
1
- import { Button, StringSelectMenu, } from "@buape/carbon";
1
+ import { Button, ChannelSelectMenu, MentionableSelectMenu, Modal, RoleSelectMenu, StringSelectMenu, UserSelectMenu, } from "@buape/carbon";
2
2
  import { ButtonStyle, ChannelType } from "discord-api-types/v10";
3
+ import { resolveHumanDelayConfig } from "../../agents/identity.js";
4
+ import { resolveChunkMode, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
5
+ import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../../auto-reply/envelope.js";
6
+ import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
7
+ import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
8
+ import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-reference.js";
9
+ import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
10
+ import { recordInboundSession } from "../../channels/session.js";
11
+ import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
12
+ import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
3
13
  import { logVerbose } from "../../globals.js";
4
14
  import { enqueueSystemEvent } from "../../infra/system-events.js";
5
15
  import { logDebug, logError } from "../../logger.js";
6
16
  import { buildPairingReply } from "../../pairing/pairing-messages.js";
7
17
  import { readChannelAllowFromStore, upsertChannelPairingRequest, } from "../../pairing/pairing-store.js";
8
18
  import { resolveAgentRoute } from "../../routing/resolve-route.js";
9
- import { normalizeDiscordAllowList, normalizeDiscordSlug, resolveDiscordAllowListMatch, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordUserAllowed, } from "./allow-list.js";
19
+ import { createNonExitingRuntime } from "../../runtime.js";
20
+ import { resolveDiscordComponentEntry, resolveDiscordModalEntry } from "../components-registry.js";
21
+ import { createDiscordFormModal, formatDiscordComponentEventText, parseDiscordComponentCustomId, parseDiscordComponentCustomIdForCarbon, parseDiscordModalCustomId, parseDiscordModalCustomIdForCarbon, } from "../components.js";
22
+ import { normalizeDiscordAllowList, normalizeDiscordSlug, resolveDiscordAllowListMatch, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordMemberAccessState, resolveDiscordOwnerAllowFrom, } from "./allow-list.js";
10
23
  import { formatDiscordUserTag } from "./format.js";
24
+ import { buildDirectLabel, buildGuildLabel } from "./reply-context.js";
25
+ import { deliverDiscordReply } from "./reply-delivery.js";
26
+ import { sendTyping } from "./typing.js";
11
27
  const AGENT_BUTTON_KEY = "agent";
12
28
  const AGENT_SELECT_KEY = "agentsel";
29
+ function resolveAgentComponentRoute(params) {
30
+ return resolveAgentRoute({
31
+ cfg: params.ctx.cfg,
32
+ channel: "discord",
33
+ accountId: params.ctx.accountId,
34
+ guildId: params.rawGuildId,
35
+ memberRoleIds: params.memberRoleIds,
36
+ peer: {
37
+ kind: params.isDirectMessage ? "direct" : "channel",
38
+ id: params.isDirectMessage ? params.userId : params.channelId,
39
+ },
40
+ parentPeer: params.parentId ? { kind: "channel", id: params.parentId } : undefined,
41
+ });
42
+ }
43
+ async function ackComponentInteraction(params) {
44
+ try {
45
+ await params.interaction.reply({
46
+ content: "✓",
47
+ ...params.replyOpts,
48
+ });
49
+ }
50
+ catch (err) {
51
+ logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`);
52
+ }
53
+ }
54
+ function resolveDiscordChannelContext(interaction) {
55
+ const channel = interaction.channel;
56
+ const channelName = channel && "name" in channel ? channel.name : undefined;
57
+ const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
58
+ const channelType = channel && "type" in channel ? channel.type : undefined;
59
+ const isThread = isThreadChannelType(channelType);
60
+ let parentId;
61
+ let parentName;
62
+ let parentSlug = "";
63
+ if (isThread && channel && "parentId" in channel) {
64
+ parentId = channel.parentId ?? undefined;
65
+ if ("parent" in channel) {
66
+ const parent = channel.parent;
67
+ if (parent?.name) {
68
+ parentName = parent.name;
69
+ parentSlug = normalizeDiscordSlug(parentName);
70
+ }
71
+ }
72
+ }
73
+ return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug };
74
+ }
75
+ async function resolveComponentInteractionContext(params) {
76
+ const { interaction, label } = params;
77
+ // Use interaction's actual channel_id (trusted source from Discord)
78
+ // This prevents channel spoofing attacks
79
+ const channelId = interaction.rawData.channel_id;
80
+ if (!channelId) {
81
+ logError(`${label}: missing channel_id in interaction`);
82
+ return null;
83
+ }
84
+ const user = interaction.user;
85
+ if (!user) {
86
+ logError(`${label}: missing user in interaction`);
87
+ return null;
88
+ }
89
+ const shouldDefer = params.defer !== false && "defer" in interaction;
90
+ let didDefer = false;
91
+ // Defer immediately to satisfy Discord's 3-second interaction ACK requirement.
92
+ // We use an ephemeral deferred reply so subsequent interaction.reply() calls
93
+ // can safely edit the original deferred response.
94
+ if (shouldDefer) {
95
+ try {
96
+ await interaction.defer({ ephemeral: true });
97
+ didDefer = true;
98
+ }
99
+ catch (err) {
100
+ logError(`${label}: failed to defer interaction: ${String(err)}`);
101
+ }
102
+ }
103
+ const replyOpts = didDefer ? {} : { ephemeral: true };
104
+ const username = formatUsername(user);
105
+ const userId = user.id;
106
+ // P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
107
+ // when guild is not cached even though guild_id is present in rawData
108
+ const rawGuildId = interaction.rawData.guild_id;
109
+ const isDirectMessage = !rawGuildId;
110
+ const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
111
+ ? interaction.rawData.member.roles.map((roleId) => String(roleId))
112
+ : [];
113
+ return {
114
+ channelId,
115
+ user,
116
+ username,
117
+ userId,
118
+ replyOpts,
119
+ rawGuildId,
120
+ isDirectMessage,
121
+ memberRoleIds,
122
+ };
123
+ }
124
+ async function ensureGuildComponentMemberAllowed(params) {
125
+ const { interaction, guildInfo, channelId, rawGuildId, channelCtx, memberRoleIds, user, replyOpts, componentLabel, unauthorizedReply, } = params;
126
+ if (!rawGuildId) {
127
+ return true;
128
+ }
129
+ const channelConfig = resolveDiscordChannelConfigWithFallback({
130
+ guildInfo,
131
+ channelId,
132
+ channelName: channelCtx.channelName,
133
+ channelSlug: channelCtx.channelSlug,
134
+ parentId: channelCtx.parentId,
135
+ parentName: channelCtx.parentName,
136
+ parentSlug: channelCtx.parentSlug,
137
+ scope: channelCtx.isThread ? "thread" : "channel",
138
+ });
139
+ const { memberAllowed } = resolveDiscordMemberAccessState({
140
+ channelConfig,
141
+ guildInfo,
142
+ memberRoleIds,
143
+ sender: {
144
+ id: user.id,
145
+ name: user.username,
146
+ tag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
147
+ },
148
+ });
149
+ if (memberAllowed) {
150
+ return true;
151
+ }
152
+ logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
153
+ try {
154
+ await interaction.reply({
155
+ content: unauthorizedReply,
156
+ ...replyOpts,
157
+ });
158
+ }
159
+ catch {
160
+ // Interaction may have expired
161
+ }
162
+ return false;
163
+ }
164
+ async function ensureComponentUserAllowed(params) {
165
+ const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
166
+ "discord:",
167
+ "user:",
168
+ "pk:",
169
+ ]);
170
+ if (!allowList) {
171
+ return true;
172
+ }
173
+ const match = resolveDiscordAllowListMatch({
174
+ allowList,
175
+ candidate: {
176
+ id: params.user.id,
177
+ name: params.user.username,
178
+ tag: formatDiscordUserTag(params.user),
179
+ },
180
+ });
181
+ if (match.allowed) {
182
+ return true;
183
+ }
184
+ logVerbose(`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`);
185
+ try {
186
+ await params.interaction.reply({
187
+ content: params.unauthorizedReply,
188
+ ...params.replyOpts,
189
+ });
190
+ }
191
+ catch {
192
+ // Interaction may have expired
193
+ }
194
+ return false;
195
+ }
196
+ async function ensureAgentComponentInteractionAllowed(params) {
197
+ const guildInfo = resolveDiscordGuildEntry({
198
+ guild: params.interaction.guild ?? undefined,
199
+ guildEntries: params.ctx.guildEntries,
200
+ });
201
+ const channelCtx = resolveDiscordChannelContext(params.interaction);
202
+ const memberAllowed = await ensureGuildComponentMemberAllowed({
203
+ interaction: params.interaction,
204
+ guildInfo,
205
+ channelId: params.channelId,
206
+ rawGuildId: params.rawGuildId,
207
+ channelCtx,
208
+ memberRoleIds: params.memberRoleIds,
209
+ user: params.user,
210
+ replyOpts: params.replyOpts,
211
+ componentLabel: params.componentLabel,
212
+ unauthorizedReply: params.unauthorizedReply,
213
+ });
214
+ if (!memberAllowed) {
215
+ return null;
216
+ }
217
+ return { parentId: channelCtx.parentId };
218
+ }
13
219
  /**
14
220
  * Build agent button custom ID: agent:componentId=<id>
15
221
  * The channelId is NOT embedded in customId - we use interaction.rawData.channel_id instead
@@ -59,14 +265,14 @@ function isThreadChannelType(channelType) {
59
265
  channelType === ChannelType.AnnouncementThread);
60
266
  }
61
267
  async function ensureDmComponentAuthorized(params) {
62
- const { ctx, interaction, user, componentLabel } = params;
268
+ const { ctx, interaction, user, componentLabel, replyOpts } = params;
63
269
  const dmPolicy = ctx.dmPolicy ?? "pairing";
64
270
  if (dmPolicy === "disabled") {
65
271
  logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
66
272
  try {
67
273
  await interaction.reply({
68
274
  content: "DM interactions are disabled.",
69
- ephemeral: true,
275
+ ...replyOpts,
70
276
  });
71
277
  }
72
278
  catch {
@@ -111,7 +317,7 @@ async function ensureDmComponentAuthorized(params) {
111
317
  code,
112
318
  })
113
319
  : "Pairing already requested. Ask the bot owner to approve your code.",
114
- ephemeral: true,
320
+ ...replyOpts,
115
321
  });
116
322
  }
117
323
  catch {
@@ -123,7 +329,7 @@ async function ensureDmComponentAuthorized(params) {
123
329
  try {
124
330
  await interaction.reply({
125
331
  content: `You are not authorized to use this ${componentLabel}.`,
126
- ephemeral: true,
332
+ ...replyOpts,
127
333
  });
128
334
  }
129
335
  catch {
@@ -131,6 +337,597 @@ async function ensureDmComponentAuthorized(params) {
131
337
  }
132
338
  return false;
133
339
  }
340
+ async function resolveInteractionContextWithDmAuth(params) {
341
+ const interactionCtx = await resolveComponentInteractionContext({
342
+ interaction: params.interaction,
343
+ label: params.label,
344
+ defer: params.defer,
345
+ });
346
+ if (!interactionCtx) {
347
+ return null;
348
+ }
349
+ if (interactionCtx.isDirectMessage) {
350
+ const authorized = await ensureDmComponentAuthorized({
351
+ ctx: params.ctx,
352
+ interaction: params.interaction,
353
+ user: interactionCtx.user,
354
+ componentLabel: params.componentLabel,
355
+ replyOpts: interactionCtx.replyOpts,
356
+ });
357
+ if (!authorized) {
358
+ return null;
359
+ }
360
+ }
361
+ return interactionCtx;
362
+ }
363
+ function normalizeComponentId(value) {
364
+ if (typeof value === "string") {
365
+ const trimmed = value.trim();
366
+ return trimmed ? trimmed : undefined;
367
+ }
368
+ if (typeof value === "number" && Number.isFinite(value)) {
369
+ return String(value);
370
+ }
371
+ return undefined;
372
+ }
373
+ function parseDiscordComponentData(data, customId) {
374
+ if (!data || typeof data !== "object") {
375
+ return null;
376
+ }
377
+ const rawComponentId = "cid" in data
378
+ ? data.cid
379
+ : data.componentId;
380
+ const rawModalId = "mid" in data ? data.mid : data.modalId;
381
+ let componentId = normalizeComponentId(rawComponentId);
382
+ let modalId = normalizeComponentId(rawModalId);
383
+ if (!componentId && customId) {
384
+ const parsed = parseDiscordComponentCustomId(customId);
385
+ if (parsed) {
386
+ componentId = parsed.componentId;
387
+ modalId = parsed.modalId;
388
+ }
389
+ }
390
+ if (!componentId) {
391
+ return null;
392
+ }
393
+ return { componentId, modalId };
394
+ }
395
+ function parseDiscordModalId(data, customId) {
396
+ if (data && typeof data === "object") {
397
+ const rawModalId = "mid" in data ? data.mid : data.modalId;
398
+ const modalId = normalizeComponentId(rawModalId);
399
+ if (modalId) {
400
+ return modalId;
401
+ }
402
+ }
403
+ if (customId) {
404
+ return parseDiscordModalCustomId(customId);
405
+ }
406
+ return null;
407
+ }
408
+ function resolveInteractionCustomId(interaction) {
409
+ if (!interaction?.rawData || typeof interaction.rawData !== "object") {
410
+ return undefined;
411
+ }
412
+ if (!("data" in interaction.rawData)) {
413
+ return undefined;
414
+ }
415
+ const data = interaction.rawData.data;
416
+ const customId = data?.custom_id;
417
+ if (typeof customId !== "string") {
418
+ return undefined;
419
+ }
420
+ const trimmed = customId.trim();
421
+ return trimmed ? trimmed : undefined;
422
+ }
423
+ function mapOptionLabels(options, values) {
424
+ if (!options || options.length === 0) {
425
+ return values;
426
+ }
427
+ const map = new Map(options.map((option) => [option.value, option.label]));
428
+ return values.map((value) => map.get(value) ?? value);
429
+ }
430
+ function mapSelectValues(entry, values) {
431
+ if (entry.selectType === "string") {
432
+ return mapOptionLabels(entry.options, values);
433
+ }
434
+ if (entry.selectType === "user") {
435
+ return values.map((value) => `user:${value}`);
436
+ }
437
+ if (entry.selectType === "role") {
438
+ return values.map((value) => `role:${value}`);
439
+ }
440
+ if (entry.selectType === "mentionable") {
441
+ return values.map((value) => `mentionable:${value}`);
442
+ }
443
+ if (entry.selectType === "channel") {
444
+ return values.map((value) => `channel:${value}`);
445
+ }
446
+ return values;
447
+ }
448
+ function resolveModalFieldValues(field, interaction) {
449
+ const fields = interaction.fields;
450
+ const optionLabels = field.options?.map((option) => ({
451
+ value: option.value,
452
+ label: option.label,
453
+ }));
454
+ const required = field.required === true;
455
+ try {
456
+ switch (field.type) {
457
+ case "text": {
458
+ const value = required ? fields.getText(field.id, true) : fields.getText(field.id);
459
+ return value ? [value] : [];
460
+ }
461
+ case "select":
462
+ case "checkbox":
463
+ case "radio": {
464
+ const values = required
465
+ ? fields.getStringSelect(field.id, true)
466
+ : (fields.getStringSelect(field.id) ?? []);
467
+ return mapOptionLabels(optionLabels, values);
468
+ }
469
+ case "role-select": {
470
+ try {
471
+ const roles = required
472
+ ? fields.getRoleSelect(field.id, true)
473
+ : (fields.getRoleSelect(field.id) ?? []);
474
+ return roles.map((role) => role.name ?? role.id);
475
+ }
476
+ catch {
477
+ const values = required
478
+ ? fields.getStringSelect(field.id, true)
479
+ : (fields.getStringSelect(field.id) ?? []);
480
+ return values;
481
+ }
482
+ }
483
+ case "user-select": {
484
+ const users = required
485
+ ? fields.getUserSelect(field.id, true)
486
+ : (fields.getUserSelect(field.id) ?? []);
487
+ return users.map((user) => formatDiscordUserTag(user));
488
+ }
489
+ default:
490
+ return [];
491
+ }
492
+ }
493
+ catch (err) {
494
+ logError(`agent modal: failed to read field ${field.id}: ${String(err)}`);
495
+ return [];
496
+ }
497
+ }
498
+ function formatModalSubmissionText(entry, interaction) {
499
+ const lines = [`Form "${entry.title}" submitted.`];
500
+ for (const field of entry.fields) {
501
+ const values = resolveModalFieldValues(field, interaction);
502
+ if (values.length === 0) {
503
+ continue;
504
+ }
505
+ lines.push(`- ${field.label}: ${values.join(", ")}`);
506
+ }
507
+ if (lines.length === 1) {
508
+ lines.push("- (no values)");
509
+ }
510
+ return lines.join("\n");
511
+ }
512
+ async function dispatchDiscordComponentEvent(params) {
513
+ const { ctx, interaction, interactionCtx, channelCtx, guildInfo, eventText } = params;
514
+ const runtime = ctx.runtime ?? createNonExitingRuntime();
515
+ const route = resolveAgentRoute({
516
+ cfg: ctx.cfg,
517
+ channel: "discord",
518
+ accountId: ctx.accountId,
519
+ guildId: interactionCtx.rawGuildId,
520
+ memberRoleIds: interactionCtx.memberRoleIds,
521
+ peer: {
522
+ kind: interactionCtx.isDirectMessage ? "direct" : "channel",
523
+ id: interactionCtx.isDirectMessage ? interactionCtx.userId : interactionCtx.channelId,
524
+ },
525
+ parentPeer: channelCtx.parentId ? { kind: "channel", id: channelCtx.parentId } : undefined,
526
+ });
527
+ const sessionKey = params.routeOverrides?.sessionKey ?? route.sessionKey;
528
+ const agentId = params.routeOverrides?.agentId ?? route.agentId;
529
+ const accountId = params.routeOverrides?.accountId ?? route.accountId;
530
+ const fromLabel = interactionCtx.isDirectMessage
531
+ ? buildDirectLabel(interactionCtx.user)
532
+ : buildGuildLabel({
533
+ guild: interaction.guild ?? undefined,
534
+ channelName: channelCtx.channelName ?? interactionCtx.channelId,
535
+ channelId: interactionCtx.channelId,
536
+ });
537
+ const senderName = interactionCtx.user.globalName ?? interactionCtx.user.username;
538
+ const senderUsername = interactionCtx.user.username;
539
+ const senderTag = formatDiscordUserTag(interactionCtx.user);
540
+ const groupChannel = !interactionCtx.isDirectMessage && channelCtx.channelSlug
541
+ ? `#${channelCtx.channelSlug}`
542
+ : undefined;
543
+ const groupSubject = interactionCtx.isDirectMessage ? undefined : groupChannel;
544
+ const channelConfig = resolveDiscordChannelConfigWithFallback({
545
+ guildInfo,
546
+ channelId: interactionCtx.channelId,
547
+ channelName: channelCtx.channelName,
548
+ channelSlug: channelCtx.channelSlug,
549
+ parentId: channelCtx.parentId,
550
+ parentName: channelCtx.parentName,
551
+ parentSlug: channelCtx.parentSlug,
552
+ scope: channelCtx.isThread ? "thread" : "channel",
553
+ });
554
+ const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
555
+ const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
556
+ channelConfig,
557
+ guildInfo,
558
+ sender: { id: interactionCtx.user.id, name: interactionCtx.user.username, tag: senderTag },
559
+ });
560
+ const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId });
561
+ const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
562
+ const previousTimestamp = readSessionUpdatedAt({
563
+ storePath,
564
+ sessionKey,
565
+ });
566
+ const timestamp = Date.now();
567
+ const combinedBody = formatInboundEnvelope({
568
+ channel: "Discord",
569
+ from: fromLabel,
570
+ timestamp,
571
+ body: eventText,
572
+ chatType: interactionCtx.isDirectMessage ? "direct" : "channel",
573
+ senderLabel: senderName,
574
+ previousTimestamp,
575
+ envelope: envelopeOptions,
576
+ });
577
+ const ctxPayload = finalizeInboundContext({
578
+ Body: combinedBody,
579
+ BodyForAgent: eventText,
580
+ RawBody: eventText,
581
+ CommandBody: eventText,
582
+ From: interactionCtx.isDirectMessage
583
+ ? `discord:${interactionCtx.userId}`
584
+ : `discord:channel:${interactionCtx.channelId}`,
585
+ To: `channel:${interactionCtx.channelId}`,
586
+ SessionKey: sessionKey,
587
+ AccountId: accountId,
588
+ ChatType: interactionCtx.isDirectMessage ? "direct" : "channel",
589
+ ConversationLabel: fromLabel,
590
+ SenderName: senderName,
591
+ SenderId: interactionCtx.userId,
592
+ SenderUsername: senderUsername,
593
+ SenderTag: senderTag,
594
+ GroupSubject: groupSubject,
595
+ GroupChannel: groupChannel,
596
+ GroupSystemPrompt: interactionCtx.isDirectMessage ? undefined : groupSystemPrompt,
597
+ GroupSpace: guildInfo?.id ?? guildInfo?.slug ?? interactionCtx.rawGuildId ?? undefined,
598
+ OwnerAllowFrom: ownerAllowFrom,
599
+ Provider: "discord",
600
+ Surface: "discord",
601
+ WasMentioned: true,
602
+ CommandAuthorized: true,
603
+ CommandSource: "text",
604
+ MessageSid: interaction.rawData.id,
605
+ Timestamp: timestamp,
606
+ OriginatingChannel: "discord",
607
+ OriginatingTo: `channel:${interactionCtx.channelId}`,
608
+ });
609
+ await recordInboundSession({
610
+ storePath,
611
+ sessionKey: ctxPayload.SessionKey ?? sessionKey,
612
+ ctx: ctxPayload,
613
+ updateLastRoute: interactionCtx.isDirectMessage
614
+ ? {
615
+ sessionKey: route.mainSessionKey,
616
+ channel: "discord",
617
+ to: `user:${interactionCtx.userId}`,
618
+ accountId,
619
+ }
620
+ : undefined,
621
+ onRecordError: (err) => {
622
+ logVerbose(`discord: failed updating component session meta: ${String(err)}`);
623
+ },
624
+ });
625
+ const deliverTarget = `channel:${interactionCtx.channelId}`;
626
+ const typingChannelId = interactionCtx.channelId;
627
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
628
+ cfg: ctx.cfg,
629
+ agentId,
630
+ channel: "discord",
631
+ accountId,
632
+ });
633
+ const tableMode = resolveMarkdownTableMode({
634
+ cfg: ctx.cfg,
635
+ channel: "discord",
636
+ accountId,
637
+ });
638
+ const textLimit = resolveTextChunkLimit(ctx.cfg, "discord", accountId, {
639
+ fallbackLimit: 2000,
640
+ });
641
+ const token = ctx.token ?? "";
642
+ const replyToMode = ctx.discordConfig?.replyToMode ?? ctx.cfg.channels?.discord?.replyToMode ?? "off";
643
+ const replyReference = createReplyReferencePlanner({
644
+ replyToMode,
645
+ startId: params.replyToId,
646
+ });
647
+ await dispatchReplyWithBufferedBlockDispatcher({
648
+ ctx: ctxPayload,
649
+ cfg: ctx.cfg,
650
+ replyOptions: { onModelSelected },
651
+ dispatcherOptions: {
652
+ ...prefixOptions,
653
+ humanDelay: resolveHumanDelayConfig(ctx.cfg, agentId),
654
+ deliver: async (payload) => {
655
+ const replyToId = replyReference.use();
656
+ await deliverDiscordReply({
657
+ replies: [payload],
658
+ target: deliverTarget,
659
+ token,
660
+ accountId,
661
+ rest: interaction.client.rest,
662
+ runtime,
663
+ replyToId,
664
+ textLimit,
665
+ maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
666
+ tableMode,
667
+ chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
668
+ });
669
+ replyReference.markSent();
670
+ },
671
+ onReplyStart: async () => {
672
+ try {
673
+ await sendTyping({ client: interaction.client, channelId: typingChannelId });
674
+ }
675
+ catch (err) {
676
+ logVerbose(`discord: typing failed for component reply: ${String(err)}`);
677
+ }
678
+ },
679
+ onError: (err) => {
680
+ logError(`discord component dispatch failed: ${String(err)}`);
681
+ },
682
+ },
683
+ });
684
+ }
685
+ async function handleDiscordComponentEvent(params) {
686
+ const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
687
+ if (!parsed) {
688
+ logError(`${params.label}: failed to parse component data`);
689
+ try {
690
+ await params.interaction.reply({
691
+ content: "This component is no longer valid.",
692
+ ephemeral: true,
693
+ });
694
+ }
695
+ catch {
696
+ // Interaction may have expired
697
+ }
698
+ return;
699
+ }
700
+ const entry = resolveDiscordComponentEntry({ id: parsed.componentId, consume: false });
701
+ if (!entry) {
702
+ try {
703
+ await params.interaction.reply({
704
+ content: "This component has expired.",
705
+ ephemeral: true,
706
+ });
707
+ }
708
+ catch {
709
+ // Interaction may have expired
710
+ }
711
+ return;
712
+ }
713
+ const interactionCtx = await resolveInteractionContextWithDmAuth({
714
+ ctx: params.ctx,
715
+ interaction: params.interaction,
716
+ label: params.label,
717
+ componentLabel: params.componentLabel,
718
+ });
719
+ if (!interactionCtx) {
720
+ return;
721
+ }
722
+ const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
723
+ const guildInfo = resolveDiscordGuildEntry({
724
+ guild: params.interaction.guild ?? undefined,
725
+ guildEntries: params.ctx.guildEntries,
726
+ });
727
+ const channelCtx = resolveDiscordChannelContext(params.interaction);
728
+ const unauthorizedReply = `You are not authorized to use this ${params.componentLabel}.`;
729
+ const memberAllowed = await ensureGuildComponentMemberAllowed({
730
+ interaction: params.interaction,
731
+ guildInfo,
732
+ channelId,
733
+ rawGuildId,
734
+ channelCtx,
735
+ memberRoleIds,
736
+ user,
737
+ replyOpts,
738
+ componentLabel: params.componentLabel,
739
+ unauthorizedReply,
740
+ });
741
+ if (!memberAllowed) {
742
+ return;
743
+ }
744
+ const componentAllowed = await ensureComponentUserAllowed({
745
+ entry,
746
+ interaction: params.interaction,
747
+ user,
748
+ replyOpts,
749
+ componentLabel: params.componentLabel,
750
+ unauthorizedReply,
751
+ });
752
+ if (!componentAllowed) {
753
+ return;
754
+ }
755
+ const consumed = resolveDiscordComponentEntry({
756
+ id: parsed.componentId,
757
+ consume: !entry.reusable,
758
+ });
759
+ if (!consumed) {
760
+ try {
761
+ await params.interaction.reply({
762
+ content: "This component has expired.",
763
+ ephemeral: true,
764
+ });
765
+ }
766
+ catch {
767
+ // Interaction may have expired
768
+ }
769
+ return;
770
+ }
771
+ if (consumed.kind === "modal-trigger") {
772
+ try {
773
+ await params.interaction.reply({
774
+ content: "This form is no longer available.",
775
+ ephemeral: true,
776
+ });
777
+ }
778
+ catch {
779
+ // Interaction may have expired
780
+ }
781
+ return;
782
+ }
783
+ const values = params.values ? mapSelectValues(consumed, params.values) : undefined;
784
+ const eventText = formatDiscordComponentEventText({
785
+ kind: consumed.kind === "select" ? "select" : "button",
786
+ label: consumed.label,
787
+ values,
788
+ });
789
+ try {
790
+ await params.interaction.reply({ content: "✓", ...replyOpts });
791
+ }
792
+ catch (err) {
793
+ logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`);
794
+ }
795
+ await dispatchDiscordComponentEvent({
796
+ ctx: params.ctx,
797
+ interaction: params.interaction,
798
+ interactionCtx,
799
+ channelCtx,
800
+ guildInfo,
801
+ eventText,
802
+ replyToId: consumed.messageId ?? params.interaction.message?.id,
803
+ routeOverrides: {
804
+ sessionKey: consumed.sessionKey,
805
+ agentId: consumed.agentId,
806
+ accountId: consumed.accountId,
807
+ },
808
+ });
809
+ }
810
+ async function handleDiscordModalTrigger(params) {
811
+ const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
812
+ if (!parsed) {
813
+ logError(`${params.label}: failed to parse modal trigger data`);
814
+ try {
815
+ await params.interaction.reply({
816
+ content: "This button is no longer valid.",
817
+ ephemeral: true,
818
+ });
819
+ }
820
+ catch {
821
+ // Interaction may have expired
822
+ }
823
+ return;
824
+ }
825
+ const entry = resolveDiscordComponentEntry({ id: parsed.componentId, consume: false });
826
+ if (!entry || entry.kind !== "modal-trigger") {
827
+ try {
828
+ await params.interaction.reply({
829
+ content: "This button has expired.",
830
+ ephemeral: true,
831
+ });
832
+ }
833
+ catch {
834
+ // Interaction may have expired
835
+ }
836
+ return;
837
+ }
838
+ const modalId = entry.modalId ?? parsed.modalId;
839
+ if (!modalId) {
840
+ try {
841
+ await params.interaction.reply({
842
+ content: "This form is no longer available.",
843
+ ephemeral: true,
844
+ });
845
+ }
846
+ catch {
847
+ // Interaction may have expired
848
+ }
849
+ return;
850
+ }
851
+ const interactionCtx = await resolveInteractionContextWithDmAuth({
852
+ ctx: params.ctx,
853
+ interaction: params.interaction,
854
+ label: params.label,
855
+ componentLabel: "form",
856
+ defer: false,
857
+ });
858
+ if (!interactionCtx) {
859
+ return;
860
+ }
861
+ const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
862
+ const guildInfo = resolveDiscordGuildEntry({
863
+ guild: params.interaction.guild ?? undefined,
864
+ guildEntries: params.ctx.guildEntries,
865
+ });
866
+ const channelCtx = resolveDiscordChannelContext(params.interaction);
867
+ const unauthorizedReply = "You are not authorized to use this form.";
868
+ const memberAllowed = await ensureGuildComponentMemberAllowed({
869
+ interaction: params.interaction,
870
+ guildInfo,
871
+ channelId,
872
+ rawGuildId,
873
+ channelCtx,
874
+ memberRoleIds,
875
+ user,
876
+ replyOpts,
877
+ componentLabel: "form",
878
+ unauthorizedReply,
879
+ });
880
+ if (!memberAllowed) {
881
+ return;
882
+ }
883
+ const componentAllowed = await ensureComponentUserAllowed({
884
+ entry,
885
+ interaction: params.interaction,
886
+ user,
887
+ replyOpts,
888
+ componentLabel: "form",
889
+ unauthorizedReply,
890
+ });
891
+ if (!componentAllowed) {
892
+ return;
893
+ }
894
+ const consumed = resolveDiscordComponentEntry({
895
+ id: parsed.componentId,
896
+ consume: !entry.reusable,
897
+ });
898
+ if (!consumed) {
899
+ try {
900
+ await params.interaction.reply({
901
+ content: "This form has expired.",
902
+ ephemeral: true,
903
+ });
904
+ }
905
+ catch {
906
+ // Interaction may have expired
907
+ }
908
+ return;
909
+ }
910
+ const resolvedModalId = consumed.modalId ?? modalId;
911
+ const modalEntry = resolveDiscordModalEntry({ id: resolvedModalId, consume: false });
912
+ if (!modalEntry) {
913
+ try {
914
+ await params.interaction.reply({
915
+ content: "This form has expired.",
916
+ ephemeral: true,
917
+ });
918
+ }
919
+ catch {
920
+ // Interaction may have expired
921
+ }
922
+ return;
923
+ }
924
+ try {
925
+ await params.interaction.showModal(createDiscordFormModal(modalEntry));
926
+ }
927
+ catch (err) {
928
+ logError(`${params.label}: failed to show modal: ${String(err)}`);
929
+ }
930
+ }
134
931
  export class AgentComponentButton extends Button {
135
932
  label = AGENT_BUTTON_KEY;
136
933
  customId = `${AGENT_BUTTON_KEY}:seed=1`;
@@ -157,112 +954,41 @@ export class AgentComponentButton extends Button {
157
954
  return;
158
955
  }
159
956
  const { componentId } = parsed;
160
- // P1 FIX: Use interaction's actual channel_id instead of trusting customId
161
- // This prevents channel ID spoofing attacks where an attacker crafts a button
162
- // with a different channelId to inject events into other sessions
163
- const channelId = interaction.rawData.channel_id;
164
- if (!channelId) {
165
- logError("agent button: missing channel_id in interaction");
166
- return;
167
- }
168
- const user = interaction.user;
169
- if (!user) {
170
- logError("agent button: missing user in interaction");
957
+ const interactionCtx = await resolveInteractionContextWithDmAuth({
958
+ ctx: this.ctx,
959
+ interaction,
960
+ label: "agent button",
961
+ componentLabel: "button",
962
+ });
963
+ if (!interactionCtx) {
171
964
  return;
172
965
  }
173
- const username = formatUsername(user);
174
- const userId = user.id;
175
- // P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
176
- // when guild is not cached even though guild_id is present in rawData
177
- const rawGuildId = interaction.rawData.guild_id;
178
- const isDirectMessage = !rawGuildId;
179
- if (isDirectMessage) {
180
- const authorized = await ensureDmComponentAuthorized({
181
- ctx: this.ctx,
182
- interaction,
183
- user,
184
- componentLabel: "button",
185
- });
186
- if (!authorized) {
187
- return;
188
- }
189
- }
190
- // P2 FIX: Check user allowlist before processing component interaction
191
- // This prevents unauthorized users from injecting system events
192
- const guild = interaction.guild;
193
- const guildInfo = resolveDiscordGuildEntry({
194
- guild: guild ?? undefined,
195
- guildEntries: this.ctx.guildEntries,
966
+ const { channelId, user, username, userId, replyOpts, rawGuildId, isDirectMessage, memberRoleIds, } = interactionCtx;
967
+ // Check user allowlist before processing component interaction
968
+ // This prevents unauthorized users from injecting system events.
969
+ const allowed = await ensureAgentComponentInteractionAllowed({
970
+ ctx: this.ctx,
971
+ interaction,
972
+ channelId,
973
+ rawGuildId,
974
+ memberRoleIds,
975
+ user,
976
+ replyOpts,
977
+ componentLabel: "button",
978
+ unauthorizedReply: "You are not authorized to use this button.",
196
979
  });
197
- // Resolve channel info for thread detection and allowlist inheritance
198
- const channel = interaction.channel;
199
- const channelName = channel && "name" in channel ? channel.name : undefined;
200
- const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
201
- const channelType = channel && "type" in channel ? channel.type : undefined;
202
- const isThread = isThreadChannelType(channelType);
203
- // Resolve thread parent for allowlist inheritance
204
- // Note: We can get parentId from channel but cannot fetch parent name without a client.
205
- // The parentId alone enables ID-based parent config matching. Name-based matching
206
- // requires the channel cache to have parent info available.
207
- let parentId;
208
- let parentName;
209
- let parentSlug = "";
210
- if (isThread && channel && "parentId" in channel) {
211
- parentId = channel.parentId ?? undefined;
212
- // Try to get parent name from channel's parent if available
213
- if ("parent" in channel) {
214
- const parent = channel.parent;
215
- if (parent?.name) {
216
- parentName = parent.name;
217
- parentSlug = normalizeDiscordSlug(parentName);
218
- }
219
- }
220
- }
221
- // Only check guild allowlists if this is a guild interaction
222
- if (rawGuildId) {
223
- const channelConfig = resolveDiscordChannelConfigWithFallback({
224
- guildInfo,
225
- channelId,
226
- channelName,
227
- channelSlug,
228
- parentId,
229
- parentName,
230
- parentSlug,
231
- scope: isThread ? "thread" : "channel",
232
- });
233
- const channelUsers = channelConfig?.users ?? guildInfo?.users;
234
- if (Array.isArray(channelUsers) && channelUsers.length > 0) {
235
- const userOk = resolveDiscordUserAllowed({
236
- allowList: channelUsers,
237
- userId,
238
- userName: user.username,
239
- userTag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
240
- });
241
- if (!userOk) {
242
- logVerbose(`agent button: blocked user ${userId} (not in allowlist)`);
243
- try {
244
- await interaction.reply({
245
- content: "You are not authorized to use this button.",
246
- ephemeral: true,
247
- });
248
- }
249
- catch {
250
- // Interaction may have expired
251
- }
252
- return;
253
- }
254
- }
980
+ if (!allowed) {
981
+ return;
255
982
  }
256
- // Resolve route with full context (guildId, proper peer kind)
257
- const route = resolveAgentRoute({
258
- cfg: this.ctx.cfg,
259
- channel: "discord",
260
- accountId: this.ctx.accountId,
261
- guildId: rawGuildId,
262
- peer: {
263
- kind: isDirectMessage ? "dm" : "channel",
264
- id: isDirectMessage ? userId : channelId,
265
- },
983
+ const { parentId } = allowed;
984
+ const route = resolveAgentComponentRoute({
985
+ ctx: this.ctx,
986
+ rawGuildId,
987
+ memberRoleIds,
988
+ isDirectMessage,
989
+ userId,
990
+ channelId,
991
+ parentId,
266
992
  });
267
993
  const eventText = `[Discord component: ${componentId} clicked by ${username} (${userId})]`;
268
994
  logDebug(`agent button: enqueuing event for channel ${channelId}: ${eventText}`);
@@ -270,16 +996,7 @@ export class AgentComponentButton extends Button {
270
996
  sessionKey: route.sessionKey,
271
997
  contextKey: `discord:agent-button:${channelId}:${componentId}:${userId}`,
272
998
  });
273
- // Acknowledge the interaction
274
- try {
275
- await interaction.reply({
276
- content: "✓",
277
- ephemeral: true,
278
- });
279
- }
280
- catch (err) {
281
- logError(`agent button: failed to acknowledge interaction: ${String(err)}`);
282
- }
999
+ await ackComponentInteraction({ interaction, replyOpts, label: "agent button" });
283
1000
  }
284
1001
  }
285
1002
  export class AgentSelectMenu extends StringSelectMenu {
@@ -307,127 +1024,285 @@ export class AgentSelectMenu extends StringSelectMenu {
307
1024
  return;
308
1025
  }
309
1026
  const { componentId } = parsed;
310
- // Use interaction's actual channel_id (trusted source from Discord)
311
- // This prevents channel spoofing attacks
312
- const channelId = interaction.rawData.channel_id;
313
- if (!channelId) {
314
- logError("agent select: missing channel_id in interaction");
1027
+ const interactionCtx = await resolveInteractionContextWithDmAuth({
1028
+ ctx: this.ctx,
1029
+ interaction,
1030
+ label: "agent select",
1031
+ componentLabel: "select menu",
1032
+ });
1033
+ if (!interactionCtx) {
315
1034
  return;
316
1035
  }
317
- const user = interaction.user;
318
- if (!user) {
319
- logError("agent select: missing user in interaction");
1036
+ const { channelId, user, username, userId, replyOpts, rawGuildId, isDirectMessage, memberRoleIds, } = interactionCtx;
1037
+ // Check user allowlist before processing component interaction.
1038
+ const allowed = await ensureAgentComponentInteractionAllowed({
1039
+ ctx: this.ctx,
1040
+ interaction,
1041
+ channelId,
1042
+ rawGuildId,
1043
+ memberRoleIds,
1044
+ user,
1045
+ replyOpts,
1046
+ componentLabel: "select",
1047
+ unauthorizedReply: "You are not authorized to use this select menu.",
1048
+ });
1049
+ if (!allowed) {
320
1050
  return;
321
1051
  }
322
- const username = formatUsername(user);
323
- const userId = user.id;
324
- // P1 FIX: Use rawData.guild_id as source of truth - interaction.guild can be null
325
- // when guild is not cached even though guild_id is present in rawData
326
- const rawGuildId = interaction.rawData.guild_id;
327
- const isDirectMessage = !rawGuildId;
328
- if (isDirectMessage) {
329
- const authorized = await ensureDmComponentAuthorized({
1052
+ const { parentId } = allowed;
1053
+ // Extract selected values
1054
+ const values = interaction.values ?? [];
1055
+ const valuesText = values.length > 0 ? ` (selected: ${values.join(", ")})` : "";
1056
+ const route = resolveAgentComponentRoute({
1057
+ ctx: this.ctx,
1058
+ rawGuildId,
1059
+ memberRoleIds,
1060
+ isDirectMessage,
1061
+ userId,
1062
+ channelId,
1063
+ parentId,
1064
+ });
1065
+ const eventText = `[Discord select menu: ${componentId} interacted by ${username} (${userId})${valuesText}]`;
1066
+ logDebug(`agent select: enqueuing event for channel ${channelId}: ${eventText}`);
1067
+ enqueueSystemEvent(eventText, {
1068
+ sessionKey: route.sessionKey,
1069
+ contextKey: `discord:agent-select:${channelId}:${componentId}:${userId}`,
1070
+ });
1071
+ await ackComponentInteraction({ interaction, replyOpts, label: "agent select" });
1072
+ }
1073
+ }
1074
+ class DiscordComponentButton extends Button {
1075
+ label = "component";
1076
+ customId = "*";
1077
+ style = ButtonStyle.Primary;
1078
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1079
+ ctx;
1080
+ constructor(ctx) {
1081
+ super();
1082
+ this.ctx = ctx;
1083
+ }
1084
+ async run(interaction, data) {
1085
+ const parsed = parseDiscordComponentData(data, resolveInteractionCustomId(interaction));
1086
+ if (parsed?.modalId) {
1087
+ await handleDiscordModalTrigger({
330
1088
  ctx: this.ctx,
331
1089
  interaction,
332
- user,
333
- componentLabel: "select menu",
1090
+ data,
1091
+ label: "discord component modal",
334
1092
  });
335
- if (!authorized) {
336
- return;
337
- }
1093
+ return;
338
1094
  }
339
- // Check user allowlist before processing component interaction
340
- const guild = interaction.guild;
341
- const guildInfo = resolveDiscordGuildEntry({
342
- guild: guild ?? undefined,
343
- guildEntries: this.ctx.guildEntries,
1095
+ await handleDiscordComponentEvent({
1096
+ ctx: this.ctx,
1097
+ interaction,
1098
+ data,
1099
+ componentLabel: "button",
1100
+ label: "discord component button",
344
1101
  });
345
- // Resolve channel info for thread detection and allowlist inheritance
346
- const channel = interaction.channel;
347
- const channelName = channel && "name" in channel ? channel.name : undefined;
348
- const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
349
- const channelType = channel && "type" in channel ? channel.type : undefined;
350
- const isThread = isThreadChannelType(channelType);
351
- // Resolve thread parent for allowlist inheritance
352
- let parentId;
353
- let parentName;
354
- let parentSlug = "";
355
- if (isThread && channel && "parentId" in channel) {
356
- parentId = channel.parentId ?? undefined;
357
- // Try to get parent name from channel's parent if available
358
- if ("parent" in channel) {
359
- const parent = channel.parent;
360
- if (parent?.name) {
361
- parentName = parent.name;
362
- parentSlug = normalizeDiscordSlug(parentName);
363
- }
1102
+ }
1103
+ }
1104
+ class DiscordComponentStringSelect extends StringSelectMenu {
1105
+ customId = "*";
1106
+ options = [];
1107
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1108
+ ctx;
1109
+ constructor(ctx) {
1110
+ super();
1111
+ this.ctx = ctx;
1112
+ }
1113
+ async run(interaction, data) {
1114
+ await handleDiscordComponentEvent({
1115
+ ctx: this.ctx,
1116
+ interaction,
1117
+ data,
1118
+ componentLabel: "select menu",
1119
+ label: "discord component select",
1120
+ values: interaction.values ?? [],
1121
+ });
1122
+ }
1123
+ }
1124
+ class DiscordComponentUserSelect extends UserSelectMenu {
1125
+ customId = "*";
1126
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1127
+ ctx;
1128
+ constructor(ctx) {
1129
+ super();
1130
+ this.ctx = ctx;
1131
+ }
1132
+ async run(interaction, data) {
1133
+ await handleDiscordComponentEvent({
1134
+ ctx: this.ctx,
1135
+ interaction,
1136
+ data,
1137
+ componentLabel: "user select",
1138
+ label: "discord component user select",
1139
+ values: interaction.values ?? [],
1140
+ });
1141
+ }
1142
+ }
1143
+ class DiscordComponentRoleSelect extends RoleSelectMenu {
1144
+ customId = "*";
1145
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1146
+ ctx;
1147
+ constructor(ctx) {
1148
+ super();
1149
+ this.ctx = ctx;
1150
+ }
1151
+ async run(interaction, data) {
1152
+ await handleDiscordComponentEvent({
1153
+ ctx: this.ctx,
1154
+ interaction,
1155
+ data,
1156
+ componentLabel: "role select",
1157
+ label: "discord component role select",
1158
+ values: interaction.values ?? [],
1159
+ });
1160
+ }
1161
+ }
1162
+ class DiscordComponentMentionableSelect extends MentionableSelectMenu {
1163
+ customId = "*";
1164
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1165
+ ctx;
1166
+ constructor(ctx) {
1167
+ super();
1168
+ this.ctx = ctx;
1169
+ }
1170
+ async run(interaction, data) {
1171
+ await handleDiscordComponentEvent({
1172
+ ctx: this.ctx,
1173
+ interaction,
1174
+ data,
1175
+ componentLabel: "mentionable select",
1176
+ label: "discord component mentionable select",
1177
+ values: interaction.values ?? [],
1178
+ });
1179
+ }
1180
+ }
1181
+ class DiscordComponentChannelSelect extends ChannelSelectMenu {
1182
+ customId = "*";
1183
+ customIdParser = parseDiscordComponentCustomIdForCarbon;
1184
+ ctx;
1185
+ constructor(ctx) {
1186
+ super();
1187
+ this.ctx = ctx;
1188
+ }
1189
+ async run(interaction, data) {
1190
+ await handleDiscordComponentEvent({
1191
+ ctx: this.ctx,
1192
+ interaction,
1193
+ data,
1194
+ componentLabel: "channel select",
1195
+ label: "discord component channel select",
1196
+ values: interaction.values ?? [],
1197
+ });
1198
+ }
1199
+ }
1200
+ class DiscordComponentModal extends Modal {
1201
+ title = "Pool Bot form";
1202
+ customId = "*";
1203
+ components = [];
1204
+ customIdParser = parseDiscordModalCustomIdForCarbon;
1205
+ ctx;
1206
+ constructor(ctx) {
1207
+ super();
1208
+ this.ctx = ctx;
1209
+ }
1210
+ async run(interaction, data) {
1211
+ const modalId = parseDiscordModalId(data, resolveInteractionCustomId(interaction));
1212
+ if (!modalId) {
1213
+ logError("discord component modal: missing modal id");
1214
+ try {
1215
+ await interaction.reply({
1216
+ content: "This form is no longer valid.",
1217
+ ephemeral: true,
1218
+ });
1219
+ }
1220
+ catch {
1221
+ // Interaction may have expired
364
1222
  }
1223
+ return;
365
1224
  }
366
- // Only check guild allowlists if this is a guild interaction
367
- if (rawGuildId) {
368
- const channelConfig = resolveDiscordChannelConfigWithFallback({
369
- guildInfo,
370
- channelId,
371
- channelName,
372
- channelSlug,
373
- parentId,
374
- parentName,
375
- parentSlug,
376
- scope: isThread ? "thread" : "channel",
377
- });
378
- const channelUsers = channelConfig?.users ?? guildInfo?.users;
379
- if (Array.isArray(channelUsers) && channelUsers.length > 0) {
380
- const userOk = resolveDiscordUserAllowed({
381
- allowList: channelUsers,
382
- userId,
383
- userName: user.username,
384
- userTag: user.discriminator ? `${user.username}#${user.discriminator}` : undefined,
1225
+ const modalEntry = resolveDiscordModalEntry({ id: modalId, consume: false });
1226
+ if (!modalEntry) {
1227
+ try {
1228
+ await interaction.reply({
1229
+ content: "This form has expired.",
1230
+ ephemeral: true,
385
1231
  });
386
- if (!userOk) {
387
- logVerbose(`agent select: blocked user ${userId} (not in allowlist)`);
388
- try {
389
- await interaction.reply({
390
- content: "You are not authorized to use this select menu.",
391
- ephemeral: true,
392
- });
393
- }
394
- catch {
395
- // Interaction may have expired
396
- }
397
- return;
398
- }
399
1232
  }
1233
+ catch {
1234
+ // Interaction may have expired
1235
+ }
1236
+ return;
400
1237
  }
401
- // Extract selected values
402
- const values = interaction.values ?? [];
403
- const valuesText = values.length > 0 ? ` (selected: ${values.join(", ")})` : "";
404
- // Resolve route with full context (guildId, proper peer kind)
405
- const route = resolveAgentRoute({
406
- cfg: this.ctx.cfg,
407
- channel: "discord",
408
- accountId: this.ctx.accountId,
409
- guildId: rawGuildId,
410
- peer: {
411
- kind: isDirectMessage ? "dm" : "channel",
412
- id: isDirectMessage ? userId : channelId,
413
- },
1238
+ const interactionCtx = await resolveInteractionContextWithDmAuth({
1239
+ ctx: this.ctx,
1240
+ interaction,
1241
+ label: "discord component modal",
1242
+ componentLabel: "form",
1243
+ defer: false,
414
1244
  });
415
- const eventText = `[Discord select menu: ${componentId} interacted by ${username} (${userId})${valuesText}]`;
416
- logDebug(`agent select: enqueuing event for channel ${channelId}: ${eventText}`);
417
- enqueueSystemEvent(eventText, {
418
- sessionKey: route.sessionKey,
419
- contextKey: `discord:agent-select:${channelId}:${componentId}:${userId}`,
1245
+ if (!interactionCtx) {
1246
+ return;
1247
+ }
1248
+ const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
1249
+ const guildInfo = resolveDiscordGuildEntry({
1250
+ guild: interaction.guild ?? undefined,
1251
+ guildEntries: this.ctx.guildEntries,
1252
+ });
1253
+ const channelCtx = resolveDiscordChannelContext(interaction);
1254
+ const memberAllowed = await ensureGuildComponentMemberAllowed({
1255
+ interaction,
1256
+ guildInfo,
1257
+ channelId,
1258
+ rawGuildId,
1259
+ channelCtx,
1260
+ memberRoleIds,
1261
+ user,
1262
+ replyOpts,
1263
+ componentLabel: "form",
1264
+ unauthorizedReply: "You are not authorized to use this form.",
1265
+ });
1266
+ if (!memberAllowed) {
1267
+ return;
1268
+ }
1269
+ const consumed = resolveDiscordModalEntry({
1270
+ id: modalId,
1271
+ consume: !modalEntry.reusable,
420
1272
  });
421
- // Acknowledge the interaction
1273
+ if (!consumed) {
1274
+ try {
1275
+ await interaction.reply({
1276
+ content: "This form has expired.",
1277
+ ephemeral: true,
1278
+ });
1279
+ }
1280
+ catch {
1281
+ // Interaction may have expired
1282
+ }
1283
+ return;
1284
+ }
422
1285
  try {
423
- await interaction.reply({
424
- content: "✓",
425
- ephemeral: true,
426
- });
1286
+ await interaction.acknowledge();
427
1287
  }
428
1288
  catch (err) {
429
- logError(`agent select: failed to acknowledge interaction: ${String(err)}`);
1289
+ logError(`discord component modal: failed to acknowledge: ${String(err)}`);
430
1290
  }
1291
+ const eventText = formatModalSubmissionText(consumed, interaction);
1292
+ await dispatchDiscordComponentEvent({
1293
+ ctx: this.ctx,
1294
+ interaction,
1295
+ interactionCtx,
1296
+ channelCtx,
1297
+ guildInfo,
1298
+ eventText,
1299
+ replyToId: consumed.messageId,
1300
+ routeOverrides: {
1301
+ sessionKey: consumed.sessionKey,
1302
+ agentId: consumed.agentId,
1303
+ accountId: consumed.accountId,
1304
+ },
1305
+ });
431
1306
  }
432
1307
  }
433
1308
  export function createAgentComponentButton(ctx) {
@@ -436,3 +1311,24 @@ export function createAgentComponentButton(ctx) {
436
1311
  export function createAgentSelectMenu(ctx) {
437
1312
  return new AgentSelectMenu(ctx);
438
1313
  }
1314
+ export function createDiscordComponentButton(ctx) {
1315
+ return new DiscordComponentButton(ctx);
1316
+ }
1317
+ export function createDiscordComponentStringSelect(ctx) {
1318
+ return new DiscordComponentStringSelect(ctx);
1319
+ }
1320
+ export function createDiscordComponentUserSelect(ctx) {
1321
+ return new DiscordComponentUserSelect(ctx);
1322
+ }
1323
+ export function createDiscordComponentRoleSelect(ctx) {
1324
+ return new DiscordComponentRoleSelect(ctx);
1325
+ }
1326
+ export function createDiscordComponentMentionableSelect(ctx) {
1327
+ return new DiscordComponentMentionableSelect(ctx);
1328
+ }
1329
+ export function createDiscordComponentChannelSelect(ctx) {
1330
+ return new DiscordComponentChannelSelect(ctx);
1331
+ }
1332
+ export function createDiscordComponentModal(ctx) {
1333
+ return new DiscordComponentModal(ctx);
1334
+ }