@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -0,0 +1,330 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { hasErrnoCode } from "../infra/errors.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Scannable extensions
6
+ // ---------------------------------------------------------------------------
7
+ const SCANNABLE_EXTENSIONS = new Set([
8
+ ".js",
9
+ ".ts",
10
+ ".mjs",
11
+ ".cjs",
12
+ ".mts",
13
+ ".cts",
14
+ ".jsx",
15
+ ".tsx",
16
+ ]);
17
+ const DEFAULT_MAX_SCAN_FILES = 500;
18
+ const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
19
+ export function isScannable(filePath) {
20
+ return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
21
+ }
22
+ const LINE_RULES = [
23
+ {
24
+ ruleId: "dangerous-exec",
25
+ severity: "critical",
26
+ message: "Shell command execution detected (child_process)",
27
+ pattern: /\b(exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(/,
28
+ requiresContext: /child_process/,
29
+ },
30
+ {
31
+ ruleId: "dynamic-code-execution",
32
+ severity: "critical",
33
+ message: "Dynamic code execution detected",
34
+ pattern: /\beval\s*\(|new\s+Function\s*\(/,
35
+ },
36
+ {
37
+ ruleId: "crypto-mining",
38
+ severity: "critical",
39
+ message: "Possible crypto-mining reference detected",
40
+ pattern: /stratum\+tcp|stratum\+ssl|coinhive|cryptonight|xmrig/i,
41
+ },
42
+ {
43
+ ruleId: "suspicious-network",
44
+ severity: "warn",
45
+ message: "WebSocket connection to non-standard port",
46
+ pattern: /new\s+WebSocket\s*\(\s*["']wss?:\/\/[^"']*:(\d+)/,
47
+ },
48
+ ];
49
+ const STANDARD_PORTS = new Set([80, 443, 8080, 8443, 3000]);
50
+ const SOURCE_RULES = [
51
+ {
52
+ ruleId: "potential-exfiltration",
53
+ severity: "warn",
54
+ message: "File read combined with network send — possible data exfiltration",
55
+ pattern: /readFileSync|readFile/,
56
+ requiresContext: /\bfetch\b|\bpost\b|http\.request/i,
57
+ },
58
+ {
59
+ ruleId: "obfuscated-code",
60
+ severity: "warn",
61
+ message: "Hex-encoded string sequence detected (possible obfuscation)",
62
+ pattern: /(\\x[0-9a-fA-F]{2}){6,}/,
63
+ },
64
+ {
65
+ ruleId: "obfuscated-code",
66
+ severity: "warn",
67
+ message: "Large base64 payload with decode call detected (possible obfuscation)",
68
+ pattern: /(?:atob|Buffer\.from)\s*\(\s*["'][A-Za-z0-9+/=]{200,}["']/,
69
+ },
70
+ {
71
+ ruleId: "env-harvesting",
72
+ severity: "critical",
73
+ message: "Environment variable access combined with network send — possible credential harvesting",
74
+ pattern: /process\.env/,
75
+ requiresContext: /\bfetch\b|\bpost\b|http\.request/i,
76
+ },
77
+ ];
78
+ // ---------------------------------------------------------------------------
79
+ // Core scanner
80
+ // ---------------------------------------------------------------------------
81
+ function truncateEvidence(evidence, maxLen = 120) {
82
+ if (evidence.length <= maxLen) {
83
+ return evidence;
84
+ }
85
+ return `${evidence.slice(0, maxLen)}…`;
86
+ }
87
+ export function scanSource(source, filePath) {
88
+ const findings = [];
89
+ const lines = source.split("\n");
90
+ const matchedLineRules = new Set();
91
+ // --- Line rules ---
92
+ for (const rule of LINE_RULES) {
93
+ if (matchedLineRules.has(rule.ruleId)) {
94
+ continue;
95
+ }
96
+ // Skip rule entirely if context requirement not met
97
+ if (rule.requiresContext && !rule.requiresContext.test(source)) {
98
+ continue;
99
+ }
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const line = lines[i];
102
+ const match = rule.pattern.exec(line);
103
+ if (!match) {
104
+ continue;
105
+ }
106
+ // Special handling for suspicious-network: check port
107
+ if (rule.ruleId === "suspicious-network") {
108
+ const port = parseInt(match[1], 10);
109
+ if (STANDARD_PORTS.has(port)) {
110
+ continue;
111
+ }
112
+ }
113
+ findings.push({
114
+ ruleId: rule.ruleId,
115
+ severity: rule.severity,
116
+ file: filePath,
117
+ line: i + 1,
118
+ message: rule.message,
119
+ evidence: truncateEvidence(line.trim()),
120
+ });
121
+ matchedLineRules.add(rule.ruleId);
122
+ break; // one finding per line-rule per file
123
+ }
124
+ }
125
+ // --- Source rules ---
126
+ const matchedSourceRules = new Set();
127
+ for (const rule of SOURCE_RULES) {
128
+ // Allow multiple findings for different messages with the same ruleId
129
+ // but deduplicate exact (ruleId+message) combos
130
+ const ruleKey = `${rule.ruleId}::${rule.message}`;
131
+ if (matchedSourceRules.has(ruleKey)) {
132
+ continue;
133
+ }
134
+ if (!rule.pattern.test(source)) {
135
+ continue;
136
+ }
137
+ if (rule.requiresContext && !rule.requiresContext.test(source)) {
138
+ continue;
139
+ }
140
+ // Find the first matching line for evidence + line number
141
+ let matchLine = 0;
142
+ let matchEvidence = "";
143
+ for (let i = 0; i < lines.length; i++) {
144
+ if (rule.pattern.test(lines[i])) {
145
+ matchLine = i + 1;
146
+ matchEvidence = lines[i].trim();
147
+ break;
148
+ }
149
+ }
150
+ // For source rules, if we can't find a line match the pattern might span
151
+ // lines. Report line 0 with truncated source as evidence.
152
+ if (matchLine === 0) {
153
+ matchLine = 1;
154
+ matchEvidence = source.slice(0, 120);
155
+ }
156
+ findings.push({
157
+ ruleId: rule.ruleId,
158
+ severity: rule.severity,
159
+ file: filePath,
160
+ line: matchLine,
161
+ message: rule.message,
162
+ evidence: truncateEvidence(matchEvidence),
163
+ });
164
+ matchedSourceRules.add(ruleKey);
165
+ }
166
+ return findings;
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // Directory scanner
170
+ // ---------------------------------------------------------------------------
171
+ function normalizeScanOptions(opts) {
172
+ return {
173
+ includeFiles: opts?.includeFiles ?? [],
174
+ maxFiles: Math.max(1, opts?.maxFiles ?? DEFAULT_MAX_SCAN_FILES),
175
+ maxFileBytes: Math.max(1, opts?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES),
176
+ };
177
+ }
178
+ function isPathInside(basePath, candidatePath) {
179
+ const base = path.resolve(basePath);
180
+ const candidate = path.resolve(candidatePath);
181
+ const rel = path.relative(base, candidate);
182
+ return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel));
183
+ }
184
+ async function walkDirWithLimit(dirPath, maxFiles) {
185
+ const files = [];
186
+ const stack = [dirPath];
187
+ while (stack.length > 0 && files.length < maxFiles) {
188
+ const currentDir = stack.pop();
189
+ if (!currentDir) {
190
+ break;
191
+ }
192
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
193
+ for (const entry of entries) {
194
+ if (files.length >= maxFiles) {
195
+ break;
196
+ }
197
+ // Skip hidden dirs and node_modules
198
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
199
+ continue;
200
+ }
201
+ const fullPath = path.join(currentDir, entry.name);
202
+ if (entry.isDirectory()) {
203
+ stack.push(fullPath);
204
+ }
205
+ else if (isScannable(entry.name)) {
206
+ files.push(fullPath);
207
+ }
208
+ }
209
+ }
210
+ return files;
211
+ }
212
+ async function resolveForcedFiles(params) {
213
+ if (params.includeFiles.length === 0) {
214
+ return [];
215
+ }
216
+ const seen = new Set();
217
+ const out = [];
218
+ for (const rawIncludePath of params.includeFiles) {
219
+ const includePath = path.resolve(params.rootDir, rawIncludePath);
220
+ if (!isPathInside(params.rootDir, includePath)) {
221
+ continue;
222
+ }
223
+ if (!isScannable(includePath)) {
224
+ continue;
225
+ }
226
+ if (seen.has(includePath)) {
227
+ continue;
228
+ }
229
+ let st = null;
230
+ try {
231
+ st = await fs.stat(includePath);
232
+ }
233
+ catch (err) {
234
+ if (hasErrnoCode(err, "ENOENT")) {
235
+ continue;
236
+ }
237
+ throw err;
238
+ }
239
+ if (!st?.isFile()) {
240
+ continue;
241
+ }
242
+ out.push(includePath);
243
+ seen.add(includePath);
244
+ }
245
+ return out;
246
+ }
247
+ async function collectScannableFiles(dirPath, opts) {
248
+ const forcedFiles = await resolveForcedFiles({
249
+ rootDir: dirPath,
250
+ includeFiles: opts.includeFiles,
251
+ });
252
+ if (forcedFiles.length >= opts.maxFiles) {
253
+ return forcedFiles.slice(0, opts.maxFiles);
254
+ }
255
+ const walkedFiles = await walkDirWithLimit(dirPath, opts.maxFiles);
256
+ const seen = new Set(forcedFiles.map((f) => path.resolve(f)));
257
+ const out = [...forcedFiles];
258
+ for (const walkedFile of walkedFiles) {
259
+ if (out.length >= opts.maxFiles) {
260
+ break;
261
+ }
262
+ const resolved = path.resolve(walkedFile);
263
+ if (seen.has(resolved)) {
264
+ continue;
265
+ }
266
+ out.push(walkedFile);
267
+ seen.add(resolved);
268
+ }
269
+ return out;
270
+ }
271
+ async function readScannableSource(filePath, maxFileBytes) {
272
+ let st = null;
273
+ try {
274
+ st = await fs.stat(filePath);
275
+ }
276
+ catch (err) {
277
+ if (hasErrnoCode(err, "ENOENT")) {
278
+ return null;
279
+ }
280
+ throw err;
281
+ }
282
+ if (!st?.isFile() || st.size > maxFileBytes) {
283
+ return null;
284
+ }
285
+ try {
286
+ return await fs.readFile(filePath, "utf-8");
287
+ }
288
+ catch (err) {
289
+ if (hasErrnoCode(err, "ENOENT")) {
290
+ return null;
291
+ }
292
+ throw err;
293
+ }
294
+ }
295
+ export async function scanDirectory(dirPath, opts) {
296
+ const scanOptions = normalizeScanOptions(opts);
297
+ const files = await collectScannableFiles(dirPath, scanOptions);
298
+ const allFindings = [];
299
+ for (const file of files) {
300
+ const source = await readScannableSource(file, scanOptions.maxFileBytes);
301
+ if (source == null) {
302
+ continue;
303
+ }
304
+ const findings = scanSource(source, file);
305
+ allFindings.push(...findings);
306
+ }
307
+ return allFindings;
308
+ }
309
+ export async function scanDirectoryWithSummary(dirPath, opts) {
310
+ const scanOptions = normalizeScanOptions(opts);
311
+ const files = await collectScannableFiles(dirPath, scanOptions);
312
+ const allFindings = [];
313
+ let scannedFiles = 0;
314
+ for (const file of files) {
315
+ const source = await readScannableSource(file, scanOptions.maxFileBytes);
316
+ if (source == null) {
317
+ continue;
318
+ }
319
+ scannedFiles += 1;
320
+ const findings = scanSource(source, file);
321
+ allFindings.push(...findings);
322
+ }
323
+ return {
324
+ scannedFiles,
325
+ critical: allFindings.filter((f) => f.severity === "critical").length,
326
+ warn: allFindings.filter((f) => f.severity === "warn").length,
327
+ info: allFindings.filter((f) => f.severity === "info").length,
328
+ findings: allFindings,
329
+ };
330
+ }
@@ -13,6 +13,13 @@ export function parseAgentSessionKey(sessionKey) {
13
13
  return null;
14
14
  return { agentId, rest };
15
15
  }
16
+ export function isCronRunSessionKey(sessionKey) {
17
+ const parsed = parseAgentSessionKey(sessionKey);
18
+ if (!parsed) {
19
+ return false;
20
+ }
21
+ return /^cron:[^:]+:run:[^:]+$/.test(parsed.rest);
22
+ }
16
23
  export function isSubagentSessionKey(sessionKey) {
17
24
  const raw = (sessionKey ?? "").trim();
18
25
  if (!raw)
@@ -3,14 +3,17 @@ import { hasControlCommand } from "../../auto-reply/command-detection.js";
3
3
  import { formatInboundEnvelope, formatInboundFromLabel, resolveEnvelopeFormatOptions, } from "../../auto-reply/envelope.js";
4
4
  import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js";
5
5
  import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
6
- import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from "../../auto-reply/reply/history.js";
6
+ import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled, } from "../../auto-reply/reply/history.js";
7
+ import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
7
8
  import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
8
9
  import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
9
10
  import { logInboundDrop, logTypingFailure } from "../../channels/logging.js";
11
+ import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
10
12
  import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
11
13
  import { recordInboundSession } from "../../channels/session.js";
12
14
  import { createTypingCallbacks } from "../../channels/typing.js";
13
15
  import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
16
+ import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
14
17
  import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
15
18
  import { enqueueSystemEvent } from "../../infra/system-events.js";
16
19
  import { mediaKindFromMime } from "../../media/constants.js";
@@ -79,8 +82,17 @@ export function createSignalEventHandler(deps) {
79
82
  });
80
83
  }
81
84
  const signalTo = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
85
+ const inboundHistory = entry.isGroup && historyKey && deps.historyLimit > 0
86
+ ? (deps.groupHistories.get(historyKey) ?? []).map((historyEntry) => ({
87
+ sender: historyEntry.sender,
88
+ body: historyEntry.body,
89
+ timestamp: historyEntry.timestamp,
90
+ }))
91
+ : undefined;
82
92
  const ctxPayload = finalizeInboundContext({
83
93
  Body: combinedBody,
94
+ BodyForAgent: entry.bodyText,
95
+ InboundHistory: inboundHistory,
84
96
  RawBody: entry.bodyText,
85
97
  CommandBody: entry.bodyText,
86
98
  From: entry.isGroup
@@ -101,6 +113,7 @@ export function createSignalEventHandler(deps) {
101
113
  MediaPath: entry.mediaPath,
102
114
  MediaType: entry.mediaType,
103
115
  MediaUrl: entry.mediaPath,
116
+ WasMentioned: entry.isGroup ? entry.wasMentioned === true : undefined,
104
117
  CommandAuthorized: entry.commandAuthorized,
105
118
  OriginatingChannel: "signal",
106
119
  OriginatingTo: signalTo,
@@ -417,6 +430,71 @@ export function createSignalEventHandler(deps) {
417
430
  });
418
431
  return;
419
432
  }
433
+ const route = resolveAgentRoute({
434
+ cfg: deps.cfg,
435
+ channel: "signal",
436
+ accountId: deps.accountId,
437
+ peer: {
438
+ kind: isGroup ? "group" : "dm",
439
+ id: isGroup ? (groupId ?? "unknown") : senderPeerId,
440
+ },
441
+ });
442
+ const mentionRegexes = buildMentionRegexes(deps.cfg, route.agentId);
443
+ const wasMentioned = isGroup && matchesMentionPatterns(messageText, mentionRegexes);
444
+ const requireMention = isGroup &&
445
+ resolveChannelGroupRequireMention({
446
+ cfg: deps.cfg,
447
+ channel: "signal",
448
+ groupId,
449
+ accountId: deps.accountId,
450
+ });
451
+ const canDetectMention = mentionRegexes.length > 0;
452
+ const mentionGate = resolveMentionGatingWithBypass({
453
+ isGroup,
454
+ requireMention: Boolean(requireMention),
455
+ canDetectMention,
456
+ wasMentioned,
457
+ implicitMention: false,
458
+ hasAnyMention: false,
459
+ allowTextCommands: true,
460
+ hasControlCommand: hasControlCommandInMessage,
461
+ commandAuthorized,
462
+ });
463
+ const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
464
+ if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
465
+ logInboundDrop({
466
+ log: logVerbose,
467
+ channel: "signal",
468
+ reason: "no mention",
469
+ target: senderDisplay,
470
+ });
471
+ const quoteText = dataMessage.quote?.text?.trim() || "";
472
+ const pendingPlaceholder = (() => {
473
+ if (!dataMessage.attachments?.length) {
474
+ return "";
475
+ }
476
+ if (deps.ignoreAttachments) {
477
+ return "<media:attachment>";
478
+ }
479
+ const firstContentType = dataMessage.attachments?.[0]?.contentType;
480
+ const pendingKind = mediaKindFromMime(firstContentType ?? undefined);
481
+ return pendingKind ? `<media:${pendingKind}>` : "<media:attachment>";
482
+ })();
483
+ const pendingBodyText = messageText || pendingPlaceholder || quoteText;
484
+ const historyKey = groupId ?? "unknown";
485
+ recordPendingHistoryEntryIfEnabled({
486
+ historyMap: deps.groupHistories,
487
+ historyKey,
488
+ limit: deps.historyLimit,
489
+ entry: {
490
+ sender: envelope.sourceName ?? senderDisplay,
491
+ body: pendingBodyText,
492
+ timestamp: envelope.timestamp ?? undefined,
493
+ messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : undefined,
494
+ },
495
+ });
496
+ return;
497
+ }
420
498
  let mediaPath;
421
499
  let mediaType;
422
500
  let placeholder = "";
@@ -487,6 +565,7 @@ export function createSignalEventHandler(deps) {
487
565
  mediaPath,
488
566
  mediaType,
489
567
  commandAuthorized,
568
+ wasMentioned: effectiveWasMentioned,
490
569
  });
491
570
  };
492
571
  }
