@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
@@ -0,0 +1,24 @@
1
+ import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
2
+ import { normalizeStringEntries } from "../shared/string-normalization.js";
3
+ export async function resolveDmAllowState(params) {
4
+ const configAllowFrom = normalizeStringEntries(Array.isArray(params.allowFrom) ? params.allowFrom : undefined);
5
+ const hasWildcard = configAllowFrom.includes("*");
6
+ const storeAllowFrom = await (params.readStore ?? readChannelAllowFromStore)(params.provider).catch(() => []);
7
+ const normalizeEntry = params.normalizeEntry ?? ((value) => value);
8
+ const normalizedCfg = configAllowFrom
9
+ .filter((value) => value !== "*")
10
+ .map((value) => normalizeEntry(value))
11
+ .map((value) => value.trim())
12
+ .filter(Boolean);
13
+ const normalizedStore = storeAllowFrom
14
+ .map((value) => normalizeEntry(value))
15
+ .map((value) => value.trim())
16
+ .filter(Boolean);
17
+ const allowCount = Array.from(new Set([...normalizedCfg, ...normalizedStore])).length;
18
+ return {
19
+ configAllowFrom,
20
+ hasWildcard,
21
+ allowCount,
22
+ isMultiUserDm: hasWildcard || allowCount > 1,
23
+ };
24
+ }
@@ -62,14 +62,28 @@ const EXTERNAL_SOURCE_LABELS = {
62
62
  email: "Email",
63
63
  webhook: "Webhook",
64
64
  api: "API",
65
+ browser: "Browser",
65
66
  channel_metadata: "Channel metadata",
66
67
  web_search: "Web Search",
67
68
  web_fetch: "Web Fetch",
68
69
  unknown: "External",
69
70
  };
70
71
  const FULLWIDTH_ASCII_OFFSET = 0xfee0;
71
- const FULLWIDTH_LEFT_ANGLE = 0xff1c;
72
- const FULLWIDTH_RIGHT_ANGLE = 0xff1e;
72
+ // Map of Unicode angle bracket homoglyphs to their ASCII equivalents.
73
+ const ANGLE_BRACKET_MAP = {
74
+ 0xff1c: "<", // fullwidth <
75
+ 0xff1e: ">", // fullwidth >
76
+ 0x2329: "<", // left-pointing angle bracket
77
+ 0x232a: ">", // right-pointing angle bracket
78
+ 0x3008: "<", // CJK left angle bracket
79
+ 0x3009: ">", // CJK right angle bracket
80
+ 0x2039: "<", // single left-pointing angle quotation mark
81
+ 0x203a: ">", // single right-pointing angle quotation mark
82
+ 0x27e8: "<", // mathematical left angle bracket
83
+ 0x27e9: ">", // mathematical right angle bracket
84
+ 0xfe64: "<", // small less-than sign
85
+ 0xfe65: ">", // small greater-than sign
86
+ };
73
87
  function foldMarkerChar(char) {
74
88
  const code = char.charCodeAt(0);
75
89
  if (code >= 0xff21 && code <= 0xff3a) {
@@ -78,16 +92,14 @@ function foldMarkerChar(char) {
78
92
  if (code >= 0xff41 && code <= 0xff5a) {
79
93
  return String.fromCharCode(code - FULLWIDTH_ASCII_OFFSET);
80
94
  }
81
- if (code === FULLWIDTH_LEFT_ANGLE) {
82
- return "<";
83
- }
84
- if (code === FULLWIDTH_RIGHT_ANGLE) {
85
- return ">";
95
+ const bracket = ANGLE_BRACKET_MAP[code];
96
+ if (bracket) {
97
+ return bracket;
86
98
  }
87
99
  return char;
88
100
  }
89
101
  function foldMarkerText(input) {
90
- return input.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E]/g, (char) => foldMarkerChar(char));
102
+ return input.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E\u2329\u232A\u3008\u3009\u2039\u203A\u27E8\u27E9\uFE64\uFE65]/g, (char) => foldMarkerChar(char));
91
103
  }
