@poolzin/pool-bot 2026.1.34 → 2026.1.35

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 (33) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/agents/tool-security-helpers.js +66 -0
  3. package/dist/agents/tool-security.js +96 -0
  4. package/dist/agents/workspace.js +5 -1
  5. package/dist/build-info.json +3 -3
  6. package/dist/channels/plugins/agent-tools/whatsapp-login.js +17 -1
  7. package/dist/commands/doctor-state-integrity.js +14 -2
  8. package/dist/config/types.js +1 -0
  9. package/dist/config/types.security.js +5 -0
  10. package/dist/config/zod-schema.js +6 -0
  11. package/dist/discord/monitor/message-handler.process.js +4 -6
  12. package/dist/gateway/client.js +14 -0
  13. package/dist/gateway/url-validation.js +94 -0
  14. package/dist/media/path-sanitization.js +78 -0
  15. package/dist/slack/monitor/message-handler/prepare.js +4 -10
  16. package/dist/slack/monitor/slash.js +4 -10
  17. package/extensions/googlechat/node_modules/.bin/poolbot +0 -0
  18. package/extensions/line/node_modules/.bin/poolbot +0 -0
  19. package/extensions/matrix/node_modules/.bin/markdown-it +0 -0
  20. package/extensions/matrix/node_modules/.bin/poolbot +0 -0
  21. package/extensions/memory-core/node_modules/.bin/poolbot +0 -0
  22. package/extensions/memory-lancedb/node_modules/.bin/openai +0 -0
  23. package/extensions/msteams/node_modules/.bin/poolbot +0 -0
  24. package/extensions/nostr/node_modules/.bin/poolbot +0 -0
  25. package/extensions/twitch/node_modules/.bin/poolbot +0 -0
  26. package/extensions/zalo/node_modules/.bin/poolbot +0 -0
  27. package/extensions/zalouser/node_modules/.bin/poolbot +0 -0
  28. package/git-hooks/pre-commit +0 -0
  29. package/package.json +79 -69
  30. package/skills/nano-banana-pro/scripts/generate_image.py +0 -0
  31. package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +0 -0
  32. package/skills/tmux/scripts/find-sessions.sh +0 -0
  33. package/skills/tmux/scripts/wait-for-text.sh +0 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to Pool Bot will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2026.1.35] - 2026-02-06
