@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
@@ -1,571 +1,98 @@
1
1
  import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { Type } from "@sinclair/typebox";
4
- import { addAllowlistEntry, evaluateShellAllowlist, maxAsk, minSecurity, requiresExecApproval, resolveSafeBins, recordAllowlistUse, resolveExecApprovals, resolveExecApprovalsFromFile, } from "../infra/exec-approvals.js";
5
- import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
4
+ import { addAllowlistEntry, evaluateShellAllowlist, maxAsk, minSecurity, requiresExecApproval, resolveSafeBins, recordAllowlistUse, resolveExecApprovals, resolveExecApprovalsFromFile, buildSafeShellCommand, buildSafeBinsShellCommand, } from "../infra/exec-approvals.js";
6
5
  import { buildNodeShellCommand } from "../infra/node-shell.js";
7
6
  import { getShellPathFromLoginShell, resolveShellEnvFallbackTimeoutMs, } from "../infra/shell-env.js";
8
- import { enqueueSystemEvent } from "../infra/system-events.js";
9
- import { logInfo, logWarn } from "../logger.js";
10
- import { formatSpawnError, spawnWithFallback } from "../process/spawn-utils.js";
11
- import { addSession, appendOutput, createSessionSlug, markBackgrounded, markExited, tail, } from "./bash-process-registry.js";
12
- import { buildDockerExecArgs, buildSandboxEnv, chunkString, clampNumber, coerceEnv, killSession, readEnvInt, resolveSandboxWorkdir, resolveWorkdir, truncateMiddle, } from "./bash-tools.shared.js";
7
+ import { logInfo } from "../logger.js";
8
+ import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
9
+ import { markBackgrounded, tail } from "./bash-process-registry.js";
10
+ import { DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS, DEFAULT_APPROVAL_TIMEOUT_MS, DEFAULT_MAX_OUTPUT, DEFAULT_NOTIFY_TAIL_CHARS, DEFAULT_PATH, DEFAULT_PENDING_MAX_OUTPUT, applyPathPrepend, applyShellPath, createApprovalSlug, emitExecSystemEvent, normalizeExecAsk, normalizeExecHost, normalizeExecSecurity, normalizeNotifyOutput, normalizePathPrepend, renderExecHostLabel, resolveApprovalRunningNoticeMs, runExecProcess, execSchema, validateHostEnv, } from "./bash-tools.exec-runtime.js";
11
+ import { buildSandboxEnv, clampWithDefault, coerceEnv, readEnvInt, resolveSandboxWorkdir, resolveWorkdir, truncateMiddle, } from "./bash-tools.shared.js";
13
12
  import { callGatewayTool } from "./tools/gateway.js";
14
13
  import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js";