92
104
  function replaceMarkers(content) {
93
105
  const folded = foldMarkerText(content);
@@ -1,13 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import JSON5 from "json5";
3
+ import { resolveDefaultAgentId } from "../agents/agent-scope.js";
4
4
  import { createConfigIO } from "../config/config.js";
5
+ import { collectIncludePathsRecursive } from "../config/includes-scan.js";
5
6
  import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js";
6
- import { resolveDefaultAgentId } from "../agents/agent-scope.js";
7
- import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js";
8
- import { normalizeAgentId } from "../routing/session-key.js";
9
7
  import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
10
8
  import { runExec } from "../process/exec.js";
9
+ import { normalizeAgentId } from "../routing/session-key.js";
11
10
  import { createIcaclsResetCommand, formatIcaclsResetCommand } from "./windows-acl.js";
12
11
  async function safeChmod(params) {
13
12
  try {
@@ -144,11 +143,13 @@ async function safeAclReset(params) {
144
143
  }
145
144
  }
146
145
  function setGroupPolicyAllowlist(params) {
147
- if (!params.cfg.channels)
146
+ if (!params.cfg.channels) {
148
147
  return;
148
+ }
149
149
  const section = params.cfg.channels[params.channel];
150
- if (!section || typeof section !== "object")
150
+ if (!section || typeof section !== "object") {
151
151
  return;
152
+ }
152
153
  const topPolicy = section.groupPolicy;
153
154
  if (topPolicy === "open") {
154
155
  section.groupPolicy = "allowlist";
@@ -156,13 +157,16 @@ function setGroupPolicyAllowlist(params) {
156
157
  params.policyFlips.add(`channels.${params.channel}.`);
157
158
  }
158
159
  const accounts = section.accounts;
159
- if (!accounts || typeof accounts !== "object")
160
+ if (!accounts || typeof accounts !== "object") {
160
161
  return;
162
+ }
161
163
  for (const [accountId, accountValue] of Object.entries(accounts)) {
162
- if (!accountId)
164
+ if (!accountId) {
163
165
  continue;
164
- if (!accountValue || typeof accountValue !== "object")
166
+ }
167
+ if (!accountValue || typeof accountValue !== "object") {
165
168
  continue;
169
+ }
166
170
  const account = accountValue;
167
171
  if (account.groupPolicy === "open") {
168
172
  account.groupPolicy = "allowlist";
@@ -173,29 +177,36 @@ function setGroupPolicyAllowlist(params) {
173
177
  }
174
178
  function setWhatsAppGroupAllowFromFromStore(params) {
175
179
  const section = params.cfg.channels?.whatsapp;
176
- if (!section || typeof section !== "object")
180
+ if (!section || typeof section !== "object") {
177
181
  return;
178
- if (params.storeAllowFrom.length === 0)
182
+ }
183
+ if (params.storeAllowFrom.length === 0) {
179
184
  return;
185
+ }
180
186
  const maybeApply = (prefix, obj) => {
181
- if (!params.policyFlips.has(prefix))
187
+ if (!params.policyFlips.has(prefix)) {
182
188
  return;
189
+ }
183
190
  const allowFrom = Array.isArray(obj.allowFrom) ? obj.allowFrom : [];
184
191
  const groupAllowFrom = Array.isArray(obj.groupAllowFrom) ? obj.groupAllowFrom : [];
185
- if (allowFrom.length > 0)
192
+ if (allowFrom.length > 0) {
186
193
  return;
187
- if (groupAllowFrom.length > 0)
194
+ }
195
+ if (groupAllowFrom.length > 0) {
188
196
  return;
197
+ }
189
198
  obj.groupAllowFrom = params.storeAllowFrom;
190
199
  params.changes.push(`${prefix}groupAllowFrom=pairing-store`);
191
200
  };
192
201
  maybeApply("channels.whatsapp.", section);
193
202
  const accounts = section.accounts;
194
- if (!accounts || typeof accounts !== "object")
203
+ if (!accounts || typeof accounts !== "object") {
195
204
  return;
205
+ }
196
206
  for (const [accountId, accountValue] of Object.entries(accounts)) {
197
- if (!accountValue || typeof accountValue !== "object")
207
+ if (!accountValue || typeof accountValue !== "object") {
198
208
  continue;
209
+ }
199
210
  const account = accountValue;
200
211
  maybeApply(`channels.whatsapp.accounts.${accountId}.`, account);
201
212
  }
@@ -221,80 +232,17 @@ function applyConfigFixes(params) {
221
232
  }
222
233
  return { cfg: next, changes, policyFlips };
223
234
  }
224
- function listDirectIncludes(parsed) {
225
- const out = [];
226
- const visit = (value) => {
227
- if (!value)
228
- return;
229
- if (Array.isArray(value)) {
230
- for (const item of value)
231
- visit(item);
232
- return;
233
- }
234
- if (typeof value !== "object")
235
- return;
236
- const rec = value;
237
- const includeVal = rec[INCLUDE_KEY];
238
- if (typeof includeVal === "string")
239
- out.push(includeVal);
240
- else if (Array.isArray(includeVal)) {
241
- for (const item of includeVal) {
242
- if (typeof item === "string")
243
- out.push(item);
244
- }
245
- }
246
- for (const v of Object.values(rec))
247
- visit(v);
248
- };
249
- visit(parsed);
250
- return out;
251
- }
252
- function resolveIncludePath(baseConfigPath, includePath) {
253
- return path.normalize(path.isAbsolute(includePath)
254
- ? includePath
255
- : path.resolve(path.dirname(baseConfigPath), includePath));
256
- }
257
- async function collectIncludePathsRecursive(params) {
258
- const visited = new Set();
259
- const result = [];
260
- const walk = async (basePath, parsed, depth) => {
261
- if (depth > MAX_INCLUDE_DEPTH)
262
- return;
263
- for (const raw of listDirectIncludes(parsed)) {
264
- const resolved = resolveIncludePath(basePath, raw);
265
- if (visited.has(resolved))
266
- continue;
267
- visited.add(resolved);
268
- result.push(resolved);
269
- const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
270
- if (!rawText)
271
- continue;
272
- const nestedParsed = (() => {
273
- try {
274
- return JSON5.parse(rawText);
275
- }
276
- catch {
277
- return null;
278
- }
279
- })();
280
- if (nestedParsed) {
281
- // eslint-disable-next-line no-await-in-loop
282
- await walk(resolved, nestedParsed, depth + 1);
283
- }
284
- }
285
- };
286
- await walk(params.configPath, params.parsed, 0);
287
- return result;
288
- }
289
235
  async function chmodCredentialsAndAgentState(params) {
290
236
  const credsDir = resolveOAuthDir(params.env, params.stateDir);
291
237
  params.actions.push(await safeChmod({ path: credsDir, mode: 0o700, require: "dir" }));
292
238
  const credsEntries = await fs.readdir(credsDir, { withFileTypes: true }).catch(() => []);
293
239
  for (const entry of credsEntries) {
294
- if (!entry.isFile())
240
+ if (!entry.isFile()) {
295
241
  continue;
296
- if (!entry.name.endsWith(".json"))
242
+ }
243
+ if (!entry.name.endsWith(".json")) {
297
244
  continue;
245
+ }
298
246
  const p = path.join(credsDir, entry.name);
299
247
  // eslint-disable-next-line no-await-in-loop
300
248
  params.actions.push(await safeChmod({ path: p, mode: 0o600, require: "file" }));
@@ -303,11 +251,13 @@ async function chmodCredentialsAndAgentState(params) {
303
251
  ids.add(resolveDefaultAgentId(params.cfg));
304
252
  const list = Array.isArray(params.cfg.agents?.list) ? params.cfg.agents?.list : [];
305
253
  for (const agent of list ?? []) {
306
- if (!agent || typeof agent !== "object")
254
+ if (!agent || typeof agent !== "object") {
307
255
  continue;
256
+ }
308
257
  const id = typeof agent.id === "string" ? agent.id.trim() : "";
309
- if (id)
258
+ if (id) {
310
259
  ids.add(id);
260
+ }
311
261
  }
312
262
  for (const agentId of ids) {
313
263
  const normalizedAgentId = normalizeAgentId(agentId);
@@ -326,6 +276,20 @@ async function chmodCredentialsAndAgentState(params) {
326
276
  const storePath = path.join(sessionsDir, "sessions.json");
327
277
  // eslint-disable-next-line no-await-in-loop
328
278
  params.actions.push(await params.applyPerms({ path: storePath, mode: 0o600, require: "file" }));
279
+ // Fix permissions on session transcript files (*.jsonl)
280
+ // eslint-disable-next-line no-await-in-loop
281
+ const sessionEntries = await fs.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
282
+ for (const entry of sessionEntries) {
283
+ if (!entry.isFile()) {
284
+ continue;
285
+ }
286
+ if (!entry.name.endsWith(".jsonl")) {
287
+ continue;
288
+ }
289
+ const p = path.join(sessionsDir, entry.name);
290
+ // eslint-disable-next-line no-await-in-loop
291
+ params.actions.push(await params.applyPerms({ path: p, mode: 0o600, require: "file" }));
292
+ }
329
293
  }
330
294
  }
331
295
  export async function fixSecurityFootguns(opts) {
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  export function isPathInside(basePath, candidatePath) {
3
4
  const base = path.resolve(basePath);
@@ -5,6 +6,25 @@ export function isPathInside(basePath, candidatePath) {
5
6
  const rel = path.relative(base, candidate);
6
7
  return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel));
7
8
  }
9
+ function safeRealpathSync(filePath) {
10
+ try {
11
+ return fs.realpathSync(filePath);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function isPathInsideWithRealpath(basePath, candidatePath, opts) {
18
+ if (!isPathInside(basePath, candidatePath)) {
19
+ return false;
20
+ }
21
+ const baseReal = safeRealpathSync(basePath);
22
+ const candidateReal = safeRealpathSync(candidatePath);
23
+ if (!baseReal || !candidateReal) {
24
+ return opts?.requireRealpath !== true;
25
+ }
26
+ return isPathInside(baseReal, candidateReal);
27
+ }
8
28
  export function extensionUsesSkippedScannerPath(entry) {
9
29
  const segments = entry.split(/[\\/]+/).filter(Boolean);
10
30
  return segments.some((segment) => segment === "node_modules" ||
@@ -1,12 +1,8 @@
1
- import { timingSafeEqual } from "node:crypto";
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
2
  export function safeEqualSecret(provided, expected) {
3
3
  if (typeof provided !== "string" || typeof expected !== "string") {
4
4
  return false;
5
5
  }
6
- const providedBuffer = Buffer.from(provided);
7
- const expectedBuffer = Buffer.from(expected);
8
- if (providedBuffer.length !== expectedBuffer.length) {
9
- return false;
10
- }
11
- return timingSafeEqual(providedBuffer, expectedBuffer);
6
+ const hash = (s) => createHash("sha256").update(s).digest();
7
+ return timingSafeEqual(hash(provided), hash(expected));
12
8
  }
@@ -19,8 +19,9 @@ const TRUSTED_SUFFIXES = ["\\administrators", "\\system"];
19
19
  const normalize = (value) => value.trim().toLowerCase();
20
20
  export function resolveWindowsUserPrincipal(env) {
21
21
  const username = env?.USERNAME?.trim() || os.userInfo().username?.trim();
22
- if (!username)
22
+ if (!username) {
23
23
  return null;
24
+ }
24
25
  const domain = env?.USERDOMAIN?.trim();
25
26
  return domain ? `${domain}\\${username}` : username;
26
27
  }
@@ -31,18 +32,21 @@ function buildTrustedPrincipals(env) {
31
32
  trusted.add(normalize(principal));
32
33
  const parts = principal.split("\\");
33
34
  const userOnly = parts.at(-1);
34
- if (userOnly)
35
+ if (userOnly) {
35
36
  trusted.add(normalize(userOnly));
37
+ }
36
38
  }
37
39
  return trusted;
38
40
  }
39
41
  function classifyPrincipal(principal, env) {
40
42
  const normalized = normalize(principal);
41
43
  const trusted = buildTrustedPrincipals(env);
42
- if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s)))
44
+ if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s))) {
43
45
  return "trusted";
44
- if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s)))
46
+ }
47
+ if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s))) {
45
48
  return "world";
49
+ }
46
50
  return "group";
47
51
  }
