@poolzin/pool-bot 2026.2.21 → 2026.2.22

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 (369) hide show
  1. package/CHANGELOG.md +17 -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/diagnostics-otel/package.json +1 -1
  326. package/extensions/discord/package.json +1 -1
  327. package/extensions/feishu/package.json +1 -1
  328. package/extensions/google-antigravity-auth/package.json +1 -1
  329. package/extensions/google-gemini-cli-auth/package.json +1 -1
  330. package/extensions/googlechat/package.json +1 -1
  331. package/extensions/imessage/package.json +1 -1
  332. package/extensions/irc/package.json +1 -1
  333. package/extensions/line/package.json +1 -1
  334. package/extensions/llm-task/package.json +1 -1
  335. package/extensions/lobster/package.json +1 -1
  336. package/extensions/matrix/CHANGELOG.md +5 -0
  337. package/extensions/matrix/package.json +1 -1
  338. package/extensions/mattermost/package.json +1 -1
  339. package/extensions/memory-core/package.json +1 -1
  340. package/extensions/memory-lancedb/package.json +1 -1
  341. package/extensions/minimax-portal-auth/package.json +1 -1
  342. package/extensions/msteams/CHANGELOG.md +5 -0
  343. package/extensions/msteams/package.json +1 -1
  344. package/extensions/nextcloud-talk/package.json +1 -1
  345. package/extensions/nostr/CHANGELOG.md +5 -0
  346. package/extensions/nostr/package.json +1 -1
  347. package/extensions/open-prose/package.json +1 -1
  348. package/extensions/openai-codex-auth/package.json +1 -1
  349. package/extensions/signal/package.json +1 -1
  350. package/extensions/slack/package.json +1 -1
  351. package/extensions/telegram/package.json +1 -1
  352. package/extensions/tlon/package.json +1 -1
  353. package/extensions/twitch/CHANGELOG.md +5 -0
  354. package/extensions/twitch/package.json +1 -1
  355. package/extensions/voice-call/CHANGELOG.md +5 -0
  356. package/extensions/voice-call/package.json +1 -1
  357. package/extensions/whatsapp/package.json +1 -1
  358. package/extensions/zalo/CHANGELOG.md +5 -0
  359. package/extensions/zalo/package.json +1 -1
  360. package/extensions/zalouser/CHANGELOG.md +5 -0
  361. package/extensions/zalouser/package.json +1 -1
  362. package/package.json +1 -1
  363. package/skills/apple-reminders/SKILL.md +100 -49
  364. package/skills/coding-agent/SKILL.md +34 -28
  365. package/skills/github/SKILL.md +131 -16
  366. package/skills/imsg/SKILL.md +112 -15
  367. package/skills/openhue/SKILL.md +101 -19
  368. package/skills/tmux/SKILL.md +111 -79
  369. package/skills/weather/SKILL.md +88 -25
@@ -688,7 +688,7 @@ function summarizeKnownExec(words) {
688
688
  }
689
689
  if (bin === "poolbot") {
690
690
  const sub = firstPositional(words, 1);
691
- return sub ? `run openclaw ${sub}` : "run openclaw";
691
+ return sub ? `run poolbot ${sub}` : "run poolbot";
692
692
  }
693
693
  const arg = firstPositional(words, 1);
