@phuetz/code-buddy 0.2.0 → 0.3.0
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/dist/agent/execution/agent-executor.js +2 -2
- package/dist/agent/execution/agent-executor.js.map +1 -1
- package/dist/agent/lessons-tracker.d.ts +10 -0
- package/dist/agent/lessons-tracker.js +53 -0
- package/dist/agent/lessons-tracker.js.map +1 -1
- package/dist/commands/client-dispatcher.js +12 -1
- package/dist/commands/client-dispatcher.js.map +1 -1
- package/dist/commands/handlers/lessons-handler.d.ts +2 -0
- package/dist/commands/handlers/lessons-handler.js +63 -0
- package/dist/commands/handlers/lessons-handler.js.map +1 -0
- package/dist/commands/lessons.js +58 -0
- package/dist/commands/lessons.js.map +1 -1
- package/dist/commands/slash/builtin-commands.js +10 -0
- package/dist/commands/slash/builtin-commands.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/memory/semantic-memory-search.js +6 -4
- package/dist/memory/semantic-memory-search.js.map +1 -1
- package/dist/observability/run-store.d.ts +10 -1
- package/dist/observability/run-store.js +21 -0
- package/dist/observability/run-store.js.map +1 -1
- package/dist/optimization/cache-breakpoints.d.ts +2 -2
- package/dist/prompts/system-base.js +8 -18
- package/dist/prompts/system-base.js.map +1 -1
- package/dist/tools/registry/attention-tools.d.ts +2 -2
- package/dist/tools/registry/attention-tools.js +14 -10
- package/dist/tools/registry/attention-tools.js.map +1 -1
- package/dist/tools/registry/lessons-tools.d.ts +4 -4
- package/dist/tools/registry/lessons-tools.js +36 -19
- package/dist/tools/registry/lessons-tools.js.map +1 -1
- package/dist/tools/registry/tool-aliases.js +16 -10
- package/dist/tools/registry/tool-aliases.js.map +1 -1
- package/dist/tools/web-search.js +34 -5
- package/dist/tools/web-search.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/bash.d.ts +0 -128
- package/dist/tools/bash.js +0 -973
- package/dist/tools/bash.js.map +0 -1
package/dist/tools/bash.js
DELETED
|
@@ -1,973 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { ConfirmationService } from '../utils/confirmation-service.js';
|
|
3
|
-
import { getSandboxManager } from '../security/sandbox.js';
|
|
4
|
-
import { getSelfHealingEngine } from '../utils/self-healing.js';
|
|
5
|
-
import { parseTestOutput, isLikelyTestOutput } from '../utils/test-output-parser.js';
|
|
6
|
-
import { registerDisposable } from '../utils/disposable.js';
|
|
7
|
-
import { bashToolSchemas, validateWithSchema, validateCommand as validateCommandSafety, sanitizeForShell } from '../utils/input-validator.js';
|
|
8
|
-
import { rgPath } from '@vscode/ripgrep';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
/**
|
|
12
|
-
* Dangerous command patterns that are always blocked
|
|
13
|
-
*/
|
|
14
|
-
const BLOCKED_PATTERNS = [
|
|
15
|
-
// Filesystem destruction
|
|
16
|
-
/rm\s+(-rf?|--recursive)\s+[/~]/i, // rm -rf / or ~
|
|
17
|
-
/rm\s+.*\/\s*$/i, // rm something/
|
|
18
|
-
/>\s*\/dev\/sd[a-z]/i, // Write to disk device
|
|
19
|
-
/dd\s+.*if=.*of=\/dev/i, // dd to device
|
|
20
|
-
/mkfs/i, // Format filesystem
|
|
21
|
-
/:\(\)\s*\{\s*:\|:&\s*\};:/, // Fork bomb :(){ :|:& };:
|
|
22
|
-
/chmod\s+-R\s+777\s+\//i, // chmod 777 /
|
|
23
|
-
// Remote code execution via pipe to shell
|
|
24
|
-
/wget.*\|\s*(ba)?sh/i, // wget | sh
|
|
25
|
-
/curl.*\|\s*(ba)?sh/i, // curl | sh
|
|
26
|
-
/sudo\s+(rm|dd|mkfs)/i, // sudo dangerous commands
|
|
27
|
-
// Command injection via command substitution
|
|
28
|
-
/\$\([^)]*(?:rm|dd|mkfs|chmod|chown|curl|wget|nc|netcat|bash|sh|eval|exec)/i, // $(dangerous_cmd)
|
|
29
|
-
/`[^`]*(?:rm|dd|mkfs|chmod|chown|curl|wget|nc|netcat|bash|sh|eval|exec)/i, // `dangerous_cmd`
|
|
30
|
-
// Dangerous variable expansion that could leak secrets
|
|
31
|
-
/\$\{?(?:GROK_API_KEY|AWS_SECRET|AWS_ACCESS_KEY|AWS_SESSION_TOKEN|GITHUB_TOKEN|NPM_TOKEN|MORPH_API_KEY|DATABASE_URL|DB_PASSWORD|SECRET_KEY|PRIVATE_KEY|API_KEY|API_SECRET|AUTH_TOKEN|ACCESS_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|SLACK_TOKEN|DISCORD_TOKEN)\}?/i,
|
|
32
|
-
// Eval and exec injection
|
|
33
|
-
/\beval\s+.*\$/i, // eval with variable expansion
|
|
34
|
-
/\bexec\s+\d*[<>]/i, // exec with redirections
|
|
35
|
-
// Hex/octal encoded dangerous commands (bypass attempts)
|
|
36
|
-
/\\x[0-9a-f]{2}/i, // Hex escape sequences
|
|
37
|
-
/\\[0-7]{3}/, // Octal escape sequences
|
|
38
|
-
/\$'\\x/i, // ANSI-C quoting with hex
|
|
39
|
-
/\$'\\[0-7]/, // ANSI-C quoting with octal
|
|
40
|
-
/\$'[^']*\\[nrtbfv]/i, // ANSI-C with escape sequences
|
|
41
|
-
// Base64 decode piped to shell
|
|
42
|
-
/base64\s+(-d|--decode).*\|\s*(ba)?sh/i,
|
|
43
|
-
// Network exfiltration patterns
|
|
44
|
-
/\|\s*(nc|netcat|curl|wget)\s+[^|]*(>|>>)/i, // pipe to network tool with redirect
|
|
45
|
-
/>\s*\/dev\/(tcp|udp)\//i, // bash network redirection
|
|
46
|
-
/\bnc\s+-[elp]/i, // netcat listen/exec modes
|
|
47
|
-
/\bbash\s+-i\s+>&?\s*\/dev\/(tcp|udp)/i, // bash reverse shell
|
|
48
|
-
// Additional bypass patterns
|
|
49
|
-
/\bprintf\s+['"]%b['"].*\\x/i, // printf %b with hex (bypass)
|
|
50
|
-
/\becho\s+-e\s+.*\\x/i, // echo -e with hex
|
|
51
|
-
/\becho\s+\$'\\x/i, // echo with ANSI-C quoting
|
|
52
|
-
/\bxxd\s+-r.*\|\s*(ba)?sh/i, // xxd decode to shell
|
|
53
|
-
/\bpython[23]?\s+-c\s+['"].*(?:exec|eval|os\.system|subprocess|__import__)/i, // Python code exec
|
|
54
|
-
/\bperl\s+-e\s+['"].*(?:system|exec|`)/i, // Perl code exec
|
|
55
|
-
/\bruby\s+-e\s+['"].*(?:system|exec|`)/i, // Ruby code exec
|
|
56
|
-
/\bnode\s+-e\s+['"].*(?:exec|spawn|child_process)/i, // Node.js code exec
|
|
57
|
-
/\bawk\s+.*\bsystem\s*\(/i, // awk system() call
|
|
58
|
-
// Unicode/special character bypass attempts
|
|
59
|
-
// eslint-disable-next-line no-control-regex
|
|
60
|
-
/[\u0000-\u001f]/, // Control characters (except common whitespace handled separately)
|
|
61
|
-
/[\u007f-\u009f]/, // Delete and C1 control codes
|
|
62
|
-
/[\u200b-\u200f]/, // Zero-width and directional chars
|
|
63
|
-
/[\u2028\u2029]/, // Line/paragraph separators
|
|
64
|
-
/[\ufeff]/, // BOM
|
|
65
|
-
/[\ufff0-\uffff]/, // Specials block
|
|
66
|
-
];
|
|
67
|
-
/**
|
|
68
|
-
* Control characters that are never allowed in commands
|
|
69
|
-
* These could be used to manipulate terminal output or bypass validation
|
|
70
|
-
*/
|
|
71
|
-
// eslint-disable-next-line no-control-regex
|
|
72
|
-
const BLOCKED_CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
73
|
-
/**
|
|
74
|
-
* ANSI escape sequences that could manipulate terminal display
|
|
75
|
-
*/
|
|
76
|
-
// eslint-disable-next-line no-control-regex
|
|
77
|
-
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b[PX^_][^\x1b]*\x1b\\|\x1b\][^\x07]*\x07/;
|
|
78
|
-
/**
|
|
79
|
-
* Allowlist of safe base commands
|
|
80
|
-
* Only commands starting with these are allowed in strict mode
|
|
81
|
-
* Reserved for future strict mode implementation
|
|
82
|
-
*/
|
|
83
|
-
const _ALLOWED_COMMANDS = new Set([
|
|
84
|
-
// File operations (read-only or safe)
|
|
85
|
-
'ls', 'cat', 'head', 'tail', 'less', 'more', 'file', 'stat', 'wc',
|
|
86
|
-
'find', 'locate', 'which', 'whereis', 'type',
|
|
87
|
-
// Text processing
|
|
88
|
-
'grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack',
|
|
89
|
-
'sed', 'awk', 'cut', 'sort', 'uniq', 'tr', 'diff', 'comm',
|
|
90
|
-
// Development tools
|
|
91
|
-
'git', 'npm', 'npx', 'yarn', 'pnpm', 'bun',
|
|
92
|
-
'node', 'deno', 'python', 'python3', 'pip', 'pip3',
|
|
93
|
-
'cargo', 'rustc', 'go', 'java', 'javac', 'mvn', 'gradle',
|
|
94
|
-
'make', 'cmake', 'gcc', 'g++', 'clang',
|
|
95
|
-
// Build and test
|
|
96
|
-
'jest', 'vitest', 'mocha', 'pytest', 'tsc', 'esbuild', 'vite', 'webpack',
|
|
97
|
-
'eslint', 'prettier', 'biome',
|
|
98
|
-
// System info (safe read-only)
|
|
99
|
-
'echo', 'printf', 'pwd', 'date', 'whoami', 'hostname', 'uname',
|
|
100
|
-
'env', 'printenv', 'id', 'groups',
|
|
101
|
-
// Process info
|
|
102
|
-
'ps', 'top', 'htop', 'pgrep',
|
|
103
|
-
// Network diagnostics (read-only)
|
|
104
|
-
'ping', 'dig', 'nslookup', 'host',
|
|
105
|
-
// Archives (read operations)
|
|
106
|
-
'tar', 'zip', 'unzip', 'gzip', 'gunzip', 'bzip2', 'xz',
|
|
107
|
-
// Directory operations
|
|
108
|
-
'mkdir', 'rmdir', 'cd',
|
|
109
|
-
// Safe file operations
|
|
110
|
-
'cp', 'mv', 'touch', 'ln',
|
|
111
|
-
// Docker (controlled)
|
|
112
|
-
'docker', 'docker-compose', 'podman',
|
|
113
|
-
// Kubernetes (controlled)
|
|
114
|
-
'kubectl', 'helm',
|
|
115
|
-
// Cloud CLI (controlled)
|
|
116
|
-
'aws', 'gcloud', 'az',
|
|
117
|
-
// Misc safe commands
|
|
118
|
-
'jq', 'yq', 'tree', 'realpath', 'basename', 'dirname',
|
|
119
|
-
'sleep', 'true', 'false', 'test', '[',
|
|
120
|
-
// Package managers
|
|
121
|
-
'apt', 'apt-get', 'brew', 'dnf', 'yum', 'pacman',
|
|
122
|
-
]);
|
|
123
|
-
/**
|
|
124
|
-
* Commands that should be completely blocked even in non-strict mode
|
|
125
|
-
*/
|
|
126
|
-
const BLOCKED_COMMANDS = new Set([
|
|
127
|
-
'rm', 'shred', 'wipefs', // Destructive file operations (blocked without confirmation path)
|
|
128
|
-
'mkfs', 'fdisk', 'parted', // Disk operations
|
|
129
|
-
'dd', // Raw disk operations
|
|
130
|
-
'chmod', 'chown', 'chgrp', // Permission changes (blocked at base level)
|
|
131
|
-
'sudo', 'su', 'doas', // Privilege escalation
|
|
132
|
-
'nc', 'netcat', 'ncat', // Network tools that can be dangerous
|
|
133
|
-
'socat', // Socket relay
|
|
134
|
-
'telnet', 'ftp', // Insecure protocols
|
|
135
|
-
'nmap', 'masscan', // Port scanning
|
|
136
|
-
'tcpdump', 'wireshark', 'tshark', // Packet capture
|
|
137
|
-
'strace', 'ltrace', 'ptrace', // Process tracing
|
|
138
|
-
'gdb', 'lldb', // Debuggers (can be abused)
|
|
139
|
-
'reboot', 'shutdown', 'poweroff', 'halt', // System control
|
|
140
|
-
'init', 'systemctl', 'service', // Service control
|
|
141
|
-
'iptables', 'nft', 'firewall-cmd', // Firewall
|
|
142
|
-
'mount', 'umount', // Mount operations
|
|
143
|
-
'insmod', 'rmmod', 'modprobe', // Kernel modules
|
|
144
|
-
'sysctl', // Kernel parameters
|
|
145
|
-
'crontab', 'at', // Scheduled tasks
|
|
146
|
-
'useradd', 'userdel', 'usermod', // User management
|
|
147
|
-
'passwd', 'chpasswd', // Password changes
|
|
148
|
-
'visudo', // Sudoers editing
|
|
149
|
-
'ssh-keygen', 'ssh-add', // SSH key operations
|
|
150
|
-
'gpg', // GPG operations
|
|
151
|
-
'openssl', // Certificate operations (can leak keys)
|
|
152
|
-
]);
|
|
153
|
-
/**
|
|
154
|
-
* Whitelist of safe environment variables to pass to child processes
|
|
155
|
-
* All other env vars (especially secrets) are filtered out
|
|
156
|
-
*/
|
|
157
|
-
const SAFE_ENV_VARS = new Set([
|
|
158
|
-
// System paths and locale
|
|
159
|
-
'PATH',
|
|
160
|
-
'HOME',
|
|
161
|
-
'USER',
|
|
162
|
-
'SHELL',
|
|
163
|
-
'LANG',
|
|
164
|
-
'LC_ALL',
|
|
165
|
-
'LC_CTYPE',
|
|
166
|
-
'TERM',
|
|
167
|
-
'TZ',
|
|
168
|
-
'TMPDIR',
|
|
169
|
-
'TEMP',
|
|
170
|
-
'TMP',
|
|
171
|
-
// Node.js
|
|
172
|
-
'NODE_ENV',
|
|
173
|
-
'NODE_PATH',
|
|
174
|
-
'NODE_OPTIONS',
|
|
175
|
-
// Development tools
|
|
176
|
-
'EDITOR',
|
|
177
|
-
'VISUAL',
|
|
178
|
-
'PAGER',
|
|
179
|
-
'LESS',
|
|
180
|
-
// Git (non-sensitive)
|
|
181
|
-
'GIT_AUTHOR_NAME',
|
|
182
|
-
'GIT_AUTHOR_EMAIL',
|
|
183
|
-
'GIT_COMMITTER_NAME',
|
|
184
|
-
'GIT_COMMITTER_EMAIL',
|
|
185
|
-
'GIT_TERMINAL_PROMPT',
|
|
186
|
-
// CI/CD flags (non-sensitive)
|
|
187
|
-
'CI',
|
|
188
|
-
'CONTINUOUS_INTEGRATION',
|
|
189
|
-
// Display
|
|
190
|
-
'DISPLAY',
|
|
191
|
-
'COLORTERM',
|
|
192
|
-
// Python
|
|
193
|
-
'PYTHONPATH',
|
|
194
|
-
'PYTHONIOENCODING',
|
|
195
|
-
'VIRTUAL_ENV',
|
|
196
|
-
// Package managers (non-sensitive config)
|
|
197
|
-
'NPM_CONFIG_YES',
|
|
198
|
-
'YARN_ENABLE_PROGRESS_BARS',
|
|
199
|
-
'DEBIAN_FRONTEND',
|
|
200
|
-
// History control
|
|
201
|
-
'HISTFILE',
|
|
202
|
-
'HISTSIZE',
|
|
203
|
-
// Output control
|
|
204
|
-
'NO_COLOR',
|
|
205
|
-
'FORCE_COLOR',
|
|
206
|
-
'NO_TTY',
|
|
207
|
-
// Current working directory
|
|
208
|
-
'PWD',
|
|
209
|
-
'OLDPWD',
|
|
210
|
-
]);
|
|
211
|
-
/**
|
|
212
|
-
* Extract the base command from a command string
|
|
213
|
-
* Handles paths, env var prefixes, and common shell constructs
|
|
214
|
-
*/
|
|
215
|
-
function extractBaseCommand(command) {
|
|
216
|
-
// Trim and handle empty
|
|
217
|
-
const trimmed = command.trim();
|
|
218
|
-
if (!trimmed)
|
|
219
|
-
return null;
|
|
220
|
-
// Skip leading environment variable assignments (VAR=value cmd)
|
|
221
|
-
let remaining = trimmed;
|
|
222
|
-
while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/.test(remaining)) {
|
|
223
|
-
remaining = remaining.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, '');
|
|
224
|
-
}
|
|
225
|
-
// Get the first token
|
|
226
|
-
const match = remaining.match(/^(\S+)/);
|
|
227
|
-
if (!match)
|
|
228
|
-
return null;
|
|
229
|
-
let cmd = match[1];
|
|
230
|
-
// Remove path prefix (e.g., /usr/bin/ls -> ls)
|
|
231
|
-
if (cmd.includes('/')) {
|
|
232
|
-
cmd = cmd.split('/').pop() || cmd;
|
|
233
|
-
}
|
|
234
|
-
// Handle ./ prefix
|
|
235
|
-
if (cmd.startsWith('./')) {
|
|
236
|
-
cmd = cmd.slice(2);
|
|
237
|
-
}
|
|
238
|
-
return cmd.toLowerCase();
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Check if command uses shell features that could bypass validation
|
|
242
|
-
*/
|
|
243
|
-
function hasShellBypassFeatures(command) {
|
|
244
|
-
// Check for multiple commands via && || ; |
|
|
245
|
-
// But allow single pipes for grep, etc.
|
|
246
|
-
const multiCommandPatterns = [
|
|
247
|
-
{ pattern: /;\s*\S/, reason: 'Command chaining with semicolon' },
|
|
248
|
-
{ pattern: /&&\s*\S/, reason: 'Command chaining with &&' },
|
|
249
|
-
{ pattern: /\|\|\s*\S/, reason: 'Command chaining with ||' },
|
|
250
|
-
{ pattern: /\|\s*(?:bash|sh|zsh|ksh|csh|fish|dash)\b/i, reason: 'Pipe to shell' },
|
|
251
|
-
];
|
|
252
|
-
for (const { pattern, reason } of multiCommandPatterns) {
|
|
253
|
-
if (pattern.test(command)) {
|
|
254
|
-
// Check if this is a safe pipe (e.g., grep | wc)
|
|
255
|
-
if (reason === 'Pipe to shell') {
|
|
256
|
-
return { bypass: true, reason };
|
|
257
|
-
}
|
|
258
|
-
// For other chaining, check if the second command is safe
|
|
259
|
-
// For now, we'll allow chaining but each command gets validated separately
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// Check for process substitution
|
|
263
|
-
if (/[<>]\(/.test(command)) {
|
|
264
|
-
return { bypass: true, reason: 'Process substitution detected' };
|
|
265
|
-
}
|
|
266
|
-
// Check for here-string/here-doc that could contain encoded payloads
|
|
267
|
-
if (/<<</.test(command)) {
|
|
268
|
-
return { bypass: true, reason: 'Here-string detected' };
|
|
269
|
-
}
|
|
270
|
-
return { bypass: false };
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Paths that should never be accessed
|
|
274
|
-
*/
|
|
275
|
-
const BLOCKED_PATHS = [
|
|
276
|
-
path.join(os.homedir(), '.ssh'),
|
|
277
|
-
path.join(os.homedir(), '.gnupg'),
|
|
278
|
-
path.join(os.homedir(), '.aws'),
|
|
279
|
-
path.join(os.homedir(), '.docker'),
|
|
280
|
-
path.join(os.homedir(), '.npmrc'),
|
|
281
|
-
path.join(os.homedir(), '.gitconfig'),
|
|
282
|
-
path.join(os.homedir(), '.netrc'),
|
|
283
|
-
path.join(os.homedir(), '.env'),
|
|
284
|
-
path.join(os.homedir(), '.config/gh'),
|
|
285
|
-
path.join(os.homedir(), '.config/gcloud'),
|
|
286
|
-
path.join(os.homedir(), '.kube'),
|
|
287
|
-
'/etc/passwd',
|
|
288
|
-
'/etc/shadow',
|
|
289
|
-
'/etc/sudoers',
|
|
290
|
-
];
|
|
291
|
-
/**
|
|
292
|
-
* Bash Tool
|
|
293
|
-
*
|
|
294
|
-
* Executes shell commands with comprehensive security measures:
|
|
295
|
-
* - Blocked dangerous patterns (rm -rf /, fork bombs, etc.)
|
|
296
|
-
* - Protected paths (~/.ssh, ~/.aws, /etc/shadow, etc.)
|
|
297
|
-
* - User confirmation for commands (unless session-approved)
|
|
298
|
-
* - Self-healing: automatic error recovery for common failures
|
|
299
|
-
* - Process isolation via spawn with process group management
|
|
300
|
-
* - Graceful termination with SIGTERM before SIGKILL
|
|
301
|
-
*
|
|
302
|
-
* Security modes are controlled by SandboxManager configuration.
|
|
303
|
-
* Self-healing can be disabled via --no-self-heal flag.
|
|
304
|
-
*/
|
|
305
|
-
export class BashTool {
|
|
306
|
-
currentDirectory = process.cwd();
|
|
307
|
-
confirmationService = ConfirmationService.getInstance();
|
|
308
|
-
sandboxManager = getSandboxManager();
|
|
309
|
-
selfHealingEngine = getSelfHealingEngine();
|
|
310
|
-
selfHealingEnabled = true;
|
|
311
|
-
runningProcesses = new Set();
|
|
312
|
-
constructor() {
|
|
313
|
-
registerDisposable(this);
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Clean up resources - kill any running processes
|
|
317
|
-
*/
|
|
318
|
-
dispose() {
|
|
319
|
-
for (const proc of this.runningProcesses) {
|
|
320
|
-
try {
|
|
321
|
-
proc.kill('SIGTERM');
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
// Process may already be dead
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
this.runningProcesses.clear();
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Validate command for dangerous patterns
|
|
331
|
-
*
|
|
332
|
-
* Security checks performed (in order):
|
|
333
|
-
* 1. Control characters - blocks terminal manipulation
|
|
334
|
-
* 2. ANSI escape sequences - blocks display manipulation
|
|
335
|
-
* 3. Shell bypass features - blocks process substitution, here-strings, etc.
|
|
336
|
-
* 4. Base command blocklist - blocks known dangerous commands
|
|
337
|
-
* 5. Blocked command patterns - blocks known dangerous patterns
|
|
338
|
-
* 6. Protected paths - blocks access to sensitive directories
|
|
339
|
-
* 7. Sandbox manager validation - additional runtime checks
|
|
340
|
-
*/
|
|
341
|
-
validateCommand(command) {
|
|
342
|
-
// Check for dangerous control characters
|
|
343
|
-
if (BLOCKED_CONTROL_CHARS.test(command)) {
|
|
344
|
-
return {
|
|
345
|
-
valid: false,
|
|
346
|
-
reason: 'Command contains blocked control characters'
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
// Check for ANSI escape sequences that could manipulate terminal
|
|
350
|
-
if (ANSI_ESCAPE_PATTERN.test(command)) {
|
|
351
|
-
return {
|
|
352
|
-
valid: false,
|
|
353
|
-
reason: 'Command contains blocked ANSI escape sequences'
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
// Check for shell bypass features
|
|
357
|
-
const bypassCheck = hasShellBypassFeatures(command);
|
|
358
|
-
if (bypassCheck.bypass) {
|
|
359
|
-
return {
|
|
360
|
-
valid: false,
|
|
361
|
-
reason: `Shell bypass blocked: ${bypassCheck.reason}`
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
// Extract base command and check against blocklist
|
|
365
|
-
const baseCmd = extractBaseCommand(command);
|
|
366
|
-
if (baseCmd && BLOCKED_COMMANDS.has(baseCmd)) {
|
|
367
|
-
return {
|
|
368
|
-
valid: false,
|
|
369
|
-
reason: `Blocked command: ${baseCmd}`
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
// Check for blocked patterns
|
|
373
|
-
for (const pattern of BLOCKED_PATTERNS) {
|
|
374
|
-
if (pattern.test(command)) {
|
|
375
|
-
return {
|
|
376
|
-
valid: false,
|
|
377
|
-
reason: `Blocked command pattern detected: ${pattern.source}`
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Check for access to blocked paths
|
|
382
|
-
for (const blockedPath of BLOCKED_PATHS) {
|
|
383
|
-
if (command.includes(blockedPath)) {
|
|
384
|
-
return {
|
|
385
|
-
valid: false,
|
|
386
|
-
reason: `Access to protected path blocked: ${blockedPath}`
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
// Also use sandbox manager validation
|
|
391
|
-
const sandboxValidation = this.sandboxManager.validateCommand(command);
|
|
392
|
-
if (!sandboxValidation.valid) {
|
|
393
|
-
return sandboxValidation;
|
|
394
|
-
}
|
|
395
|
-
return { valid: true };
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Filter environment variables to only include safe ones
|
|
399
|
-
* This prevents credential leakage to child processes
|
|
400
|
-
*
|
|
401
|
-
* Security measures:
|
|
402
|
-
* - Only allowlisted variable names are passed through
|
|
403
|
-
* - Values containing shell metacharacters are sanitized
|
|
404
|
-
* - Values that look like secrets are excluded
|
|
405
|
-
*/
|
|
406
|
-
getFilteredEnv() {
|
|
407
|
-
const filtered = {};
|
|
408
|
-
// Patterns that suggest a value is a secret (even if var name is allowed)
|
|
409
|
-
const secretPatterns = [
|
|
410
|
-
/^sk-[a-zA-Z0-9]{20,}$/, // OpenAI-style keys
|
|
411
|
-
/^xai-[a-zA-Z0-9]{20,}$/, // xAI keys
|
|
412
|
-
/^ghp_[a-zA-Z0-9]{36}$/, // GitHub PAT
|
|
413
|
-
/^gho_[a-zA-Z0-9]{36}$/, // GitHub OAuth
|
|
414
|
-
/^github_pat_/i, // GitHub fine-grained PAT
|
|
415
|
-
/^AKIA[A-Z0-9]{16}$/, // AWS Access Key
|
|
416
|
-
/^npm_[a-zA-Z0-9]{36}$/, // NPM token
|
|
417
|
-
/^eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/, // JWT
|
|
418
|
-
/^[a-f0-9]{64}$/i, // Hex-encoded secrets (64 chars)
|
|
419
|
-
/^-----BEGIN.*PRIVATE KEY-----/m, // Private keys
|
|
420
|
-
];
|
|
421
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
422
|
-
if (value === undefined)
|
|
423
|
-
continue;
|
|
424
|
-
// Only allow safe variable names
|
|
425
|
-
if (!SAFE_ENV_VARS.has(key))
|
|
426
|
-
continue;
|
|
427
|
-
// Check if value looks like a secret
|
|
428
|
-
const looksLikeSecret = secretPatterns.some(pattern => pattern.test(value));
|
|
429
|
-
if (looksLikeSecret)
|
|
430
|
-
continue;
|
|
431
|
-
// Sanitize value - remove control characters
|
|
432
|
-
// eslint-disable-next-line no-control-regex
|
|
433
|
-
const sanitized = value.replace(/[\x00-\x1f\x7f]/g, '');
|
|
434
|
-
filtered[key] = sanitized;
|
|
435
|
-
}
|
|
436
|
-
return filtered;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Execute a command with streaming output.
|
|
440
|
-
* Yields each line of stdout/stderr as it arrives.
|
|
441
|
-
* Validates and confirms the command before execution.
|
|
442
|
-
*/
|
|
443
|
-
async *executeStreaming(command, timeout = 30000) {
|
|
444
|
-
// Validate command
|
|
445
|
-
const validation = this.validateCommand(command);
|
|
446
|
-
if (!validation.valid) {
|
|
447
|
-
return { success: false, error: `Command blocked: ${validation.reason}` };
|
|
448
|
-
}
|
|
449
|
-
const commandSafetyValidation = validateCommandSafety(command);
|
|
450
|
-
if (!commandSafetyValidation.valid) {
|
|
451
|
-
return { success: false, error: `Command blocked: ${commandSafetyValidation.error}` };
|
|
452
|
-
}
|
|
453
|
-
// Check confirmation
|
|
454
|
-
const sessionFlags = this.confirmationService.getSessionFlags();
|
|
455
|
-
if (!sessionFlags.bashCommands && !sessionFlags.allOperations) {
|
|
456
|
-
const confirmationResult = await this.confirmationService.requestConfirmation({
|
|
457
|
-
operation: 'Run bash command (streaming)',
|
|
458
|
-
filename: command,
|
|
459
|
-
showVSCodeOpen: false,
|
|
460
|
-
content: `Command: ${command}\nWorking directory: ${this.currentDirectory}`,
|
|
461
|
-
}, 'bash');
|
|
462
|
-
if (!confirmationResult.confirmed) {
|
|
463
|
-
return { success: false, error: confirmationResult.feedback || 'Cancelled by user' };
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
// Spawn the process
|
|
467
|
-
const isWindows = process.platform === 'win32';
|
|
468
|
-
const filteredEnv = this.getFilteredEnv();
|
|
469
|
-
const controlledEnv = {
|
|
470
|
-
...filteredEnv,
|
|
471
|
-
HISTFILE: '/dev/null',
|
|
472
|
-
HISTSIZE: '0',
|
|
473
|
-
CI: 'true',
|
|
474
|
-
NO_COLOR: '1',
|
|
475
|
-
TERM: 'dumb',
|
|
476
|
-
NO_TTY: '1',
|
|
477
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
478
|
-
NPM_CONFIG_YES: 'true',
|
|
479
|
-
LC_ALL: 'C.UTF-8',
|
|
480
|
-
LANG: 'C.UTF-8',
|
|
481
|
-
PYTHONIOENCODING: 'utf-8',
|
|
482
|
-
DEBIAN_FRONTEND: 'noninteractive',
|
|
483
|
-
};
|
|
484
|
-
const proc = spawn('bash', ['-c', command], {
|
|
485
|
-
shell: false,
|
|
486
|
-
cwd: this.currentDirectory,
|
|
487
|
-
env: controlledEnv,
|
|
488
|
-
detached: !isWindows,
|
|
489
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
490
|
-
});
|
|
491
|
-
this.runningProcesses.add(proc);
|
|
492
|
-
let stdout = '';
|
|
493
|
-
let stderr = '';
|
|
494
|
-
let timedOut = false;
|
|
495
|
-
const timer = setTimeout(() => {
|
|
496
|
-
timedOut = true;
|
|
497
|
-
try {
|
|
498
|
-
proc.kill('SIGTERM');
|
|
499
|
-
}
|
|
500
|
-
catch { /* ignore */ }
|
|
501
|
-
}, timeout);
|
|
502
|
-
try {
|
|
503
|
-
// Create a readable stream from stdout and stderr combined
|
|
504
|
-
const chunks = [];
|
|
505
|
-
let resolve = null;
|
|
506
|
-
let done = false;
|
|
507
|
-
const onData = (data, isStderr) => {
|
|
508
|
-
const text = data.toString();
|
|
509
|
-
if (isStderr)
|
|
510
|
-
stderr += text;
|
|
511
|
-
else
|
|
512
|
-
stdout += text;
|
|
513
|
-
chunks.push(text);
|
|
514
|
-
if (resolve) {
|
|
515
|
-
resolve();
|
|
516
|
-
resolve = null;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
proc.stdout?.on('data', (data) => onData(data, false));
|
|
520
|
-
proc.stderr?.on('data', (data) => onData(data, true));
|
|
521
|
-
proc.on('close', () => { done = true; if (resolve) {
|
|
522
|
-
resolve();
|
|
523
|
-
resolve = null;
|
|
524
|
-
} });
|
|
525
|
-
while (!done) {
|
|
526
|
-
if (chunks.length > 0) {
|
|
527
|
-
while (chunks.length > 0) {
|
|
528
|
-
yield chunks.shift();
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
await new Promise(r => { resolve = r; });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
// Yield remaining chunks
|
|
536
|
-
while (chunks.length > 0) {
|
|
537
|
-
yield chunks.shift();
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
finally {
|
|
541
|
-
clearTimeout(timer);
|
|
542
|
-
this.runningProcesses.delete(proc);
|
|
543
|
-
}
|
|
544
|
-
if (timedOut) {
|
|
545
|
-
return { success: false, error: `Command timed out after ${timeout}ms` };
|
|
546
|
-
}
|
|
547
|
-
const exitCode = proc.exitCode ?? 0;
|
|
548
|
-
if (exitCode !== 0) {
|
|
549
|
-
return { success: false, error: stderr || `Exit code ${exitCode}`, output: stdout };
|
|
550
|
-
}
|
|
551
|
-
return { success: true, output: stdout };
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Execute a command using spawn with process group isolation (safer than exec)
|
|
555
|
-
* Inspired by mistral-vibe's robust process handling
|
|
556
|
-
*/
|
|
557
|
-
executeWithSpawn(command, options) {
|
|
558
|
-
return new Promise((resolve) => {
|
|
559
|
-
let stdout = '';
|
|
560
|
-
let stderr = '';
|
|
561
|
-
let timedOut = false;
|
|
562
|
-
const isWindows = process.platform === 'win32';
|
|
563
|
-
// Start with filtered environment (only safe vars, no secrets)
|
|
564
|
-
const filteredEnv = this.getFilteredEnv();
|
|
565
|
-
// Controlled environment variables for deterministic output
|
|
566
|
-
const controlledEnv = {
|
|
567
|
-
...filteredEnv,
|
|
568
|
-
// Disable history to prevent command logging
|
|
569
|
-
HISTFILE: '/dev/null',
|
|
570
|
-
HISTSIZE: '0',
|
|
571
|
-
// CI mode for consistent behavior
|
|
572
|
-
CI: 'true',
|
|
573
|
-
// Disable color output for clean parsing
|
|
574
|
-
NO_COLOR: '1',
|
|
575
|
-
TERM: 'dumb',
|
|
576
|
-
// Disable TTY for non-interactive mode
|
|
577
|
-
NO_TTY: '1',
|
|
578
|
-
// Disable interactive features
|
|
579
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
580
|
-
NPM_CONFIG_YES: 'true',
|
|
581
|
-
YARN_ENABLE_PROGRESS_BARS: 'false',
|
|
582
|
-
// Locale settings for consistent encoding
|
|
583
|
-
LC_ALL: 'C.UTF-8',
|
|
584
|
-
LANG: 'C.UTF-8',
|
|
585
|
-
PYTHONIOENCODING: 'utf-8',
|
|
586
|
-
// Force non-interactive for common tools
|
|
587
|
-
DEBIAN_FRONTEND: 'noninteractive',
|
|
588
|
-
};
|
|
589
|
-
const spawnOptions = {
|
|
590
|
-
// IMPORTANT: shell must be false when using bash -c
|
|
591
|
-
// Using shell: true with bash -c creates double-shell that breaks commands
|
|
592
|
-
shell: false,
|
|
593
|
-
cwd: options.cwd,
|
|
594
|
-
env: controlledEnv,
|
|
595
|
-
// Process group isolation on Unix (allows killing entire process tree)
|
|
596
|
-
detached: !isWindows,
|
|
597
|
-
// Don't inherit stdin - commands should be non-interactive
|
|
598
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
599
|
-
};
|
|
600
|
-
const proc = spawn('bash', ['-c', command], spawnOptions);
|
|
601
|
-
// Store process group ID for cleanup
|
|
602
|
-
const pgid = proc.pid;
|
|
603
|
-
// Graceful termination: SIGTERM first, then SIGKILL after grace period
|
|
604
|
-
const gracePeriod = 3000; // 3 seconds grace period
|
|
605
|
-
let gracefulTerminationTimer = null;
|
|
606
|
-
const killProcess = (signal = 'SIGKILL') => {
|
|
607
|
-
try {
|
|
608
|
-
if (!isWindows && pgid) {
|
|
609
|
-
// Kill the entire process group
|
|
610
|
-
process.kill(-pgid, signal);
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
proc.kill(signal);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
catch {
|
|
617
|
-
// Process may have already exited
|
|
618
|
-
try {
|
|
619
|
-
proc.kill('SIGKILL');
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
// Ignore - process is already gone
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
};
|
|
626
|
-
const timer = setTimeout(() => {
|
|
627
|
-
timedOut = true;
|
|
628
|
-
// Try graceful termination first (SIGTERM)
|
|
629
|
-
killProcess('SIGTERM');
|
|
630
|
-
// If still running after grace period, force kill
|
|
631
|
-
gracefulTerminationTimer = setTimeout(() => {
|
|
632
|
-
killProcess('SIGKILL');
|
|
633
|
-
}, gracePeriod);
|
|
634
|
-
}, options.timeout);
|
|
635
|
-
const maxBuffer = 1024 * 1024; // 1MB limit
|
|
636
|
-
proc.stdout?.on('data', (data) => {
|
|
637
|
-
const chunk = data.toString();
|
|
638
|
-
if (stdout.length + chunk.length <= maxBuffer) {
|
|
639
|
-
stdout += chunk;
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
proc.stderr?.on('data', (data) => {
|
|
643
|
-
const chunk = data.toString();
|
|
644
|
-
if (stderr.length + chunk.length <= maxBuffer) {
|
|
645
|
-
stderr += chunk;
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
proc.on('close', (exitCode) => {
|
|
649
|
-
clearTimeout(timer);
|
|
650
|
-
if (gracefulTerminationTimer) {
|
|
651
|
-
clearTimeout(gracefulTerminationTimer);
|
|
652
|
-
}
|
|
653
|
-
if (timedOut) {
|
|
654
|
-
resolve({
|
|
655
|
-
stdout: stdout.trim(),
|
|
656
|
-
stderr: 'Command timed out (graceful termination attempted)',
|
|
657
|
-
exitCode: 124
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
resolve({
|
|
662
|
-
stdout: stdout.trim(),
|
|
663
|
-
stderr: stderr.trim(),
|
|
664
|
-
exitCode: exitCode ?? 1
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
});
|
|
668
|
-
proc.on('error', (error) => {
|
|
669
|
-
clearTimeout(timer);
|
|
670
|
-
if (gracefulTerminationTimer) {
|
|
671
|
-
clearTimeout(gracefulTerminationTimer);
|
|
672
|
-
}
|
|
673
|
-
resolve({
|
|
674
|
-
stdout: '',
|
|
675
|
-
stderr: error.message,
|
|
676
|
-
exitCode: 1
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Execute a shell command
|
|
683
|
-
*
|
|
684
|
-
* Validates command safety, requests user confirmation, and executes
|
|
685
|
-
* via spawn with process isolation. Failed commands trigger self-healing
|
|
686
|
-
* attempts if enabled.
|
|
687
|
-
*
|
|
688
|
-
* Special handling for `cd` commands to update working directory state.
|
|
689
|
-
*
|
|
690
|
-
* @param command - Shell command to execute
|
|
691
|
-
* @param timeout - Maximum execution time in ms (default: 30000)
|
|
692
|
-
* @returns Command output or error message; test output is parsed and structured
|
|
693
|
-
*
|
|
694
|
-
* @example
|
|
695
|
-
* // Simple command
|
|
696
|
-
* await bash.execute('ls -la');
|
|
697
|
-
*
|
|
698
|
-
* // With custom timeout (2 minutes)
|
|
699
|
-
* await bash.execute('npm install', 120000);
|
|
700
|
-
*/
|
|
701
|
-
async execute(command, timeout = 30000) {
|
|
702
|
-
try {
|
|
703
|
-
// Validate input with schema (enhanced validation)
|
|
704
|
-
const schemaValidation = validateWithSchema(bashToolSchemas.execute, { command, timeout }, 'execute');
|
|
705
|
-
if (!schemaValidation.valid) {
|
|
706
|
-
return {
|
|
707
|
-
success: false,
|
|
708
|
-
error: `Invalid input: ${schemaValidation.error}`,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
// Additional command safety validation
|
|
712
|
-
const commandSafetyValidation = validateCommandSafety(command);
|
|
713
|
-
if (!commandSafetyValidation.valid) {
|
|
714
|
-
return {
|
|
715
|
-
success: false,
|
|
716
|
-
error: `Command blocked: ${commandSafetyValidation.error}`,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
// Validate command before any execution (legacy validation)
|
|
720
|
-
const validation = this.validateCommand(command);
|
|
721
|
-
if (!validation.valid) {
|
|
722
|
-
return {
|
|
723
|
-
success: false,
|
|
724
|
-
error: `Command blocked: ${validation.reason}`,
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
// Check if user has already accepted bash commands for this session
|
|
728
|
-
const sessionFlags = this.confirmationService.getSessionFlags();
|
|
729
|
-
if (!sessionFlags.bashCommands && !sessionFlags.allOperations) {
|
|
730
|
-
// Request confirmation showing the command
|
|
731
|
-
const confirmationResult = await this.confirmationService.requestConfirmation({
|
|
732
|
-
operation: 'Run bash command',
|
|
733
|
-
filename: command,
|
|
734
|
-
showVSCodeOpen: false,
|
|
735
|
-
content: `Command: ${command}\nWorking directory: ${this.currentDirectory}`,
|
|
736
|
-
}, 'bash');
|
|
737
|
-
if (!confirmationResult.confirmed) {
|
|
738
|
-
return {
|
|
739
|
-
success: false,
|
|
740
|
-
error: confirmationResult.feedback || 'Command execution cancelled by user',
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
// Handle cd command separately
|
|
745
|
-
if (command.startsWith('cd ')) {
|
|
746
|
-
const newDir = command.substring(3).trim();
|
|
747
|
-
// Remove quotes if present
|
|
748
|
-
const cleanDir = newDir.replace(/^["']|["']$/g, '');
|
|
749
|
-
try {
|
|
750
|
-
process.chdir(cleanDir);
|
|
751
|
-
this.currentDirectory = process.cwd();
|
|
752
|
-
return {
|
|
753
|
-
success: true,
|
|
754
|
-
output: `Changed directory to: ${this.currentDirectory}`,
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
catch (error) {
|
|
758
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
759
|
-
return {
|
|
760
|
-
success: false,
|
|
761
|
-
error: `Cannot change directory: ${errorMessage}`,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
// Execute using spawn (safer than exec)
|
|
766
|
-
const result = await this.executeWithSpawn(command, {
|
|
767
|
-
timeout,
|
|
768
|
-
cwd: this.currentDirectory,
|
|
769
|
-
});
|
|
770
|
-
if (result.exitCode !== 0) {
|
|
771
|
-
const errorMessage = result.stderr || `Command exited with code ${result.exitCode}`;
|
|
772
|
-
// Attempt self-healing if enabled
|
|
773
|
-
if (this.selfHealingEnabled) {
|
|
774
|
-
const healingResult = await this.selfHealingEngine.attemptHealing(command, errorMessage, async (fixCmd) => {
|
|
775
|
-
// Execute fix command without self-healing to avoid recursion
|
|
776
|
-
const fixResult = await this.executeWithSpawn(fixCmd, {
|
|
777
|
-
timeout: timeout * 2, // Give more time for fix commands
|
|
778
|
-
cwd: this.currentDirectory,
|
|
779
|
-
});
|
|
780
|
-
if (fixResult.exitCode === 0) {
|
|
781
|
-
return {
|
|
782
|
-
success: true,
|
|
783
|
-
output: fixResult.stdout || 'Fix applied successfully',
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
return {
|
|
787
|
-
success: false,
|
|
788
|
-
error: fixResult.stderr || `Fix failed with code ${fixResult.exitCode}`,
|
|
789
|
-
};
|
|
790
|
-
});
|
|
791
|
-
if (healingResult.success && healingResult.finalResult) {
|
|
792
|
-
return {
|
|
793
|
-
success: true,
|
|
794
|
-
output: `🔧 Self-healed after ${healingResult.attempts.length} attempt(s)\n` +
|
|
795
|
-
`Fix applied: ${healingResult.fixedCommand}\n\n` +
|
|
796
|
-
(healingResult.finalResult.output || 'Success'),
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
// If healing failed, return original error with healing info
|
|
800
|
-
if (healingResult.attempts.length > 0) {
|
|
801
|
-
return {
|
|
802
|
-
success: false,
|
|
803
|
-
error: `${errorMessage}\n\n🔧 Self-healing attempted ${healingResult.attempts.length} fix(es) but failed.`,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
return {
|
|
808
|
-
success: false,
|
|
809
|
-
error: errorMessage,
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
const output = result.stdout + (result.stderr ? `\nSTDERR: ${result.stderr}` : '');
|
|
813
|
-
const trimmedOutput = output.trim() || 'Command executed successfully (no output)';
|
|
814
|
-
// Check if this looks like test output and enrich it
|
|
815
|
-
if (isLikelyTestOutput(trimmedOutput)) {
|
|
816
|
-
const parsed = parseTestOutput(trimmedOutput);
|
|
817
|
-
if (parsed.isTestOutput && parsed.data) {
|
|
818
|
-
// Return structured test data as JSON for the renderer
|
|
819
|
-
return {
|
|
820
|
-
success: true,
|
|
821
|
-
output: JSON.stringify(parsed.data),
|
|
822
|
-
data: { type: 'test-results', framework: parsed.data.framework },
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return {
|
|
827
|
-
success: true,
|
|
828
|
-
output: trimmedOutput,
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
833
|
-
return {
|
|
834
|
-
success: false,
|
|
835
|
-
error: `Command failed: ${errorMessage}`,
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Enable or disable self-healing
|
|
841
|
-
*/
|
|
842
|
-
setSelfHealing(enabled) {
|
|
843
|
-
this.selfHealingEnabled = enabled;
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Check if self-healing is enabled
|
|
847
|
-
*/
|
|
848
|
-
isSelfHealingEnabled() {
|
|
849
|
-
return this.selfHealingEnabled;
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Get self-healing engine for configuration
|
|
853
|
-
*/
|
|
854
|
-
getSelfHealingEngine() {
|
|
855
|
-
return this.selfHealingEngine;
|
|
856
|
-
}
|
|
857
|
-
getCurrentDirectory() {
|
|
858
|
-
return this.currentDirectory;
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Escape shell argument to prevent command injection
|
|
862
|
-
*/
|
|
863
|
-
escapeShellArg(arg) {
|
|
864
|
-
// Use single quotes and escape any single quotes in the string
|
|
865
|
-
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* List files in a directory (wrapper for `ls -la`)
|
|
869
|
-
*
|
|
870
|
-
* @param directory - Directory path to list (default: current directory)
|
|
871
|
-
* @returns Formatted directory listing or error
|
|
872
|
-
*/
|
|
873
|
-
async listFiles(directory = '.') {
|
|
874
|
-
// Validate input with schema
|
|
875
|
-
const validation = validateWithSchema(bashToolSchemas.listFiles, { directory }, 'listFiles');
|
|
876
|
-
if (!validation.valid) {
|
|
877
|
-
return {
|
|
878
|
-
success: false,
|
|
879
|
-
error: `Invalid input: ${validation.error}`,
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const safeDir = sanitizeForShell(directory);
|
|
883
|
-
return this.execute(`ls -la ${safeDir}`);
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Find files matching a pattern (wrapper for `find -name -type f`)
|
|
887
|
-
*
|
|
888
|
-
* @param pattern - Glob pattern to match (e.g., "*.ts", "package.json")
|
|
889
|
-
* @param directory - Directory to search in (default: current directory)
|
|
890
|
-
* @returns List of matching file paths or error
|
|
891
|
-
*/
|
|
892
|
-
async findFiles(pattern, directory = '.') {
|
|
893
|
-
// Validate input with schema
|
|
894
|
-
const validation = validateWithSchema(bashToolSchemas.findFiles, { pattern, directory }, 'findFiles');
|
|
895
|
-
if (!validation.valid) {
|
|
896
|
-
return {
|
|
897
|
-
success: false,
|
|
898
|
-
error: `Invalid input: ${validation.error}`,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
const safeDir = sanitizeForShell(directory);
|
|
902
|
-
const safePattern = sanitizeForShell(pattern);
|
|
903
|
-
return this.execute(`find ${safeDir} -name ${safePattern} -type f`);
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* Search for a pattern in files using ripgrep
|
|
907
|
-
*
|
|
908
|
-
* Uses @vscode/ripgrep for ultra-fast searching. Results are limited
|
|
909
|
-
* to 100 matches for performance.
|
|
910
|
-
*
|
|
911
|
-
* @param pattern - Regex pattern to search for
|
|
912
|
-
* @param files - File or directory to search in (default: current directory)
|
|
913
|
-
* @returns Matching lines with file paths and line numbers, or error
|
|
914
|
-
*/
|
|
915
|
-
async grep(pattern, files = '.') {
|
|
916
|
-
// Validate input with schema
|
|
917
|
-
const validation = validateWithSchema(bashToolSchemas.grep, { pattern, files }, 'grep');
|
|
918
|
-
if (!validation.valid) {
|
|
919
|
-
return {
|
|
920
|
-
success: false,
|
|
921
|
-
error: `Invalid input: ${validation.error}`,
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
// Use ripgrep for ultra-fast searching
|
|
925
|
-
return new Promise((resolve) => {
|
|
926
|
-
const args = [
|
|
927
|
-
'--no-heading',
|
|
928
|
-
'--line-number',
|
|
929
|
-
'--color', 'never',
|
|
930
|
-
'--max-count', '100', // Limit results for performance
|
|
931
|
-
pattern,
|
|
932
|
-
files
|
|
933
|
-
];
|
|
934
|
-
const rg = spawn(rgPath, args, {
|
|
935
|
-
cwd: this.currentDirectory,
|
|
936
|
-
env: this.getFilteredEnv(),
|
|
937
|
-
});
|
|
938
|
-
let stdout = '';
|
|
939
|
-
let stderr = '';
|
|
940
|
-
rg.stdout?.on('data', (data) => {
|
|
941
|
-
if (stdout.length < 5_000_000)
|
|
942
|
-
stdout += data.toString();
|
|
943
|
-
});
|
|
944
|
-
rg.stderr?.on('data', (data) => {
|
|
945
|
-
if (stderr.length < 100_000)
|
|
946
|
-
stderr += data.toString();
|
|
947
|
-
});
|
|
948
|
-
rg.on('close', (code) => {
|
|
949
|
-
// ripgrep returns 1 if no matches found (not an error)
|
|
950
|
-
if (code === 0 || code === 1) {
|
|
951
|
-
resolve({
|
|
952
|
-
success: true,
|
|
953
|
-
output: stdout || 'No matches found',
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
resolve({
|
|
958
|
-
success: false,
|
|
959
|
-
error: stderr || `ripgrep exited with code ${code}`,
|
|
960
|
-
output: stdout,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
rg.on('error', (error) => {
|
|
965
|
-
resolve({
|
|
966
|
-
success: false,
|
|
967
|
-
error: `ripgrep error: ${error.message}`,
|
|
968
|
-
});
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
//# sourceMappingURL=bash.js.map
|