48
52
  function rightsFromTokens(tokens) {
@@ -59,8 +63,9 @@ export function parseIcaclsOutput(output, targetPath) {
59
63
  const quotedLower = quotedTarget.toLowerCase();
60
64
  for (const rawLine of output.split(/\r?\n/)) {
61
65
  const line = rawLine.trimEnd();
62
- if (!line.trim())
66
+ if (!line.trim()) {
63
67
  continue;
68
+ }
64
69
  const trimmed = line.trim();
65
70
  const lower = trimmed.toLowerCase();
66
71
  if (lower.startsWith("successfully processed") ||
@@ -76,22 +81,26 @@ export function parseIcaclsOutput(output, targetPath) {
76
81
  else if (lower.startsWith(quotedLower)) {
77
82
  entry = trimmed.slice(quotedTarget.length).trim();
78
83
  }
79
- if (!entry)
84
+ if (!entry) {
80
85
  continue;
86
+ }
81
87
  const idx = entry.indexOf(":");
82
- if (idx === -1)
88
+ if (idx === -1) {
83
89
  continue;
90
+ }
84
91
  const principal = entry.slice(0, idx).trim();
85
92
  const rawRights = entry.slice(idx + 1).trim();
86
93
  const tokens = rawRights
87
94
  .match(/\(([^)]+)\)/g)
88
95
  ?.map((token) => token.slice(1, -1).trim())
89
96
  .filter(Boolean) ?? [];
90
- if (tokens.some((token) => token.toUpperCase() === "DENY"))
97
+ if (tokens.some((token) => token.toUpperCase() === "DENY")) {
91
98
  continue;
99
+ }
92
100
  const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase()));
93
- if (rights.length === 0)
101
+ if (rights.length === 0) {
94
102
  continue;
103
+ }
95
104
  const { canRead, canWrite } = rightsFromTokens(rights);
96
105
  entries.push({ principal, rights, rawRights, canRead, canWrite });
97
106
  }
@@ -103,12 +112,15 @@ export function summarizeWindowsAcl(entries, env) {
103
112
  const untrustedGroup = [];
104
113
  for (const entry of entries) {
105
114
  const classification = classifyPrincipal(entry.principal, env);
106
- if (classification === "trusted")
115
+ if (classification === "trusted") {
107
116
  trusted.push(entry);
108
- else if (classification === "world")
117
+ }
118
+ else if (classification === "world") {
109
119
  untrustedWorld.push(entry);
110
- else
120
+ }
121
+ else {
111
122
  untrustedGroup.push(entry);
123
+ }
112
124
  }
113
125
  return { trusted, untrustedWorld, untrustedGroup };
114
126
  }
@@ -133,11 +145,13 @@ export async function inspectWindowsAcl(targetPath, opts) {
133
145
  }
134
146
  }
