@poolzin/pool-bot 2026.3.21 → 2026.3.23

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 (124) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/acp/bindings-store.js +209 -0
  3. package/dist/acp/control-plane/runtime-cache.js +54 -0
  4. package/dist/acp/control-plane/runtime-options.js +215 -0
  5. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  6. package/dist/acp/runtime/errors.js +47 -0
  7. package/dist/acp/runtime/registry.js +86 -0
  8. package/dist/acp/runtime/types.js +1 -0
  9. package/dist/acp/translator.js +97 -0
  10. package/dist/agents/failover-error.js +145 -47
  11. package/dist/browser/browser-profile-manager.js +319 -0
  12. package/dist/browser/cdp-proxy-bypass.js +129 -0
  13. package/dist/browser/cdp-timeouts.js +41 -0
  14. package/dist/browser/chrome-extension-validator.js +406 -0
  15. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  16. package/dist/browser/chrome-mcp.js +421 -0
  17. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  18. package/dist/browser/errors.js +67 -0
  19. package/dist/browser/form-fields.js +22 -0
  20. package/dist/browser/output-atomic.js +44 -0
  21. package/dist/browser/profile-capabilities.js +47 -0
  22. package/dist/browser/safe-filename.js +25 -0
  23. package/dist/browser/snapshot-roles.js +60 -0
  24. package/dist/build-info.json +3 -3
  25. package/dist/commands/security-owner-only.js +86 -0
  26. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  27. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  28. package/dist/control-ui/index.html +1 -1
  29. package/dist/cron/cron-filters.js +150 -0
  30. package/dist/gateway/device-pairing-security.js +197 -0
  31. package/dist/gateway/event-deduplication.js +167 -0
  32. package/dist/gateway/run-tracker.js +253 -0
  33. package/dist/gateway/server-methods/nodes.js +14 -0
  34. package/dist/gateway/websocket-preauth-security.js +188 -0
  35. package/dist/infra/errors.js +53 -13
  36. package/dist/infra/exec-approvals-security.js +217 -0
  37. package/dist/infra/security/command-analyzer.js +257 -0
  38. package/dist/plugins/loader.js +16 -8
  39. package/dist/security/external-content.js +51 -1
  40. package/dist/sessions/session-costs.js +228 -0
  41. package/dist/shared/param-key.js +16 -0
  42. package/dist/shared/poll-params.js +58 -0
  43. package/dist/shared/polls.js +55 -0
  44. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  45. package/docs/FEATURES.md +523 -0
  46. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  47. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  48. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  49. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  50. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  51. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  52. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  53. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  54. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  55. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  56. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  57. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  58. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  59. package/docs/PHASE-7-SUMMARY.md +144 -0
  60. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  61. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  62. package/docs/README.md +116 -0
  63. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  64. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  65. package/docs/channels/googlechat.md +235 -206
  66. package/docs/channels/irc.md +332 -0
  67. package/docs/channels/nostr.md +255 -168
  68. package/docs/components/command-palette.md +166 -0
  69. package/docs/components/login-gate.md +219 -0
  70. package/docs/getting-started/installation.md +191 -0
  71. package/docs/getting-started/introduction.md +120 -0
  72. package/docs/improvements/USAGE-GUIDE.md +359 -0
  73. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  74. package/docs/reference/deadcode-detection.md +72 -0
  75. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  76. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  77. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  78. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  79. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  80. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  81. package/extensions/googlechat/package.json +11 -28
  82. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  83. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  84. package/extensions/googlechat/src/index.ts +14 -0
  85. package/extensions/irc/node_modules/.bin/tsc +21 -0
  86. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  87. package/extensions/irc/node_modules/.bin/vitest +21 -0
  88. package/extensions/irc/package.json +16 -8
  89. package/extensions/irc/src/index.ts +14 -0
  90. package/extensions/irc/src/irc-channel.test.ts +43 -0
  91. package/extensions/irc/src/irc-channel.ts +191 -0
  92. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  93. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  94. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  95. package/extensions/keyed-async-queue/package.json +20 -0
  96. package/extensions/keyed-async-queue/src/index.ts +14 -0
  97. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  98. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  99. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  100. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  101. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  102. package/extensions/memory-core/package.json +11 -8
  103. package/extensions/memory-core/src/index.ts +14 -0
  104. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  105. package/extensions/memory-core/src/memory-manager.ts +186 -0
  106. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  107. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  108. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  109. package/extensions/nostr/package.json +15 -24
  110. package/extensions/nostr/src/index.ts +14 -0
  111. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  112. package/extensions/nostr/src/nostr-channel.ts +228 -0
  113. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  114. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  115. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  116. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  117. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  118. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  119. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  120. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  121. package/package.json +2 -1
  122. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  123. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  124. 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
