@poolzin/pool-bot 2026.2.8 → 2026.2.10

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 CHANGED
@@ -1,3 +1,22 @@
1
+ ## v2026.2.10 (2026-02-16)
2
+
3
+ ### Fixes
4
+ - Reduce default exec tool timeout from 1800s (30 min) to 120s (2 min) — prevents bot from silently hanging on stuck bash commands; per-call timeout param and config `tools.exec.timeoutSec` still override; new env vars `POOLBOT_EXEC_TIMEOUT_SEC` / `CLAWDBOT_EXEC_TIMEOUT_SEC` for further control
5
+
6
+ ---
7
+
8
+ ## v2026.2.9 (2026-02-15)
9
+
10
+ ### Improvements
11
+ - Security audit hardening: warn on missing auth rate limiting for non-loopback gateways, flag dangerous HTTP tool allow-lists
12
+ - Embedding input limits: new helpers to split oversized text chunks before sending to embedding providers
13
+ - Updated default local embedding model to `embeddinggemma-300m-qat-Q8_0` (quantization-aware trained variant)
14
+
15
+ ### Cleanup
16
+ - Removed 6 dead code files (836 lines) from upstream port f494b78 — ollama-stream, tool-policy-pipeline, tool-mutation, compaction-safety-timeout, compaction-timeout, wait-for-idle-before-flush
17
+
18
+ ---
19
+
1
20
  ## v2026.2.8 (2026-02-15)
2
21
 
3
22
  ### Features
@@ -58,6 +58,11 @@ function validateHostEnv(env) {
58
58
  const DEFAULT_MAX_OUTPUT = clampNumber(readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
59
59
  const DEFAULT_PENDING_MAX_OUTPUT = clampNumber(readEnvInt("POOLBOT_BASH_PENDING_MAX_OUTPUT_CHARS") ??
60
60
  readEnvInt("CLAWDBOT_BASH_PENDING_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000);
61
+ // Default exec timeout: 120s is generous for typical commands (grep, cat, ls, build).
62
+ // The AI can still pass a per-call `timeout` for intentionally long operations,
63
+ // and users can override via config (`tools.exec.timeoutSec`) or env var.
64
+ // Previous default (1800s / 30 min) caused the bot to hang silently on stuck commands.
65
+ const DEFAULT_EXEC_TIMEOUT_SEC = clampNumber(readEnvInt("POOLBOT_EXEC_TIMEOUT_SEC") ?? readEnvInt("CLAWDBOT_EXEC_TIMEOUT_SEC"), 120, 1, 86_400);
61
66
  const DEFAULT_PATH = process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
62
67
  const DEFAULT_NOTIFY_TAIL_CHARS = 400;
63
68
  const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000;
@@ -73,7 +78,7 @@ const execSchema = Type.Object({
73
78
  })),
74
79
  background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })),
75
80
  timeout: Type.Optional(Type.Number({
76
- description: "Timeout in seconds (optional, kills process on expiry)",
81
+ description: "Timeout in seconds (default 120, kills process on expiry). Set higher for long builds/downloads.",
77
82
  })),