135
147
  export function formatWindowsAclSummary(summary) {
136
- if (!summary.ok)
148
+ if (!summary.ok) {
137
149
  return "unknown";
150
+ }
138
151
  const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup];
139
- if (untrusted.length === 0)
152
+ if (untrusted.length === 0) {
140
153
  return "trusted-only";
154
+ }
141
155
  return untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
142
156
  }
143
157
  export function formatIcaclsResetCommand(targetPath, opts) {
@@ -147,8 +161,9 @@ export function formatIcaclsResetCommand(targetPath, opts) {
147
161
  }
148
162
  export function createIcaclsResetCommand(targetPath, opts) {
149
163
  const user = resolveWindowsUserPrincipal(opts.env);
150
- if (!user)
164
+ if (!user) {
151
165
  return null;
166
+ }
152
167
  const grant = opts.isDir ? "(OI)(CI)F" : "F";
153
168
  const args = [
154
169
  targetPath,
@@ -0,0 +1,13 @@
1
+ function asRecord(value) {
2
+ return typeof value === "object" && value !== null ? value : {};
3
+ }
4
+ export function parsePairingList(value) {
5
+ const obj = asRecord(value);
6
+ const pending = Array.isArray(obj.pending) ? obj.pending : [];
7
+ const paired = Array.isArray(obj.paired) ? obj.paired : [];
8
+ return { pending, paired };
9
+ }
10
+ export function parseNodeList(value) {
11
+ const obj = asRecord(value);
12
+ return Array.isArray(obj.nodes) ? obj.nodes : [];
13
+ }
@@ -0,0 +1,37 @@
1
+ const OPERATOR_ROLE = "operator";
2
+ const OPERATOR_ADMIN_SCOPE = "operator.admin";
3
+ const OPERATOR_READ_SCOPE = "operator.read";
4
+ const OPERATOR_WRITE_SCOPE = "operator.write";
5
+ function normalizeScopeList(scopes) {
6
+ const out = new Set();
7
+ for (const scope of scopes) {
8
+ const trimmed = scope.trim();
9
+ if (trimmed) {
10
+ out.add(trimmed);
11
+ }
12
+ }
13
+ return [...out];
14
+ }
15
+ function operatorScopeSatisfied(requestedScope, granted) {
16
+ if (requestedScope === OPERATOR_READ_SCOPE) {
17
+ return (granted.has(OPERATOR_READ_SCOPE) ||
18
+ granted.has(OPERATOR_WRITE_SCOPE) ||
19
+ granted.has(OPERATOR_ADMIN_SCOPE));
20
+ }
21
+ return granted.has(requestedScope);
22
+ }
23
+ export function roleScopesAllow(params) {
24
+ const requested = normalizeScopeList(params.requestedScopes);
25
+ if (requested.length === 0) {
26
+ return true;
27
+ }
28
+ const allowed = normalizeScopeList(params.allowedScopes);
29
+ if (allowed.length === 0) {
30
+ return false;
31
+ }
32
+ const allowedSet = new Set(allowed);
33
+ if (params.role.trim() !== OPERATOR_ROLE) {
34
+ return requested.every((scope) => allowedSet.has(scope));
35
+ }
36
+ return requested.every((scope) => operatorScopeSatisfied(scope, allowedSet));
37
+ }
@@ -0,0 +1,29 @@
1
+ export function chunkTextByBreakResolver(text, limit, resolveBreakIndex) {
2
+ if (!text) {
3
+ return [];
4
+ }
5
+ if (limit <= 0 || text.length <= limit) {
6
+ return [text];
7
+ }
8
+ const chunks = [];
9
+ let remaining = text;
10
+ while (remaining.length > limit) {
11
+ const window = remaining.slice(0, limit);
12
+ const candidateBreak = resolveBreakIndex(window);
13
+ const breakIdx = Number.isFinite(candidateBreak) && candidateBreak > 0 && candidateBreak <= limit
14
+ ? candidateBreak
15
+ : limit;
16
+ const rawChunk = remaining.slice(0, breakIdx);
17
+ const chunk = rawChunk.trimEnd();
18
+ if (chunk.length > 0) {
19
+ chunks.push(chunk);
20
+ }
21
+ const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
22
+ const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
23
+ remaining = remaining.slice(nextStart).trimStart();
24
+ }
25
+ if (remaining.length) {
26
+ chunks.push(remaining);
27
+ }
28
+ return chunks;
29
+ }
@@ -0,0 +1,31 @@
1
+ import { vi } from "vitest";
2
+ export function installSlackBlockTestMocks() {
3
+ vi.mock("../config/config.js", () => ({
4
+ loadConfig: () => ({}),
5
+ }));
6
+ vi.mock("./accounts.js", () => ({
7
+ resolveSlackAccount: () => ({
8
+ accountId: "default",
9
+ botToken: "xoxb-test",
10
+ botTokenSource: "config",
11
+ config: {},
12
+ }),
13
+ }));
14
+ }
15
+ export function createSlackEditTestClient() {
16
+ return {
17
+ chat: {
18
+ update: vi.fn(async () => ({ ok: true })),
19
+ },
20
+ };
21
+ }
22
+ export function createSlackSendTestClient() {
23
+ return {
24
+ conversations: {
25
+ open: vi.fn(async () => ({ channel: { id: "D123" } })),
26
+ },
27
+ chat: {
28
+ postMessage: vi.fn(async () => ({ ts: "171234.567" })),
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,8 @@
1
+ export function escapeSlackMrkdwn(value) {
2
+ return value
3
+ .replaceAll("\\", "\\\\")
4
+ .replaceAll("&", "&amp;")
5
+ .replaceAll("<", "&lt;")
6
+ .replaceAll(">", "&gt;")
7
+ .replace(/([*_`~])/g, "\\$1");
8
+ }