9
+
10
+ ### 📦 Stability Snapshot
11
+
12
+ **Release Type:** Pre-enhancement checkpoint
13
+
14
+ #### Summary
15
+ This release is a stable checkpoint before implementing local memory search enhancements.
16
+
17
+ #### Changes
18
+ - **Version bump:** 2026.1.30 → 2026.1.35
19
+ - **Repository sync:** All repositories clean and synchronized
20
+ - **Backup verification:** Complete backup of configurations and state
21
+
22
+ #### Technical Details
23
+ - Package: @poolzin/pool-bot
24
+ - Node.js: v22.22.0 compatible
25
+ - Backup: /root/poolbot-backup-20260206-130344.tar.gz (35MB)
26
+
27
+ #### Next Steps (Post-Release)
28
+ - Implement Ollama integration for local memory_search
29
+ - Complete QMD embeddings (220 pending → 0)
30
+ - Remove auto-study skill (user-managed cron)
31
+ - Enhance memory architecture with local embeddings
32
+
33
+ #### Maintenance
34
+ - All repositories verified clean
35
+ - Git states documented
36
+ - Gateway status captured
37
+
38
+ ---
39
+
40
+ ## v2026.1.30 (2026-02-05)
41
+
42
+ ### 🔒 Security Release - CRITICAL VULNERABILITY FIXES
43
+
44
+ #### Critical Vulnerabilities Fixed 🔴
45
+ - **Gateway URL Override (CVE-level):** Prevent credential leakage via malicious gateway redirects
46
+ - Added `validateGatewayUrlSecurity()` requiring explicit token confirmation for custom URLs
47
+ - Custom URLs now require 32+ character tokens
48
+ - Validation applied in `src/gateway/client.ts` before connection
49
+
50
+ - **Discord Prompt Injection (HIGH):** Block arbitrary command injection via channel.topic
51
+ - Removed `channel.topic` from system prompts in `src/discord/monitor/message-handler.process.ts`
52
+ - Only admin-controlled metadata is now included
53
+ - Prevents users with edit permissions from manipulating agent behavior
54
+
55
+ - **Slack Prompt Injection (HIGH):** Block arbitrary command injection via topic/purpose
56
+ - Removed `channel.topic` and `channel.purpose` from system prompts
57
+ - Fixed in `src/slack/monitor/message-handler/prepare.ts` and `src/slack/monitor/slash.ts`
58
+ - Same attack vector as Discord, now mitigated
59
+
60
+ #### Security Features Added ✅
61
+ - **Tool Gating System:** Owner-only access to sensitive tools
62
+ - `validateToolAccess()` function in `src/agents/tool-security.ts`
63
+ - Protected tools: `whatsapp_login`, `gateway`, `cron`, `sessions_spawn`
64
+ - Configurable via `security.owners` in poolbot.json
65
+
66
+ - **Security Utilities:** Prevention and validation functions
67
+ - Path sanitization utilities in `src/media/path-sanitization.ts`
68
+ - URL validation in `src/gateway/url-validation.ts`
69
+ - Security config schema in `src/config/types.security.ts`
70
+
71
+ #### Documentation
72
+ - **New:** `SECURITY.md` - Complete security policy and vulnerability disclosure
73
+ - Breaking changes documented
74
+ - Security best practices for users and developers
75
+ - Reporting guidelines for security issues
76
+
77
+ #### Credits
78
+ - Security improvements inspired by [OpenClaw](https://github.com/openclaw/openclaw) security hardening
79
+ - Internal security audit based on OpenClaw v2026.2.4 analysis
80
+
81
+ **UPGRADE RECOMMENDED:** All users should upgrade to v2026.1.30 due to critical security fixes.
82
+
83
+ ---
84
+
1
85
  ## v1.0.1 (2026-02-02)
2
86
 
3
87
  ### WhatsApp Gateway Stability
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tool Security Integration Helper
3
+ *
4
+ * Provides helper functions for tool security validation in plugins
5
+ * until full sender context integration is complete.
6
+ *
7
+ * @version 2026.1.30
8
+ */
9
+ import { validateToolAccess, canUseTool } from "./tool-security.js";
10
+ /**
11
+ * Create access denied response for tool execution
12
+ *
13
+ * @param toolName - Name of the tool that was denied
14
+ * @param reason - Optional reason for denial
15
+ */
16
+ export function createAccessDeniedResponse(toolName, reason) {
17
+ const message = reason ||
18
+ `â›” Access Denied\n\n` +
19
+ `Tool "${toolName}" is restricted to owners only.\n` +
20
+ `This tool requires owner permissions for security reasons.\n\n` +
21
+ `Contact the bot owner for access.`;
22
+ return {
23
+ content: [{ type: "text", text: message }],
24
+ details: { error: "access_denied", tool: toolName },
25
+ };
26
+ }
27
+ /**
28
+ * Check if a tool can be used with optional sender validation
29
+ *
30
+ * @param toolName - Tool name to validate
31
+ * @param config - Pool Bot config (for owners list)
32
+ * @param sender - Optional sender identifier
33
+ * @returns true if tool can be used, false otherwise
34
+ *
35
+ * NOTE: If sender is undefined, sensitive tools will be denied.
36
+ * This is a safe default until sender context integration is complete.
37
+ */
38
+ export function canUseToolSafe(toolName, config, sender) {
39
+ const owners = config?.security?.owners || [];
40
+ return canUseTool(toolName, sender, owners);
41
+ }
42
+ /**
43
+ * Validate tool access and return error response if denied
44
+ *
45
+ * @param toolName - Tool name to validate
46
+ * @param config - Pool Bot config (for owners list)
47
+ * @param sender - Optional sender identifier
48
+ * @returns null if access is allowed, error response if denied
49
+ *
50
+ * Usage:
51
+ * ```typescript
52
+ * const denied = validateToolAccessResponse("whatsapp_login", config, sender);
53
+ * if (denied) return denied;
54
+ * // ... proceed with tool execution
55
+ * ```
56
+ */
57
+ export function validateToolAccessResponse(toolName, config, sender) {
58
+ const owners = config?.security?.owners || [];
59
+ try {
60
+ validateToolAccess(toolName, sender, owners);
61
+ return null; // Access allowed
62
+ }
63
+ catch (err) {
64
+ return createAccessDeniedResponse(toolName, err.message);
65
+ }
66
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tool Security - Tool Gating and Access Control
3
+ *
4
+ * Based on OpenClaw 2026.2.4 security fixes:
5
+ * - Gate sensitive tools to owner senders only
6
+ * - Prevent tool hijacking by non-owners
7
+ *
8
+ * @version 2026.1.30
9
+ */
10
+ /**
11
+ * List of sensitive tools that require owner permissions
12
+ */
13
+ export const SENSITIVE_TOOLS = [
14
+ // WhatsApp
15
+ "whatsapp_login",
16
+ // Gateway management
17
+ "gateway",
18
+ "gateway:restart",
19
+ "gateway:update",
20
+ // Config management
21
+ "gateway:config:apply",
22
+ "gateway:config:patch",
23
+ // Cron management
24
+ "cron:add",
25
+ "cron:remove",
26
+ "cron:update",
27
+ // Session management
28
+ "sessions_spawn", // Pode criar sub-agentes
29
+ ];
30
+ /**
31
+ * Check if a tool is sensitive (requires owner permission)
32
+ */
33
+ export function isSensitiveTool(toolName) {
34
+ const normalized = toolName.trim().toLowerCase();
35
+ return SENSITIVE_TOOLS.some((tool) => {
36
+ const normalizedTool = tool.toLowerCase();
37
+ // Exact match ou prefix match (ex: "gateway" match "gateway:restart")
38
+ return normalized === normalizedTool || normalized.startsWith(`${normalizedTool}:`);
39
+ });
40
+ }
41
+ /**
42
+ * Check if a sender is an owner
43
+ *
44
+ * Owners are configured in poolbot.json:
45
+ * {
46
+ * "security": {
47
+ * "owners": ["+554498569337", "5140768830"]
48
+ * }
49
+ * }
50
+ */
51
+ export function isOwner(sender, owners = []) {
52
+ if (!sender)
53
+ return false;
54
+ const normalizedSender = sender.trim();
55
+ if (!normalizedSender)
56
+ return false;
57
+ // Check if sender is in owners list
58
+ return owners.some((owner) => {
59
+ const normalizedOwner = owner.trim();
60
+ return normalizedOwner === normalizedSender;
61
+ });
62
+ }
63
+ /**
64
+ * Check if a user can use a tool
65
+ *
66
+ * @param toolName - Tool name (ex: "whatsapp_login")
67
+ * @param sender - Sender identifier (phone number, user ID, etc.)
68
+ * @param owners - List of owner identifiers
69
+ * @returns true if user can use the tool, false otherwise
70
+ */
71
+ export function canUseTool(toolName, sender, owners = []) {
72
+ // Non-sensitive tools são permitidas para todos
73
+ if (!isSensitiveTool(toolName)) {
74
+ return true;
75
+ }
76
+ // Sensitive tools requerem owner permission
77
+ return isOwner(sender, owners);
78
+ }
79
+ /**
80
+ * Error message for denied tool access
81
+ */
82
+ export function toolAccessDeniedMessage(toolName) {
83
+ return (`Tool "${toolName}" is restricted to owners only. ` +
84
+ `This tool requires owner permissions for security reasons. ` +
85
+ `Contact the bot owner for access.`);
86
+ }
87
+ /**
88
+ * Validate tool access and throw if denied
89
+ *
90
+ * @throws Error if access is denied
91
+ */
92
+ export function validateToolAccess(toolName, sender, owners = []) {
93
+ if (!canUseTool(toolName, sender, owners)) {
94
+ throw new Error(toolAccessDeniedMessage(toolName));
95
+ }
96
+ }
@@ -208,7 +208,11 @@ export async function loadWorkspaceBootstrapFiles(dir) {
208
208
  filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME),
209
209
  },
210
210
  ];
