@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,9 +1,13 @@
1
1
  import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js";
2
2
  import { resolveSandboxConfigForAgent, resolveSandboxToolPolicyForAgent, } from "../agents/sandbox.js";
3
+ import { getBlockedBindReason } from "../agents/sandbox/validate-sandbox-security.js";
3
4
  import { resolveToolProfilePolicy } from "../agents/tool-policy.js";
4
5
  import { resolveBrowserConfig } from "../browser/config.js";
5
6
  import { formatCliCommand } from "../cli/command-format.js";
6
7
  import { resolveGatewayAuth } from "../gateway/auth.js";
8
+ import { resolveNodeCommandAllowlist } from "../gateway/node-command-policy.js";
9
+ import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
10
+ import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
7
11
  const SMALL_MODEL_PARAM_B_MAX = 300;
8
12
  // --------------------------------------------------------------------------
9
13
  // Helpers
@@ -46,6 +50,14 @@ function looksLikeEnvRef(value) {
46
50
  const v = value.trim();
47
51
  return v.startsWith("${") && v.endsWith("}");
48
52
  }
53
+ function isGatewayRemotelyExposed(cfg) {
54
+ const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
55
+ if (bind !== "loopback") {
56
+ return true;
57
+ }
58
+ const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
59
+ return tailscaleMode === "serve" || tailscaleMode === "funnel";
60
+ }
49
61
  function addModel(models, raw, source) {
50
62
  if (typeof raw !== "string") {
51
63
  return;
@@ -96,25 +108,6 @@ const LEGACY_MODEL_PATTERNS = [
96
108
  const WEAK_TIER_MODEL_PATTERNS = [
97
109
  { id: "anthropic.haiku", re: /\bhaiku\b/i, label: "Haiku tier (smaller model)" },
98
110
  ];
99
- function inferParamBFromIdOrName(text) {
100
- const raw = text.toLowerCase();
101
- const matches = raw.matchAll(/(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g);
102
- let best = null;
103
- for (const match of matches) {
104
- const numRaw = match[1];
105
- if (!numRaw) {
106
- continue;
107
- }
108
- const value = Number(numRaw);
109
- if (!Number.isFinite(value) || value <= 0) {
110
- continue;
111
- }
112
- if (best === null || value > best) {
113
- best = value;
114
- }
115
- }
116
- return best;
117
- }
118
111
  function isGptModel(id) {
119
112
  return /\bgpt-/i.test(id);
120
113
  }
@@ -132,16 +125,52 @@ function extractAgentIdFromSource(source) {
132
125
  const match = source.match(/^agents\.list\.([^.]*)\./);
133
126
  return match?.[1] ?? null;
134
127
  }
135
- function pickToolPolicy(config) {
136
- if (!config) {
137
- return null;
128
+ function hasConfiguredDockerConfig(docker) {
129
+ if (!docker || typeof docker !== "object") {
130
+ return false;
138
131
  }
139
- const allow = Array.isArray(config.allow) ? config.allow : undefined;
140
- const deny = Array.isArray(config.deny) ? config.deny : undefined;
141
- if (!allow && !deny) {
142
- return null;
132
+ return Object.values(docker).some((value) => value !== undefined);
133
+ }
134
+ function normalizeNodeCommand(value) {
135
+ return typeof value === "string" ? value.trim() : "";
136
+ }
137
+ function listKnownNodeCommands(cfg) {
138
+ const baseCfg = {
139
+ ...cfg,
140
+ gateway: {
141
+ ...cfg.gateway,
142
+ nodes: {
143
+ ...cfg.gateway?.nodes,
144
+ denyCommands: [],
145
+ },
146
+ },
147
+ };
148
+ const out = new Set();
149
+ for (const platform of ["ios", "android", "macos", "linux", "windows", "unknown"]) {
150
+ const allow = resolveNodeCommandAllowlist(baseCfg, { platform });
151
+ for (const cmd of allow) {
152
+ const normalized = normalizeNodeCommand(cmd);
153
+ if (normalized) {
154
+ out.add(normalized);
155
+ }
156
+ }
143
157
  }
144
- return { allow, deny };
158
+ return out;
159
+ }
160
+ function looksLikeNodeCommandPattern(value) {
161
+ if (!value) {
162
+ return false;
163
+ }
164
+ if (/[?*[\]{}(),|]/.test(value)) {
165
+ return true;
166
+ }
167
+ if (value.startsWith("/") ||
168
+ value.endsWith("/") ||
169
+ value.startsWith("^") ||
170
+ value.endsWith("$")) {
171
+ return true;
172
+ }
173
+ return /\s/.test(value) || value.includes("group:");
145
174
  }
146
175
  function resolveToolPolicies(params) {
147
176
  const policies = [];
@@ -150,11 +179,11 @@ function resolveToolPolicies(params) {
150
179
  if (profilePolicy) {
151
180
  policies.push(profilePolicy);
152
181
  }
153
- const globalPolicy = pickToolPolicy(params.cfg.tools ?? undefined);
182
+ const globalPolicy = pickSandboxToolPolicy(params.cfg.tools ?? undefined);
154
183
  if (globalPolicy) {
155
184
  policies.push(globalPolicy);
156
185
  }
157
- const agentPolicy = pickToolPolicy(params.agentTools);
186
+ const agentPolicy = pickSandboxToolPolicy(params.agentTools);
158
187
  if (agentPolicy) {
159
188
  policies.push(agentPolicy);
160
189
  }
@@ -232,13 +261,16 @@ function listGroupPolicyOpen(cfg) {
232
261
  export function collectAttackSurfaceSummaryFindings(cfg) {
233
262
  const group = summarizeGroupPolicy(cfg);
234
263
  const elevated = cfg.tools?.elevated?.enabled !== false;
235
- const hooksEnabled = cfg.hooks?.enabled === true;
264
+ const webhooksEnabled = cfg.hooks?.enabled === true;
265
+ const internalHooksEnabled = cfg.hooks?.internal?.enabled === true;
236
266
  const browserEnabled = cfg.browser?.enabled ?? true;
237
267
  const detail = `groups: open=${group.open}, allowlist=${group.allowlist}` +
238
268
  `\n` +
239
269
  `tools.elevated: ${elevated ? "enabled" : "disabled"}` +
240
270
  `\n` +
241
- `hooks: ${hooksEnabled ? "enabled" : "disabled"}` +
271
+ `hooks.webhooks: ${webhooksEnabled ? "enabled" : "disabled"}` +
272
+ `\n` +
273
+ `hooks.internal: ${internalHooksEnabled ? "enabled" : "disabled"}` +
242
274
  `\n` +
243
275
  `browser control: ${browserEnabled ? "enabled" : "disabled"}`;
244
276
  return [
@@ -286,7 +318,7 @@ export function collectSecretsInConfigFindings(cfg) {
286
318
  }
287
319
  return findings;
288
320
  }
289
- export function collectHooksHardeningFindings(cfg) {
321
+ export function collectHooksHardeningFindings(cfg, env = process.env) {
290
322
  const findings = [];
291
323
  if (cfg.hooks?.enabled !== true) {
292
324
  return findings;
@@ -303,12 +335,18 @@ export function collectHooksHardeningFindings(cfg) {
303
335
  const gatewayAuth = resolveGatewayAuth({
304
336
  authConfig: cfg.gateway?.auth,
305
337
  tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
338
+ env,
306
339
  });
340
+ const envGatewayToken = typeof env.CLAWDBOT_GATEWAY_TOKEN === "string" && env.CLAWDBOT_GATEWAY_TOKEN.trim()
341
+ ? env.CLAWDBOT_GATEWAY_TOKEN.trim()
342
+ : null;
307
343
  const gatewayToken = gatewayAuth.mode === "token" &&
308
344
  typeof gatewayAuth.token === "string" &&
309
345
  gatewayAuth.token.trim()
310
346
  ? gatewayAuth.token.trim()
311
- : null;
347
+ : envGatewayToken
348
+ ? envGatewayToken
349
+ : null;
312
350
  if (token && gatewayToken && token === gatewayToken) {
313
351
  findings.push({
314
352
  checkId: "hooks.token_reuse_gateway_token",
@@ -328,6 +366,275 @@ export function collectHooksHardeningFindings(cfg) {
328
366
  remediation: "Use a dedicated path like '/hooks'.",
329
367
  });
330
368
  }
369
+ const allowRequestSessionKey = cfg.hooks?.allowRequestSessionKey === true;
370
+ const defaultSessionKey = typeof cfg.hooks?.defaultSessionKey === "string" ? cfg.hooks.defaultSessionKey.trim() : "";
371
+ const allowedPrefixes = Array.isArray(cfg.hooks?.allowedSessionKeyPrefixes)
372
+ ? cfg.hooks.allowedSessionKeyPrefixes
373
+ .map((prefix) => prefix.trim())
374
+ .filter((prefix) => prefix.length > 0)
375
+ : [];
376
+ const remoteExposure = isGatewayRemotelyExposed(cfg);
377
+ if (!defaultSessionKey) {
378
+ findings.push({
379
+ checkId: "hooks.default_session_key_unset",
380
+ severity: "warn",
381
+ title: "hooks.defaultSessionKey is not configured",
382
+ detail: "Hook agent runs without explicit sessionKey use generated per-request keys. Set hooks.defaultSessionKey to keep hook ingress scoped to a known session.",
383
+ remediation: 'Set hooks.defaultSessionKey (for example, "hook:ingress").',
384
+ });
385
+ }
386
+ if (allowRequestSessionKey) {
387
+ findings.push({
388
+ checkId: "hooks.request_session_key_enabled",
389
+ severity: remoteExposure ? "critical" : "warn",
390
+ title: "External hook payloads may override sessionKey",
391
+ detail: "hooks.allowRequestSessionKey=true allows `/hooks/agent` callers to choose the session key. Treat hook token holders as full-trust unless you also restrict prefixes.",
392
+ remediation: "Set hooks.allowRequestSessionKey=false (recommended) or constrain hooks.allowedSessionKeyPrefixes.",
393
+ });
394
+ }
395
+ if (allowRequestSessionKey && allowedPrefixes.length === 0) {
396
+ findings.push({
397
+ checkId: "hooks.request_session_key_prefixes_missing",
398
+ severity: remoteExposure ? "critical" : "warn",
399
+ title: "Request sessionKey override is enabled without prefix restrictions",
400
+ detail: "hooks.allowRequestSessionKey=true and hooks.allowedSessionKeyPrefixes is unset/empty, so request payloads can target arbitrary session key shapes.",
401
+ remediation: 'Set hooks.allowedSessionKeyPrefixes (for example, ["hook:"]) or disable request overrides.',
402
+ });
403
+ }
404
+ return findings;
405
+ }
406
+ export function collectGatewayHttpSessionKeyOverrideFindings(cfg) {
407
+ const findings = [];
408
+ const chatCompletionsEnabled = cfg.gateway?.http?.endpoints?.chatCompletions?.enabled === true;
409
+ const responsesEnabled = cfg.gateway?.http?.endpoints?.responses?.enabled === true;
410
+ if (!chatCompletionsEnabled && !responsesEnabled) {
411
+ return findings;
412
+ }
413
+ const enabledEndpoints = [
414
+ chatCompletionsEnabled ? "/v1/chat/completions" : null,
415
+ responsesEnabled ? "/v1/responses" : null,
416
+ ].filter((entry) => Boolean(entry));
417
+ findings.push({
418
+ checkId: "gateway.http.session_key_override_enabled",
419
+ severity: "info",
420
+ title: "HTTP API session-key override is enabled",
421
+ detail: `${enabledEndpoints.join(", ")} accept x-poolbot-session-key for per-request session routing. ` +
422
+ "Treat API credential holders as trusted principals.",
423
+ });
424
+ return findings;
425
+ }
426
+ export function collectGatewayHttpNoAuthFindings(cfg, env) {
427
+ const findings = [];
428
+ const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
429
+ const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode, env });
430
+ if (auth.mode !== "none") {
431
+ return findings;
432
+ }
433
+ const chatCompletionsEnabled = cfg.gateway?.http?.endpoints?.chatCompletions?.enabled === true;
434
+ const responsesEnabled = cfg.gateway?.http?.endpoints?.responses?.enabled === true;
435
+ const enabledEndpoints = [
436
+ "/tools/invoke",
437
+ chatCompletionsEnabled ? "/v1/chat/completions" : null,
438
+ responsesEnabled ? "/v1/responses" : null,
439
+ ].filter((entry) => Boolean(entry));
440
+ const remoteExposure = isGatewayRemotelyExposed(cfg);
441
+ findings.push({
442
+ checkId: "gateway.http.no_auth",
443
+ severity: remoteExposure ? "critical" : "warn",
444
+ title: "Gateway HTTP APIs are reachable without auth",
445
+ detail: `gateway.auth.mode="none" leaves ${enabledEndpoints.join(", ")} callable without a shared secret. ` +
446
+ "Treat this as trusted-local only and avoid exposing the gateway beyond loopback.",
447
+ remediation: "Set gateway.auth.mode to token/password (recommended). If you intentionally keep mode=none, keep gateway.bind=loopback and disable optional HTTP endpoints.",
448
+ });
449
+ return findings;
450
+ }
451
+ export function collectSandboxDockerNoopFindings(cfg) {
452
+ const findings = [];
453
+ const configuredPaths = [];
454
+ const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
455
+ const defaultsSandbox = cfg.agents?.defaults?.sandbox;
456
+ const hasDefaultDocker = hasConfiguredDockerConfig(defaultsSandbox?.docker);
457
+ const defaultMode = defaultsSandbox?.mode ?? "off";
458
+ const hasAnySandboxEnabledAgent = agents.some((entry) => {
459
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
460
+ return false;
461
+ }
462
+ return resolveSandboxConfigForAgent(cfg, entry.id).mode !== "off";
463
+ });
464
+ if (hasDefaultDocker && defaultMode === "off" && !hasAnySandboxEnabledAgent) {
465
+ configuredPaths.push("agents.defaults.sandbox.docker");
466
+ }
467
+ for (const entry of agents) {
468
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
469
+ continue;
470
+ }
471
+ if (!hasConfiguredDockerConfig(entry.sandbox?.docker)) {
472
+ continue;
473
+ }
474
+ if (resolveSandboxConfigForAgent(cfg, entry.id).mode === "off") {
475
+ configuredPaths.push(`agents.list.${entry.id}.sandbox.docker`);
476
+ }
477
+ }
478
+ if (configuredPaths.length === 0) {
479
+ return findings;
480
+ }
481
+ findings.push({
482
+ checkId: "sandbox.docker_config_mode_off",
483
+ severity: "warn",
484
+ title: "Sandbox docker settings configured while sandbox mode is off",
485
+ detail: "These docker settings will not take effect until sandbox mode is enabled:\n" +
486
+ configuredPaths.map((entry) => `- ${entry}`).join("\n"),
487
+ remediation: 'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) where needed, or remove unused docker settings.',
488
+ });
489
+ return findings;
490
+ }
491
+ export function collectSandboxDangerousConfigFindings(cfg) {
492
+ const findings = [];
493
+ const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
494
+ const configs = [];
495
+ const defaultDocker = cfg.agents?.defaults?.sandbox?.docker;
496
+ if (defaultDocker && typeof defaultDocker === "object") {
497
+ configs.push({
498
+ source: "agents.defaults.sandbox.docker",
499
+ docker: defaultDocker,
500
+ });
501
+ }
502
+ for (const entry of agents) {
503
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
504
+ continue;
505
+ }
506
+ const agentDocker = entry.sandbox?.docker;
507
+ if (agentDocker && typeof agentDocker === "object") {
508
+ configs.push({
509
+ source: `agents.list.${entry.id}.sandbox.docker`,
510
+ docker: agentDocker,
511
+ });
512
+ }
513
+ }
514
+ for (const { source, docker } of configs) {
515
+ const binds = Array.isArray(docker.binds) ? docker.binds : [];
516
+ for (const bind of binds) {
517
+ if (typeof bind !== "string") {
518
+ continue;
519
+ }
520
+ const blocked = getBlockedBindReason(bind);
521
+ if (!blocked) {
522
+ continue;
523
+ }
524
+ if (blocked.kind === "non_absolute") {
525
+ findings.push({
526
+ checkId: "sandbox.bind_mount_non_absolute",
527
+ severity: "warn",
528
+ title: "Sandbox bind mount uses a non-absolute source path",
529
+ detail: `${source}.binds contains "${bind}" which uses source path "${blocked.sourcePath}". ` +
530
+ "Non-absolute bind sources are hard to validate safely and may resolve unexpectedly.",
531
+ remediation: `Rewrite "${bind}" to use an absolute host path (for example: /home/user/project:/project:ro).`,
532
+ });
533
+ continue;
534
+ }
535
+ const verb = blocked.kind === "covers" ? "covers" : "targets";
536
+ findings.push({
537
+ checkId: "sandbox.dangerous_bind_mount",
538
+ severity: "critical",
539
+ title: "Dangerous bind mount in sandbox config",
540
+ detail: `${source}.binds contains "${bind}" which ${verb} blocked path "${blocked.blockedPath}". ` +
541
+ "This can expose host system directories or the Docker socket to sandbox containers.",
542
+ remediation: `Remove "${bind}" from ${source}.binds. Use project-specific paths instead.`,
543
+ });
544
+ }
545
+ const network = typeof docker.network === "string" ? docker.network : undefined;
546
+ if (network && network.trim().toLowerCase() === "host") {
547
+ findings.push({
548
+ checkId: "sandbox.dangerous_network_mode",
549
+ severity: "critical",
550
+ title: "Network host mode in sandbox config",
551
+ detail: `${source}.network is "host" which bypasses container network isolation entirely.`,
552
+ remediation: `Set ${source}.network to "bridge" or "none".`,
553
+ });
554
+ }
555
+ const seccompProfile = typeof docker.seccompProfile === "string" ? docker.seccompProfile : undefined;
556
+ if (seccompProfile && seccompProfile.trim().toLowerCase() === "unconfined") {
557
+ findings.push({
558
+ checkId: "sandbox.dangerous_seccomp_profile",
559
+ severity: "critical",
560
+ title: "Seccomp unconfined in sandbox config",
561
+ detail: `${source}.seccompProfile is "unconfined" which disables syscall filtering.`,
562
+ remediation: `Remove ${source}.seccompProfile or use a custom seccomp profile file.`,
563
+ });
564
+ }
565
+ const apparmorProfile = typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : undefined;
566
+ if (apparmorProfile && apparmorProfile.trim().toLowerCase() === "unconfined") {
567
+ findings.push({
568
+ checkId: "sandbox.dangerous_apparmor_profile",
569
+ severity: "critical",
570
+ title: "AppArmor unconfined in sandbox config",
571
+ detail: `${source}.apparmorProfile is "unconfined" which disables AppArmor enforcement.`,
572
+ remediation: `Remove ${source}.apparmorProfile or use a named AppArmor profile.`,
573
+ });
574
+ }
575
+ }
576
+ return findings;
577
+ }
578
+ export function collectNodeDenyCommandPatternFindings(cfg) {
579
+ const findings = [];
580
+ const denyListRaw = cfg.gateway?.nodes?.denyCommands;
581
+ if (!Array.isArray(denyListRaw) || denyListRaw.length === 0) {
582
+ return findings;
583
+ }
584
+ const denyList = denyListRaw.map(normalizeNodeCommand).filter(Boolean);
585
+ if (denyList.length === 0) {
586
+ return findings;
587
+ }
588
+ const knownCommands = listKnownNodeCommands(cfg);
589
+ const patternLike = denyList.filter((entry) => looksLikeNodeCommandPattern(entry));
590
+ const unknownExact = denyList.filter((entry) => !looksLikeNodeCommandPattern(entry) && !knownCommands.has(entry));
591
+ if (patternLike.length === 0 && unknownExact.length === 0) {
592
+ return findings;
593
+ }
594
+ const detailParts = [];
595
+ if (patternLike.length > 0) {
596
+ detailParts.push(`Pattern-like entries (not supported by exact matching): ${patternLike.join(", ")}`);
597
+ }
598
+ if (unknownExact.length > 0) {
599
+ detailParts.push(`Unknown command names (not in defaults/allowCommands): ${unknownExact.join(", ")}`);
600
+ }
601
+ const examples = Array.from(knownCommands).slice(0, 8);
602
+ findings.push({
603
+ checkId: "gateway.nodes.deny_commands_ineffective",
604
+ severity: "warn",
605
+ title: "Some gateway.nodes.denyCommands entries are ineffective",
606
+ detail: "gateway.nodes.denyCommands uses exact command-name matching only.\n" +
607
+ detailParts.map((entry) => `- ${entry}`).join("\n"),
608
+ remediation: `Use exact command names (for example: ${examples.join(", ")}). ` +
609
+ "If you need broader restrictions, remove risky commands from allowCommands/default workflows.",
610
+ });
611
+ return findings;
612
+ }
613
+ export function collectMinimalProfileOverrideFindings(cfg) {
614
+ const findings = [];
615
+ if (cfg.tools?.profile !== "minimal") {
616
+ return findings;
617
+ }
618
+ const overrides = (cfg.agents?.list ?? [])
619
+ .filter((entry) => {
620
+ return Boolean(entry &&
621
+ typeof entry === "object" &&
622
+ typeof entry.id === "string" &&
623
+ entry.tools?.profile &&
624
+ entry.tools.profile !== "minimal");
625
+ })
626
+ .map((entry) => `${entry.id}=${entry.tools?.profile}`);
627
+ if (overrides.length === 0) {
628
+ return findings;
629
+ }
630
+ findings.push({
631
+ checkId: "tools.profile_minimal_overridden",
632
+ severity: "warn",
633
+ title: "Global tools.profile=minimal is overridden by agent profiles",
634
+ detail: "Global minimal profile is set, but these agent profiles take precedence:\n" +
635
+ overrides.map((entry) => `- agents.list.${entry}`).join("\n"),
636
+ remediation: 'Set those agents to `tools.profile="minimal"` (or remove the agent override) if you want minimal tools enforced globally.',
637
+ });
331
638
  return findings;
332
639
  }
333
640
  export function collectModelHygieneFindings(cfg) {
@@ -41,7 +41,19 @@ export async function inspectPathPermissions(targetPath, opts) {
41
41
  error: st.error,
42
42
  };
43
43
  }
44
- const bits = modeBits(st.mode);
44
+ let effectiveMode = st.mode;
45
+ let effectiveIsDir = st.isDir;
46
+ if (st.isSymlink) {
47
+ try {
48
+ const target = await fs.stat(targetPath);
49
+ effectiveMode = typeof target.mode === "number" ? target.mode : st.mode;
50
+ effectiveIsDir = target.isDirectory();
51
+ }
52
+ catch {
53
+ // Keep lstat-derived metadata when target lookup fails.
54
+ }
55
+ }
56
+ const bits = modeBits(effectiveMode);
45
57
  const platform = opts?.platform ?? process.platform;
46
58
  if (platform === "win32") {
47
59
  const acl = await inspectWindowsAcl(targetPath, { env: opts?.env, exec: opts?.exec });
@@ -49,8 +61,8 @@ export async function inspectPathPermissions(targetPath, opts) {
49
61
  return {
50
62
  ok: true,
51
63
  isSymlink: st.isSymlink,
52
- isDir: st.isDir,
53
- mode: st.mode,
64
+ isDir: effectiveIsDir,
65
+ mode: effectiveMode,
54
66
  bits,
55
67
  source: "unknown",
56
68
  worldWritable: false,
@@ -63,8 +75,8 @@ export async function inspectPathPermissions(targetPath, opts) {
63
75
  return {
64
76
  ok: true,
65
77
  isSymlink: st.isSymlink,
66
- isDir: st.isDir,
67
- mode: st.mode,
78
+ isDir: effectiveIsDir,
79
+ mode: effectiveMode,
68
80
  bits,
69
81
  source: "windows-acl",
70
82
  worldWritable: acl.untrustedWorld.some((entry) => entry.canWrite),
@@ -77,8 +89,8 @@ export async function inspectPathPermissions(targetPath, opts) {
77
89
  return {
78
90
  ok: true,
79
91
  isSymlink: st.isSymlink,
80
- isDir: st.isDir,
81
- mode: st.mode,
92
+ isDir: effectiveIsDir,
93
+ mode: effectiveMode,
82
94
  bits,
83
95
  source: "posix",
84
96
  worldWritable: isWorldWritable(bits),
@@ -102,32 +114,38 @@ export function formatPermissionRemediation(params) {
102
114
  return `chmod ${mode} ${params.targetPath}`;
103
115
  }
104
116
  export function modeBits(mode) {
105
- if (mode == null)
117
+ if (mode == null) {
106
118
  return null;
119
+ }
107
120
  return mode & 0o777;
108
121
  }
109
122
  export function formatOctal(bits) {
110
- if (bits == null)
123
+ if (bits == null) {
111
124
  return "unknown";
125
+ }
112
126
  return bits.toString(8).padStart(3, "0");
113
127
  }
114
128
  export function isWorldWritable(bits) {
115
- if (bits == null)
129
+ if (bits == null) {
116
130
  return false;
131
+ }
117
132
  return (bits & 0o002) !== 0;
118
133
  }
119
134
  export function isGroupWritable(bits) {
120
- if (bits == null)
135
+ if (bits == null) {
121
136
  return false;
137
+ }
122
138
  return (bits & 0o020) !== 0;
123
139
  }
124
140
  export function isWorldReadable(bits) {
125
- if (bits == null)
141
+ if (bits == null) {
126
142
  return false;
143
+ }
127
144
  return (bits & 0o004) !== 0;
128
145
  }
129
146
  export function isGroupReadable(bits) {
130
- if (bits == null)
147
+ if (bits == null) {
131
148
  return false;
149
+ }
132
150
  return (bits & 0o040) !== 0;
133
151
  }