78
83
  pty: Type.Optional(Type.Boolean({
79
84
  description: "Run in a pseudo-terminal (PTY) when available (TTY-required CLIs, coding agents)",
@@ -559,7 +564,7 @@ export function createExecTool(defaults) {
559
564
  const allowBackground = defaults?.allowBackground ?? true;
560
565
  const defaultTimeoutSec = typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
561
566
  ? defaults.timeoutSec
562
- : 1800;
567
+ : DEFAULT_EXEC_TIMEOUT_SEC;
563
568
  const defaultPathPrepend = normalizePathPrepend(defaults?.pathPrepend);
564
569
  const safeBins = resolveSafeBins(defaults?.safeBins);
565
570
  const notifyOnExit = defaults?.notifyOnExit !== false;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.2.8",
3
- "commit": "4f7a76b8949932fd105f49e6b89efd93bc69d3cc",
4
- "builtAt": "2026-02-15T16:46:23.867Z"
2
+ "version": "2026.2.10",
3
+ "commit": "e66cd57a7cc2217c8fa111edab895a49da8dc5e0",
4
+ "builtAt": "2026-02-16T05:50:01.218Z"
5
5
  }
@@ -0,0 +1,22 @@
1
+ import { estimateUtf8Bytes, splitTextToUtf8ByteLimit } from "./embedding-input-limits.js";
2
+ import { resolveEmbeddingMaxInputTokens } from "./embedding-model-limits.js";
3
+ import { hashText } from "./internal.js";
4
+ export function enforceEmbeddingMaxInputTokens(provider, chunks) {
5
+ const maxInputTokens = resolveEmbeddingMaxInputTokens(provider);
6
+ const out = [];
7
+ for (const chunk of chunks) {
8
+ if (estimateUtf8Bytes(chunk.text) <= maxInputTokens) {
9
+ out.push(chunk);
10
+ continue;
11
+ }
12
+ for (const text of splitTextToUtf8ByteLimit(chunk.text, maxInputTokens)) {
13
+ out.push({
14
+ startLine: chunk.startLine,
15
+ endLine: chunk.endLine,
16
+ text,
17
+ hash: hashText(text),
18
+ });
19
+ }
20
+ }
21
+ return out;
22
+ }
@@ -0,0 +1,56 @@
1
+ // Helpers for enforcing embedding model input size limits.
2
+ //
3
+ // We use UTF-8 byte length as a conservative upper bound for tokenizer output.
4
+ // Tokenizers operate over bytes; a token must contain at least one byte, so
5
+ // token_count <= utf8_byte_length.
6
+ export function estimateUtf8Bytes(text) {
7
+ if (!text) {
8
+ return 0;
9
+ }
10
+ return Buffer.byteLength(text, "utf8");
11
+ }
12
+ export function splitTextToUtf8ByteLimit(text, maxUtf8Bytes) {
13
+ if (maxUtf8Bytes <= 0) {
14
+ return [text];
15
+ }
16
+ if (estimateUtf8Bytes(text) <= maxUtf8Bytes) {
17
+ return [text];
18
+ }
19
+ const parts = [];
20
+ let cursor = 0;
21
+ while (cursor < text.length) {
22
+ let low = cursor + 1;
23
+ let high = Math.min(text.length, cursor + maxUtf8Bytes);
24
+ let best = cursor;
25
+ while (low <= high) {
26
+ const mid = Math.floor((low + high) / 2);
27
+ const bytes = estimateUtf8Bytes(text.slice(cursor, mid));
28
+ if (bytes <= maxUtf8Bytes) {
29
+ best = mid;
30
+ low = mid + 1;
31
+ }
32
+ else {
33
+ high = mid - 1;
34
+ }
35
+ }
36
+ if (best <= cursor) {
37
+ best = Math.min(text.length, cursor + 1);
38
+ }
39
+ // Avoid splitting inside a surrogate pair.
40
+ if (best < text.length &&
41
+ best > cursor &&
42
+ text.charCodeAt(best - 1) >= 0xd800 &&
43
+ text.charCodeAt(best - 1) <= 0xdbff &&
44
+ text.charCodeAt(best) >= 0xdc00 &&
45
+ text.charCodeAt(best) <= 0xdfff) {
46
+ best -= 1;
47
+ }
48
+ const part = text.slice(cursor, best);
49
+ if (!part) {
50
+ break;
51
+ }
52
+ parts.push(part);
53
+ cursor = best;
54
+ }
55
+ return parts;
56
+ }
@@ -0,0 +1,24 @@
1
+ const DEFAULT_EMBEDDING_MAX_INPUT_TOKENS = 8192;
2
+ const KNOWN_EMBEDDING_MAX_INPUT_TOKENS = {
3
+ "openai:text-embedding-3-small": 8192,
4
+ "openai:text-embedding-3-large": 8192,
5
+ "openai:text-embedding-ada-002": 8191,
6
+ "gemini:text-embedding-004": 2048,
7
+ "voyage:voyage-3": 32000,
8
+ "voyage:voyage-3-lite": 16000,
9
+ "voyage:voyage-code-3": 32000,
10
+ };
11
+ export function resolveEmbeddingMaxInputTokens(provider) {
12
+ if (typeof provider.maxInputTokens === "number") {
13
+ return provider.maxInputTokens;
14
+ }
15
+ const key = `${provider.id}:${provider.model}`.toLowerCase();
16
+ const known = KNOWN_EMBEDDING_MAX_INPUT_TOKENS[key];
17
+ if (typeof known === "number") {
18
+ return known;
19
+ }
20
+ if (provider.id.toLowerCase() === "gemini") {
21
+ return 2048;
22
+ }
23
+ return DEFAULT_EMBEDDING_MAX_INPUT_TOKENS;
24
+ }
@@ -13,7 +13,7 @@ function sanitizeAndNormalizeEmbedding(vec) {
13
13
  }
14
14
  return sanitized.map((value) => value / magnitude);
15
15
  }
16
- const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
16
+ export const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf";
17
17
  function canAutoSelectLocal(options) {
18
18
  const modelPath = options.local?.modelPath?.trim();
19
19
  if (!modelPath) {
@@ -6,6 +6,7 @@ import { resolveGatewayAuth } from "../gateway/auth.js";
6
6
  import { formatCliCommand } from "../cli/command-format.js";
7
7
  import { buildGatewayConnectionDetails } from "../gateway/call.js";
8
8
  import { probeGateway } from "../gateway/probe.js";
9
+ import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js";
9
10
  import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectModelHygieneFindings, collectSmallModelRiskFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
10
11
  import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
11
12
  import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js";
@@ -182,6 +183,26 @@ function collectGatewayConfigFindings(cfg, env) {
182
183
  const hasSharedSecret = (auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
183
184
  const hasTailscaleAuth = auth.allowTailscale === true && tailscaleMode === "serve";
184
185
  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 : [];
190
+ const gatewayToolsAllow = new Set(gatewayToolsAllowRaw
191
+ .map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
192
+ .filter(Boolean));
193
+ const reenabledOverHttp = DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => gatewayToolsAllow.has(name));
194
+ if (reenabledOverHttp.length > 0) {
195
+ const extraRisk = bind !== "loopback" || tailscaleMode === "funnel";
196
+ findings.push({
197
+ checkId: "gateway.tools_invoke_http.dangerous_allow",
198
+ severity: extraRisk ? "critical" : "warn",
199
+ title: "Gateway HTTP /tools/invoke re-enables dangerous tools",
200
+ detail: `gateway.tools.allow includes ${reenabledOverHttp.join(", ")} which removes them from the default HTTP deny list. ` +
201
+ "This can allow remote session spawning / control-plane actions via HTTP and increases RCE blast radius if the gateway is reachable.",
202
+ remediation: "Remove these entries from gateway.tools.allow (recommended). " +
203
+ "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
+ });
205
+ }
185
206
  if (bind !== "loopback" && !hasSharedSecret) {
186
207
  findings.push({
187
208
  checkId: "gateway.bind_no_auth",
@@ -256,6 +277,17 @@ function collectGatewayConfigFindings(cfg, env) {
256
277
  detail: `gateway auth token is ${token.length} chars; prefer a long random token.`,
257
278
  });
258
279
  }
280
+ const authCfgAny = cfg.gateway?.auth;
281
+ if (bind !== "loopback" && !authCfgAny?.rateLimit) {
282
+ findings.push({
283
+ checkId: "gateway.auth_no_rate_limit",
284
+ severity: "warn",
285
+ title: "No auth rate limiting configured",
286
+ detail: "gateway.bind is not loopback but no gateway.auth.rateLimit is configured. " +
287
+ "Without rate limiting, brute-force auth attacks are not mitigated.",
288
+ remediation: "Set gateway.auth.rateLimit (e.g. { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000 }).",
289
+ });
290
+ }
259
291
  return findings;
260
292
  }
261
293
  function collectBrowserControlFindings(cfg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolzin/pool-bot",
3
- "version": "2026.2.8",
3
+ "version": "2026.2.10",
4
4
  "description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
5
5
  "keywords": [],
6
6
  "license": "MIT",