211
- entries.push(...(await resolveMemoryBootstrapEntries(resolvedDir)));
211
+ // MEMORY.md is deliberately NOT loaded as a bootstrap file.
212
+ // Per AGENTS.md guidance: "use memory_search() on demand when user asks about past"
213
+ // This prevents ~9.8K tokens (~52.7% of bootstrap) from being injected into every conversation.
214
+ // MEMORY.md remains accessible via the memory_search tool when needed.
215
+ // entries.push(...(await resolveMemoryBootstrapEntries(resolvedDir)));
212
216
  const result = [];
213
217
  for (const entry of entries) {
214
218
  try {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.34",
3
- "commit": "84710f9a55bc3eca77fd7ffa882d9f11b9f92acc",
4
- "builtAt": "2026-02-05T14:57:54.102Z"
2
+ "version": "2026.1.35",
3
+ "commit": "582e7d0d30d89c8f4b16bcfdc684fd60300fb9a9",
4
+ "builtAt": "2026-02-06T13:08:41.796Z"
5
5
  }
@@ -1,5 +1,14 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- export function createWhatsAppLoginTool() {
2
+ import { validateToolAccessResponse } from "../../../agents/tool-security-helpers.js";
3
+ /**
4
+ * Create WhatsApp Login tool
5
+ *
6
+ * SECURITY: This tool is restricted to owners only.
7
+ * Unauthorized access could allow hijacking the WhatsApp instance.
8
+ *
9
+ * @param config - Optional Pool Bot config for security validation
10
+ */
11
+ export function createWhatsAppLoginTool(config) {
3
12
  return {
4
13
  label: "WhatsApp Login",
5
14
  name: "whatsapp_login",
@@ -15,6 +24,13 @@ export function createWhatsAppLoginTool() {
15
24
  force: Type.Optional(Type.Boolean()),
16
25
  }),
17
26
  execute: async (_toolCallId, args) => {
27
+ // SECURITY: Validate owner access
28
+ // NOTE: sender context integration is pending. For now, sensitive tools
29
+ // are denied unless explicitly configured with owners list.
30
+ // TODO: Integrate sender context from session/channel
31
+ const denied = validateToolAccessResponse("whatsapp_login", config, undefined);
32
+ if (denied)
33
+ return denied;
18
34
  const { startWebLoginWithQr, waitForWebLogin } = await import("../../../web/login-qr.js");
19
35
  const action = args?.action ?? "start";
20
36
  if (action === "wait") {
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { resolveDefaultAgentId } from "../agents/agent-scope.js";
5
5
  import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
6
- import { loadSessionStore, resolveMainSessionKey, resolveSessionFilePath, resolveSessionTranscriptsDirForAgent, resolveStorePath, } from "../config/sessions.js";
6
+ import { loadSessionStore, resolveMainSessionKey, resolveSessionFilePath, resolveSessionTranscriptPath, resolveSessionTranscriptsDirForAgent, resolveStorePath, } from "../config/sessions.js";
7
7
  import { note } from "../terminal/note.js";
8
8
  import { shortenHomePath } from "../utils.js";
9
9
  function existsDir(dir) {
@@ -291,7 +291,19 @@ export async function noteStateIntegrity(cfg, prompter, configPath) {
291
291
  const transcriptPath = resolveSessionFilePath(sessionId, entry, {
292
292
  agentId,
293
293
  });
294
- return !existsFile(transcriptPath);
294
+ // Fix: Verificar se o arquivo existe antes de declarar "missing"
295
+ // Se o arquivo não existe no path esperado, verificar também o path alternativo
296
+ if (!existsFile(transcriptPath)) {
297
+ // Tentar verificar se o arquivo existe com o path alternativo (sem sessionFile)
298
+ const altPath = resolveSessionTranscriptPath(sessionId, agentId);
299
+ if (existsFile(altPath)) {
300
+ // Arquivo existe em path alternativo - atualizar entry se necessário
301
+ return false;
302
+ }
303
+ // Arquivo realmente não existe em nenhum path
304
+ return true;
305
+ }
306
+ return false;
295
307
  });
296
308
  if (missing.length > 0) {
297
309
  warnings.push(`- ${missing.length}/${recent.length} recent sessions are missing transcripts. Check for deleted session files or split state dirs.`);
@@ -20,6 +20,7 @@ export * from "./types.msteams.js";
20
20
  export * from "./types.plugins.js";
21
21
  export * from "./types.queue.js";
22
22
  export * from "./types.sandbox.js";
23
+ export * from "./types.security.js";
23
24
  export * from "./types.signal.js";
24
25
  export * from "./types.skills.js";
25
26
  export * from "./types.slack.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Security configuration types
3
+ * @version 2026.1.30
4
+ */
5
+ export {};
@@ -436,6 +436,12 @@ export const PoolBotSchema = z
436
436
  })
437
437
  .strict()
438
438
  .optional(),
439
+ security: z
440
+ .object({
441
+ owners: z.array(z.string()).optional(),
442
+ })
443
+ .strict()
444
+ .optional(),
439
445
  skills: z
440
446
  .object({
441
447
  allowBundled: z.array(z.string()).optional(),
@@ -73,12 +73,10 @@ export async function processDiscordMessage(ctx) {
73
73
  const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null;
74
74
  const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
75
75
  const groupSubject = isDirectMessage ? undefined : groupChannel;
76
- const channelDescription = channelInfo?.topic?.trim();
77
- const systemPromptParts = [
78
- channelDescription ? `Channel topic: ${channelDescription}` : null,
79
- channelConfig?.systemPrompt?.trim() || null,
80
- ].filter((entry) => Boolean(entry));
81
- const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
76
+ // SECURITY: Do NOT include channel topic/description in system prompt
77
+ // Channel metadata is user-controlled and can cause prompt injection
78
+ // Only include channelConfig.systemPrompt (admin-controlled)
79
+ const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
82
80
  const storePath = resolveStorePath(cfg.session?.store, {
83
81
  agentId: route.agentId,
84
82
  });
@@ -3,6 +3,7 @@ import { WebSocket } from "ws";
3
3
  import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
4
4
  import { rawDataToString } from "../infra/ws.js";
5
5
  import { logDebug, logError } from "../logger.js";
6
+ import { validateGatewayUrlSecurity } from "./url-validation.js";
6
7
  import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload, } from "../infra/device-identity.js";
7
8
  import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken, } from "../infra/device-auth-store.js";
8
9
  import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, } from "../utils/message-channel.js";
@@ -41,6 +42,19 @@ export class GatewayClient {
41
42
  if (this.closed)
42
43
  return;
43
44
  const url = this.opts.url ?? "ws://127.0.0.1:18789";
45
+ // SECURITY: Validate gateway URL override
46
+ // Prevents credential leakage via redirects to untrusted gateways
47
+ try {
48
+ validateGatewayUrlSecurity({
49
+ url,
50
+ token: this.opts.token,
51
+ requireExplicitToken: this.opts.requireExplicitToken,
52
+ });
53
+ }
54
+ catch (err) {
55
+ this.opts.onConnectError?.(err instanceof Error ? err : new Error(String(err)));
56
+ return;
57
+ }
44
58
  if (this.opts.tlsFingerprint && !url.startsWith("wss://")) {
45
59
  this.opts.onConnectError?.(new Error("gateway tls fingerprint requires wss:// gateway url"));
46
60
  return;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Gateway URL Override Security
3
+ * Prevents credential leakage via gateway URL redirects
4
+ *
5
+ * Based on OpenClaw 2026.2.4 security fix:
6
+ * - Require explicit credentials for gateway URL overrides
7
+ *
8
+ * @version 2026.1.30
9
+ */
10
+ /**
11
+ * Default gateway URLs (considered safe)
12
+ */
13
+ export const DEFAULT_GATEWAY_URLS = [
14
+ "http://127.0.0.1:18789",
15
+ "http://localhost:18789",
16
+ "http://[::1]:18789",
17
+ ];
18
+ /**
19
+ * Minimum token length for security
20
+ */
21
+ export const MIN_TOKEN_LENGTH = 32;
22
+ /**
23
+ * Check if URL is a default/safe gateway URL
24
+ */
25
+ export function isDefaultGatewayUrl(url) {
26
+ if (!url)
27
+ return true; // No URL means use default
28
+ const normalized = url.trim().toLowerCase();
29
+ return DEFAULT_GATEWAY_URLS.some((defaultUrl) => normalized === defaultUrl.toLowerCase());
30
+ }
31
+ /**
32
+ * Validate gateway URL override security
33
+ *
34
+ * @param config - Gateway configuration
35
+ * @throws Error if security requirements are not met
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Safe: Using default URL
40
+ * validateGatewayUrlSecurity({ url: "http://localhost:18789" });
41
+ *
42
+ * // Safe: Custom URL with explicit token
43
+ * validateGatewayUrlSecurity({
44
+ * url: "http://custom.gateway.com",
45
+ * token: "very-long-secure-token-32chars+",
46
+ * requireExplicitToken: true
47
+ * });
48
+ *
49
+ * // Unsafe: Custom URL without explicit flag
50
+ * validateGatewayUrlSecurity({
51
+ * url: "http://malicious.com",
52
+ * token: "token"
53
+ * });
54
+ * // Throws: "Gateway URL override requires explicit token flag"
55
+ * ```
56
+ */
57
+ export function validateGatewayUrlSecurity(config) {
58
+ // If URL is default/safe, no additional validation needed
59
+ if (isDefaultGatewayUrl(config.url)) {
60
+ return;
61
+ }
62
+ // Custom URL detected - require explicit confirmation
63
+ if (!config.requireExplicitToken) {
64
+ throw new Error("Gateway URL override requires explicit token flag. " +
65
+ "Set 'requireExplicitToken: true' in your gateway config to confirm " +
66
+ "you want to use a custom gateway URL. This prevents accidental " +
67
+ "credential leakage to untrusted gateways.");
68
+ }
69
+ // Require token when using custom URL
70
+ if (!config.token) {
71
+ throw new Error("Gateway URL override requires a token. " +
72
+ "Provide a secure token when using a custom gateway URL.");
73
+ }
74
+ // Require strong token (32+ characters)
75
+ if (config.token.length < MIN_TOKEN_LENGTH) {
76
+ throw new Error(`Gateway URL override requires a strong token (${MIN_TOKEN_LENGTH}+ characters). ` +
77
+ `Current token length: ${config.token.length} characters.`);
78
+ }
79
+ }
80
+ /**
81
+ * Check if gateway config is safe without throwing
82
+ *
83
+ * @param config - Gateway configuration
84
+ * @returns true if config is safe, false otherwise
85
+ */
86
+ export function isGatewayConfigSafe(config) {
87
+ try {
88
+ validateGatewayUrlSecurity(config);
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Media Path Sanitization
3
+ * Prevents path traversal attacks in media attachments
4
+ *
5
+ * Based on OpenClaw 2026.2.4 security fix:
6
+ * - Enforce sandboxed media paths for message tool attachments
7
+ *
8
+ * @version 2026.1.30
9
+ */
10
+ import path from "node:path";
11
+ import fs from "node:fs";
12
+ /**
13
+ * Sanitize media path to prevent path traversal
14
+ *
15
+ * @param inputPath - User-provided path (may contain ../ or absolute paths)
16
+ * @param baseDir - Base directory for media files (sandbox root)
17
+ * @returns Sanitized absolute path within baseDir
18
+ * @throws Error if path is outside baseDir or file doesn't exist
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Safe usage:
23
+ * const safe = sanitizeMediaPath("photo.jpg", "/root/.poolbot/media/");
24
+ * // Returns: "/root/.poolbot/media/photo.jpg"
25
+ *
26
+ * // Blocked (path traversal):
27
+ * sanitizeMediaPath("../../etc/passwd", "/root/.poolbot/media/");
28
+ * // Throws: "Path traversal detected"
29
+ * ```
30
+ */
31
+ export function sanitizeMediaPath(inputPath, baseDir) {
32
+ // 1. Normalize base directory (ensure trailing slash)
33
+ const normalizedBase = path.resolve(baseDir);
34
+ // 2. Resolve absolute path (follows ../ and resolves relative paths)
35
+ const resolved = path.resolve(normalizedBase, inputPath);
36
+ // 3. Security check: must be within baseDir
37
+ if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
38
+ throw new Error(`Path traversal detected: "${inputPath}" resolves to "${resolved}" ` +
39
+ `which is outside the allowed directory "${normalizedBase}"`);
40
+ }
41
+ // 4. Check if file exists
42
+ if (!fs.existsSync(resolved)) {
43
+ throw new Error(`File not found: ${inputPath}`);
44
+ }
45
+ // 5. Check if it's a file (not directory)
46
+ const stats = fs.statSync(resolved);
47
+ if (!stats.isFile()) {
48
+ throw new Error(`Path is not a file: ${inputPath}`);
49
+ }
50
+ return resolved;
51
+ }
52
+ /**
53
+ * Sanitize multiple media paths
54
+ *
55
+ * @param inputPaths - Array of user-provided paths
56
+ * @param baseDir - Base directory for media files
57
+ * @returns Array of sanitized paths
58
+ * @throws Error if any path is invalid
59
+ */
60
+ export function sanitizeMediaPaths(inputPaths, baseDir) {
61
+ return inputPaths.map((p) => sanitizeMediaPath(p, baseDir));
62
+ }
63
+ /**
64
+ * Check if path is safe without throwing
65
+ *
66
+ * @param inputPath - User-provided path
67
+ * @param baseDir - Base directory
68
+ * @returns true if path is safe, false otherwise
69
+ */
70
+ export function isMediaPathSafe(inputPath, baseDir) {
71
+ try {
72
+ sanitizeMediaPath(inputPath, baseDir);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
@@ -350,16 +350,10 @@ export async function prepareSlackMessage(params) {
350
350
  });
351
351
  }
352
352
  const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`;
353
- const channelDescription = [channelInfo?.topic, channelInfo?.purpose]
354
- .map((entry) => entry?.trim())
355
- .filter((entry) => Boolean(entry))
356
- .filter((entry, index, list) => list.indexOf(entry) === index)
357
- .join("\n");
358
- const systemPromptParts = [
359
- channelDescription ? `Channel description: ${channelDescription}` : null,
360
- channelConfig?.systemPrompt?.trim() || null,
361
- ].filter((entry) => Boolean(entry));
362
- const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
353
+ // SECURITY: Do NOT include channel topic/purpose in system prompt
354
+ // Channel metadata is user-controlled and can cause prompt injection
355
+ // Only include channelConfig.systemPrompt (admin-controlled)
356
+ const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
363
357
  let threadStarterBody;
364
358
  let threadLabel;
365
359
  let threadStarterMedia = null;
@@ -288,16 +288,10 @@ export function registerSlackMonitorSlashCommands(params) {
288
288
  id: isDirectMessage ? command.user_id : command.channel_id,
289
289
  },
290
290
  });
291
- const channelDescription = [channelInfo?.topic, channelInfo?.purpose]
292
- .map((entry) => entry?.trim())
293
- .filter((entry) => Boolean(entry))
294
- .filter((entry, index, list) => list.indexOf(entry) === index)
295
- .join("\n");
296
- const systemPromptParts = [
297
- channelDescription ? `Channel description: ${channelDescription}` : null,
298
- channelConfig?.systemPrompt?.trim() || null,
299
- ].filter((entry) => Boolean(entry));
300
- const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
291
+ // SECURITY: Do NOT include channel topic/purpose in system prompt
292
+ // Channel metadata is user-controlled and can cause prompt injection
293
+ // Only include channelConfig.systemPrompt (admin-controlled)
294
+ const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
301
295
  const ctxPayload = finalizeInboundContext({
302
296
  Body: prompt,
303
297
  RawBody: prompt,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolzin/pool-bot",
3
- "version": "2026.1.34",
3
+ "version": "2026.1.35",
4
4
  "description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -76,12 +76,81 @@
76
76
  "dist/pairing/**",
77
77
  "dist/whatsapp/**"
78
78
  ],
79
+ "scripts": {
80
+ "dev": "node scripts/run-node.mjs",
81
+ "postinstall": "node scripts/postinstall.js",
82
+ "prepack": "pnpm build && pnpm ui:build",
83
+ "docs:list": "node scripts/docs-list.js",
84
+ "docs:bin": "node scripts/build-docs-list.mjs",
85
+ "docs:dev": "cd docs && mint dev",
86
+ "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
87
+ "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
88
+ "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
89
+ "release:check": "node --import tsx scripts/release-check.ts",
90
+ "ui:install": "node scripts/ui.js install",
91
+ "ui:dev": "node scripts/ui.js dev",
92
+ "ui:build": "node scripts/ui.js build",
93
+ "start": "node scripts/run-node.mjs",
94
+ "poolbot": "node scripts/run-node.mjs",
95
+ "gateway:watch": "node scripts/watch-node.mjs gateway --force",
96
+ "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
97
+ "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
98
+ "tui": "node scripts/run-node.mjs tui",
99
+ "tui:dev": "CLAWDBOT_PROFILE=dev node scripts/run-node.mjs tui",
100
+ "poolbot:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
101
+ "ios:gen": "cd apps/ios && xcodegen generate",
102
+ "ios:open": "cd apps/ios && xcodegen generate && open Moltbot.xcodeproj",
103
+ "ios:build": "bash -lc 'cd apps/ios && xcodegen generate && xcodebuild -project Moltbot.xcodeproj -scheme Moltbot -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'",
104
+ "ios:run": "bash -lc 'cd apps/ios && xcodegen generate && xcodebuild -project Moltbot.xcodeproj -scheme Moltbot -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build && xcrun simctl boot \"${IOS_SIM:-iPhone 17}\" || true && xcrun simctl launch booted com.poolbot.ios'",
105
+ "android:assemble": "cd apps/android && ./gradlew :app:assembleDebug",
106
+ "android:install": "cd apps/android && ./gradlew :app:installDebug",
107
+ "android:run": "cd apps/android && ./gradlew :app:installDebug && adb shell am start -n com.poolbot.android/.MainActivity",
108
+ "android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest",
109
+ "mac:restart": "bash scripts/restart-mac.sh",
110
+ "mac:package": "bash scripts/package-mac-app.sh",
111
+ "mac:open": "open dist/Moltbot.app",
112
+ "lint": "oxlint --type-aware src test",
113
+ "lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
114
+ "lint:all": "pnpm lint && pnpm lint:swift",
115
+ "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
116
+ "format": "oxfmt --check src test",
117
+ "format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
118
+ "format:all": "pnpm format && pnpm format:swift",
119
+ "format:fix": "oxfmt --write src test",
120
+ "test": "node scripts/test-parallel.mjs",
121
+ "test:watch": "vitest",
122
+ "test:ui": "pnpm --dir ui test",
123
+ "test:force": "node --import tsx scripts/test-force.ts",
124
+ "test:coverage": "vitest run --coverage",
125
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
126
+ "test:live": "CLAWDBOT_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
127
+ "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
128
+ "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
129
+ "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
130
+ "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
131
+ "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
132
+ "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
133
+ "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
134
+ "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
135
+ "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
136
+ "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
137
+ "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
138
+ "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
139
+ "test:install:e2e:openai": "CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
140
+ "test:install:e2e:anthropic": "CLAWDBOT_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
141
+ "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
142
+ "protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
143
+ "protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
144
+ "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
145
+ "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
146
+ },
79
147
  "keywords": [],
80
148
  "author": "João Vitor Cunha <jvsantos.cunha@gmail.com>",
81
149
  "license": "MIT",
82
150
  "engines": {
83
151
  "node": ">=22.12.0"
84
152
  },
153
+ "packageManager": "pnpm@10.23.0",
85
154
  "dependencies": {
86
155
  "@agentclientprotocol/sdk": "0.13.1",
87
156
  "@aws-sdk/client-bedrock": "^3.975.0",
@@ -173,6 +242,14 @@
173
242
  "overrides": {
174
243
  "tar": "7.5.4"
175
244
  },
245
+ "pnpm": {
246
+ "minimumReleaseAge": 2880,
247
+ "overrides": {
248
+ "@sinclair/typebox": "0.34.47",
249
+ "hono": "4.11.4",
250
+ "tar": "7.5.4"
251
+ }
252
+ },
176
253
  "vitest": {
177
254
  "coverage": {
178
255
  "provider": "v8",
@@ -204,72 +281,5 @@
204
281
  "apps/macos/.build/**",
205
282
  "dist/Moltbot.app/**"
206
283
  ]
207
- },
208
- "scripts": {
209
- "dev": "node scripts/run-node.mjs",
210
- "postinstall": "node scripts/postinstall.js",
211
- "docs:list": "node scripts/docs-list.js",
212
- "docs:bin": "node scripts/build-docs-list.mjs",
213
- "docs:dev": "cd docs && mint dev",
214
- "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
215
- "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
216
- "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
217
- "release:check": "node --import tsx scripts/release-check.ts",
218
- "ui:install": "node scripts/ui.js install",
219
- "ui:dev": "node scripts/ui.js dev",
220
- "ui:build": "node scripts/ui.js build",
221
- "start": "node scripts/run-node.mjs",
222
- "poolbot": "node scripts/run-node.mjs",
223
- "gateway:watch": "node scripts/watch-node.mjs gateway --force",
224
- "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
225
- "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
226
- "tui": "node scripts/run-node.mjs tui",
227
- "tui:dev": "CLAWDBOT_PROFILE=dev node scripts/run-node.mjs tui",
228
- "poolbot:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
229
- "ios:gen": "cd apps/ios && xcodegen generate",
230
- "ios:open": "cd apps/ios && xcodegen generate && open Moltbot.xcodeproj",
231
- "ios:build": "bash -lc 'cd apps/ios && xcodegen generate && xcodebuild -project Moltbot.xcodeproj -scheme Moltbot -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'",
232
- "ios:run": "bash -lc 'cd apps/ios && xcodegen generate && xcodebuild -project Moltbot.xcodeproj -scheme Moltbot -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build && xcrun simctl boot \"${IOS_SIM:-iPhone 17}\" || true && xcrun simctl launch booted com.poolbot.ios'",
233
- "android:assemble": "cd apps/android && ./gradlew :app:assembleDebug",
234
- "android:install": "cd apps/android && ./gradlew :app:installDebug",
235
- "android:run": "cd apps/android && ./gradlew :app:installDebug && adb shell am start -n com.poolbot.android/.MainActivity",
236
- "android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest",
237
- "mac:restart": "bash scripts/restart-mac.sh",
238
- "mac:package": "bash scripts/package-mac-app.sh",
239
- "mac:open": "open dist/Moltbot.app",
240
- "lint": "oxlint --type-aware src test",
241
- "lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
242
- "lint:all": "pnpm lint && pnpm lint:swift",
243
- "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
244
- "format": "oxfmt --check src test",
245
- "format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
246
- "format:all": "pnpm format && pnpm format:swift",
247
- "format:fix": "oxfmt --write src test",
248
- "test": "node scripts/test-parallel.mjs",
249
- "test:watch": "vitest",
250
- "test:ui": "pnpm --dir ui test",
251
- "test:force": "node --import tsx scripts/test-force.ts",
252
- "test:coverage": "vitest run --coverage",
253
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
254
- "test:live": "CLAWDBOT_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
255
- "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
256
- "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
257
- "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
258
- "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
259
- "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
260
- "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
261
- "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
262
- "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
263
- "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
264
- "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
265
- "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
266
- "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
267
- "test:install:e2e:openai": "CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
268
- "test:install:e2e:anthropic": "CLAWDBOT_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
269
- "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
270
- "protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
271
- "protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
272
- "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
273
- "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
274
284
  }
275
- }
285
+ }
File without changes
File without changes
File without changes
File without changes