@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Exec Approvals Security Hardening
3
+ *
4
+ * Security fixes for exec approvals:
5
+ * - Wrapper detection (env, nice, nohup, bash -c)
6
+ * - Shell line continuation detection (\\\n)
7
+ * - Unicode invisible character escaping
8
+ */
9
+ /**
10
+ * Known shell wrapper binaries that can be used to bypass approval checks
11
+ */
12
+ const SHELL_WRAPPERS = new Set([
13
+ "env",
14
+ "nice",
15
+ "nohup",
16
+ "timeout",
17
+ "bash",
18
+ "sh",
19
+ "zsh",
20
+ "ksh",
21
+ "csh",
22
+ "tcsh",
23
+ "fish",
24
+ "dash",
25
+ "node",
26
+ "python",
27
+ "python3",
28
+ "perl",
29
+ "ruby",
30
+ "php",
31
+ "java",
32
+ "dotnet",
33
+ "mono",
34
+ ]);
35
+ /**
36
+ * Unicode invisible/format characters that can be used for spoofing
37
+ */
38
+ const INVISIBLE_UNICODE_RANGES = [
39
+ [0x0000, 0x001f], // C0 control characters
40
+ [0x007f, 0x009f], // Delete and C1 control
41
+ [0x00ad, 0x00ad], // Soft hyphen
42
+ [0x0600, 0x0605], // Arabic number signs
43
+ [0x061c, 0x061c], // Arabic letter mark
44
+ [0x06dd, 0x06dd], // Ayah
45
+ [0x070f, 0x070f], // Syriac abbreviation
46
+ [0x180e, 0x180e], // Mongolian vowel separator
47
+ [0x200b, 0x200f], // Zero-width chars
48
+ [0x2028, 0x202e], // Line/paragraph separators
49
+ [0x2060, 0x206f], // Invisible operators
50
+ [0xfeff, 0xfeff], // BOM/zero-width no-break space
51
+ [0xfff0, 0xfff8], // Specials
52
+ [0xd800, 0xdfff], // Surrogate pairs
53
+ ];
54
+ /**
55
+ * Check if a character is an invisible/format Unicode character
56
+ */
57
+ function isInvisibleUnicode(code) {
58
+ for (const [start, end] of INVISIBLE_UNICODE_RANGES) {
59
+ if (code >= start && code <= end) {
60
+ return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * Escape invisible Unicode characters in a string for safe display
67
+ *
68
+ * Security: Prevents spoofing of approval prompts via zero-width characters
69
+ */
70
+ export function escapeInvisibleUnicode(text) {
71
+ let result = "";
72
+ for (const char of text) {
73
+ const code = char.codePointAt(0) ?? 0;
74
+ if (isInvisibleUnicode(code)) {
75
+ result += `\\u{${code.toString(16)}}`;
76
+ }
77
+ else {
78
+ result += char;
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ /**
84
+ * Detect shell line continuations (backslash-newline sequences)
85
+ *
86
+ * Security: Prevents bypass of command-substitution checks via line continuations
87
+ */
88
+ export function hasShellLineContinuation(command) {
89
+ // Match backslash followed by newline (with optional carriage return)
90
+ return /\\\r?\n/.test(command);
91
+ }
92
+ /**
93
+ * Normalize shell line continuations by joining continued lines
94
+ *
95
+ * Security: Ensures continued commands are analyzed as single unit
96
+ */
97
+ export function normalizeShellLineContinuations(command) {
98
+ return command.replace(/\\\r?\n/g, " ");
99
+ }
100
+ /**
101
+ * Detect if command uses a shell wrapper
102
+ */
103
+ export function detectShellWrapper(command) {
104
+ const trimmed = command.trim();
105
+ if (!trimmed) {
106
+ return null;
107
+ }
108
+ // Extract first token (potential wrapper)
109
+ const firstTokenMatch = trimmed.match(/^(\S+)/);
110
+ if (!firstTokenMatch) {
111
+ return null;
112
+ }
113
+ const firstToken = firstTokenMatch[1];
114
+ const basename = firstToken.split("/").pop()?.toLowerCase() ?? "";
115
+ if (SHELL_WRAPPERS.has(basename)) {
116
+ return basename;
117
+ }
118
+ return null;
119
+ }
120
+ /**
121
+ * Unwrap shell wrapper to get inner command
122
+ *
123
+ * Security: Extract actual executable from wrapper for approval checking
124
+ */
125
+ export function unwrapShellWrapper(command) {
126
+ const wrapper = detectShellWrapper(command);
127
+ if (!wrapper) {
128
+ return { wrapper: null, innerCommand: command, wasUnwrapped: false };
129
+ }
130
+ const trimmed = command.trim();
131
+ // Handle bash/sh -c "command" pattern
132
+ if (wrapper === "bash" || wrapper === "sh" || wrapper === "zsh" || wrapper === "ksh") {
133
+ const match = trimmed.match(/^(?:bash|sh|zsh|ksh)\s+(?:-[a-zA-Z]+\s+)?-c\s+(['"]?)(.+)\1/);
134
+ if (match) {
135
+ return { wrapper, innerCommand: match[2], wasUnwrapped: true };
136
+ }
137
+ }
138
+ // Handle env VAR=value command pattern
139
+ if (wrapper === "env") {
140
+ const match = trimmed.match(/^env\s+(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(.+)$/);
141
+ if (match) {
142
+ return { wrapper, innerCommand: match[1], wasUnwrapped: true };
143
+ }
144
+ }
145
+ // Handle nice/timeout/nohup -n/command pattern
146
+ if (wrapper === "nice" || wrapper === "nohup" || wrapper === "timeout") {
147
+ const match = trimmed.match(/^(?:nice|nohup|timeout)\s+(?:-[a-zA-Z]+\s+\S+\s+|\S+\s+)?(.+)$/);
148
+ if (match) {
149
+ return { wrapper, innerCommand: match[1], wasUnwrapped: true };
150
+ }
151
+ }
152
+ // Generic wrapper: just remove first token
153
+ const parts = trimmed.split(/\s+/);
154
+ if (parts.length > 1) {
155
+ return { wrapper, innerCommand: parts.slice(1).join(" "), wasUnwrapped: true };
156
+ }
157
+ return { wrapper, innerCommand: command, wasUnwrapped: false };
158
+ }
159
+ /**
160
+ * Perform comprehensive security analysis on a command
161
+ *
162
+ * Security: Detects and normalizes various bypass attempts
163
+ */
164
+ export function analyzeCommandSecurity(command) {
165
+ const issues = [];
166
+ // Check for line continuations
167
+ const hasLineContinuation = hasShellLineContinuation(command);
168
+ if (hasLineContinuation) {
169
+ issues.push("Shell line continuation detected (\\\n)");
170
+ }
171
+ // Normalize line continuations
172
+ const normalizedAfterContinuation = normalizeShellLineContinuations(command);
173
+ // Check for invisible Unicode
174
+ const hasInvisibleUnicode = normalizedAfterContinuation !== escapeInvisibleUnicode(normalizedAfterContinuation);
175
+ if (hasInvisibleUnicode) {
176
+ issues.push("Invisible Unicode characters detected");
177
+ }
178
+ // Escape invisible Unicode
179
+ const escapedCommand = escapeInvisibleUnicode(normalizedAfterContinuation);
180
+ // Detect and unwrap wrapper
181
+ const { wrapper, innerCommand, wasUnwrapped } = unwrapShellWrapper(escapedCommand);
182
+ if (wrapper) {
183
+ issues.push(`Shell wrapper detected: ${wrapper}`);
184
+ }
185
+ return {
186
+ originalCommand: command,
187
+ normalizedCommand: wasUnwrapped ? innerCommand : escapedCommand,
188
+ hasLineContinuation,
189
+ hasInvisibleUnicode,
190
+ escapedCommand,
191
+ wrapper,
192
+ unwrappedCommand: innerCommand,
193
+ wasUnwrapped,
194
+ securityIssues: issues,
195
+ };
196
+ }
197
+ /**
198
+ * Validate command for security issues before approval
199
+ *
200
+ * Returns null if command is safe, or error message if issues found
201
+ */
202
+ export function validateCommandSecurity(command) {
203
+ const analysis = analyzeCommandSecurity(command);
204
+ // Fail closed on line continuations that can't be resolved
205
+ if (analysis.hasLineContinuation) {
206
+ return "Command contains shell line continuations (\\\n) which are not allowed for security reasons";
207
+ }
208
+ // Fail closed on invisible Unicode
209
+ if (analysis.hasInvisibleUnicode) {
210
+ return "Command contains invisible Unicode characters which are not allowed for security reasons";
211
+ }
212
+ // For wrappers, ensure we can resolve the inner command
213
+ if (analysis.wrapper && !analysis.wasUnwrapped) {
214
+ return `Unable to unwrap shell wrapper (${analysis.wrapper}) for security validation`;
215
+ }
216
+ return null;
217
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Command Analysis System
3
+ *
4
+ * Analyzes commands for security risks including:
5
+ * - Shell wrapper detection (sh -c, bash -c, etc.)
6
+ * - Dangerous command patterns (rm -rf, dd, etc.)
7
+ * - PATH traversal attempts
8
+ * - Command injection via arguments
9
+ */
10
+ export class CommandAnalyzer {
11
+ SHELL_WRAPPERS = [
12
+ /^sh\s+-c\s+/i,
13
+ /^bash\s+-c\s+/i,
14
+ /^cmd\.exe\s+\/c\s+/i,
15
+ /^cmd\s+\/c\s+/i,
16
+ /^powershell\s+-c\s+/i,
17
+ /^powershell\.exe\s+-c\s+/i,
18
+ /^zsh\s+-c\s+/i,
19
+ /^ksh\s+-c\s+/i,
20
+ /^csh\s+-c\s+/i,
21
+ /^fish\s+-c\s+/i,
22
+ ];
23
+ DANGEROUS_COMMANDS = [
24
+ // Destructive file operations
25
+ { pattern: /rm\s+(-[rf]+\s+)+/i, reason: "Recursive/force delete", level: "critical" },
26
+ { pattern: /rm\s+-rf/i, reason: "Recursive force delete", level: "critical" },
27
+ {
28
+ pattern: /del\s+\/(f|q|s)/i,
29
+ reason: "Windows delete with force/quiet",
30
+ level: "critical",
31
+ },
32
+ { pattern: /format\s+/i, reason: "Disk format", level: "critical" },
33
+ { pattern: /mkfs/i, reason: "Create filesystem", level: "critical" },
34
+ { pattern: /dd\s+/i, reason: "Disk dump/clone", level: "critical" },
35
+ { pattern: /cipher\s+\/w/i, reason: "Windows secure delete", level: "critical" },
36
+ // Permission changes
37
+ { pattern: /chmod\s+777/i, reason: "World-writable permissions", level: "high" },
38
+ { pattern: /chmod\s+\+s/i, reason: "Setuid/setgid bit", level: "high" },
39
+ { pattern: /chown\s+root/i, reason: "Change owner to root", level: "high" },
40
+ { pattern: /icacls\s+.*\/grant/i, reason: "Windows ACL modification", level: "high" },
41
+ // Privilege escalation
42
+ { pattern: /sudo\s+/i, reason: "Sudo execution", level: "high" },
43
+ { pattern: /su\s+-/i, reason: "Su to root", level: "high" },
44
+ { pattern: /runas\s+/i, reason: "Windows runas", level: "high" },
45
+ { pattern: /pkexec\s+/i, reason: "Polkit execution", level: "high" },
46
+ // Remote code execution
47
+ {
48
+ pattern: /curl.*\|\s*(sh|bash|zsh)/i,
49
+ reason: "Curl pipe to shell",
50
+ level: "critical",
51
+ },
52
+ {
53
+ pattern: /wget.*\|\s*(sh|bash|zsh)/i,
54
+ reason: "Wget pipe to shell",
55
+ level: "critical",
56
+ },
57
+ {
58
+ pattern: /curl.*>\s*\/dev\/.*\|\s*(sh|bash)/i,
59
+ reason: "Curl to device pipe",
60
+ level: "critical",
61
+ },
62
+ // Process manipulation
63
+ { pattern: /kill\s+-9/i, reason: "Force kill", level: "medium" },
64
+ { pattern: /pkill\s+-9/i, reason: "Force kill by name", level: "medium" },
65
+ { pattern: /taskkill\s+\/f/i, reason: "Windows force kill", level: "medium" },
66
+ // Network operations
67
+ { pattern: /nc\s+-[le]/i, reason: "Netcat listener", level: "high" },
68
+ { pattern: /netcat\s+-[le]/i, reason: "Netcat listener", level: "high" },
69
+ { pattern: /socat\s+/i, reason: "Socket cat", level: "medium" },
70
+ // System modifications
71
+ { pattern: /mount\s+/i, reason: "Mount filesystem", level: "high" },
72
+ { pattern: /umount\s+/i, reason: "Unmount filesystem", level: "medium" },
73
+ { pattern: /fdisk\s+/i, reason: "Partition manipulation", level: "critical" },
74
+ { pattern: /parted\s+/i, reason: "Partition manipulation", level: "critical" },
75
+ // Credential access
76
+ { pattern: /cat\s+.*passwd/i, reason: "Read password file", level: "high" },
77
+ { pattern: /cat\s+.*shadow/i, reason: "Read shadow file", level: "critical" },
78
+ { pattern: /cat\s+.*\.ssh\//i, reason: "Read SSH keys", level: "high" },
79
+ {
80
+ pattern: /reg\s+query.*password/i,
81
+ reason: "Windows registry password query",
82
+ level: "high",
83
+ },
84
+ ];
85
+ INJECTION_PATTERNS = [
86
+ /\$\(/, // Command substitution
87
+ /`[^`]+`/, // Backtick command substitution
88
+ /\|/, // Pipe
89
+ /;/, // Command separator
90
+ /&&/, // AND operator
91
+ /\|\|/, // OR operator
92
+ />/, // Redirect
93
+ /</, // Input redirect
94
+ /\$/, // Variable expansion
95
+ /\\\\n/, // Newline injection
96
+ /\\\\r/, // Carriage return injection
97
+ /\\\\x00/, // Null byte
98
+ /\\\\0/, // Null byte alternate
99
+ ];
100
+ /**
101
+ * Analyze a command for security risks
102
+ */
103
+ analyze(command, args = []) {
104
+ const fullCommand = [command, ...args].join(" ");
105
+ const warnings = [];
106
+ // Check for shell wrappers
107
+ const wrapperMatch = this.SHELL_WRAPPERS.some((re) => re.test(fullCommand));
108
+ if (wrapperMatch) {
109
+ return {
110
+ isShellWrapper: true,
111
+ baseCommand: command,
112
+ actualCommand: fullCommand.replace(/^(sh|bash|cmd\.exe|cmd|powershell|powershell\.exe|zsh|ksh|csh|fish)\s+-c\s+/i, ""),
113
+ riskLevel: "high",
114
+ requiresApproval: true,
115
+ warnings: ["Shell wrapper detected - command will be executed by intermediate shell"],
116
+ blockedReason: undefined,
117
+ };
118
+ }
119
+ // Check for dangerous commands
120
+ for (const { pattern, reason, level } of this.DANGEROUS_COMMANDS) {
121
+ if (pattern.test(fullCommand)) {
122
+ return {
123
+ isShellWrapper: false,
124
+ baseCommand: command,
125
+ actualCommand: fullCommand,
126
+ riskLevel: level,
127
+ requiresApproval: true,
128
+ warnings: [`Dangerous pattern: ${reason}`],
129
+ blockedReason: level === "critical" ? `Blocked: ${reason}` : undefined,
130
+ };
131
+ }
132
+ }
133
+ // Check for injection patterns in arguments
134
+ for (const arg of args) {
135
+ for (const pattern of this.INJECTION_PATTERNS) {
136
+ if (pattern.test(arg)) {
137
+ warnings.push(`Potential injection in argument: ${arg.substring(0, 50)}`);
138
+ }
139
+ }
140
+ }
141
+ // Check for PATH traversal
142
+ if (command.includes("..") || command.startsWith("/")) {
143
+ warnings.push("PATH traversal detected");
144
+ }
145
+ // Determine risk level based on warnings
146
+ let riskLevel = "low";
147
+ if (warnings.length >= 3) {
148
+ riskLevel = "high";
149
+ }
150
+ else if (warnings.length >= 1) {
151
+ riskLevel = "medium";
152
+ }
153
+ return {
154
+ isShellWrapper: false,
155
+ baseCommand: command,
156
+ actualCommand: fullCommand,
157
+ riskLevel,
158
+ requiresApproval: riskLevel !== "low" || warnings.length > 0,
159
+ warnings,
160
+ blockedReason: undefined,
161
+ };
162
+ }
163
+ /**
164
+ * Check if command is a shell wrapper
165
+ */
166
+ isShellWrapper(command, args) {
167
+ const fullCommand = [command, ...args].join(" ");
168
+ return this.SHELL_WRAPPERS.some((re) => re.test(fullCommand));
169
+ }
170
+ /**
171
+ * Extract actual command from shell wrapper
172
+ */
173
+ extractActualCommand(command, args) {
174
+ const fullCommand = [command, ...args].join(" ");
175
+ return fullCommand.replace(/^(sh|bash|cmd\.exe|cmd|powershell|powershell\.exe|zsh|ksh|csh|fish)\s+-c\s+/i, "");
176
+ }
177
+ /**
178
+ * Validate arguments for injection attempts
179
+ */
180
+ validateArguments(args) {
181
+ const issues = [];
182
+ for (let i = 0; i < args.length; i++) {
183
+ const arg = args[i];
184
+ // Check for control characters
185
+ if (/[\n\r\t]/.test(arg) || arg.includes("\0")) {
186
+ issues.push(`Argument ${i} contains control characters`);
187
+ }
188
+ // Check for command substitution
189
+ if (/\$\(|`/.test(arg)) {
190
+ issues.push(`Argument ${i} contains command substitution`);
191
+ }
192
+ // Check for excessive length
193
+ if (arg.length > 10000) {
194
+ issues.push(`Argument ${i} exceeds length limit (${arg.length} chars)`);
195
+ }
196
+ }
197
+ return {
198
+ valid: issues.length === 0,
199
+ issues,
200
+ };
201
+ }
202
+ }
203
+ /**
204
+ * Path Sanitization Utilities
205
+ */
206
+ export class PathSanitizer {
207
+ workspaceRoot;
208
+ constructor(workspaceRoot) {
209
+ this.workspaceRoot = require("path").resolve(workspaceRoot);
210
+ }
211
+ /**
212
+ * Sanitize a path to prevent directory traversal
213
+ */
214
+ sanitize(inputPath) {
215
+ const path = require("path");
216
+ // Resolve to absolute path
217
+ const resolved = path.resolve(inputPath);
218
+ // Check for path traversal
219
+ if (!resolved.startsWith(this.workspaceRoot)) {
220
+ throw new SecurityError(`Path ${inputPath} resolves to ${resolved}, which is outside workspace ${this.workspaceRoot}`);
221
+ }
222
+ // Normalize and remove redundant separators
223
+ const normalized = path.normalize(resolved);
224
+ // Check for null bytes
225
+ if (normalized.includes("\0")) {
226
+ throw new SecurityError("Path contains null byte");
227
+ }
228
+ return normalized;
229
+ }
230
+ /**
231
+ * Sanitize multiple arguments that might be paths
232
+ */
233
+ sanitizeArguments(args) {
234
+ return args.map((arg) => {
235
+ // Skip if not a path-like argument
236
+ if (!arg.startsWith("/") && !arg.startsWith("./") && !arg.startsWith("../")) {
237
+ return arg;
238
+ }
239
+ try {
240
+ return this.sanitize(arg);
241
+ }
242
+ catch {
243
+ // Return original if sanitization fails (will be caught by command analyzer)
244
+ return arg;
245
+ }
246
+ });
247
+ }
248
+ }
249
+ /**
250
+ * Custom error for security violations
251
+ */
252
+ export class SecurityError extends Error {
253
+ constructor(message) {
254
+ super(message);
255
+ this.name = "SecurityError";
256
+ }
257
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Session Cleanup System
3
+ *
4
+ * Automatically cleans up old sessions based on disk budget.
5
+ * Implemented from scratch for Pool Bot architecture.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ const DEFAULT_SESSIONS_DIR = path.join(process.cwd(), ".poolbot", "sessions");
10
+ const DEFAULT_MAX_DISK_USAGE_MB = 1000; // 1GB
11
+ const DEFAULT_MIN_SESSIONS_TO_KEEP = 5;
12
+ /**
13
+ * Get all sessions with metadata
14
+ */
15
+ export async function getSessions(sessionsDir = DEFAULT_SESSIONS_DIR) {
16
+ try {
17
+ await fs.mkdir(sessionsDir, { recursive: true });
18
+ const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
19
+ const sessions = [];
20
+ for (const entry of entries) {
21
+ if (entry.isFile() && entry.name.endsWith(".json")) {
22
+ const sessionPath = path.join(sessionsDir, entry.name);
23
+ const stats = await fs.stat(sessionPath);
24
+ sessions.push({
25
+ path: sessionPath,
26
+ size: stats.size,
27
+ createdAt: stats.birthtimeMs,
28
+ modifiedAt: stats.mtimeMs,
29
+ });
30
+ }
31
+ }
32
+ // Sort by modified date (oldest first)
33
+ return sessions.sort((a, b) => a.modifiedAt - b.modifiedAt);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ /**
40
+ * Calculate total disk usage
41
+ */
42
+ export async function calculateDiskUsage(sessions) {
43
+ return sessions.reduce((total, session) => total + session.size, 0);
44
+ }
45
+ /**
46
+ * Cleanup old sessions to stay within disk budget
47
+ */
48
+ export async function cleanupSessions(options = {}) {
49
+ const startTime = Date.now();
50
+ const { sessionsDir = DEFAULT_SESSIONS_DIR, maxDiskUsageMB = DEFAULT_MAX_DISK_USAGE_MB, minSessionsToKeep = DEFAULT_MIN_SESSIONS_TO_KEEP, dryRun = false, } = options;
51
+ try {
52
+ const maxDiskUsageBytes = maxDiskUsageMB * 1024 * 1024;
53
+ // Get all sessions
54
+ const sessions = await getSessions(sessionsDir);
55
+ let currentDiskUsage = await calculateDiskUsage(sessions);
56
+ const report = {
57
+ success: true,
58
+ sessionsDeleted: 0,
59
+ spaceFreedBytes: 0,
60
+ spaceFreedMB: 0,
61
+ sessionsRemaining: sessions.length,
62
+ currentDiskUsageBytes: currentDiskUsage,
63
+ currentDiskUsageMB: currentDiskUsage / (1024 * 1024),
64
+ dryRun,
65
+ duration: 0,
66
+ };
67
+ // If under budget, nothing to do
68
+ if (currentDiskUsage <= maxDiskUsageBytes) {
69
+ report.duration = Date.now() - startTime;
70
+ return report;
71
+ }
72
+ // Delete oldest sessions until under budget
73
+ const sessionsToDelete = [];
74
+ let spaceToFree = currentDiskUsage - maxDiskUsageBytes;
75
+ for (const session of sessions) {
76
+ // Don't delete if we're at minimum sessions
77
+ if (sessions.length - sessionsToDelete.length <= minSessionsToKeep) {
78
+ break;
79
+ }
80
+ sessionsToDelete.push(session);
81
+ spaceToFree -= session.size;
82
+ // Stop if we've freed enough space
83
+ if (spaceToFree <= 0) {
84
+ break;
85
+ }
86
+ }
87
+ // Delete sessions
88
+ let spaceFreed = 0;
89
+ for (const session of sessionsToDelete) {
90
+ if (dryRun) {
91
+ spaceFreed += session.size;
92
+ }
93
+ else {
94
+ try {
95
+ await fs.unlink(session.path);
96
+ spaceFreed += session.size;
97
+ }
98
+ catch (error) {
99
+ console.error(`Failed to delete session ${session.path}:`, error.message);
100
+ }
101
+ }
102
+ }
103
+ report.sessionsDeleted = sessionsToDelete.length;
104
+ report.spaceFreedBytes = spaceFreed;
105
+ report.spaceFreedMB = spaceFreed / (1024 * 1024);
106
+ report.sessionsRemaining = sessions.length - sessionsToDelete.length;
107
+ report.currentDiskUsageBytes = currentDiskUsage - spaceFreed;
108
+ report.currentDiskUsageMB = (currentDiskUsage - spaceFreed) / (1024 * 1024);
109
+ report.duration = Date.now() - startTime;
110
+ return report;
111
+ }
112
+ catch (error) {
113
+ const duration = Date.now() - startTime;
114
+ return {
115
+ success: false,
116
+ sessionsDeleted: 0,
117
+ spaceFreedBytes: 0,
118
+ spaceFreedMB: 0,
119
+ sessionsRemaining: 0,
120
+ currentDiskUsageBytes: 0,
121
+ currentDiskUsageMB: 0,
122
+ dryRun,
123
+ duration,
124
+ error: error.message,
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Get session cleanup status
130
+ */
131
+ export async function getSessionCleanupStatus(options) {
132
+ const { sessionsDir = DEFAULT_SESSIONS_DIR, maxDiskUsageMB = DEFAULT_MAX_DISK_USAGE_MB } = options;
133
+ const sessions = await getSessions(sessionsDir);
134
+ const currentUsage = await calculateDiskUsage(sessions);
135
+ const maxUsageBytes = maxDiskUsageMB * 1024 * 1024;
136
+ return {
137
+ currentUsageMB: currentUsage / (1024 * 1024),
138
+ maxUsageMB: maxDiskUsageMB,
139
+ usagePercent: (currentUsage / maxUsageBytes) * 100,
140
+ sessionCount: sessions.length,
141
+ overBudget: currentUsage > maxUsageBytes,
142
+ };
143
+ }
@@ -191,23 +191,21 @@ function isTrackedByProvenance(params) {
191
191
  }
192
192
  return matchesPathMatcher(params.index.loadPathMatcher, sourcePath);
193
193
  }
194
- function warnWhenAllowlistIsOpen(params) {
195
- if (!params.pluginsEnabled) {
196
- return;
197
- }
194
+ function failClosedWhenAllowlistIsEmpty(params) {
198
195
  if (params.allow.length > 0) {
199
- return;
196
+ return false;
200
197
  }
201
198
  const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled");
202
199
  if (nonBundled.length === 0) {
203
- return;
200
+ return false;
204
201
  }
205
202
  const preview = nonBundled
206
203
  .slice(0, 6)
207
204
  .map((entry) => `${entry.id} (${entry.source})`)
208
205
  .join(", ");
209
206
  const extra = nonBundled.length > 6 ? ` (+${nonBundled.length - 6} more)` : "";
210
- params.logger.warn(`[plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: ${preview}${extra}. Set plugins.allow to explicit trusted ids.`);
207
+ params.logger.error(`[plugins] SECURITY: plugins.allow is empty; refusing to auto-load discovered non-bundled plugins: ${preview}${extra}. Set plugins.allow to explicit trusted ids to enable plugins.`);
208
+ return true;
211
209
  }
212
210
  function warnAboutUntrackedLoadedPlugins(params) {
213
211
  for (const plugin of params.registry.plugins) {
@@ -270,7 +268,7 @@ export function loadPoolBotPlugins(options = {}) {
270
268
  diagnostics: discovery.diagnostics,
271
269
  });
272
270
  pushDiagnostics(registry.diagnostics, manifestRegistry.diagnostics);
273
- warnWhenAllowlistIsOpen({
271
+ const shouldFailClosed = failClosedWhenAllowlistIsEmpty({
274
272
  logger,
275
273
  pluginsEnabled: normalized.enabled,
276
274
  allow: normalized.allow,
@@ -280,6 +278,16 @@ export function loadPoolBotPlugins(options = {}) {
280
278
  origin: plugin.origin,
281
279
  })),
282
280
  });
281
+ if (shouldFailClosed) {
282
+ logger.error("[plugins] SECURITY: refusing to load plugins without explicit consent via plugins.allow");
283
+ const emptyRegistry = createPluginRegistry({
284
+ logger,
285
+ runtime,
286
+ coreGatewayHandlers: options.coreGatewayHandlers,
287
+ }).registry;
288
+ setActivePluginRegistry(emptyRegistry, cacheKey);
289
+ return emptyRegistry;
290
+ }
283
291
  const provenance = buildProvenanceIndex({
284
292
  config: cfg,
285
293
  normalizedLoadPaths: normalized.loadPaths,