@@ -1,14 +1,77 @@
1
1
  import { fetchRemoteMedia } from "../../media/fetch.js";
2
2
  import { saveMediaBuffer } from "../../media/store.js";
3
+ function normalizeHostname(hostname) {
4
+ const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
5
+ if (normalized.startsWith("[") && normalized.endsWith("]")) {
6
+ return normalized.slice(1, -1);
7
+ }
8
+ return normalized;
9
+ }
10
+ function isSlackHostname(hostname) {
11
+ const normalized = normalizeHostname(hostname);
12
+ if (!normalized) {
13
+ return false;
14
+ }
15
+ // Slack-hosted files typically come from *.slack.com and redirect to Slack CDN domains.
16
+ // Include a small allowlist of known Slack domains to avoid leaking tokens if a file URL
17
+ // is ever spoofed or mishandled.
18
+ const allowedSuffixes = ["slack.com", "slack-edge.com", "slack-files.com"];
19
+ return allowedSuffixes.some((suffix) => normalized === suffix || normalized.endsWith(`.${suffix}`));
20
+ }
21
+ function assertSlackFileUrl(rawUrl) {
22
+ let parsed;
23
+ try {
24
+ parsed = new URL(rawUrl);
25
+ }
26
+ catch {
27
+ throw new Error(`Invalid Slack file URL: ${rawUrl}`);
28
+ }
29
+ if (parsed.protocol !== "https:") {
30
+ throw new Error(`Refusing Slack file URL with non-HTTPS protocol: ${parsed.protocol}`);
31
+ }
32
+ if (!isSlackHostname(parsed.hostname)) {
33
+ throw new Error(`Refusing to send Slack token to non-Slack host "${parsed.hostname}" (url: ${rawUrl})`);
34
+ }
35
+ return parsed;
36
+ }
37
+ function resolveRequestUrl(input) {
38
+ if (typeof input === "string") {
39
+ return input;
40
+ }
41
+ if (input instanceof URL) {
42
+ return input.toString();
43
+ }
44
+ if ("url" in input && typeof input.url === "string") {
45
+ return input.url;
46
+ }
47
+ throw new Error("Unsupported fetch input: expected string, URL, or Request");
48
+ }
49
+ function createSlackMediaFetch(token) {
50
+ let includeAuth = true;
51
+ return async (input, init) => {
52
+ const url = resolveRequestUrl(input);
53
+ const { headers: initHeaders, redirect: _redirect, ...rest } = init ?? {};
54
+ const headers = new Headers(initHeaders);
55
+ if (includeAuth) {
56
+ includeAuth = false;
57
+ const parsed = assertSlackFileUrl(url);
58
+ headers.set("Authorization", `Bearer ${token}`);
59
+ return fetch(parsed.href, { ...rest, headers, redirect: "manual" });
60
+ }
61
+ headers.delete("Authorization");
62
+ return fetch(url, { ...rest, headers, redirect: "manual" });
63
+ };
64
+ }
3
65
  /**
4
66
  * Fetches a URL with Authorization header, handling cross-origin redirects.
5
67
  * Node.js fetch strips Authorization headers on cross-origin redirects for security.
6
- * Slack's files.slack.com URLs redirect to CDN domains with pre-signed URLs that
7
- * don't need the Authorization header, so we handle the initial auth request manually.
68
+ * Slack's file URLs redirect to CDN domains with pre-signed URLs that don't need the
69
+ * Authorization header, so we handle the initial auth request manually.
8
70
  */
