@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.
- package/CHANGELOG.md +111 -0
- package/dist/.buildstamp +1 -1
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/policy.js +52 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/btw.js +280 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/agents/fast-mode.js +24 -0
- package/dist/agents/live-model-errors.js +23 -0
- package/dist/agents/model-auth-env-vars.js +44 -0
- package/dist/agents/model-auth-markers.js +69 -0
- package/dist/agents/models-config.providers.discovery.js +180 -0
- package/dist/agents/models-config.providers.static.js +480 -0
- package/dist/auto-reply/reply/typing-policy.js +15 -0
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/account-snapshot-fields.js +176 -0
- package/dist/channels/draft-stream-controls.js +89 -0
- package/dist/channels/inbound-debounce-policy.js +28 -0
- package/dist/channels/typing-lifecycle.js +39 -0
- package/dist/cli/program/command-registry.js +52 -0
- package/dist/commands/agent-binding.js +123 -0
- package/dist/commands/agents.commands.bind.js +280 -0
- package/dist/commands/backup-shared.js +186 -0
- package/dist/commands/backup-verify.js +236 -0
- package/dist/commands/backup.js +166 -0
- package/dist/commands/channel-account-context.js +15 -0
- package/dist/commands/channel-account.js +190 -0
- package/dist/commands/gateway-install-token.js +117 -0
- package/dist/commands/oauth-tls-preflight.js +121 -0
- package/dist/commands/ollama-setup.js +402 -0
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/commands/self-hosted-provider-setup.js +207 -0
- package/dist/commands/session-store-targets.js +12 -0
- package/dist/commands/sessions-cleanup.js +97 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/cron/heartbeat-policy.js +26 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/hooks-mapping.js +46 -7
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/hooks/module-loader.js +28 -0
- package/dist/infra/agent-command-binding.js +144 -0
- package/dist/infra/backup.js +328 -0
- package/dist/infra/channel-account-context.js +173 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/infra/session-cleanup.js +143 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- 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
|
+
}
|
package/dist/plugins/loader.js
CHANGED
|
@@ -191,23 +191,21 @@ function isTrackedByProvenance(params) {
|
|
|
191
191
|
}
|
|
192
192
|
return matchesPathMatcher(params.index.loadPathMatcher, sourcePath);
|
|
193
193
|
}
|
|
194
|
-
function
|
|
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.
|
|
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
|
-
|
|
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,
|