@poolzin/pool-bot 2026.2.24 → 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 (191) hide show
  1. package/CHANGELOG.md +21 -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/sessions.test-helpers.js +61 -0
  63. package/dist/config/commands.js +3 -0
  64. package/dist/config/config.js +1 -1
  65. package/dist/config/env-substitution.js +62 -34
  66. package/dist/config/env-vars.js +9 -0
  67. package/dist/config/io.js +571 -171
  68. package/dist/config/merge-patch.js +50 -4
  69. package/dist/config/redact-snapshot.js +404 -76
  70. package/dist/config/schema.js +58 -570
  71. package/dist/config/validation.js +140 -85
  72. package/dist/config/zod-schema.hooks.js +40 -11
  73. package/dist/config/zod-schema.installs.js +20 -0
  74. package/dist/config/zod-schema.js +8 -7
  75. package/dist/daemon/cmd-argv.js +21 -0
  76. package/dist/daemon/cmd-set.js +58 -0
  77. package/dist/daemon/service-types.js +1 -0
  78. package/dist/discord/monitor/exec-approvals.js +357 -162
  79. package/dist/gateway/auth.js +38 -3
  80. package/dist/gateway/call.js +149 -68
  81. package/dist/gateway/canvas-capability.js +75 -0
  82. package/dist/gateway/control-plane-audit.js +28 -0
  83. package/dist/gateway/control-plane-rate-limit.js +53 -0
  84. package/dist/gateway/events.js +1 -0
  85. package/dist/gateway/hooks.js +109 -54
  86. package/dist/gateway/http-common.js +22 -0
  87. package/dist/gateway/method-scopes.js +169 -0
  88. package/dist/gateway/net.js +23 -0
  89. package/dist/gateway/openresponses-http.js +120 -110
  90. package/dist/gateway/probe-auth.js +2 -0
  91. package/dist/gateway/protocol/index.js +3 -2
  92. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  93. package/dist/gateway/protocol/schema/push.js +18 -0
  94. package/dist/gateway/protocol/schema.js +1 -0
  95. package/dist/gateway/server-http.js +236 -52
  96. package/dist/gateway/server-methods/agent.js +162 -24
  97. package/dist/gateway/server-methods/chat.js +461 -130
  98. package/dist/gateway/server-methods/config.js +193 -150
  99. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  100. package/dist/gateway/server-methods/nodes.js +251 -69
  101. package/dist/gateway/server-methods/push.js +53 -0
  102. package/dist/gateway/server-reload-handlers.js +2 -3
  103. package/dist/gateway/server-runtime-config.js +5 -0
  104. package/dist/gateway/server-runtime-state.js +2 -0
  105. package/dist/gateway/server-ws-runtime.js +1 -0
  106. package/dist/gateway/server.impl.js +296 -139
  107. package/dist/gateway/session-preview.test-helpers.js +11 -0
  108. package/dist/gateway/startup-auth.js +126 -0
  109. package/dist/gateway/test-helpers.agent-results.js +15 -0
  110. package/dist/gateway/test-helpers.mocks.js +37 -14
  111. package/dist/gateway/test-helpers.server.js +161 -77
  112. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  113. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  114. package/dist/infra/archive-path.js +49 -0
  115. package/dist/infra/device-pairing.js +148 -167
  116. package/dist/infra/exec-approvals-allowlist.js +19 -70
  117. package/dist/infra/exec-approvals-analysis.js +44 -17
  118. package/dist/infra/exec-safe-bin-policy.js +269 -0
  119. package/dist/infra/fixed-window-rate-limit.js +33 -0
  120. package/dist/infra/git-root.js +61 -0
  121. package/dist/infra/heartbeat-active-hours.js +2 -2
  122. package/dist/infra/heartbeat-reason.js +40 -0
  123. package/dist/infra/heartbeat-runner.js +72 -32
  124. package/dist/infra/install-source-utils.js +91 -7
  125. package/dist/infra/node-pairing.js +50 -105
  126. package/dist/infra/npm-integrity.js +45 -0
  127. package/dist/infra/npm-pack-install.js +40 -0
  128. package/dist/infra/outbound/channel-adapters.js +20 -7
  129. package/dist/infra/outbound/message-action-runner.js +107 -327
  130. package/dist/infra/outbound/message.js +59 -36
  131. package/dist/infra/outbound/outbound-policy.js +52 -25
  132. package/dist/infra/outbound/outbound-send-service.js +58 -71
  133. package/dist/infra/pairing-files.js +10 -0
  134. package/dist/infra/plain-object.js +9 -0
  135. package/dist/infra/push-apns.js +365 -0
  136. package/dist/infra/restart-sentinel.js +16 -1
  137. package/dist/infra/restart.js +229 -26
  138. package/dist/infra/scp-host.js +54 -0
  139. package/dist/infra/update-startup.js +86 -9
  140. package/dist/media/inbound-path-policy.js +114 -0
  141. package/dist/media/input-files.js +16 -0
  142. package/dist/memory/test-manager.js +8 -0
  143. package/dist/plugin-sdk/temp-path.js +47 -0
  144. package/dist/plugins/discovery.js +217 -23
  145. package/dist/plugins/hook-runner-global.js +16 -0
  146. package/dist/plugins/loader.js +192 -26
  147. package/dist/plugins/logger.js +8 -0
  148. package/dist/plugins/manifest-registry.js +3 -0
  149. package/dist/plugins/path-safety.js +34 -0
  150. package/dist/plugins/registry.js +5 -2
  151. package/dist/plugins/runtime/index.js +271 -206
  152. package/dist/providers/github-copilot-models.js +4 -1
  153. package/dist/security/audit-channel.js +8 -19
  154. package/dist/security/audit-extra.async.js +354 -182
  155. package/dist/security/audit-extra.js +11 -1
  156. package/dist/security/audit-extra.sync.js +340 -33
  157. package/dist/security/audit-fs.js +31 -13
  158. package/dist/security/audit.js +145 -371
  159. package/dist/security/dm-policy-shared.js +24 -0
  160. package/dist/security/external-content.js +20 -8
  161. package/dist/security/fix.js +49 -85
  162. package/dist/security/scan-paths.js +20 -0
  163. package/dist/security/secret-equal.js +3 -7
  164. package/dist/security/windows-acl.js +30 -15
  165. package/dist/shared/node-list-parse.js +13 -0
  166. package/dist/shared/operator-scope-compat.js +37 -0
  167. package/dist/shared/text-chunking.js +29 -0
  168. package/dist/slack/blocks.test-helpers.js +31 -0
  169. package/dist/slack/monitor/mrkdwn.js +8 -0
  170. package/dist/telegram/bot-message-dispatch.js +366 -164
  171. package/dist/telegram/draft-stream.js +30 -7
  172. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  173. package/dist/terminal/prompt-select-styled.js +9 -0
  174. package/dist/test-utils/command-runner.js +6 -0
  175. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  176. package/dist/test-utils/model-auth-mock.js +12 -0
  177. package/dist/test-utils/provider-usage-fetch.js +14 -0
  178. package/dist/test-utils/temp-home.js +33 -0
  179. package/dist/tui/components/chat-log.js +9 -0
  180. package/dist/tui/tui-command-handlers.js +36 -27
  181. package/dist/tui/tui-event-handlers.js +122 -32
  182. package/dist/tui/tui.js +181 -45
  183. package/dist/utils/mask-api-key.js +10 -0
  184. package/dist/utils/run-with-concurrency.js +39 -0
  185. package/dist/web/media.js +4 -0
  186. package/docs/tools/slash-commands.md +5 -1
  187. package/extensions/feishu/src/external-keys.ts +19 -0
  188. package/extensions/lobster/src/windows-spawn.ts +193 -0
  189. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  190. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  191. package/package.json +1 -1