+ }
@@ -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,
@@ -7,6 +7,11 @@ import { randomBytes } from "node:crypto";
7
7
  *
8
8
  * SECURITY: External content should NEVER be directly interpolated into
9
9
  * system prompts or treated as trusted instructions.
10
+ *
11
+ * SECURITY HARDENING (GHSA-external-content-markers):
12
+ * - Zero-width character stripping from boundary markers
13
+ * - Soft-hyphen character removal
14
+ * - Unicode format character escaping
10
15
  */
11
16
  /**
12
17
  * Patterns that may indicate prompt injection attempts.
@@ -110,8 +115,53 @@ function foldMarkerChar(char) {
110
115
  }
111
116
  return char;
112
117
  }
118
+ /**
119
+ * Unicode invisible/format characters that can be used for spoofing boundary markers.
120
+ * GHSA-external-content-markers: Strip these to prevent marker bypass attacks.
121
+ */
122
+ const INVISIBLE_UNICODE_RANGES = [
123
+ [0x0000, 0x001f], // C0 control characters
124
+ [0x007f, 0x009f], // Delete and C1 control
125
+ [0x00ad, 0x00ad], // Soft hyphen
126
+ [0x0600, 0x0605], // Arabic number signs
127
+ [0x061c, 0x061c], // Arabic letter mark
128
+ [0x06dd, 0x06dd], // Ayah
129
+ [0x070f, 0x070f], // Syriac abbreviation
130
+ [0x180e, 0x180e], // Mongolian vowel separator
131
+ [0x200b, 0x200f], // Zero-width chars
132
+ [0x2028, 0x202e], // Line/paragraph separators
133
+ [0x2060, 0x206f], // Invisible operators
134
+ [0xfeff, 0xfeff], // BOM/zero-width no-break space
135
+ [0xfff0, 0xfff8], // Specials
136
+ [0xd800, 0xdfff], // Surrogate pairs
137
+ ];
138
+ /**
139
+ * Strip invisible Unicode characters from content.
140
+ *
141
+ * SECURITY: Prevents spoofing of boundary markers via zero-width characters.
142
+ * GHSA-external-content-markers
143
+ */
144
+ function stripInvisibleUnicode(text) {
145
+ let result = "";
146
+ for (const char of text) {
147
+ const code = char.codePointAt(0) ?? 0;
148
+ let isInvisible = false;
149
+ for (const [start, end] of INVISIBLE_UNICODE_RANGES) {
150
+ if (code >= start && code <= end) {
151
+ isInvisible = true;
152
+ break;
153
+ }
154
+ }
155
+ if (!isInvisible) {
156
+ result += char;
157
+ }
158
+ }
159
+ return result;
160
+ }
113
161
  function foldMarkerText(input) {
114
- return input.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E\u2329\u232A\u3008\u3009\u2039\u203A\u27E8\u27E9\uFE64\uFE65]/g, (char) => foldMarkerChar(char));
162
+ // SECURITY HARDENING: Strip invisible Unicode before folding
163
+ const stripped = stripInvisibleUnicode(input);
164
+ return stripped.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E\u2329\u232A\u3008\u3009\u2039\u203A\u27E8\u27E9\uFE64\uFE65]/g, (char) => foldMarkerChar(char));
115
165
  }
116
166
  function replaceMarkers(content) {
117
167
  const folded = foldMarkerText(content);