694
694
  if (!arg || arg.length > 48) {
@@ -1,38 +1,54 @@
1
1
  import { createSubsystemLogger } from "../logging/subsystem.js";
2
2
  import { getImageMetadata, resizeToJpeg } from "../media/image-ops.js";
3
- // Anthropic Messages API limitations (observed in Poolbot sessions):
3
+ import { DEFAULT_IMAGE_MAX_BYTES, DEFAULT_IMAGE_MAX_DIMENSION_PX, } from "./image-sanitization.js";
4
+ // Anthropic Messages API limitations (observed in Pool Bot sessions):
4
5
  // - Images over ~2000px per side can fail in multi-image requests.
5
6
  // - Images over 5MB are rejected by the API.
6
7
  //
7
8
  // To keep sessions resilient (and avoid "silent" WhatsApp non-replies), we auto-downscale
8
9
  // and recompress base64 image blocks when they exceed these limits.
9
- const MAX_IMAGE_DIMENSION_PX = 2000;
10
- const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
10
+ const MAX_IMAGE_DIMENSION_PX = DEFAULT_IMAGE_MAX_DIMENSION_PX;
11
+ const MAX_IMAGE_BYTES = DEFAULT_IMAGE_MAX_BYTES;
11
12
  const log = createSubsystemLogger("agents/tool-images");
12
13
  function isImageBlock(block) {
13
- if (!block || typeof block !== "object")
14
+ if (!block || typeof block !== "object") {
14
15
  return false;
16
+ }
15
17
  const rec = block;
16
18
  return rec.type === "image" && typeof rec.data === "string" && typeof rec.mimeType === "string";
17
19
  }
18
20
  function isTextBlock(block) {
19
- if (!block || typeof block !== "object")
21
+ if (!block || typeof block !== "object") {
20
22
  return false;
23
+ }
21
24
  const rec = block;
22
25
  return rec.type === "text" && typeof rec.text === "string";
23
26
  }
24
27
  function inferMimeTypeFromBase64(base64) {
25
28
  const trimmed = base64.trim();
26
- if (!trimmed)
29
+ if (!trimmed) {
27
30
  return undefined;
28
- if (trimmed.startsWith("/9j/"))
31
+ }
32
+ if (trimmed.startsWith("/9j/")) {
29
33
  return "image/jpeg";
30
- if (trimmed.startsWith("iVBOR"))
34
+ }
35
+ if (trimmed.startsWith("iVBOR")) {
31
36
  return "image/png";
32
- if (trimmed.startsWith("R0lGOD"))
37
+ }
38
+ if (trimmed.startsWith("R0lGOD")) {
33
39
  return "image/gif";
40
+ }
34
41
  return undefined;
35
42
  }
43
+ function formatBytesShort(bytes) {
44
+ if (!Number.isFinite(bytes) || bytes < 1024) {
45
+ return `${Math.max(0, Math.round(bytes))}B`;
46
+ }
47
+ if (bytes < 1024 * 1024) {
48
+ return `${(bytes / 1024).toFixed(1)}KB`;
49
+ }
50
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
51
+ }
36
52
  async function resizeImageBase64IfNeeded(params) {
37
53
  const buf = Buffer.from(params.base64, "base64");
38
54
  const meta = await getImageMetadata(buf);
@@ -40,6 +56,7 @@ async function resizeImageBase64IfNeeded(params) {
40
56
  const height = meta?.height;
41
57
  const overBytes = buf.byteLength > params.maxBytes;
42
58
  const hasDimensions = typeof width === "number" && typeof height === "number";
59
+ const overDimensions = hasDimensions && (width > params.maxDimensionPx || height > params.maxDimensionPx);
43
60
  if (hasDimensions &&
44
61
  !overBytes &&
45
62
  width <= params.maxDimensionPx &&
@@ -52,23 +69,13 @@ async function resizeImageBase64IfNeeded(params) {
52
69
  height,
53
70
  };
54
71
  }
55
- if (hasDimensions &&
56
- (width > params.maxDimensionPx || height > params.maxDimensionPx || overBytes)) {
57
- log.warn("Image exceeds limits; resizing", {
58
- label: params.label,
59
- width,
60
- height,
61
- maxDimensionPx: params.maxDimensionPx,
62
- maxBytes: params.maxBytes,
63
- });
64
- }
65
72
  const qualities = [85, 75, 65, 55, 45, 35];
66
73
  const maxDim = hasDimensions ? Math.max(width ?? 0, height ?? 0) : params.maxDimensionPx;
67
74
  const sideStart = maxDim > 0 ? Math.min(params.maxDimensionPx, maxDim) : params.maxDimensionPx;
68
75
  const sideGrid = [sideStart, 1800, 1600, 1400, 1200, 1000, 800]
69
- .map((v) => Math.min(params.maxDimensionPx, v))
76
+ .filter((v) => v > 0 && v <= params.maxDimensionPx)
70
77
  .filter((v, i, arr) => v > 0 && arr.indexOf(v) === i)
71
- .sort((a, b) => b - a);
78
+ .toSorted((a, b) => b - a);
72
79
  let smallest = null;
73
80
  for (const side of sideGrid) {
74
81
  for (const quality of qualities) {
@@ -82,16 +89,27 @@ async function resizeImageBase64IfNeeded(params) {
82
89
  smallest = { buffer: out, size: out.byteLength };
83
90
  }
84
91
  if (out.byteLength <= params.maxBytes) {
85
- log.info("Image resized", {
92
+ const sourcePixels = typeof width === "number" && typeof height === "number"
93
+ ? `${width}x${height}px`
94
+ : "unknown";
95
+ const byteReductionPct = buf.byteLength > 0
96
+ ? Number((((buf.byteLength - out.byteLength) / buf.byteLength) * 100).toFixed(1))
97
+ : 0;
98
+ log.info(`Image resized to fit limits: ${sourcePixels} ${formatBytesShort(buf.byteLength)} -> ${formatBytesShort(out.byteLength)} (-${byteReductionPct}%)`, {
86
99
  label: params.label,
87
- width,
88
- height,
89
- maxDimensionPx: params.maxDimensionPx,
100
+ sourceMimeType: params.mimeType,
101
+ sourceWidth: width,
102
+ sourceHeight: height,
103
+ sourceBytes: buf.byteLength,
90
104
  maxBytes: params.maxBytes,
91
- originalBytes: buf.byteLength,
92
- resizedBytes: out.byteLength,
93
- quality,
94
- side,
105
+ maxDimensionPx: params.maxDimensionPx,
106
+ triggerOverBytes: overBytes,
107
+ triggerOverDimensions: overDimensions,
108
+ outputMimeType: "image/jpeg",
109
+ outputBytes: out.byteLength,
110
+ outputQuality: quality,
111
+ outputMaxSide: side,
112
+ byteReductionPct,
95
113
  });
96
114
  return {
97
115
  base64: out.toString("base64"),
@@ -106,6 +124,19 @@ async function resizeImageBase64IfNeeded(params) {
106
124
  const best = smallest?.buffer ?? buf;
107
125
  const maxMb = (params.maxBytes / (1024 * 1024)).toFixed(0);
108
126
  const gotMb = (best.byteLength / (1024 * 1024)).toFixed(2);
127
+ const sourcePixels = typeof width === "number" && typeof height === "number" ? `${width}x${height}px` : "unknown";
128
+ log.warn(`Image resize failed to fit limits: ${sourcePixels} best=${formatBytesShort(best.byteLength)} limit=${formatBytesShort(params.maxBytes)}`, {
129
+ label: params.label,
130
+ sourceMimeType: params.mimeType,
131
+ sourceWidth: width,
132
+ sourceHeight: height,
133
+ sourceBytes: buf.byteLength,
134
+ maxDimensionPx: params.maxDimensionPx,
135
+ maxBytes: params.maxBytes,
136
+ smallestCandidateBytes: best.byteLength,
137
+ triggerOverBytes: overBytes,
138
+ triggerOverDimensions: overDimensions,
139
+ });
109
140
  throw new Error(`Image could not be reduced below ${maxMb}MB (got ${gotMb}MB)`);
110
141
  }
111
142
  export async function sanitizeContentBlocksImages(blocks, label, opts = {}) {
@@ -151,16 +182,18 @@ export async function sanitizeContentBlocksImages(blocks, label, opts = {}) {
151
182
  return out;
152
183
  }
153
184
  export async function sanitizeImageBlocks(images, label, opts = {}) {
154
- if (images.length === 0)
185
+ if (images.length === 0) {
155
186
  return { images, dropped: 0 };
187
+ }
156
188
  const sanitized = await sanitizeContentBlocksImages(images, label, opts);
157
189
  const next = sanitized.filter(isImageBlock);
158
190
  return { images: next, dropped: Math.max(0, images.length - next.length) };
159
191
  }
160
192
  export async function sanitizeToolResultImages(result, label, opts = {}) {
161
193
  const content = Array.isArray(result.content) ? result.content : [];
162
- if (!content.some((b) => isImageBlock(b) || isTextBlock(b)))
194
+ if (!content.some((b) => isImageBlock(b) || isTextBlock(b))) {
163
195
  return result;
196
+ }
164
197
  const next = await sanitizeContentBlocksImages(content, label, opts);
165
198
  return { ...result, content: next };
166
199
  }
@@ -4,9 +4,10 @@ import { Type } from "@sinclair/typebox";
4
4
  import { writeBase64ToFile } from "../../cli/nodes-camera.js";
5
5
  import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../../cli/nodes-canvas.js";
6
6
  import { imageMimeFromFormat } from "../../media/mime.js";
7
+ import { resolveImageSanitizationLimits } from "../image-sanitization.js";
7
8
  import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
8
9
  import { imageResult, jsonResult, readStringParam } from "./common.js";
9
- import { callGatewayTool } from "./gateway.js";
10
+ import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
10
11
  import { resolveNodeId } from "./nodes-utils.js";
11
12
  const CANVAS_ACTIONS = [
12
13
  "present",
@@ -44,7 +45,8 @@ const CanvasToolSchema = Type.Object({
44
45
  jsonl: Type.Optional(Type.String()),
45
46
  jsonlPath: Type.Optional(Type.String()),
46
47
  });
47
- export function createCanvasTool() {
48
+ export function createCanvasTool(options) {
49
+ const imageSanitization = resolveImageSanitizationLimits(options?.config);
48
50
  return {
49
51
  label: "Canvas",
50
52
  name: "canvas",
@@ -53,11 +55,7 @@ export function createCanvasTool() {
53
55
  execute: async (_toolCallId, args) => {
54
56
  const params = args;
55
57
  const action = readStringParam(params, "action", { required: true });
56
- const gatewayOpts = {
57
- gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
58
- gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
59
- timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
60
- };
58
+ const gatewayOpts = readGatewayCallOptions(params);
61
59
  const nodeId = await resolveNodeId(gatewayOpts, readStringParam(params, "node", { trim: true }), true);
62
60
  const invoke = async (command, invokeParams) => await callGatewayTool("node.invoke", gatewayOpts, {
63
61
  nodeId,
@@ -74,8 +72,12 @@ export function createCanvasTool() {
74
72
  height: typeof params.height === "number" ? params.height : undefined,
75
73
  };
76
74
  const invokeParams = {};
77
- if (typeof params.target === "string" && params.target.trim()) {
78
- invokeParams.url = params.target.trim();
75
+ // Accept both `target` and `url` for present to match common caller expectations.
76
+ // `target` remains the canonical field for CLI compatibility.
77
+ const presentTarget = readStringParam(params, "target", { trim: true }) ??
78
+ readStringParam(params, "url", { trim: true });
79
+ if (presentTarget) {
80
+ invokeParams.url = presentTarget;
79
81
  }
80
82
  if (Number.isFinite(placement.x) ||
81
83
  Number.isFinite(placement.y) ||
@@ -90,7 +92,9 @@ export function createCanvasTool() {
90
92
  await invoke("canvas.hide", undefined);
91
93
  return jsonResult({ ok: true });
92
94
  case "navigate": {
93
- const url = readStringParam(params, "url", { required: true });
95
+ // Support `target` as an alias so callers can reuse the same field across present/navigate.
96
+ const url = readStringParam(params, "url", { trim: true }) ??
97
+ readStringParam(params, "target", { required: true, trim: true, label: "url" });
94
98
  await invoke("canvas.navigate", { url });
95
99
  return jsonResult({ ok: true });
96
100
  }
@@ -134,6 +138,7 @@ export function createCanvasTool() {
134
138
  base64: payload.base64,
135
139
  mimeType,
136
140
  details: { format: payload.format },
141
+ imageSanitization,
137
142
  });
138
143
  }
139
144
  case "a2ui_push": {
@@ -142,8 +147,9 @@ export function createCanvasTool() {
142
147
  : typeof params.jsonlPath === "string" && params.jsonlPath.trim()
143
148
  ? await fs.readFile(params.jsonlPath.trim(), "utf8")
144
149
  : "";
145
- if (!jsonl.trim())
150
+ if (!jsonl.trim()) {
146
151
  throw new Error("jsonl or jsonlPath required");
152
+ }
147
153
  await invoke("canvas.a2ui.pushJSONL", { jsonl });
148
154
  return jsonResult({ ok: true });
149
155
  }
@@ -1,11 +1,19 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { detectMime } from "../../media/mime.js";
3
3
  import { sanitizeToolResultImages } from "../tool-images.js";
4
+ export class ToolInputError extends Error {
5
+ status = 400;
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "ToolInputError";
9
+ }
10
+ }
4
11
  export function createActionGate(actions) {
5
12
  return (key, defaultValue = true) => {
6
13
  const value = actions?.[key];
7
- if (value === undefined)
14
+ if (value === undefined) {
8
15
  return defaultValue;
16
+ }
9
17
  return value !== false;
10
18
  };
11
19
  }
@@ -13,14 +21,16 @@ export function readStringParam(params, key, options = {}) {
13
21
  const { required = false, trim = true, label = key, allowEmpty = false } = options;
14
22
  const raw = params[key];
15
23
  if (typeof raw !== "string") {
16
- if (required)
17
- throw new Error(`${label} required`);
24
+ if (required) {
25
+ throw new ToolInputError(`${label} required`);
26
+ }
18
27
  return undefined;
19
28
  }
20
29
  const value = trim ? raw.trim() : raw;
21
30
  if (!value && !allowEmpty) {
22
- if (required)
23
- throw new Error(`${label} required`);
31
+ if (required) {
32
+ throw new ToolInputError(`${label} required`);
33
+ }
24
34
  return undefined;
25
35
  }
26
36
  return value;
@@ -33,11 +43,13 @@ export function readStringOrNumberParam(params, key, options = {}) {
33
43
  }
34
44
  if (typeof raw === "string") {
35
45
  const value = raw.trim();
36
- if (value)
46
+ if (value) {
37
47
  return value;
48
+ }
49
+ }
50
+ if (required) {
51
+ throw new ToolInputError(`${label} required`);
38
52
  }
39
- if (required)
40
- throw new Error(`${label} required`);
41
53
  return undefined;
42
54
  }
43
55
  export function readNumberParam(params, key, options = {}) {
@@ -51,13 +63,15 @@ export function readNumberParam(params, key, options = {}) {
51
63
  const trimmed = raw.trim();
52
64
  if (trimmed) {
53
65
  const parsed = Number.parseFloat(trimmed);
54
- if (Number.isFinite(parsed))
66
+ if (Number.isFinite(parsed)) {
55
67
  value = parsed;
68
+ }
56
69
  }
57
70
  }
58
71
  if (value === undefined) {
59
- if (required)
60
- throw new Error(`${label} required`);
72
+ if (required) {
73
+ throw new ToolInputError(`${label} required`);
74
+ }
61
75
  return undefined;
62
76
  }
63
77
  return integer ? Math.trunc(value) : value;
@@ -71,8 +85,9 @@ export function readStringArrayParam(params, key, options = {}) {
71
85
  .map((entry) => entry.trim())
72
86
  .filter(Boolean);
73
87
  if (values.length === 0) {
74
- if (required)
75
- throw new Error(`${label} required`);
88
+ if (required) {
89
+ throw new ToolInputError(`${label} required`);
90
+ }
76
91
  return undefined;
77
92
  }
78
93
  return values;
@@ -80,14 +95,16 @@ export function readStringArrayParam(params, key, options = {}) {
80
95
  if (typeof raw === "string") {
81
96
  const value = raw.trim();
82
97
  if (!value) {
83
- if (required)
84
- throw new Error(`${label} required`);
98
+ if (required) {
99
+ throw new ToolInputError(`${label} required`);
100
+ }
85
101
  return undefined;
86
102
  }
87
103
  return [value];
88
104
  }
89
- if (required)
90
- throw new Error(`${label} required`);
105
+ if (required) {
106
+ throw new ToolInputError(`${label} required`);
107
+ }
91
108
  return undefined;
92
109
  }
93
110
  export function readReactionParams(params, options) {
@@ -99,7 +116,7 @@ export function readReactionParams(params, options) {
99
116
  allowEmpty: true,
100
117
  });
101
118
  if (remove && !emoji) {
102
- throw new Error(options.removeErrorMessage);
119
+ throw new ToolInputError(options.removeErrorMessage);
103
120
  }
104
121
  return { emoji, remove, isEmpty: !emoji };
105
122
  }
@@ -130,7 +147,7 @@ export async function imageResult(params) {
130
147
  content,
131
148
  details: { path: params.path, ...params.details },
132
149
  };
133
- return await sanitizeToolResultImages(result, params.label);
150
+ return await sanitizeToolResultImages(result, params.label, params.imageSanitization);
134
151
  }
135
152
  export async function imageResultFromFile(params) {
136
153
  const buf = await fs.readFile(params.path);
@@ -142,5 +159,6 @@ export async function imageResultFromFile(params) {
142
159
  mimeType,
143
160
  extraText: params.extraText,
144
161
  details: params.details,
162
+ imageSanitization: params.imageSanitization,
145
163
  });
146
164
  }
@@ -1,7 +1,9 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { loadConfig } from "../../config/config.js";
3
3
  import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
4
+ import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
4
5
  import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
6
+ import { extractTextFromChatContent } from "../../shared/chat-content.js";
5
7
  import { isRecord, truncateUtf16Safe } from "../../utils.js";
6
8
  import { resolveSessionAgentId } from "../agent-scope.js";
7
9
  import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
@@ -49,37 +51,13 @@ function truncateText(input, maxLen) {
49
51
  const truncated = truncateUtf16Safe(input, Math.max(0, maxLen - 3)).trimEnd();
50
52
  return `${truncated}...`;
51
53
  }
52
- function normalizeContextText(raw) {
53
- return raw.replace(/\s+/g, " ").trim();
54
- }
55
54
  function extractMessageText(message) {
56
55
  const role = typeof message.role === "string" ? message.role : "";
57
56
  if (role !== "user" && role !== "assistant") {
58
57
  return null;
59
58
  }
60
- const content = message.content;
61
- if (typeof content === "string") {
62
- const normalized = normalizeContextText(content);
63
- return normalized ? { role, text: normalized } : null;
64
- }
65
- if (!Array.isArray(content)) {
66
- return null;
67
- }
68
- const chunks = [];
69
- for (const block of content) {
70
- if (!block || typeof block !== "object") {
71
- continue;
72
- }
73
- if (block.type !== "text") {
74
- continue;
75
- }
76
- const text = block.text;
77
- if (typeof text === "string" && text.trim()) {
78
- chunks.push(text);
79
- }
80
- }
81
- const joined = normalizeContextText(chunks.join(" "));
82
- return joined ? { role, text: joined } : null;
59
+ const text = extractTextFromChatContent(message.content);
60
+ return text ? { role, text } : null;
83
61
  }
84
62
  async function buildReminderContextLines(params) {
85
63
  const maxMessages = Math.min(REMINDER_CONTEXT_MESSAGES_MAX, Math.max(0, Math.floor(params.contextMessages)));
@@ -201,7 +179,7 @@ JOB SCHEMA (for add action):
201
179
  "name": "string (optional)",
202
180
  "schedule": { ... }, // Required: when to run
203
181
  "payload": { ... }, // Required: what to execute
204
- "delivery": { ... }, // Optional: announce summary (isolated only)
182
+ "delivery": { ... }, // Optional: announce summary or webhook POST
205
183
  "sessionTarget": "main" | "isolated", // Required
206
184
  "enabled": true | false // Optional, default true
207
185
  }
@@ -220,16 +198,19 @@ PAYLOAD TYPES (payload.kind):
220
198
  - "systemEvent": Injects text as system event into session
221
199
  { "kind": "systemEvent", "text": "<message>" }
222
200
  - "agentTurn": Runs agent with message (isolated sessions only)
223
- { "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional> }
201
+ { "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional, 0 means no timeout> }
224
202
 
225
- DELIVERY (isolated-only, top-level):
226
- { "mode": "none|announce", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
203
+ DELIVERY (top-level):
204
+ { "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
227
205
  - Default for isolated agentTurn jobs (when delivery omitted): "announce"
228
- - If the task needs to send to a specific chat/recipient, set delivery.channel/to here; do not call messaging tools inside the run.
206
+ - announce: send to chat channel (optional channel/to target)
207
+ - webhook: send finished-run event as HTTP POST to delivery.to (URL required)
208
+ - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.
229
209
 
230
210
  CRITICAL CONSTRAINTS:
231
211
  - sessionTarget="main" REQUIRES payload.kind="systemEvent"
232
212
  - sessionTarget="isolated" REQUIRES payload.kind="agentTurn"
213
+ - For webhook callbacks, use delivery.mode="webhook" with delivery.to set to a URL.
233
214
  Default: prefer isolated agentTurn jobs unless the user explicitly wants a main-session system event.
234
215
 
235
216
  WAKE MODES (for wake action):
@@ -258,7 +239,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
258
239
  // job properties to the top level alongside `action` instead of nesting
259
240
  // them inside `job`. When `params.job` is missing or empty, reconstruct
260
241
  // a synthetic job object from any recognised top-level job fields.
261
- // See: https://github.com/openclaw/openclaw/issues/11310
242
+ // See: https://github.com/poolbot/poolbot/issues/11310
262
243
  if (!params.job ||
263
244
  (typeof params.job === "object" &&
264
245
  params.job !== null &&
@@ -274,6 +255,7 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
274
255
  "description",
275
256
  "deleteAfterRun",
276
257
  "agentId",
258
+ "sessionKey",
277
259
  "message",
278
260
  "text",
279
261
  "model",
@@ -304,13 +286,22 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
304
286
  throw new Error("job required");
305
287
  }
306
288
  const job = normalizeCronJobCreate(params.job) ?? params.job;
307
- if (job && typeof job === "object" && !("agentId" in job)) {
289
+ if (job && typeof job === "object") {
308
290
  const cfg = loadConfig();
309
- const agentId = opts?.agentSessionKey
310
- ? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
291
+ const { mainKey, alias } = resolveMainSessionAlias(cfg);
292
+ const resolvedSessionKey = opts?.agentSessionKey
293
+ ? resolveInternalSessionKey({ key: opts.agentSessionKey, alias, mainKey })
311
294
  : undefined;
312
- if (agentId) {
313
- job.agentId = agentId;
295
+ if (!("agentId" in job)) {
296
+ const agentId = opts?.agentSessionKey
297
+ ? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
298
+ : undefined;
299
+ if (agentId) {
300
+ job.agentId = agentId;
301
+ }
302
+ }
303
+ if (!("sessionKey" in job) && resolvedSessionKey) {
304
+ job.sessionKey = resolvedSessionKey;
314
305
  }
315
306
  }
316
307
  if (opts?.agentSessionKey &&
@@ -322,9 +313,20 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
322
313
  const delivery = isRecord(deliveryValue) ? deliveryValue : undefined;
323
314
  const modeRaw = typeof delivery?.mode === "string" ? delivery.mode : "";
324
315
  const mode = modeRaw.trim().toLowerCase();
316
+ if (mode === "webhook") {
317
+ const webhookUrl = normalizeHttpWebhookUrl(delivery?.to);
318
+ if (!webhookUrl) {
319
+ throw new Error('delivery.mode="webhook" requires delivery.to to be a valid http(s) URL');
320
+ }
321
+ if (delivery) {
322
+ delivery.to = webhookUrl;
323
+ }
324
+ }
325
325
  const hasTarget = (typeof delivery?.channel === "string" && delivery.channel.trim()) ||
326
326
  (typeof delivery?.to === "string" && delivery.to.trim());
327
- const shouldInfer = (deliveryValue == null || delivery) && mode !== "none" && !hasTarget;
327
+ const shouldInfer = (deliveryValue == null || delivery) &&
328
+ (mode === "" || mode === "announce") &&
329
+ !hasTarget;
328
330
  if (shouldInfer) {
329
331
  const inferred = inferDeliveryFromSessionKey(opts.agentSessionKey);
330
332
  if (inferred) {
@@ -1,17 +1,85 @@
1
+ import { loadConfig, resolveGatewayPort } from "../../config/config.js";
1
2
  import { callGateway } from "../../gateway/call.js";
2
3
  import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
4
+ import { readStringParam } from "./common.js";
3
5
  export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
6
+ export function readGatewayCallOptions(params) {
7
+ return {
8
+ gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
9
+ gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
10
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
11
+ };
12
+ }
13
+ function canonicalizeToolGatewayWsUrl(raw) {
14
+ const input = raw.trim();
15
+ let url;
16
+ try {
17
+ url = new URL(input);
18
+ }
19
+ catch (error) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
22
+ }
23
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
24
+ throw new Error(`invalid gatewayUrl protocol: ${url.protocol} (expected ws:// or wss://)`);
25
+ }
26
+ if (url.username || url.password) {
27
+ throw new Error("invalid gatewayUrl: credentials are not allowed");
28
+ }
29
+ if (url.search || url.hash) {
30
+ throw new Error("invalid gatewayUrl: query/hash not allowed");
31
+ }
32
+ // Agents/tools expect the gateway websocket on the origin, not arbitrary paths.
33
+ if (url.pathname && url.pathname !== "/") {
34
+ throw new Error("invalid gatewayUrl: path not allowed");
35
+ }
36
+ const origin = url.origin;
37
+ // Key: protocol + host only, lowercased. (host includes IPv6 brackets + port when present)
38
+ const key = `${url.protocol}//${url.host.toLowerCase()}`;
39
+ return { origin, key };
40
+ }
41
+ function validateGatewayUrlOverrideForAgentTools(urlOverride) {
42
+ const cfg = loadConfig();
43
+ const port = resolveGatewayPort(cfg);
44
+ const allowed = new Set([
45
+ `ws://127.0.0.1:${port}`,
46
+ `wss://127.0.0.1:${port}`,
47
+ `ws://localhost:${port}`,
48
+ `wss://localhost:${port}`,
49
+ `ws://[::1]:${port}`,
50
+ `wss://[::1]:${port}`,
51
+ ]);
52
+ const remoteUrl = typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
53
+ if (remoteUrl) {
54
+ try {
55
+ const remote = canonicalizeToolGatewayWsUrl(remoteUrl);
56
+ allowed.add(remote.key);
57
+ }
58
+ catch {
59
+ // ignore: misconfigured remote url; tools should fall back to default resolution.
60
+ }
61
+ }
62
+ const parsed = canonicalizeToolGatewayWsUrl(urlOverride);
63
+ if (!allowed.has(parsed.key)) {
64
+ throw new Error([
65
+ "gatewayUrl override rejected.",
66
+ `Allowed: ws(s) loopback on port ${port} (127.0.0.1/localhost/[::1])`,
67
+ "Or: configure gateway.remote.url and omit gatewayUrl to use the configured remote gateway.",
68
+ ].join(" "));
69
+ }
70
+ return parsed.origin;
71
+ }
4
72
  export function resolveGatewayOptions(opts) {
5
73
  // Prefer an explicit override; otherwise let callGateway choose based on config.
6
74
  const url = typeof opts?.gatewayUrl === "string" && opts.gatewayUrl.trim()
7
- ? opts.gatewayUrl.trim()
75
+ ? validateGatewayUrlOverrideForAgentTools(opts.gatewayUrl)
8
76
  : undefined;
9
77
  const token = typeof opts?.gatewayToken === "string" && opts.gatewayToken.trim()
10
78
  ? opts.gatewayToken.trim()
11
79
  : undefined;
12
80
  const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
13
81
  ? Math.max(1, Math.floor(opts.timeoutMs))
14
- : 10_000;
82
+ : 30_000;
15
83
  return { url, token, timeoutMs };
16
84
  }
17
85
  export async function callGatewayTool(method, opts, params, extra) {