@@ -1,49 +1,39 @@
1
- import { listChannelPlugins } from "../channels/plugins/index.js";
2
- import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
1
+ import { resolveSandboxConfigForAgent } from "../agents/sandbox.js";
3
2
  import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
3
+ import { resolveBrowserControlAuth } from "../browser/control-auth.js";
4
+ import { listChannelPlugins } from "../channels/plugins/index.js";
5
+ import { formatCliCommand } from "../cli/command-format.js";
4
6
  import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
5
7
  import { resolveGatewayAuth } from "../gateway/auth.js";
6
- import { formatCliCommand } from "../cli/command-format.js";
7
8
  import { buildGatewayConnectionDetails } from "../gateway/call.js";
9
+ import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
8
10
  import { probeGateway } from "../gateway/probe.js";
9
- import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js";
10
- import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectModelHygieneFindings, collectSmallModelRiskFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
11
- import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
12
- import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js";
11
+ import { collectChannelSecurityFindings } from "./audit-channel.js";
12
+ import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectGatewayHttpNoAuthFindings, collectGatewayHttpSessionKeyOverrideFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectInstalledSkillsCodeSafetyFindings, collectMinimalProfileOverrideFindings, collectModelHygieneFindings, collectNodeDenyCommandPatternFindings, collectSmallModelRiskFindings, collectSandboxDangerousConfigFindings, collectSandboxDockerNoopFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectPluginsCodeSafetyFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
13
13
  import { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, } from "./audit-fs.js";
14
+ import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js";
14
15
  function countBySeverity(findings) {
15
16
  let critical = 0;
16
17
  let warn = 0;
17
18
  let info = 0;
18
19
  for (const f of findings) {
19
- if (f.severity === "critical")
20
+ if (f.severity === "critical") {
20
21
  critical += 1;
21
- else if (f.severity === "warn")
22
+ }
23
+ else if (f.severity === "warn") {
22
24
  warn += 1;
23
- else
25
+ }
26
+ else {
24
27
  info += 1;
28
+ }
25
29
  }
26
30
  return { critical, warn, info };
27
31
  }