9
71
  export async function fetchWithSlackAuth(url, token) {
72
+ const parsed = assertSlackFileUrl(url);
10
73
  // Initial request with auth and manual redirect handling
11
- const initialRes = await fetch(url, {
74
+ const initialRes = await fetch(parsed.href, {
12
75
  headers: { Authorization: `Bearer ${token}` },
13
76
  redirect: "manual",
14
77
  });
@@ -22,31 +85,36 @@ export async function fetchWithSlackAuth(url, token) {
22
85
  return initialRes;
23
86
  }
24
87
  // Resolve relative URLs against the original
25
- const resolvedUrl = new URL(redirectUrl, url).toString();
88
+ const resolvedUrl = new URL(redirectUrl, parsed.href);
89
+ // Only follow safe protocols (we do NOT include Authorization on redirects).
90
+ if (resolvedUrl.protocol !== "https:") {
91
+ return initialRes;
92
+ }
26
93
  // Follow the redirect without the Authorization header
27
94
  // (Slack's CDN URLs are pre-signed and don't need it)
28
- return fetch(resolvedUrl, { redirect: "follow" });
95
+ return fetch(resolvedUrl.toString(), { redirect: "follow" });
29
96
  }
30
97
  export async function resolveSlackMedia(params) {
31
98
  const files = params.files ?? [];
32
99
  for (const file of files) {
33
100
  const url = file.url_private_download ?? file.url_private;
34
- if (!url)
101
+ if (!url) {
35
102
  continue;
103
+ }
36
104
  try {
37
- // Note: We ignore init options because fetchWithSlackAuth handles
38
- // redirect behavior specially. fetchRemoteMedia only passes the URL.
39
- const fetchImpl = (input) => {
40
- const inputUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
41
- return fetchWithSlackAuth(inputUrl, params.token);
42
- };
105
+ // Note: fetchRemoteMedia calls fetchImpl(url) with the URL string today and
106
+ // handles size limits internally. Provide a fetcher that uses auth once, then lets
107
+ // the redirect chain continue without credentials.
108
+ const fetchImpl = createSlackMediaFetch(params.token);
43
109
  const fetched = await fetchRemoteMedia({
44
110
  url,
45
111
  fetchImpl,
46
112
  filePathHint: file.name,
113
+ maxBytes: params.maxBytes,
47
114
  });
48
- if (fetched.buffer.byteLength > params.maxBytes)
115
+ if (fetched.buffer.byteLength > params.maxBytes) {
49
116
  continue;
117
+ }
50
118
  const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType ?? file.mimetype, "inbound", params.maxBytes);
51
119
  const label = fetched.fileName ?? file.name;
52
120
  return {
@@ -65,8 +133,9 @@ const THREAD_STARTER_CACHE = new Map();
65
133
  export async function resolveSlackThreadStarter(params) {
66
134
  const cacheKey = `${params.channelId}:${params.threadTs}`;
67
135
  const cached = THREAD_STARTER_CACHE.get(cacheKey);
68
- if (cached)
136
+ if (cached) {
69
137
  return cached;
138
+ }
70
139
  try {
71
140
  const response = (await params.client.conversations.replies({
72
141
  channel: params.channelId,
@@ -76,8 +145,9 @@ export async function resolveSlackThreadStarter(params) {
76
145
  }));
77
146
  const message = response?.messages?.[0];
78
147
  const text = (message?.text ?? "").trim();
79
- if (!message || !text)
148
+ if (!message || !text) {
80
149
  return null;
150
+ }
81
151
  const starter = {
82
152
  text,
83
153
  userId: message.user,
@@ -65,8 +65,7 @@ async function getTailscaleStatus() {
65
65
  canFunnel: self.Capabilities?.map.includes("funnel") ?? false,
66
66
  };
67
67
  }
68
- catch (err) {
69
- // If command fails, return minimal info
68
+ catch {
70
69
  return {
71
70
  installed: await isTailscaleInstalled(),
72
71
  running: false,