@poolzin/pool-bot 2026.2.23 → 2026.2.25

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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,20 +1,8 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { splitShellArgs } from "../utils/shell-argv.js";
5
- export const DEFAULT_SAFE_BINS = ["jq", "grep", "cut", "sort", "uniq", "head", "tail", "tr", "wc"];
6
- function expandHome(value) {
7
- if (!value) {
8
- return value;
9
- }
10
- if (value === "~") {
11
- return os.homedir();
12
- }
13
- if (value.startsWith("~/")) {
14
- return path.join(os.homedir(), value.slice(2));
15
- }
16
- return value;
17
- }
4
+ import { expandHomePrefix } from "./home-dir.js";
5
+ export const DEFAULT_SAFE_BINS = ["jq", "cut", "uniq", "head", "tail", "tr", "wc"];
18
6
  function isExecutableFile(filePath) {
19
7
  try {
20
8
  const stat = fs.statSync(filePath);
@@ -47,7 +35,7 @@ function parseFirstToken(command) {
47
35
  return match ? match[0] : null;
48
36
  }
49
37
  function resolveExecutablePath(rawExecutable, cwd, env) {
50
- const expanded = rawExecutable.startsWith("~") ? expandHome(rawExecutable) : rawExecutable;
38
+ const expanded = rawExecutable.startsWith("~") ? expandHomePrefix(rawExecutable) : rawExecutable;
51
39
  if (expanded.includes("/") || expanded.includes("\\")) {
52
40
  if (path.isAbsolute(expanded)) {
53
41
  return isExecutableFile(expanded) ? expanded : undefined;
@@ -145,7 +133,7 @@ function matchesPattern(pattern, target) {
145
133
  if (!trimmed) {
146
134
  return false;
147
135
  }
148
- const expanded = trimmed.startsWith("~") ? expandHome(trimmed) : trimmed;
136
+ const expanded = trimmed.startsWith("~") ? expandHomePrefix(trimmed) : trimmed;
149
137
  const hasWildcard = /[*?]/.test(expanded);
150
138
  let normalizedPattern = expanded;
151
139
  let normalizedTarget = target;
@@ -169,7 +157,7 @@ export function resolveAllowlistCandidatePath(resolution, cwd) {
169
157
  if (!raw) {
170
158
  return undefined;
171
159
  }
172
- const expanded = raw.startsWith("~") ? expandHome(raw) : raw;
160
+ const expanded = raw.startsWith("~") ? expandHomePrefix(raw) : raw;
173
161
  if (!expanded.includes("/") && !expanded.includes("\\")) {
174
162
  return undefined;
175
163
  }
@@ -199,6 +187,45 @@ export function matchAllowlist(entries, resolution) {
199
187
  }
200
188
  return null;
201
189
  }
190
+ /**
191
+ * Tokenizes a single argv entry into a normalized option/positional model.
192
+ * Consumers can share this model to keep argv parsing behavior consistent.
193
+ */
194
+ export function parseExecArgvToken(raw) {
195
+ if (!raw) {
196
+ return { kind: "empty", raw };
197
+ }
198
+ if (raw === "--") {
199
+ return { kind: "terminator", raw };
200
+ }
201
+ if (raw === "-") {
202
+ return { kind: "stdin", raw };
203
+ }
204
+ if (!raw.startsWith("-")) {
205
+ return { kind: "positional", raw };
206
+ }
207
+ if (raw.startsWith("--")) {
208
+ const eqIndex = raw.indexOf("=");
209
+ if (eqIndex > 0) {
210
+ return {
211
+ kind: "option",
212
+ raw,
213
+ style: "long",
214
+ flag: raw.slice(0, eqIndex),
215
+ inlineValue: raw.slice(eqIndex + 1),
216
+ };
217
+ }
218
+ return { kind: "option", raw, style: "long", flag: raw };
219
+ }
220
+ const cluster = raw.slice(1);
221
+ return {
222
+ kind: "option",
223
+ raw,
224
+ style: "short-cluster",
225
+ cluster,
226
+ flags: cluster.split("").map((entry) => `-${entry}`),
227
+ };
228
+ }
202
229
  const DISALLOWED_PIPELINE_TOKENS = new Set([">", "<", "`", "\n", "\r", "(", ")"]);
203
230
  const DOUBLE_QUOTE_ESCAPES = new Set(["\\", '"', "$", "`", "\n", "\r"]);
204
231
  const WINDOWS_UNSUPPORTED_TOKENS = new Set([
@@ -0,0 +1,269 @@
1
+ import { parseExecArgvToken } from "./exec-approvals-analysis.js";
2
+ function isPathLikeToken(value) {
3
+ const trimmed = value.trim();
4
+ if (!trimmed) {
5
+ return false;
6
+ }
7
+ if (trimmed === "-") {
8
+ return false;
9
+ }
10
+ if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~")) {
11
+ return true;
12
+ }
13
+ if (trimmed.startsWith("/")) {
14
+ return true;
15
+ }
16
+ return /^[A-Za-z]:[\\/]/.test(trimmed);
17
+ }
18
+ function hasGlobToken(value) {
19
+ // Safe bins are stdin-only; globbing is both surprising and a historical bypass vector.
20
+ // Note: we still harden execution-time expansion separately.
21
+ return /[*?[\]]/.test(value);
22
+ }
23
+ const NO_FLAGS = new Set();
24
+ const toFlagSet = (flags) => {
25
+ if (!flags || flags.length === 0) {
26
+ return NO_FLAGS;
27
+ }
28
+ return new Set(flags);
29
+ };
30
+ function compileSafeBinProfile(fixture) {
31
+ return {
32
+ minPositional: fixture.minPositional,
33
+ maxPositional: fixture.maxPositional,
34
+ valueFlags: toFlagSet(fixture.valueFlags),
35
+ blockedFlags: toFlagSet(fixture.blockedFlags),
36
+ };
37
+ }
38
+ function compileSafeBinProfiles(fixtures) {
39
+ return Object.fromEntries(Object.entries(fixtures).map(([name, fixture]) => [name, compileSafeBinProfile(fixture)]));
40
+ }
41
+ export const SAFE_BIN_GENERIC_PROFILE_FIXTURE = {};
42
+ export const SAFE_BIN_PROFILE_FIXTURES = {
43
+ jq: {
44
+ maxPositional: 1,
45
+ valueFlags: [
46
+ "--arg",
47
+ "--argjson",
48
+ "--argstr",
49
+ "--argfile",
50
+ "--rawfile",
51
+ "--slurpfile",
52
+ "--from-file",
53
+ "--library-path",
54
+ "-L",
55
+ "-f",
56
+ ],
57
+ blockedFlags: [
58
+ "--argfile",
59
+ "--rawfile",
60
+ "--slurpfile",
61
+ "--from-file",
62
+ "--library-path",
63
+ "-L",
64
+ "-f",
65
+ ],
66
+ },
67
+ grep: {
68
+ maxPositional: 1,
69
+ valueFlags: [
70
+ "--regexp",
71
+ "--file",
72
+ "--max-count",
73
+ "--after-context",
74
+ "--before-context",
75
+ "--context",
76
+ "--devices",
77
+ "--directories",
78
+ "--binary-files",
79
+ "--exclude",
80
+ "--exclude-from",
81
+ "--include",
82
+ "--label",
83
+ "-e",
84
+ "-f",
85
+ "-m",
86
+ "-A",
87
+ "-B",
88
+ "-C",
89
+ "-D",
90
+ "-d",
91
+ ],
92
+ blockedFlags: [
93
+ "--file",
94
+ "--exclude-from",
95
+ "--dereference-recursive",
96
+ "--directories",
97
+ "--recursive",
98
+ "-f",
99
+ "-d",
100
+ "-r",
101
+ "-R",
102
+ ],
103
+ },
104
+ cut: {
105
+ maxPositional: 0,
106
+ valueFlags: [
107
+ "--bytes",
108
+ "--characters",
109
+ "--fields",
110
+ "--delimiter",
111
+ "--output-delimiter",
112
+ "-b",
113
+ "-c",
114
+ "-f",
115
+ "-d",
116
+ ],
117
+ },
118
+ sort: {
119
+ maxPositional: 0,
120
+ valueFlags: [
121
+ "--key",
122
+ "--field-separator",
123
+ "--buffer-size",
124
+ "--temporary-directory",
125
+ "--compress-program",
126
+ "--parallel",
127
+ "--batch-size",
128
+ "--random-source",
129
+ "--files0-from",
130
+ "--output",
131
+ "-k",
132
+ "-t",
133
+ "-S",
134
+ "-T",
135
+ "-o",
136
+ ],
137
+ blockedFlags: ["--files0-from", "--output", "-o"],
138
+ },
139
+ uniq: {
140
+ maxPositional: 0,
141
+ valueFlags: ["--skip-fields", "--skip-chars", "--check-chars", "--group", "-f", "-s", "-w"],
142
+ },
143
+ head: {
144
+ maxPositional: 0,
145
+ valueFlags: ["--lines", "--bytes", "-n", "-c"],
146
+ },
147
+ tail: {
148
+ maxPositional: 0,
149
+ valueFlags: [
150
+ "--lines",
151
+ "--bytes",
152
+ "--sleep-interval",
153
+ "--max-unchanged-stats",
154
+ "--pid",
155
+ "-n",
156
+ "-c",
157
+ ],
158
+ },
159
+ tr: {
160
+ minPositional: 1,
161
+ maxPositional: 2,
162
+ },
163
+ wc: {
164
+ maxPositional: 0,
165
+ valueFlags: ["--files0-from"],
166
+ blockedFlags: ["--files0-from"],
167
+ },
168
+ };
169
+ export const SAFE_BIN_GENERIC_PROFILE = compileSafeBinProfile(SAFE_BIN_GENERIC_PROFILE_FIXTURE);
170
+ export const SAFE_BIN_PROFILES = compileSafeBinProfiles(SAFE_BIN_PROFILE_FIXTURES);
171
+ function isSafeLiteralToken(value) {
172
+ if (!value || value === "-") {
173
+ return true;
174
+ }
175
+ return !hasGlobToken(value) && !isPathLikeToken(value);
176
+ }
177
+ function isInvalidValueToken(value) {
178
+ return !value || !isSafeLiteralToken(value);
179
+ }
180
+ function consumeLongOptionToken(args, index, flag, inlineValue, valueFlags, blockedFlags) {
181
+ if (blockedFlags.has(flag)) {
182
+ return -1;
183
+ }
184
+ if (inlineValue !== undefined) {
185
+ return isSafeLiteralToken(inlineValue) ? index + 1 : -1;
186
+ }
187
+ if (!valueFlags.has(flag)) {
188
+ return index + 1;
189
+ }
190
+ return isInvalidValueToken(args[index + 1]) ? -1 : index + 2;
191
+ }
192
+ function consumeShortOptionClusterToken(args, index, raw, cluster, flags, valueFlags, blockedFlags) {
193
+ for (let j = 0; j < flags.length; j += 1) {
194
+ const flag = flags[j];
195
+ if (blockedFlags.has(flag)) {
196
+ return -1;
197
+ }
198
+ if (!valueFlags.has(flag)) {
199
+ continue;
200
+ }
201
+ const inlineValue = cluster.slice(j + 1);
202
+ if (inlineValue) {
203
+ return isSafeLiteralToken(inlineValue) ? index + 1 : -1;
204
+ }
205
+ return isInvalidValueToken(args[index + 1]) ? -1 : index + 2;
206
+ }
207
+ return hasGlobToken(raw) ? -1 : index + 1;
208
+ }
209
+ function consumePositionalToken(token, positional) {
210
+ if (!isSafeLiteralToken(token)) {
211
+ return false;
212
+ }
213
+ positional.push(token);
214
+ return true;
215
+ }
216
+ function validatePositionalCount(positional, profile) {
217
+ const minPositional = profile.minPositional ?? 0;
218
+ if (positional.length < minPositional) {
219
+ return false;
220
+ }
221
+ return typeof profile.maxPositional !== "number" || positional.length <= profile.maxPositional;
222
+ }
223
+ export function validateSafeBinArgv(args, profile) {
224
+ const valueFlags = profile.valueFlags ?? NO_FLAGS;
225
+ const blockedFlags = profile.blockedFlags ?? NO_FLAGS;
226
+ const positional = [];
227
+ let i = 0;
228
+ while (i < args.length) {
229
+ const rawToken = args[i] ?? "";
230
+ const token = parseExecArgvToken(rawToken);
231
+ if (token.kind === "empty" || token.kind === "stdin") {
232
+ i += 1;
233
+ continue;
234
+ }
235
+ if (token.kind === "terminator") {
236
+ for (let j = i + 1; j < args.length; j += 1) {
237
+ const rest = args[j];
238
+ if (!rest || rest === "-") {
239
+ continue;
240
+ }
241
+ if (!consumePositionalToken(rest, positional)) {
242
+ return false;
243
+ }
244
+ }
245
+ break;
246
+ }
247
+ if (token.kind === "positional") {
248
+ if (!consumePositionalToken(token.raw, positional)) {
249
+ return false;
250
+ }
251
+ i += 1;
252
+ continue;
253
+ }
254
+ if (token.style === "long") {
255
+ const nextIndex = consumeLongOptionToken(args, i, token.flag, token.inlineValue, valueFlags, blockedFlags);
256
+ if (nextIndex < 0) {
257
+ return false;
258
+ }
259
+ i = nextIndex;
260
+ continue;
261
+ }
262
+ const nextIndex = consumeShortOptionClusterToken(args, i, token.raw, token.cluster, token.flags, valueFlags, blockedFlags);
263
+ if (nextIndex < 0) {
264
+ return false;
265
+ }
266
+ i = nextIndex;
267
+ }
268
+ return validatePositionalCount(positional, profile);
269
+ }
@@ -0,0 +1,33 @@
1
+ export function createFixedWindowRateLimiter(params) {
2
+ const maxRequests = Math.max(1, Math.floor(params.maxRequests));
3
+ const windowMs = Math.max(1, Math.floor(params.windowMs));
4
+ const now = params.now ?? Date.now;
5
+ let count = 0;
6
+ let windowStartMs = 0;
7
+ return {
8
+ consume() {
9
+ const nowMs = now();
10
+ if (nowMs - windowStartMs >= windowMs) {
11
+ windowStartMs = nowMs;
12
+ count = 0;
13
+ }
14
+ if (count >= maxRequests) {
15
+ return {
16
+ allowed: false,
17
+ retryAfterMs: Math.max(0, windowStartMs + windowMs - nowMs),
18
+ remaining: 0,
19
+ };
20
+ }
21
+ count += 1;
22
+ return {
23
+ allowed: true,
24
+ retryAfterMs: 0,
25
+ remaining: Math.max(0, maxRequests - count),
26
+ };
27
+ },
28
+ reset() {
29
+ count = 0;
30
+ windowStartMs = 0;
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export const DEFAULT_GIT_DISCOVERY_MAX_DEPTH = 12;
4
+ function walkUpFrom(startDir, opts, resolveAtDir) {
5
+ let current = path.resolve(startDir);
6
+ const maxDepth = opts.maxDepth ?? DEFAULT_GIT_DISCOVERY_MAX_DEPTH;
7
+ for (let i = 0; i < maxDepth; i += 1) {
8
+ const resolved = resolveAtDir(current);
9
+ if (resolved !== null && resolved !== undefined) {
10
+ return resolved;
11
+ }
12
+ const parent = path.dirname(current);
13
+ if (parent === current) {
14
+ break;
15
+ }
16
+ current = parent;
17
+ }
18
+ return null;
19
+ }
20
+ function hasGitMarker(repoRoot) {
21
+ const gitPath = path.join(repoRoot, ".git");
22
+ try {
23
+ const stat = fs.statSync(gitPath);
24
+ return stat.isDirectory() || stat.isFile();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ export function findGitRoot(startDir, opts = {}) {
31
+ // A `.git` file counts as a repo marker even if it is not a valid gitdir pointer.
32
+ return walkUpFrom(startDir, opts, (repoRoot) => (hasGitMarker(repoRoot) ? repoRoot : null));
33
+ }
34
+ function resolveGitDirFromMarker(repoRoot) {
35
+ const gitPath = path.join(repoRoot, ".git");
36
+ try {
37
+ const stat = fs.statSync(gitPath);
38
+ if (stat.isDirectory()) {
39
+ return gitPath;
40
+ }
41
+ if (!stat.isFile()) {
42
+ return null;
43
+ }
44
+ const raw = fs.readFileSync(gitPath, "utf-8");
45
+ const match = raw.match(/gitdir:\s*(.+)/i);
46
+ if (!match?.[1]) {
47
+ return null;
48
+ }
49
+ return path.resolve(repoRoot, match[1].trim());
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export function resolveGitHeadPath(startDir, opts = {}) {
56
+ // Stricter than findGitRoot: keep walking until a resolvable git dir is found.
57
+ return walkUpFrom(startDir, opts, (repoRoot) => {
58
+ const gitDir = resolveGitDirFromMarker(repoRoot);
59
+ return gitDir ? path.join(gitDir, "HEAD") : null;
60
+ });
61
+ }
@@ -1,5 +1,5 @@
1
1
  import { resolveUserTimezone } from "../agents/date-time.js";
2
- const ACTIVE_HOURS_TIME_PATTERN = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
2
+ const ACTIVE_HOURS_TIME_PATTERN = /^(?:([01]\d|2[0-3]):([0-5]\d)|24:00)$/;
3
3
  function resolveActiveHoursTimezone(cfg, raw) {
4
4
  const trimmed = raw?.trim();
5
5
  if (!trimmed || trimmed === "user") {
@@ -71,7 +71,7 @@ export function isWithinActiveHours(cfg, heartbeat, nowMs) {
71
71
  return true;
72
72
  }
73
73
  if (startMin === endMin) {
74
- return true;
74
+ return false;
75
75
  }
76
76
  const timeZone = resolveActiveHoursTimezone(cfg, active.timezone);
77
77
  const currentMin = resolveMinutesInTimeZone(nowMs ?? Date.now(), timeZone);
@@ -0,0 +1,40 @@
1
+ function trimReason(reason) {
2
+ return typeof reason === "string" ? reason.trim() : "";
3
+ }
4
+ export function normalizeHeartbeatWakeReason(reason) {
5
+ const trimmed = trimReason(reason);
6
+ return trimmed.length > 0 ? trimmed : "requested";
7
+ }
8
+ export function resolveHeartbeatReasonKind(reason) {
9
+ const trimmed = trimReason(reason);
10
+ if (trimmed === "retry") {
11
+ return "retry";
12
+ }
13
+ if (trimmed === "interval") {
14
+ return "interval";
15
+ }
16
+ if (trimmed === "manual") {
17
+ return "manual";
18
+ }
19
+ if (trimmed === "exec-event") {
20
+ return "exec-event";
21
+ }
22
+ if (trimmed === "wake") {
23
+ return "wake";
24
+ }
25
+ if (trimmed.startsWith("cron:")) {
26
+ return "cron";
27
+ }
28
+ if (trimmed.startsWith("hook:")) {
29
+ return "hook";
30
+ }
31
+ return "other";
32
+ }
33
+ export function isHeartbeatEventDrivenReason(reason) {
34
+ const kind = resolveHeartbeatReasonKind(reason);
35
+ return kind === "exec-event" || kind === "cron" || kind === "wake" || kind === "hook";
36
+ }
37
+ export function isHeartbeatActionWakeReason(reason) {
38
+ const kind = resolveHeartbeatReasonKind(reason);
39
+ return kind === "manual" || kind === "exec-event" || kind === "hook";
40
+ }
@@ -17,10 +17,11 @@ import { getQueueSize } from "../process/command-queue.js";
17
17
  import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
18
18
  import { defaultRuntime } from "../runtime.js";
19
19
  import { escapeRegExp } from "../utils.js";
20
- import { formatErrorMessage } from "./errors.js";
20
+ import { formatErrorMessage, hasErrnoCode } from "./errors.js";
21
21
  import { isWithinActiveHours } from "./heartbeat-active-hours.js";
22
22
  import { buildCronEventPrompt, isCronSystemEvent, isExecCompletionEvent, } from "./heartbeat-events-filter.js";
23
23
  import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
24
+ import { resolveHeartbeatReasonKind } from "./heartbeat-reason.js";
24
25
  import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
25
26
  import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js";
26
27
  import { deliverOutboundPayloads } from "./outbound/deliver.js";
@@ -322,6 +323,56 @@ function normalizeHeartbeatReply(payload, responsePrefix, ackMaxChars) {
322
323
  }
323
324
  return { shouldSkip: false, text: finalText, hasMedia };
324
325
  }
326
+ function resolveHeartbeatReasonFlags(reason) {
327
+ const reasonKind = resolveHeartbeatReasonKind(reason);
328
+ return {
329
+ isExecEventReason: reasonKind === "exec-event",
330
+ isCronEventReason: reasonKind === "cron",
331
+ isWakeReason: reasonKind === "wake" || reasonKind === "hook",
332
+ };
333
+ }
334
+ async function resolveHeartbeatPreflight(params) {
335
+ const reasonFlags = resolveHeartbeatReasonFlags(params.reason);
336
+ const session = resolveHeartbeatSession(params.cfg, params.agentId, params.heartbeat, params.forcedSessionKey);
337
+ const pendingEventEntries = peekSystemEventEntries(session.sessionKey);
338
+ const hasTaggedCronEvents = pendingEventEntries.some((event) => event.contextKey?.startsWith("cron:"));
339
+ const shouldInspectPendingEvents = reasonFlags.isExecEventReason || reasonFlags.isCronEventReason || hasTaggedCronEvents;
340
+ const shouldBypassFileGates = reasonFlags.isExecEventReason ||
341
+ reasonFlags.isCronEventReason ||
342
+ reasonFlags.isWakeReason ||
343
+ hasTaggedCronEvents;
344
+ const basePreflight = {
345
+ ...reasonFlags,
346
+ session,
347
+ pendingEventEntries,
348
+ hasTaggedCronEvents,
349
+ shouldInspectPendingEvents,
350
+ };
351
+ if (shouldBypassFileGates) {
352
+ return basePreflight;
353
+ }
354
+ const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
355
+ const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
356
+ try {
357
+ const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
358
+ if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent)) {
359
+ return {
360
+ ...basePreflight,
361
+ skipReason: "empty-heartbeat-file",
362
+ };
363
+ }
364
+ }
365
+ catch (err) {
366
+ if (hasErrnoCode(err, "ENOENT")) {
367
+ // Missing HEARTBEAT.md is intentional in some setups (for example, when
368
+ // heartbeat instructions live outside the file), so keep the run active.
369
+ // The heartbeat prompt already says "if it exists".
370
+ return basePreflight;
371
+ }
372
+ // For other read errors, proceed with heartbeat as before.
373
+ }
374
+ return basePreflight;
375
+ }
325
376
  export async function runHeartbeatOnce(opts) {
326
377
  const cfg = opts.cfg ?? loadConfig();
327
378
  const agentId = normalizeAgentId(opts.agentId ?? resolveDefaultAgentId(cfg));
@@ -343,34 +394,24 @@ export async function runHeartbeatOnce(opts) {
343
394
  if (queueSize > 0) {
344
395
  return { status: "skipped", reason: "requests-in-flight" };
345
396
  }
346
- // Skip heartbeat if HEARTBEAT.md exists but has no actionable content.
347
- // This saves API calls/costs when the file is effectively empty (only comments/headers).
348
- // EXCEPTION: Don't skip for exec events, cron events, or explicit wake requests -
349
- // they have pending system events to process regardless of HEARTBEAT.md content.
350
- const isExecEventReason = opts.reason === "exec-event";
351
- const isCronEventReason = Boolean(opts.reason?.startsWith("cron:"));
352
- const isWakeReason = opts.reason === "wake" || Boolean(opts.reason?.startsWith("hook:"));
353
- const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
354
- const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
355
- try {
356
- const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
357
- if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) &&
358
- !isExecEventReason &&
359
- !isCronEventReason &&
360
- !isWakeReason) {
361
- emitHeartbeatEvent({
362
- status: "skipped",
363
- reason: "empty-heartbeat-file",
364
- durationMs: Date.now() - startedAt,
365
- });
366
- return { status: "skipped", reason: "empty-heartbeat-file" };
367
- }
368
- }
369
- catch {
370
- // File doesn't exist or can't be read - proceed with heartbeat.
371
- // The LLM prompt says "if it exists" so this is expected behavior.
397
+ // Preflight centralizes trigger classification, event inspection, and HEARTBEAT.md gating.
398
+ const preflight = await resolveHeartbeatPreflight({
399
+ cfg,
400
+ agentId,
401
+ heartbeat,
402
+ forcedSessionKey: opts.sessionKey,
403
+ reason: opts.reason,
404
+ });
405
+ if (preflight.skipReason) {
406
+ emitHeartbeatEvent({
407
+ status: "skipped",
408
+ reason: preflight.skipReason,
409
+ durationMs: Date.now() - startedAt,
410
+ });
411
+ return { status: "skipped", reason: preflight.skipReason };
372
412
  }
373
- const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat, opts.sessionKey);
413
+ const { entry, sessionKey, storePath } = preflight.session;
414
+ const { isCronEventReason, pendingEventEntries } = preflight;
374
415
  const previousUpdatedAt = entry?.updatedAt;
375
416
  const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
376
417
  const heartbeatAccountId = heartbeat?.accountId?.trim();
@@ -402,10 +443,7 @@ export async function runHeartbeatOnce(opts) {
402
443
  // Check if this is an exec event or cron event with pending system events.
403
444
  // If so, use a specialized prompt that instructs the model to relay the result
404
445
  // instead of the standard heartbeat prompt with "reply HEARTBEAT_OK".
405
- const isExecEvent = opts.reason === "exec-event";
406
- const pendingEventEntries = peekSystemEventEntries(sessionKey);
407
- const hasTaggedCronEvents = pendingEventEntries.some((event) => event.contextKey?.startsWith("cron:"));
408
- const shouldInspectPendingEvents = isExecEvent || isCronEventReason || hasTaggedCronEvents;
446
+ const shouldInspectPendingEvents = preflight.shouldInspectPendingEvents;
409
447
  const pendingEvents = shouldInspectPendingEvents
410
448
  ? pendingEventEntries.map((event) => event.text)
411
449
  : [];
@@ -459,6 +497,7 @@ export async function runHeartbeatOnce(opts) {
459
497
  channel: delivery.channel,
460
498
  to: delivery.to,
461
499
  accountId: delivery.accountId,
500
+ threadId: delivery.threadId,
462
501
  payloads: [{ text: heartbeatOkText }],
463
502
  agentId,
464
503
  deps: opts.deps,
@@ -635,6 +674,7 @@ export async function runHeartbeatOnce(opts) {
635
674
  to: delivery.to,
636
675
  accountId: deliveryAccountId,
637
676
  agentId,
677
+ threadId: delivery.threadId,
638
678
  payloads: [
639
679
  ...reasoningPayloads,
640
680
  ...(shouldSkipMain