@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.
- package/CHANGELOG.md +29 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/onboard-helpers.js +4 -4
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/compat/legacy-names.js +2 -2
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
- package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- 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
|
|
136
|
-
if (!
|
|
137
|
-
return
|
|
128
|
+
function hasConfiguredDockerConfig(docker) {
|
|
129
|
+
if (!docker || typeof docker !== "object") {
|
|
130
|
+
return false;
|
|
138
131
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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 =
|
|
182
|
+
const globalPolicy = pickSandboxToolPolicy(params.cfg.tools ?? undefined);
|
|
154
183
|
if (globalPolicy) {
|
|
155
184
|
policies.push(globalPolicy);
|
|
156
185
|
}
|
|
157
|
-
const agentPolicy =
|
|
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
|
|
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: ${
|
|
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
|
-
:
|
|
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
|
-
|
|
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:
|
|
53
|
-
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:
|
|
67
|
-
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:
|
|
81
|
-
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
|
}
|