15
- import { getShellConfig, sanitizeBinaryOutput } from "./shell-utils.js";
16
- import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
17
- import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
18
- // Security: Blocklist of environment variables that could alter execution flow
19
- // or inject code when running on non-sandboxed hosts (Gateway/Node).
20
- const DANGEROUS_HOST_ENV_VARS = new Set([
21
- "LD_PRELOAD",
22
- "LD_LIBRARY_PATH",
23
- "LD_AUDIT",
24
- "DYLD_INSERT_LIBRARIES",
25
- "DYLD_LIBRARY_PATH",
26
- "NODE_OPTIONS",
27
- "NODE_PATH",
28
- "PYTHONPATH",
29
- "PYTHONHOME",
30
- "RUBYLIB",
31
- "PERL5LIB",
32
- "BASH_ENV",
33
- "ENV",
34
- "GCONV_PATH",
35
- "IFS",
36
- "SSLKEYLOGFILE",
37
- ]);
38
- const DANGEROUS_HOST_ENV_PREFIXES = ["DYLD_", "LD_"];
39
- // Centralized sanitization helper.
40
- // Throws an error if dangerous variables or PATH modifications are detected on the host.
41
- function validateHostEnv(env) {
42
- for (const key of Object.keys(env)) {
43
- const upperKey = key.toUpperCase();
44
- // 1. Block known dangerous variables (Fail Closed)
45
- if (DANGEROUS_HOST_ENV_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
46
- throw new Error(`Security Violation: Environment variable '${key}' is forbidden during host execution.`);
47
- }
48
- if (DANGEROUS_HOST_ENV_VARS.has(upperKey)) {
49
- throw new Error(`Security Violation: Environment variable '${key}' is forbidden during host execution.`);
50
- }
51
- // 2. Strictly block PATH modification on host
52
- // Allowing custom PATH on the gateway/node can lead to binary hijacking.
53
- if (upperKey === "PATH") {
54
- throw new Error("Security Violation: Custom 'PATH' variable is forbidden during host execution.");
55
- }
56
- }
57
- }
58
- const DEFAULT_MAX_OUTPUT = clampNumber(readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
59
- const DEFAULT_PENDING_MAX_OUTPUT = clampNumber(readEnvInt("POOLBOT_BASH_PENDING_MAX_OUTPUT_CHARS") ??
60
- readEnvInt("CLAWDBOT_BASH_PENDING_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
61
- // Default exec timeout: 1800s (30 min) to accommodate long installs/builds.
62
- // Users can override via config (`tools.exec.timeoutSec`) or env var.
63
- const DEFAULT_EXEC_TIMEOUT_SEC = clampNumber(readEnvInt("POOLBOT_EXEC_TIMEOUT_SEC") ?? readEnvInt("CLAWDBOT_EXEC_TIMEOUT_SEC"), 1800, 1, 86_400);
64
- const DEFAULT_PATH = process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
65
- const DEFAULT_NOTIFY_TAIL_CHARS = 400;
66
- const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000;
67
- const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = 130_000;
68
- const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000;
69
- const APPROVAL_SLUG_LENGTH = 8;
70
- const execSchema = Type.Object({
71
- command: Type.String({ description: "Shell command to execute" }),
72
- workdir: Type.Optional(Type.String({ description: "Working directory (defaults to cwd)" })),
73
- env: Type.Optional(Type.Record(Type.String(), Type.String())),
74
- yieldMs: Type.Optional(Type.Number({
75
- description: "Milliseconds to wait before backgrounding (default 10000)",
76
- })),
77
- background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })),
78
- timeout: Type.Optional(Type.Number({
79
- description: "Timeout in seconds (default 1800, kills process on expiry).",
80
- })),
81
- pty: Type.Optional(Type.Boolean({
82
- description: "Run in a pseudo-terminal (PTY) when available (TTY-required CLIs, coding agents)",
83
- })),
84
- elevated: Type.Optional(Type.Boolean({
85
- description: "Run on the host with elevated permissions (if allowed)",
86
- })),
87
- host: Type.Optional(Type.String({
88
- description: "Exec host (sandbox|gateway|node).",
89
- })),
90
- security: Type.Optional(Type.String({
91
- description: "Exec security mode (deny|allowlist|full).",
92
- })),
93
- ask: Type.Optional(Type.String({
94
- description: "Exec ask mode (off|on-miss|always).",
95
- })),
96
- node: Type.Optional(Type.String({
97
- description: "Node id/name for host=node.",
98
- })),
99
- });
100
- function normalizeExecHost(value) {
101
- const normalized = value?.trim().toLowerCase();
102
- if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") {
103
- return normalized;
14
+ function extractScriptTargetFromCommand(command) {
15
+ const raw = command.trim();
16
+ if (!raw) {
17
+ return null;
104
18
  }
105
- return null;
106
- }
107
- function normalizeExecSecurity(value) {
108
- const normalized = value?.trim().toLowerCase();
109
- if (normalized === "deny" || normalized === "allowlist" || normalized === "full") {
110
- return normalized;
19
+ // Intentionally simple parsing: we only support common forms like
20
+ // python file.py
21
+ // python3 -u file.py
22
+ // node --experimental-something file.js
23
+ // If the command is more complex (pipes, heredocs, quoted paths with spaces), skip preflight.
24
+ const pythonMatch = raw.match(/^\s*(python3?|python)\s+(?:-[^\s]+\s+)*([^\s]+\.py)\b/i);
25
+ if (pythonMatch?.[2]) {
26
+ return { kind: "python", relOrAbsPath: pythonMatch[2] };
111
27
  }
112
- return null;
113
- }
114
- function normalizeExecAsk(value) {
115
- const normalized = value?.trim().toLowerCase();
116
- if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
117
- return normalized;
28
+ const nodeMatch = raw.match(/^\s*(node)\s+(?:--[^\s]+\s+)*([^\s]+\.js)\b/i);
29
+ if (nodeMatch?.[2]) {
30
+ return { kind: "node", relOrAbsPath: nodeMatch[2] };
118
31
  }
119
32
  return null;
120
33
  }
121
- function renderExecHostLabel(host) {
122
- return host === "sandbox" ? "sandbox" : host === "gateway" ? "gateway" : "node";
123
- }
124
- function normalizeNotifyOutput(value) {
125
- return value.replace(/\s+/g, " ").trim();
126
- }
127
- function normalizePathPrepend(entries) {
128
- if (!Array.isArray(entries))
129
- return [];
130
- const seen = new Set();
131
- const normalized = [];
132
- for (const entry of entries) {
133
- if (typeof entry !== "string")
134
- continue;
135
- const trimmed = entry.trim();
136
- if (!trimmed || seen.has(trimmed))
137
- continue;
138
- seen.add(trimmed);
139
- normalized.push(trimmed);
34
+ async function validateScriptFileForShellBleed(params) {
35
+ const target = extractScriptTargetFromCommand(params.command);
36
+ if (!target) {
37
+ return;
140
38
  }
141
- return normalized;
142
- }
143
- function mergePathPrepend(existing, prepend) {
144
- if (prepend.length === 0)
145
- return existing;
146
- const partsExisting = (existing ?? "")
147
- .split(path.delimiter)
148
- .map((part) => part.trim())
149
- .filter(Boolean);
150
- const merged = [];
151
- const seen = new Set();
152
- for (const part of [...prepend, ...partsExisting]) {
153
- if (seen.has(part))
154
- continue;
155
- seen.add(part);
156
- merged.push(part);
39
+ const absPath = path.isAbsolute(target.relOrAbsPath)
40
+ ? path.resolve(target.relOrAbsPath)
41
+ : path.resolve(params.workdir, target.relOrAbsPath);
42
+ // Best-effort: only validate if file exists and is reasonably small.
43
+ let stat;
44
+ try {
45
+ stat = await fs.stat(absPath);
157
46
  }
158
- return merged.join(path.delimiter);
159
- }
160
- function applyPathPrepend(env, prepend, options) {
161
- if (prepend.length === 0)
162
- return;
163
- if (options?.requireExisting && !env.PATH)
164
- return;
165
- const merged = mergePathPrepend(env.PATH, prepend);
166
- if (merged)
167
- env.PATH = merged;
168
- }
169
- function applyShellPath(env, shellPath) {
170
- if (!shellPath)
171
- return;
172
- const entries = shellPath
173
- .split(path.delimiter)
174
- .map((part) => part.trim())
175
- .filter(Boolean);
176
- if (entries.length === 0)
177
- return;
178
- const merged = mergePathPrepend(env.PATH, entries);
179
- if (merged)
180
- env.PATH = merged;
181
- }
182
- function maybeNotifyOnExit(session, status) {
183
- if (!session.backgrounded || !session.notifyOnExit || session.exitNotified)
184
- return;
185
- const sessionKey = session.sessionKey?.trim();
186
- if (!sessionKey)
47
+ catch {
187
48
  return;
188
- session.exitNotified = true;
189
- const exitLabel = session.exitSignal
190
- ? `signal ${session.exitSignal}`
191
- : `code ${session.exitCode ?? 0}`;
192
- const output = normalizeNotifyOutput(tail(session.tail || session.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS));
193
- const summary = output
194
- ? `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel}) :: ${output}`
195
- : `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel})`;
196
- enqueueSystemEvent(summary, { sessionKey });
197
- requestHeartbeatNow({ reason: `exec:${session.id}:exit` });
198
- }
199
- function createApprovalSlug(id) {
200
- return id.slice(0, APPROVAL_SLUG_LENGTH);
201
- }
202
- function resolveApprovalRunningNoticeMs(value) {
203
- if (typeof value !== "number" || !Number.isFinite(value)) {
204
- return DEFAULT_APPROVAL_RUNNING_NOTICE_MS;
205
49
  }
206
- if (value <= 0)
207
- return 0;
208
- return Math.floor(value);
209
- }
210
- function emitExecSystemEvent(text, opts) {
211
- const sessionKey = opts.sessionKey?.trim();
212
- if (!sessionKey)
50
+ if (!stat.isFile()) {
213
51
  return;
214
- enqueueSystemEvent(text, { sessionKey, contextKey: opts.contextKey });
215
- requestHeartbeatNow({ reason: "exec-event" });
216
- }
217
- async function runExecProcess(opts) {
218
- const startedAt = Date.now();
219
- const sessionId = createSessionSlug();
220
- let child = null;
221
- let pty = null;
222
- let stdin;
223
- if (opts.sandbox) {
224
- const { child: spawned } = await spawnWithFallback({
225
- argv: [
226
- "docker",
227
- ...buildDockerExecArgs({
228
- containerName: opts.sandbox.containerName,
229
- command: opts.command,
230
- workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
231
- env: opts.env,
232
- tty: opts.usePty,
233
- }),
234
- ],
235
- options: {
236
- cwd: opts.workdir,
237
- env: process.env,
238
- detached: process.platform !== "win32",
239
- stdio: ["pipe", "pipe", "pipe"],
240
- windowsHide: true,
241
- },
242
- fallbacks: [
243
- {
244
- label: "no-detach",
245
- options: { detached: false },
246
- },
247
- ],
248
- onFallback: (err, fallback) => {
249
- const errText = formatSpawnError(err);
250
- const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
251
- logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
252
- opts.warnings.push(warning);
253
- },
254
- });
255
- child = spawned;
256
- stdin = child.stdin;
257
52
  }
258
- else if (opts.usePty) {
259
- const { shell, args: shellArgs } = getShellConfig();
260
- try {
261
- const ptyModule = (await import("@lydell/node-pty"));
262
- const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
263
- if (!spawnPty) {
264
- throw new Error("PTY support is unavailable (node-pty spawn not found).");
265
- }
266
- pty = spawnPty(shell, [...shellArgs, opts.command], {
267
- cwd: opts.workdir,
268
- env: opts.env,
269
- name: process.env.TERM ?? "xterm-256color",
270
- cols: 120,
271
- rows: 30,
272
- });
273
- stdin = {
274
- destroyed: false,
275
- write: (data, cb) => {
276
- try {
277
- pty?.write(data);
278
- cb?.(null);
279
- }
280
- catch (err) {
281
- cb?.(err);
282
- }
283
- },
284
- end: () => {
285
- try {
286
- const eof = process.platform === "win32" ? "\x1a" : "\x04";
287
- pty?.write(eof);
288
- }
289
- catch {
290
- // ignore EOF errors
291
- }
292
- },
293
- };
294
- }
295
- catch (err) {
296
- const errText = String(err);
297
- const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
298
- logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
299
- opts.warnings.push(warning);
300
- const { child: spawned } = await spawnWithFallback({
301
- argv: [shell, ...shellArgs, opts.command],
302
- options: {
303
- cwd: opts.workdir,
304
- env: opts.env,
305
- detached: process.platform !== "win32",
306
- stdio: ["pipe", "pipe", "pipe"],
307
- windowsHide: true,
308
- },
309
- fallbacks: [
310
- {
311
- label: "no-detach",
312
- options: { detached: false },
313
- },
314
- ],
315
- onFallback: (fallbackErr, fallback) => {
316
- const fallbackText = formatSpawnError(fallbackErr);
317
- const fallbackWarning = `Warning: spawn failed (${fallbackText}); retrying with ${fallback.label}.`;
318
- logWarn(`exec: spawn failed (${fallbackText}); retrying with ${fallback.label}.`);
319
- opts.warnings.push(fallbackWarning);
320
- },
321
- });
322
- child = spawned;
323
- stdin = child.stdin;
324
- }
53
+ if (stat.size > 512 * 1024) {
54
+ return;
325
55
  }
326
- else {
327
- const { shell, args: shellArgs } = getShellConfig();
328
- const { child: spawned } = await spawnWithFallback({
329
- argv: [shell, ...shellArgs, opts.command],
330
- options: {
331
- cwd: opts.workdir,
332
- env: opts.env,
333
- detached: process.platform !== "win32",
334
- stdio: ["pipe", "pipe", "pipe"],
335
- windowsHide: true,
336
- },
337
- fallbacks: [
338
- {
339
- label: "no-detach",
340
- options: { detached: false },
341
- },
342
- ],
343
- onFallback: (err, fallback) => {
344
- const errText = formatSpawnError(err);
345
- const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
346
- logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
347
- opts.warnings.push(warning);
348
- },
349
- });
350
- child = spawned;
351
- stdin = child.stdin;
56
+ const content = await fs.readFile(absPath, "utf-8");
57
+ // Common failure mode: shell env var syntax leaking into Python/JS.
58
+ // We deliberately match all-caps/underscore vars to avoid false positives with `$` as a JS identifier.
59
+ const envVarRegex = /\$[A-Z_][A-Z0-9_]{1,}/g;
60
+ const first = envVarRegex.exec(content);
61
+ if (first) {
62
+ const idx = first.index;
63
+ const before = content.slice(0, idx);
64
+ const line = before.split("\n").length;
65
+ const token = first[0];
66
+ throw new Error([
67
+ `exec preflight: detected likely shell variable injection (${token}) in ${target.kind} script: ${path.basename(absPath)}:${line}.`,
68
+ target.kind === "python"
69
+ ? `In Python, use os.environ.get(${JSON.stringify(token.slice(1))}) instead of raw ${token}.`
70
+ : `In Node.js, use process.env[${JSON.stringify(token.slice(1))}] instead of raw ${token}.`,
71
+ "(If this is inside a string literal on purpose, escape it or restructure the code.)",
72
+ ].join("\n"));
352
73
  }
353
- const session = {
354
- id: sessionId,
355
- command: opts.command,
356
- scopeKey: opts.scopeKey,
357
- sessionKey: opts.sessionKey,
358
- notifyOnExit: opts.notifyOnExit,
359
- exitNotified: false,
360
- child: child ?? undefined,
361
- stdin,
362
- pid: child?.pid ?? pty?.pid,
363
- startedAt,
364
- cwd: opts.workdir,
365
- maxOutputChars: opts.maxOutput,
366
- pendingMaxOutputChars: opts.pendingMaxOutput,
367
- totalOutputChars: 0,
368
- pendingStdout: [],
369
- pendingStderr: [],
370
- pendingStdoutChars: 0,
371
- pendingStderrChars: 0,
372
- aggregated: "",
373
- tail: "",
374
- exited: false,
375
- exitCode: undefined,
376
- exitSignal: undefined,
377
- truncated: false,
378
- backgrounded: false,
379
- };
380
- addSession(session);
381
- let settled = false;
382
- let timeoutTimer = null;
383
- let timeoutFinalizeTimer = null;
384
- let timedOut = false;
385
- const timeoutFinalizeMs = 1000;
386
- let resolveFn = null;
387
- const settle = (outcome) => {
388
- if (settled)
389
- return;
390
- settled = true;
391
- resolveFn?.(outcome);
392
- };
393
- const finalizeTimeout = () => {
394
- if (session.exited)
395
- return;
396
- markExited(session, null, "SIGKILL", "failed");
397
- maybeNotifyOnExit(session, "failed");
398
- const aggregated = session.aggregated.trim();
399
- const reason = `Command timed out after ${opts.timeoutSec} seconds`;
400
- settle({
401
- status: "failed",
402
- exitCode: null,
403
- exitSignal: "SIGKILL",
404
- durationMs: Date.now() - startedAt,
405
- aggregated,
406
- timedOut: true,
407
- reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
408
- });
409
- };
410
- const onTimeout = () => {
411
- timedOut = true;
412
- killSession(session);
413
- if (!timeoutFinalizeTimer) {
414
- timeoutFinalizeTimer = setTimeout(() => {
415
- finalizeTimeout();
416
- }, timeoutFinalizeMs);
74
+ // Another recurring pattern from the issue: shell commands accidentally emitted as JS.
75
+ if (target.kind === "node") {
76
+ const firstNonEmpty = content
77
+ .split(/\r?\n/)
78
+ .map((l) => l.trim())
79
+ .find((l) => l.length > 0);
80
+ if (firstNonEmpty && /^NODE\b/.test(firstNonEmpty)) {
81
+ throw new Error(`exec preflight: JS file starts with shell syntax (${firstNonEmpty}). ` +
82
+ `This looks like a shell command, not JavaScript.`);
417
83
  }
418
- };
419
- if (opts.timeoutSec > 0) {
420
- timeoutTimer = setTimeout(() => {
421
- onTimeout();
422
- }, opts.timeoutSec * 1000);
423
84
  }
424
- const emitUpdate = () => {
425
- if (!opts.onUpdate)
426
- return;
427
- const tailText = session.tail || session.aggregated;
428
- const warningText = opts.warnings.length ? `${opts.warnings.join("\n")}\n\n` : "";
429
- opts.onUpdate({
430
- content: [{ type: "text", text: warningText + (tailText || "") }],
431
- details: {
432
- status: "running",
433
- sessionId,
434
- pid: session.pid ?? undefined,
435
- startedAt,
436
- cwd: session.cwd,
437
- tail: session.tail,
438
- },
439
- });
440
- };
441
- const handleStdout = (data) => {
442
- const str = sanitizeBinaryOutput(data.toString());
443
- for (const chunk of chunkString(str)) {
444
- appendOutput(session, "stdout", chunk);
445
- emitUpdate();
446
- }
447
- };
448
- const handleStderr = (data) => {
449
- const str = sanitizeBinaryOutput(data.toString());
450
- for (const chunk of chunkString(str)) {
451
- appendOutput(session, "stderr", chunk);
452
- emitUpdate();
453
- }
454
- };
455
- if (pty) {
456
- const cursorResponse = buildCursorPositionResponse();
457
- pty.onData((data) => {
458
- const raw = data.toString();
459
- const { cleaned, requests } = stripDsrRequests(raw);
460
- if (requests > 0) {
461
- for (let i = 0; i < requests; i += 1) {
462
- pty.write(cursorResponse);
463
- }
464
- }
465
- handleStdout(cleaned);
466
- });
467
- }
468
- else if (child) {
469
- child.stdout.on("data", handleStdout);
470
- child.stderr.on("data", handleStderr);
471
- }
472
- const promise = new Promise((resolve) => {
473
- resolveFn = resolve;
474
- const handleExit = (code, exitSignal) => {
475
- if (timeoutTimer)
476
- clearTimeout(timeoutTimer);
477
- if (timeoutFinalizeTimer)
478
- clearTimeout(timeoutFinalizeTimer);
479
- const durationMs = Date.now() - startedAt;
480
- const wasSignal = exitSignal != null;
481
- const isSuccess = code === 0 && !wasSignal && !timedOut;
482
- const status = isSuccess ? "completed" : "failed";
483
- markExited(session, code, exitSignal, status);
484
- maybeNotifyOnExit(session, status);
485
- if (!session.child && session.stdin) {
486
- session.stdin.destroyed = true;
487
- }
488
- if (settled)
489
- return;
490
- const aggregated = session.aggregated.trim();
491
- if (!isSuccess) {
492
- const reason = timedOut
493
- ? `Command timed out after ${opts.timeoutSec} seconds`
494
- : wasSignal && exitSignal
495
- ? `Command aborted by signal ${exitSignal}`
496
- : code === null
497
- ? "Command aborted before exit code was captured"
498
- : `Command exited with code ${code}`;
499
- const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
500
- settle({
501
- status: "failed",
502
- exitCode: code ?? null,
503
- exitSignal: exitSignal ?? null,
504
- durationMs,
505
- aggregated,
506
- timedOut,
507
- reason: message,
508
- });
509
- return;
510
- }
511
- settle({
512
- status: "completed",
513
- exitCode: code ?? 0,
514
- exitSignal: exitSignal ?? null,
515
- durationMs,
516
- aggregated,
517
- timedOut: false,
518
- });
519
- };
520
- if (pty) {
521
- pty.onExit((event) => {
522
- const rawSignal = event.signal ?? null;
523
- const normalizedSignal = rawSignal === 0 ? null : rawSignal;
524
- handleExit(event.exitCode ?? null, normalizedSignal);
525
- });
526
- }
527
- else if (child) {
528
- child.once("close", (code, exitSignal) => {
529
- handleExit(code, exitSignal);
530
- });
531
- child.once("error", (err) => {
532
- if (timeoutTimer)
533
- clearTimeout(timeoutTimer);
534
- if (timeoutFinalizeTimer)
535
- clearTimeout(timeoutFinalizeTimer);
536
- markExited(session, null, null, "failed");
537
- maybeNotifyOnExit(session, "failed");
538
- const aggregated = session.aggregated.trim();
539
- const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
540
- settle({
541
- status: "failed",
542
- exitCode: null,
543
- exitSignal: null,
544
- durationMs: Date.now() - startedAt,
545
- aggregated,
546
- timedOut,
547
- reason: message,
548
- });
549
- });
550
- }
551
- });
552
- return {
553
- session,
554
- startedAt,
555
- pid: session.pid ?? undefined,
556
- promise,
557
- kill: () => killSession(session),
558
- };
559
85
  }
560
86
  export function createExecTool(defaults) {
561
- const defaultBackgroundMs = clampNumber(defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, 120_000);
87
+ const defaultBackgroundMs = clampWithDefault(defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, 120_000);
562
88
  const allowBackground = defaults?.allowBackground ?? true;
563
89
  const defaultTimeoutSec = typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
564
90
  ? defaults.timeoutSec
565
- : DEFAULT_EXEC_TIMEOUT_SEC;
91
+ : 1800;
566
92
  const defaultPathPrepend = normalizePathPrepend(defaults?.pathPrepend);
567
93
  const safeBins = resolveSafeBins(defaults?.safeBins);
568
94
  const notifyOnExit = defaults?.notifyOnExit !== false;
95
+ const notifyOnExitEmptySuccess = defaults?.notifyOnExitEmptySuccess === true;
569
96
  const notifySessionKey = defaults?.sessionKey?.trim() || undefined;
570
97
  const approvalRunningNoticeMs = resolveApprovalRunningNoticeMs(defaults?.approvalRunningNoticeMs);
571
98
  // Derive agentId only when sessionKey is an agent session key.
@@ -585,6 +112,7 @@ export function createExecTool(defaults) {
585
112
  const maxOutput = DEFAULT_MAX_OUTPUT;
586
113
  const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
587
114
  const warnings = [];
115
+ let execCommandOverride;
588
116
  const backgroundRequested = params.background === true;
589
117
  const yieldRequested = typeof params.yieldMs === "number";
590
118
  if (!allowBackground && (backgroundRequested || yieldRequested)) {
@@ -593,7 +121,7 @@ export function createExecTool(defaults) {
593
121
  const yieldWindow = allowBackground
594
122
  ? backgroundRequested
595
123
  ? 0
596
- : clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
124
+ : clampWithDefault(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
597
125
  : null;
598
126
  const elevatedDefaults = defaults?.elevated;
599
127
  const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed);
@@ -620,10 +148,12 @@ export function createExecTool(defaults) {
620
148
  const contextParts = [];
621
149
  const provider = defaults?.messageProvider?.trim();
622
150
  const sessionKey = defaults?.sessionKey?.trim();
623
- if (provider)
151
+ if (provider) {
624
152
  contextParts.push(`provider=${provider}`);
625
- if (sessionKey)
153
+ }
154
+ if (sessionKey) {
626
155
  contextParts.push(`session=${sessionKey}`);
156
+ }
627
157
  if (!elevatedDefaults?.enabled) {
628
158
  gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
629
159
  }
@@ -708,7 +238,14 @@ export function createExecTool(defaults) {
708
238
  });
709
239
  applyShellPath(env, shellPath);
710
240
  }
711
- applyPathPrepend(env, defaultPathPrepend);
241
+ // `tools.exec.pathPrepend` is only meaningful when exec runs locally (gateway) or in the sandbox.
242
+ // Node hosts intentionally ignore request-scoped PATH overrides, so don't pretend this applies.
243
+ if (host === "node" && defaultPathPrepend.length > 0) {
244
+ warnings.push("Warning: tools.exec.pathPrepend is ignored for host=node. Configure PATH on the node host/service instead.");
245
+ }
246
+ else {
247
+ applyPathPrepend(env, defaultPathPrepend);
248
+ }
712
249
  if (host === "node") {
713
250
  const approvals = resolveExecApprovals(agentId, { security, ask });
714
251
  const hostSecurity = minSecurity(security, approvals.agent.security);
@@ -746,9 +283,6 @@ export function createExecTool(defaults) {
746
283
  }
747
284
  const argv = buildNodeShellCommand(params.command, nodeInfo?.platform);
748
285
  const nodeEnv = params.env ? { ...params.env } : undefined;
749
- if (nodeEnv) {
750
- applyPathPrepend(nodeEnv, defaultPathPrepend, { requireExisting: true });
751
- }
752
286
  const baseAllowlistEval = evaluateShellAllowlist({
753
287
  command: params.command,
754
288
  allowlist: [],
@@ -887,8 +421,9 @@ export function createExecTool(defaults) {
887
421
  emitExecSystemEvent(`Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${commandText}`, { sessionKey: notifySessionKey, contextKey });
888
422
  }
889
423
  finally {
890
- if (runningTimer)
424
+ if (runningTimer) {
891
425
  clearTimeout(runningTimer);
426
+ }
892
427
  }
893
428
  })();
894
429
  return {
@@ -1042,8 +577,9 @@ export function createExecTool(defaults) {
1042
577
  if (allowlistMatches.length > 0) {
1043
578
  const seen = new Set();
1044
579
  for (const match of allowlistMatches) {
1045
- if (seen.has(match.pattern))
580
+ if (seen.has(match.pattern)) {
1046
581
  continue;
582
+ }
1047
583
  seen.add(match.pattern);
1048
584
  recordAllowlistUse(approvals.file, agentId, match, commandText, resolvedPath ?? undefined);
1049
585
  }
@@ -1061,6 +597,7 @@ export function createExecTool(defaults) {
1061
597
  maxOutput,
1062
598
  pendingMaxOutput,
1063
599
  notifyOnExit: false,
600
+ notifyOnExitEmptySuccess: false,
1064
601
  scopeKey: defaults?.scopeKey,
1065
602
  sessionKey: notifySessionKey,
1066
603
  timeoutSec: effectiveTimeout,
@@ -1078,8 +615,9 @@ export function createExecTool(defaults) {
1078
615
  }, approvalRunningNoticeMs);
1079
616
  }
1080
617
  const outcome = await run.promise;
1081
- if (runningTimer)
618
+ if (runningTimer) {
1082
619
  clearTimeout(runningTimer);
620
+ }
1083
621
  const output = normalizeNotifyOutput(tail(outcome.aggregated || "", DEFAULT_NOTIFY_TAIL_CHARS));
1084
622
  const exitLabel = outcome.timedOut ? "timeout" : `code ${outcome.exitCode ?? "?"}`;
1085
623
  const summary = output
@@ -1109,11 +647,41 @@ export function createExecTool(defaults) {
1109
647
  if (hostSecurity === "allowlist" && (!analysisOk || !allowlistSatisfied)) {
1110
648
  throw new Error("exec denied: allowlist miss");
1111
649
  }
650
+ // If allowlist uses safeBins, sanitize only those stdin-only segments:
651
+ // disable glob/var expansion by forcing argv tokens to be literal via single-quoting.
652
+ if (hostSecurity === "allowlist" &&
653
+ analysisOk &&
654
+ allowlistSatisfied &&
655
+ allowlistEval.segmentSatisfiedBy.some((by) => by === "safeBins")) {
656
+ const safe = buildSafeBinsShellCommand({
657
+ command: params.command,
658
+ segments: allowlistEval.segments,
659
+ segmentSatisfiedBy: allowlistEval.segmentSatisfiedBy,
660
+ platform: process.platform,
661
+ });
662
+ if (!safe.ok || !safe.command) {
663
+ // Fallback: quote everything (safe, but may change glob behavior).
664
+ const fallback = buildSafeShellCommand({
665
+ command: params.command,
666
+ platform: process.platform,
667
+ });
668
+ if (!fallback.ok || !fallback.command) {
669
+ throw new Error(`exec denied: safeBins sanitize failed (${safe.reason ?? "unknown"})`);
670
+ }
671
+ warnings.push("Warning: safeBins hardening used fallback quoting due to parser mismatch.");
672
+ execCommandOverride = fallback.command;
673
+ }
674
+ else {
675
+ warnings.push("Warning: safeBins hardening disabled glob/variable expansion for stdin-only segments.");
676
+ execCommandOverride = safe.command;
677
+ }
678
+ }
1112
679
  if (allowlistMatches.length > 0) {
1113
680
  const seen = new Set();
1114
681
  for (const match of allowlistMatches) {
1115
- if (seen.has(match.pattern))
682
+ if (seen.has(match.pattern)) {
1116
683
  continue;
684
+ }
1117
685
  seen.add(match.pattern);
1118
686
  recordAllowlistUse(approvals.file, agentId, match, params.command, allowlistEval.segments[0]?.resolution?.resolvedPath);
1119
687
  }
@@ -1122,8 +690,12 @@ export function createExecTool(defaults) {
1122
690
  const effectiveTimeout = typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
1123
691
  const getWarningText = () => (warnings.length ? `${warnings.join("\n")}\n\n` : "");
1124
692
  const usePty = params.pty === true && !sandbox;
693
+ // Preflight: catch a common model failure mode (shell syntax leaking into Python/JS sources)
694
+ // before we execute and burn tokens in cron loops.
695
+ await validateScriptFileForShellBleed({ command: params.command, workdir });
1125
696
  const run = await runExecProcess({
1126
697
  command: params.command,
698
+ execCommand: execCommandOverride,
1127
699
  workdir,
1128
700
  env,
1129
701
  sandbox,
@@ -1133,6 +705,7 @@ export function createExecTool(defaults) {
1133
705
  maxOutput,
1134
706
  pendingMaxOutput,
1135
707
  notifyOnExit,
708
+ notifyOnExitEmptySuccess,
1136
709
  scopeKey: defaults?.scopeKey,
1137
710
  sessionKey: notifySessionKey,
1138
711
  timeoutSec: effectiveTimeout,
@@ -1142,12 +715,14 @@ export function createExecTool(defaults) {
1142
715
  let yieldTimer = null;
1143
716
  // Tool-call abort should not kill backgrounded sessions; timeouts still must.
1144
717
  const onAbortSignal = () => {
1145
- if (yielded || run.session.backgrounded)
718
+ if (yielded || run.session.backgrounded) {
1146
719
  return;
720
+ }
1147
721
  run.kill();
1148
722
  };
1149
- if (signal?.aborted)
723
+ if (signal?.aborted) {
1150
724
  onAbortSignal();
725
+ }
1151
726
  else if (signal) {
1152
727
  signal.addEventListener("abort", onAbortSignal, { once: true });
1153
728
  }
@@ -1169,10 +744,12 @@ export function createExecTool(defaults) {
1169
744
  },
1170
745
  });
1171
746
  const onYieldNow = () => {
1172
- if (yieldTimer)
747
+ if (yieldTimer) {
1173
748
  clearTimeout(yieldTimer);
1174
- if (yielded)
749
+ }
750
+ if (yielded) {
1175
751
  return;
752
+ }
1176
753
  yielded = true;
1177
754
  markBackgrounded(run.session);
1178
755
  resolveRunning();
@@ -1183,8 +760,9 @@ export function createExecTool(defaults) {
1183
760
  }
1184
761
  else {
1185
762
  yieldTimer = setTimeout(() => {
1186
- if (yielded)
763
+ if (yielded) {
1187
764
  return;
765
+ }
1188
766
  yielded = true;
1189
767
  markBackgrounded(run.session);
1190
768
  resolveRunning();
@@ -1193,10 +771,12 @@ export function createExecTool(defaults) {
1193
771
  }
1194
772
  run.promise
1195
773
  .then((outcome) => {
1196
- if (yieldTimer)
774
+ if (yieldTimer) {
1197
775
  clearTimeout(yieldTimer);
1198
- if (yielded || run.session.backgrounded)
776
+ }
777
+ if (yielded || run.session.backgrounded) {
1199
778
  return;
779
+ }
1200
780
  if (outcome.status === "failed") {
1201
781
  reject(new Error(outcome.reason ?? "Command failed."));
1202
782
  return;
@@ -1218,10 +798,12 @@ export function createExecTool(defaults) {
1218
798
  });
1219
799
  })
1220
800
  .catch((err) => {
1221
- if (yieldTimer)
801
+ if (yieldTimer) {
1222
802
  clearTimeout(yieldTimer);
1223
- if (yielded || run.session.backgrounded)
803
+ }
804
+ if (yielded || run.session.backgrounded) {
1224
805
  return;
806
+ }
1225
807
  reject(err);
1226
808
  });
1227
809
  });