28
32
  function normalizeAllowFromList(list) {
29
- if (!Array.isArray(list))
33
+ if (!Array.isArray(list)) {
30
34
  return [];
31
- return list.map((v) => String(v).trim()).filter(Boolean);
32
- }
33
- function classifyChannelWarningSeverity(message) {
34
- const s = message.toLowerCase();
35
- if (s.includes("dms: open") ||
36
- s.includes('grouppolicy="open"') ||
37
- s.includes('dmpolicy="open"')) {
38
- return "critical";
39
- }
40
- if (s.includes("allows any") || s.includes("anyone can dm") || s.includes("public")) {
41
- return "critical";
42
- }
43
- if (s.includes("locked") || s.includes("disabled")) {
44
- return "info";
45
35
  }
46
- return "warn";
36
+ return list.map((v) => String(v).trim()).filter(Boolean);
47
37
  }
48
38
  async function collectFilesystemFindings(params) {
49
39
  const findings = [];
@@ -66,7 +56,7 @@ async function collectFilesystemFindings(params) {
66
56
  checkId: "fs.state_dir.perms_world_writable",
67
57
  severity: "critical",
68
58
  title: "State dir is world-writable",
69
- detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your Poolbot state.`,
59
+ detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your Pool Bot state.`,
70
60
  remediation: formatPermissionRemediation({
71
61
  targetPath: params.stateDir,
72
62
  perms: stateDirPerms,
@@ -81,7 +71,7 @@ async function collectFilesystemFindings(params) {
81
71
  checkId: "fs.state_dir.perms_group_writable",
82
72
  severity: "warn",
83
73
  title: "State dir is group-writable",
84
- detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your Poolbot state.`,
74
+ detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your Pool Bot state.`,
85
75
  remediation: formatPermissionRemediation({
86
76
  targetPath: params.stateDir,
87
77
  perms: stateDirPerms,
@@ -113,6 +103,7 @@ async function collectFilesystemFindings(params) {
113
103
  exec: params.execIcacls,
114
104
  });
115
105
  if (configPerms.ok) {
106
+ const skipReadablePermWarnings = configPerms.isSymlink;
116
107
  if (configPerms.isSymlink) {
117
108
  findings.push({
118
109
  checkId: "fs.config.symlink",
@@ -136,7 +127,7 @@ async function collectFilesystemFindings(params) {
136
127
  }),
137
128
  });
138
129
  }
139
- else if (configPerms.worldReadable) {
130
+ else if (!skipReadablePermWarnings && configPerms.worldReadable) {
140
131
  findings.push({
141
132
  checkId: "fs.config.perms_world_readable",
142
133
  severity: "critical",
@@ -151,7 +142,7 @@ async function collectFilesystemFindings(params) {
151
142
  }),
152
143
  });
153
144
  }
154
- else if (configPerms.groupReadable) {
145
+ else if (!skipReadablePermWarnings && configPerms.groupReadable) {
155
146
  findings.push({
156
147
  checkId: "fs.config.perms_group_readable",
157
148
  severity: "warn",
@@ -181,12 +172,13 @@ function collectGatewayConfigFindings(cfg, env) {
181
172
  const hasToken = typeof auth.token === "string" && auth.token.trim().length > 0;
182
173
  const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0;
183
174
  const hasSharedSecret = (auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
184
- const hasTailscaleAuth = auth.allowTailscale === true && tailscaleMode === "serve";
175
+ const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
185
176
  const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
186
- // HTTP /tools/invoke: warn if operators re-enable dangerous tools over HTTP
187
- const gatewayCfgAny = cfg.gateway;
188
- const toolsCfg = gatewayCfgAny?.tools;
189
- const gatewayToolsAllowRaw = Array.isArray(toolsCfg?.allow) ? toolsCfg.allow : [];
177
+ // HTTP /tools/invoke is intended for narrow automation, not session orchestration/admin operations.
178
+ // If operators opt-in to re-enabling these tools over HTTP, warn loudly so the choice is explicit.
179
+ const gatewayToolsAllowRaw = Array.isArray(cfg.gateway?.tools?.allow)
180
+ ? cfg.gateway?.tools?.allow
181
+ : [];
190
182
  const gatewayToolsAllow = new Set(gatewayToolsAllowRaw
191
183
  .map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
192
184
  .filter(Boolean));
@@ -203,7 +195,7 @@ function collectGatewayConfigFindings(cfg, env) {
203
195
  "If you keep them enabled, keep gateway.bind loopback-only (or tailnet-only), restrict network exposure, and treat the gateway token/password as full-admin.",
204
196
  });
205
197
  }
206
- if (bind !== "loopback" && !hasSharedSecret) {
198
+ if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") {
207
199
  findings.push({
208
200
  checkId: "gateway.bind_no_auth",
209
201
  severity: "critical",
@@ -277,8 +269,54 @@ function collectGatewayConfigFindings(cfg, env) {
277
269
  detail: `gateway auth token is ${token.length} chars; prefer a long random token.`,
278
270
  });
279
271
  }
280
- const authCfgAny = cfg.gateway?.auth;
281
- if (bind !== "loopback" && !authCfgAny?.rateLimit) {
272
+ if (auth.mode === "trusted-proxy") {
273
+ const trustedProxies = cfg.gateway?.trustedProxies ?? [];
274
+ const trustedProxyConfig = cfg.gateway?.auth?.trustedProxy;
275
+ findings.push({
276
+ checkId: "gateway.trusted_proxy_auth",
277
+ severity: "critical",
278
+ title: "Trusted-proxy auth mode enabled",
279
+ detail: 'gateway.auth.mode="trusted-proxy" delegates authentication to a reverse proxy. ' +
280
+ "Ensure your proxy (Pomerium, Caddy, nginx) handles auth correctly and that gateway.trustedProxies " +
281
+ "only contains IPs of your actual proxy servers.",
282
+ remediation: "Verify: (1) Your proxy terminates TLS and authenticates users. " +
283
+ "(2) gateway.trustedProxies is restricted to proxy IPs only. " +
284
+ "(3) Direct access to the Gateway port is blocked by firewall. " +
285
+ "See /gateway/trusted-proxy-auth for setup guidance.",
286
+ });
287
+ if (trustedProxies.length === 0) {
288
+ findings.push({
289
+ checkId: "gateway.trusted_proxy_no_proxies",
290
+ severity: "critical",
291
+ title: "Trusted-proxy auth enabled but no trusted proxies configured",
292
+ detail: 'gateway.auth.mode="trusted-proxy" but gateway.trustedProxies is empty. ' +
293
+ "All requests will be rejected.",
294
+ remediation: "Set gateway.trustedProxies to the IP(s) of your reverse proxy.",
295
+ });
296
+ }
297
+ if (!trustedProxyConfig?.userHeader) {
298
+ findings.push({
299
+ checkId: "gateway.trusted_proxy_no_user_header",
300
+ severity: "critical",
301
+ title: "Trusted-proxy auth missing userHeader config",
302
+ detail: 'gateway.auth.mode="trusted-proxy" but gateway.auth.trustedProxy.userHeader is not configured.',
303
+ remediation: "Set gateway.auth.trustedProxy.userHeader to the header name your proxy uses " +
304
+ '(e.g., "x-forwarded-user", "x-pomerium-claim-email").',
305
+ });
306
+ }
307
+ const allowUsers = trustedProxyConfig?.allowUsers ?? [];
308
+ if (allowUsers.length === 0) {
309
+ findings.push({
310
+ checkId: "gateway.trusted_proxy_no_allowlist",
311
+ severity: "warn",
312
+ title: "Trusted-proxy auth allows all authenticated users",
313
+ detail: "gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the Gateway.",
314
+ remediation: "Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users " +
315
+ '(e.g., ["nick@example.com"]).',
316
+ });
317
+ }
318
+ }
319
+ if (bind !== "loopback" && auth.mode !== "trusted-proxy" && !cfg.gateway?.auth?.rateLimit) {
282
320
  findings.push({
283
321
  checkId: "gateway.auth_no_rate_limit",
284
322
  severity: "warn",
@@ -290,7 +328,7 @@ function collectGatewayConfigFindings(cfg, env) {
290
328
  }
291
329
  return findings;
292
330
  }
293
- function collectBrowserControlFindings(cfg) {
331
+ function collectBrowserControlFindings(cfg, env) {
294
332
  const findings = [];
295
333
  let resolved;
296
334
  try {
@@ -306,12 +344,25 @@ function collectBrowserControlFindings(cfg) {
306
344
  });
307
345
  return findings;
308
346
  }
309
- if (!resolved.enabled)
347
+ if (!resolved.enabled) {
310
348
  return findings;
349
+ }
350
+ const browserAuth = resolveBrowserControlAuth(cfg, env);
351
+ if (!browserAuth.token && !browserAuth.password) {
352
+ findings.push({
353
+ checkId: "browser.control_no_auth",
354
+ severity: "critical",
355
+ title: "Browser control has no auth",
356
+ detail: "Browser control HTTP routes are enabled but no gateway.auth token/password is configured. " +
357
+ "Any local process (or SSRF to loopback) can call browser control endpoints.",
358
+ remediation: "Set gateway.auth.token (recommended) or gateway.auth.password so browser control HTTP routes require authentication. Restarting the gateway will auto-generate gateway.auth.token when browser control is enabled.",
359
+ });
360
+ }
311
361
  for (const name of Object.keys(resolved.profiles)) {
312
362
  const profile = resolveProfile(resolved, name);
313
- if (!profile || profile.cdpIsLoopback)
363
+ if (!profile || profile.cdpIsLoopback) {
314
364
  continue;
365
+ }
315
366
  let url;
316
367
  try {
317
368
  url = new URL(profile.cdpUrl);
@@ -333,8 +384,9 @@ function collectBrowserControlFindings(cfg) {
333
384
  }
334
385
  function collectLoggingFindings(cfg) {
335
386
  const redact = cfg.logging?.redactSensitive;
336
- if (redact !== "off")
387
+ if (redact !== "off") {
337
388
  return [];
389
+ }
338
390
  return [
339
391
  {
340
392
  checkId: "logging.redact_off",
@@ -350,10 +402,12 @@ function collectElevatedFindings(cfg) {
350
402
  const enabled = cfg.tools?.elevated?.enabled;
351
403
  const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
352
404
  const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
353
- if (enabled === false)
405
+ if (enabled === false) {
354
406
  return findings;
355
- if (!anyAllowFromKeys)
407
+ }
408
+ if (!anyAllowFromKeys) {
356
409
  return findings;
410
+ }
357
411
  for (const [provider, list] of Object.entries(allowFrom)) {
358
412
  const normalized = normalizeAllowFromList(list);
359
413
  if (normalized.includes("*")) {
@@ -375,310 +429,39 @@ function collectElevatedFindings(cfg) {
375
429
  }
376
430
  return findings;
377
431
  }
378
- async function collectChannelSecurityFindings(params) {
432
+ function collectExecRuntimeFindings(cfg) {
379
433
  const findings = [];
380
- const coerceNativeSetting = (value) => {
381
- if (value === true)
382
- return true;
383
- if (value === false)
384
- return false;
385
- if (value === "auto")
386
- return "auto";
387
- return undefined;
388
- };
389
- const warnDmPolicy = async (input) => {
390
- const policyPath = input.policyPath ?? `${input.allowFromPath}policy`;
391
- const configAllowFrom = normalizeAllowFromList(input.allowFrom);
392
- const hasWildcard = configAllowFrom.includes("*");
393
- const dmScope = params.cfg.session?.dmScope ?? "main";
394
- const storeAllowFrom = await readChannelAllowFromStore(input.provider).catch(() => []);
395
- const normalizeEntry = input.normalizeEntry ?? ((value) => value);
396
- const normalizedCfg = configAllowFrom
397
- .filter((value) => value !== "*")
398
- .map((value) => normalizeEntry(value))
399
- .map((value) => value.trim())
400
- .filter(Boolean);
401
- const normalizedStore = storeAllowFrom
402
- .map((value) => normalizeEntry(value))
403
- .map((value) => value.trim())
404
- .filter(Boolean);
405
- const allowCount = Array.from(new Set([...normalizedCfg, ...normalizedStore])).length;
406
- const isMultiUserDm = hasWildcard || allowCount > 1;
407
- if (input.dmPolicy === "open") {
408
- const allowFromKey = `${input.allowFromPath}allowFrom`;
409
- findings.push({
410
- checkId: `channels.${input.provider}.dm.open`,
411
- severity: "critical",
412
- title: `${input.label} DMs are open`,
413
- detail: `${policyPath}="open" allows anyone to DM the bot.`,
414
- remediation: `Use pairing/allowlist; if you really need open DMs, ensure ${allowFromKey} includes "*".`,
415
- });
416
- if (!hasWildcard) {
417
- findings.push({
418
- checkId: `channels.${input.provider}.dm.open_invalid`,
419
- severity: "warn",
420
- title: `${input.label} DM config looks inconsistent`,
421
- detail: `"open" requires ${allowFromKey} to include "*".`,
422
- });
423
- }
424
- }
425
- if (input.dmPolicy === "disabled") {
426
- findings.push({
427
- checkId: `channels.${input.provider}.dm.disabled`,
428
- severity: "info",
429
- title: `${input.label} DMs are disabled`,
430
- detail: `${policyPath}="disabled" ignores inbound DMs.`,
431
- });
432
- return;
433
- }
434
- if (dmScope === "main" && isMultiUserDm) {
435
- findings.push({
436
- checkId: `channels.${input.provider}.dm.scope_main_multiuser`,
437
- severity: "warn",
438
- title: `${input.label} DMs share the main session`,
439
- detail: "Multiple DM senders currently share the main session, which can leak context across users.",
440
- remediation: 'Set session.dmScope="per-channel-peer" to isolate DM sessions per sender.',
441
- });
442
- }
443
- };
444
- for (const plugin of params.plugins) {
445
- if (!plugin.security)
446
- continue;
447
- const accountIds = plugin.config.listAccountIds(params.cfg);
448
- const defaultAccountId = resolveChannelDefaultAccountId({
449
- plugin,
450
- cfg: params.cfg,
451
- accountIds,
434
+ const globalExecHost = cfg.tools?.exec?.host;
435
+ const defaultSandboxMode = resolveSandboxConfigForAgent(cfg).mode;
436
+ const defaultHostIsExplicitSandbox = globalExecHost === "sandbox";
437
+ if (defaultHostIsExplicitSandbox && defaultSandboxMode === "off") {
438
+ findings.push({
439
+ checkId: "tools.exec.host_sandbox_no_sandbox_defaults",
440
+ severity: "warn",
441
+ title: "Exec host is sandbox but sandbox mode is off",
442
+ detail: "tools.exec.host is explicitly set to sandbox while agents.defaults.sandbox.mode=off. " +
443
+ "In this mode, exec runs directly on the gateway host.",
444
+ remediation: 'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) or set tools.exec.host to "gateway" with approvals.',
452
445
  });
453
- const account = plugin.config.resolveAccount(params.cfg, defaultAccountId);
454
- const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true;
455
- if (!enabled)
456
- continue;
457
- const configured = plugin.config.isConfigured
458
- ? await plugin.config.isConfigured(account, params.cfg)
459
- : true;
460
- if (!configured)
461
- continue;
462
- if (plugin.id === "discord") {
463
- const discordCfg = account?.config ??
464
- {};
465
- const nativeEnabled = resolveNativeCommandsEnabled({
466
- providerId: "discord",
467
- providerSetting: coerceNativeSetting(discordCfg.commands?.native),
468
- globalSetting: params.cfg.commands?.native,
469
- });
470
- const nativeSkillsEnabled = resolveNativeSkillsEnabled({
471
- providerId: "discord",
472
- providerSetting: coerceNativeSetting(discordCfg.commands?.nativeSkills),
473
- globalSetting: params.cfg.commands?.nativeSkills,
474
- });
475
- const slashEnabled = nativeEnabled || nativeSkillsEnabled;
476
- if (slashEnabled) {
477
- const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
478
- const groupPolicy = discordCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
479
- const guildEntries = discordCfg.guilds ?? {};
480
- const guildsConfigured = Object.keys(guildEntries).length > 0;
481
- const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
482
- if (!guild || typeof guild !== "object")
483
- return false;
484
- const g = guild;
485
- if (Array.isArray(g.users) && g.users.length > 0)
486
- return true;
487
- const channels = g.channels;
488
- if (!channels || typeof channels !== "object")
489
- return false;
490
- return Object.values(channels).some((channel) => {
491
- if (!channel || typeof channel !== "object")
492
- return false;
493
- const c = channel;
494
- return Array.isArray(c.users) && c.users.length > 0;
495
- });
496
- });
497
- const dmAllowFromRaw = discordCfg.dm?.allowFrom;
498
- const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
499
- const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
500
- const ownerAllowFromConfigured = normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
501
- const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
502
- if (!useAccessGroups &&
503
- groupPolicy !== "disabled" &&
504
- guildsConfigured &&
505
- !hasAnyUserAllowlist) {
506
- findings.push({
507
- checkId: "channels.discord.commands.native.unrestricted",
508
- severity: "critical",
509
- title: "Discord slash commands are unrestricted",
510
- detail: "commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
511
- remediation: "Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
512
- });
513
- }
514
- else if (useAccessGroups &&
515
- groupPolicy !== "disabled" &&
516
- guildsConfigured &&
517
- !ownerAllowFromConfigured &&
518
- !hasAnyUserAllowlist) {
519
- findings.push({
520
- checkId: "channels.discord.commands.native.no_allowlists",
521
- severity: "warn",
522
- title: "Discord slash commands have no allowlists",
523
- detail: "Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
524
- remediation: "Add your user id to channels.discord.dm.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
525
- });
526
- }
527
- }
528
- }
529
- if (plugin.id === "slack") {
530
- const slackCfg = account
531
- ?.config ?? {};
532
- const nativeEnabled = resolveNativeCommandsEnabled({
533
- providerId: "slack",
534
- providerSetting: coerceNativeSetting(slackCfg.commands?.native),
535
- globalSetting: params.cfg.commands?.native,
536
- });
537
- const nativeSkillsEnabled = resolveNativeSkillsEnabled({
538
- providerId: "slack",
539
- providerSetting: coerceNativeSetting(slackCfg.commands?.nativeSkills),
540
- globalSetting: params.cfg.commands?.nativeSkills,
541
- });
542
- const slashCommandEnabled = nativeEnabled ||
543
- nativeSkillsEnabled ||
544
- slackCfg.slashCommand?.enabled === true;
545
- if (slashCommandEnabled) {
546
- const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
547
- if (!useAccessGroups) {
548
- findings.push({
549
- checkId: "channels.slack.commands.slash.useAccessGroups_off",
550
- severity: "critical",
551
- title: "Slack slash commands bypass access groups",
552
- detail: "Slack slash/native commands are enabled while commands.useAccessGroups=false; this can allow unrestricted /… command execution from channels/users you didn't explicitly authorize.",
553
- remediation: "Set commands.useAccessGroups=true (recommended).",
554
- });
555
- }
556
- else {
557
- const dmAllowFromRaw = account?.dm
558
- ?.allowFrom;
559
- const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
560
- const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
561
- const ownerAllowFromConfigured = normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
562
- const channels = slackCfg.channels ?? {};
563
- const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
564
- if (!value || typeof value !== "object")
565
- return false;
566
- const channel = value;
567
- return Array.isArray(channel.users) && channel.users.length > 0;
568
- });
569
- if (!ownerAllowFromConfigured && !hasAnyChannelUsersAllowlist) {
570
- findings.push({
571
- checkId: "channels.slack.commands.slash.no_allowlists",
572
- severity: "warn",
573
- title: "Slack slash commands have no allowlists",
574
- detail: "Slack slash/native commands are enabled, but neither an owner allowFrom list nor any channels.<id>.users allowlist is configured; /… commands will be rejected for everyone.",
575
- remediation: "Approve yourself via pairing (recommended), or set channels.slack.dm.allowFrom and/or channels.slack.channels.<id>.users.",
576
- });
577
- }
578
- }
579
- }
580
- }
581
- const dmPolicy = plugin.security.resolveDmPolicy?.({
582
- cfg: params.cfg,
583
- accountId: defaultAccountId,
584
- account,
446
+ }
447
+ const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
448
+ const riskyAgents = agents
449
+ .filter((entry) => entry &&
450
+ typeof entry === "object" &&
451
+ typeof entry.id === "string" &&
452
+ entry.tools?.exec?.host === "sandbox" &&
453
+ resolveSandboxConfigForAgent(cfg, entry.id).mode === "off")
454
+ .map((entry) => entry.id)
455
+ .slice(0, 5);
456
+ if (riskyAgents.length > 0) {
457
+ findings.push({
458
+ checkId: "tools.exec.host_sandbox_no_sandbox_agents",
459
+ severity: "warn",
460
+ title: "Agent exec host uses sandbox while sandbox mode is off",
461
+ detail: `agents.list.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. ` +
462
+ "With sandbox mode off, exec runs directly on the gateway host.",
463
+ remediation: 'Enable sandbox mode for these agents (`agents.list[].sandbox.mode`) or set their tools.exec.host to "gateway".',
585
464
  });
586
- if (dmPolicy) {
587
- await warnDmPolicy({
588
- label: plugin.meta.label ?? plugin.id,
589
- provider: plugin.id,
590
- dmPolicy: dmPolicy.policy,
591
- allowFrom: dmPolicy.allowFrom,
592
- policyPath: dmPolicy.policyPath,
593
- allowFromPath: dmPolicy.allowFromPath,
594
- normalizeEntry: dmPolicy.normalizeEntry,
595
- });
596
- }
597
- if (plugin.security.collectWarnings) {
598
- const warnings = await plugin.security.collectWarnings({
599
- cfg: params.cfg,
600
- accountId: defaultAccountId,
601
- account,
602
- });
603
- for (const message of warnings ?? []) {
604
- const trimmed = String(message).trim();
605
- if (!trimmed)
606
- continue;
607
- findings.push({
608
- checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
609
- severity: classifyChannelWarningSeverity(trimmed),
610
- title: `${plugin.meta.label ?? plugin.id} security warning`,
611
- detail: trimmed.replace(/^-\s*/, ""),
612
- });
613
- }
614
- }
615
- if (plugin.id === "telegram") {
616
- const allowTextCommands = params.cfg.commands?.text !== false;
617
- if (!allowTextCommands)
618
- continue;
619
- const telegramCfg = account?.config ??
620
- {};
621
- const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
622
- const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
623
- const groups = telegramCfg.groups;
624
- const groupsConfigured = Boolean(groups) && Object.keys(groups ?? {}).length > 0;
625
- const groupAccessPossible = groupPolicy === "open" || (groupPolicy === "allowlist" && groupsConfigured);
626
- if (!groupAccessPossible)
627
- continue;
628
- const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []);
629
- const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*");
630
- const groupAllowFrom = Array.isArray(telegramCfg.groupAllowFrom)
631
- ? telegramCfg.groupAllowFrom
632
- : [];
633
- const groupAllowFromHasWildcard = groupAllowFrom.some((v) => String(v).trim() === "*");
634
- const anyGroupOverride = Boolean(groups &&
635
- Object.values(groups).some((value) => {
636
- if (!value || typeof value !== "object")
637
- return false;
638
- const group = value;
639
- const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : [];
640
- if (allowFrom.length > 0)
641
- return true;
642
- const topics = group.topics;
643
- if (!topics || typeof topics !== "object")
644
- return false;
645
- return Object.values(topics).some((topicValue) => {
646
- if (!topicValue || typeof topicValue !== "object")
647
- return false;
648
- const topic = topicValue;
649
- const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : [];
650
- return topicAllow.length > 0;
651
- });
652
- }));
653
- const hasAnySenderAllowlist = storeAllowFrom.length > 0 || groupAllowFrom.length > 0 || anyGroupOverride;
654
- if (storeHasWildcard || groupAllowFromHasWildcard) {
655
- findings.push({
656
- checkId: "channels.telegram.groups.allowFrom.wildcard",
657
- severity: "critical",
658
- title: "Telegram group allowlist contains wildcard",
659
- detail: 'Telegram group sender allowlist contains "*", which allows any group member to run /… commands and control directives.',
660
- remediation: 'Remove "*" from channels.telegram.groupAllowFrom and pairing store; prefer explicit user ids/usernames.',
661
- });
662
- continue;
663
- }
664
- if (!hasAnySenderAllowlist) {
665
- const providerSetting = telegramCfg.commands
666
- ?.nativeSkills;
667
- const skillsEnabled = resolveNativeSkillsEnabled({
668
- providerId: "telegram",
669
- providerSetting,
670
- globalSetting: params.cfg.commands?.nativeSkills,
671
- });
672
- findings.push({
673
- checkId: "channels.telegram.groups.allowFrom.missing",
674
- severity: "critical",
675
- title: "Telegram group commands have no sender allowlist",
676
- detail: `Telegram group access is enabled but no sender allowlist is configured; this allows any group member to invoke /… commands` +
677
- (skillsEnabled ? " (including skill commands)." : "."),
678
- remediation: "Approve yourself via pairing (recommended), or set channels.telegram.groupAllowFrom (or per-group groups.<id>.allowFrom).",
679
- });
680
- }
681
- }
682
465
  }
683
466
  return findings;
684
467
  }
@@ -688,29 +471,9 @@ async function maybeProbeGateway(params) {
688
471
  const isRemoteMode = params.cfg.gateway?.mode === "remote";
689
472
  const remoteUrlRaw = typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url.trim() : "";
690
473
  const remoteUrlMissing = isRemoteMode && !remoteUrlRaw;
691
- const resolveAuth = (mode) => {
692
- const authToken = params.cfg.gateway?.auth?.token;
693
- const authPassword = params.cfg.gateway?.auth?.password;
694
- const remote = params.cfg.gateway?.remote;
695
- const token = mode === "remote"
696
- ? typeof remote?.token === "string" && remote.token.trim()
697
- ? remote.token.trim()
698
- : undefined
699
- : process.env.POOLBOT_GATEWAY_TOKEN?.trim() ||
700
- process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
701
- (typeof authToken === "string" && authToken.trim() ? authToken.trim() : undefined);
702
- const password = process.env.POOLBOT_GATEWAY_PASSWORD?.trim() ||
703
- process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
704
- (mode === "remote"
705
- ? typeof remote?.password === "string" && remote.password.trim()
706
- ? remote.password.trim()
707
- : undefined
708
- : typeof authPassword === "string" && authPassword.trim()
709
- ? authPassword.trim()
710
- : undefined);
711
- return { token, password };
712
- };
713
- const auth = !isRemoteMode || remoteUrlMissing ? resolveAuth("local") : resolveAuth("remote");
474
+ const auth = !isRemoteMode || remoteUrlMissing
475
+ ? resolveGatewayProbeAuth({ cfg: params.cfg, mode: "local" })
476
+ : resolveGatewayProbeAuth({ cfg: params.cfg, mode: "remote" });
714
477
  const res = await params.probe({ url, auth, timeoutMs: params.timeoutMs }).catch((err) => ({
715
478
  ok: false,
716
479
  url,
@@ -743,10 +506,17 @@ export async function runSecurityAudit(opts) {
743
506
  findings.push(...collectAttackSurfaceSummaryFindings(cfg));
744
507
  findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
745
508
  findings.push(...collectGatewayConfigFindings(cfg, env));
746
- findings.push(...collectBrowserControlFindings(cfg));
509
+ findings.push(...collectBrowserControlFindings(cfg, env));
747
510
  findings.push(...collectLoggingFindings(cfg));
748
511
  findings.push(...collectElevatedFindings(cfg));
749
- findings.push(...collectHooksHardeningFindings(cfg));
512
+ findings.push(...collectExecRuntimeFindings(cfg));
513
+ findings.push(...collectHooksHardeningFindings(cfg, env));
514
+ findings.push(...collectGatewayHttpNoAuthFindings(cfg, env));
515
+ findings.push(...collectGatewayHttpSessionKeyOverrideFindings(cfg));
516
+ findings.push(...collectSandboxDockerNoopFindings(cfg));
517
+ findings.push(...collectSandboxDangerousConfigFindings(cfg));
518
+ findings.push(...collectNodeDenyCommandPatternFindings(cfg));
519
+ findings.push(...collectMinimalProfileOverrideFindings(cfg));
750
520
  findings.push(...collectSecretsInConfigFindings(cfg));
751
521
  findings.push(...collectModelHygieneFindings(cfg));
752
522
  findings.push(...collectSmallModelRiskFindings({ cfg, env }));
@@ -767,6 +537,10 @@ export async function runSecurityAudit(opts) {
767
537
  }
768
538
  findings.push(...(await collectStateDeepFilesystemFindings({ cfg, env, stateDir, platform, execIcacls })));
769
539
  findings.push(...(await collectPluginsTrustFindings({ cfg, stateDir })));
540
+ if (opts.deep === true) {
541
+ findings.push(...(await collectPluginsCodeSafetyFindings({ stateDir })));
542
+ findings.push(...(await collectInstalledSkillsCodeSafetyFindings({ cfg, stateDir })));
543
+ }
770
544
  }
771
545
  if (opts.includeChannelSecurity !== false) {
772
546
  const plugins = opts.plugins ?? listChannelPlugins();
@@ -779,7 +553,7 @@ export async function runSecurityAudit(opts) {
779
553
  probe: opts.probeGatewayFn ?? probeGateway,
780
554
  })
781
555
  : undefined;
782
- if (deep?.gateway?.attempted && deep.gateway.ok === false) {
556
+ if (deep?.gateway?.attempted && !deep.gateway.ok) {
783
557
  findings.push({
784
558
  checkId: "gateway.probe_failed",
785
559
  severity: "warn",