@probelabs/probe 0.6.0-rc105 → 0.6.0-rc107

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.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Bash command executor with security and timeout controls
3
+ * @module agent/bashExecutor
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import { resolve, join } from 'path';
8
+ import { existsSync } from 'fs';
9
+ import { parseCommandForExecution } from './bashCommandUtils.js';
10
+
11
+ /**
12
+ * Execute a bash command with security controls
13
+ * @param {string} command - Command to execute
14
+ * @param {Object} options - Execution options
15
+ * @param {string} [options.workingDirectory] - Working directory for command execution
16
+ * @param {number} [options.timeout=120000] - Timeout in milliseconds
17
+ * @param {Object} [options.env={}] - Additional environment variables
18
+ * @param {number} [options.maxBuffer=10485760] - Maximum buffer size (10MB)
19
+ * @param {boolean} [options.debug=false] - Enable debug logging
20
+ * @returns {Promise<Object>} Execution result
21
+ */
22
+ export async function executeBashCommand(command, options = {}) {
23
+ const {
24
+ workingDirectory = process.cwd(),
25
+ timeout = 120000, // 2 minutes default
26
+ env = {},
27
+ maxBuffer = 10 * 1024 * 1024, // 10MB
28
+ debug = false
29
+ } = options;
30
+
31
+ // Validate working directory
32
+ let cwd = workingDirectory;
33
+ try {
34
+ cwd = resolve(cwd);
35
+ if (!existsSync(cwd)) {
36
+ throw new Error(`Working directory does not exist: ${cwd}`);
37
+ }
38
+ } catch (error) {
39
+ return {
40
+ success: false,
41
+ error: `Invalid working directory: ${error.message}`,
42
+ stdout: '',
43
+ stderr: '',
44
+ exitCode: 1,
45
+ command,
46
+ workingDirectory: cwd,
47
+ duration: 0
48
+ };
49
+ }
50
+
51
+ const startTime = Date.now();
52
+
53
+ if (debug) {
54
+ console.log(`[BashExecutor] Executing command: "${command}"`);
55
+ console.log(`[BashExecutor] Working directory: "${cwd}"`);
56
+ console.log(`[BashExecutor] Timeout: ${timeout}ms`);
57
+ }
58
+
59
+ return new Promise((resolve, reject) => {
60
+ // Create environment
61
+ const processEnv = {
62
+ ...process.env,
63
+ ...env
64
+ };
65
+
66
+ // Parse command for shell execution
67
+ // We use shell: false for security, so we need to parse manually
68
+ const args = parseCommandForExecution(command);
69
+ if (!args || args.length === 0) {
70
+ resolve({
71
+ success: false,
72
+ error: 'Failed to parse command',
73
+ stdout: '',
74
+ stderr: '',
75
+ exitCode: 1,
76
+ command,
77
+ workingDirectory: cwd,
78
+ duration: Date.now() - startTime
79
+ });
80
+ return;
81
+ }
82
+
83
+ const [cmd, ...cmdArgs] = args;
84
+
85
+ // Spawn the process
86
+ const child = spawn(cmd, cmdArgs, {
87
+ cwd,
88
+ env: processEnv,
89
+ stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, capture stdout/stderr
90
+ shell: false, // For security
91
+ windowsHide: true
92
+ });
93
+
94
+ let stdout = '';
95
+ let stderr = '';
96
+ let killed = false;
97
+ let timeoutHandle;
98
+
99
+ // Set timeout
100
+ if (timeout > 0) {
101
+ timeoutHandle = setTimeout(() => {
102
+ if (!killed) {
103
+ killed = true;
104
+ child.kill('SIGTERM');
105
+
106
+ // Force kill after 5 seconds if still running
107
+ setTimeout(() => {
108
+ if (child.exitCode === null) {
109
+ child.kill('SIGKILL');
110
+ }
111
+ }, 5000);
112
+ }
113
+ }, timeout);
114
+ }
115
+
116
+ // Handle stdout
117
+ child.stdout.on('data', (data) => {
118
+ const chunk = data.toString();
119
+ if (stdout.length + chunk.length <= maxBuffer) {
120
+ stdout += chunk;
121
+ } else {
122
+ // Buffer overflow
123
+ if (!killed) {
124
+ killed = true;
125
+ child.kill('SIGTERM');
126
+ }
127
+ }
128
+ });
129
+
130
+ // Handle stderr
131
+ child.stderr.on('data', (data) => {
132
+ const chunk = data.toString();
133
+ if (stderr.length + chunk.length <= maxBuffer) {
134
+ stderr += chunk;
135
+ } else {
136
+ // Buffer overflow
137
+ if (!killed) {
138
+ killed = true;
139
+ child.kill('SIGTERM');
140
+ }
141
+ }
142
+ });
143
+
144
+ // Handle process exit
145
+ child.on('close', (code, signal) => {
146
+ if (timeoutHandle) {
147
+ clearTimeout(timeoutHandle);
148
+ }
149
+
150
+ const duration = Date.now() - startTime;
151
+
152
+ if (debug) {
153
+ console.log(`[BashExecutor] Command completed - Code: ${code}, Signal: ${signal}, Duration: ${duration}ms`);
154
+ console.log(`[BashExecutor] Stdout length: ${stdout.length}, Stderr length: ${stderr.length}`);
155
+ }
156
+
157
+ let success = true;
158
+ let error = '';
159
+
160
+ if (killed) {
161
+ success = false;
162
+ if (stdout.length + stderr.length > maxBuffer) {
163
+ error = `Command output exceeded maximum buffer size (${maxBuffer} bytes)`;
164
+ } else {
165
+ error = `Command timed out after ${timeout}ms`;
166
+ }
167
+ } else if (code !== 0) {
168
+ success = false;
169
+ error = `Command exited with code ${code}`;
170
+ }
171
+
172
+ resolve({
173
+ success,
174
+ error,
175
+ stdout: stdout.trim(),
176
+ stderr: stderr.trim(),
177
+ exitCode: code,
178
+ signal,
179
+ command,
180
+ workingDirectory: cwd,
181
+ duration,
182
+ killed
183
+ });
184
+ });
185
+
186
+ // Handle spawn errors
187
+ child.on('error', (error) => {
188
+ if (timeoutHandle) {
189
+ clearTimeout(timeoutHandle);
190
+ }
191
+
192
+ if (debug) {
193
+ console.log(`[BashExecutor] Spawn error:`, error);
194
+ }
195
+
196
+ resolve({
197
+ success: false,
198
+ error: `Failed to execute command: ${error.message}`,
199
+ stdout: '',
200
+ stderr: '',
201
+ exitCode: 1,
202
+ command,
203
+ workingDirectory: cwd,
204
+ duration: Date.now() - startTime
205
+ });
206
+ });
207
+ });
208
+ }
209
+
210
+
211
+ /**
212
+ * Format execution result for display
213
+ * @param {Object} result - Execution result
214
+ * @param {boolean} [includeMetadata=false] - Include metadata in output
215
+ * @returns {string} Formatted result
216
+ */
217
+ export function formatExecutionResult(result, includeMetadata = false) {
218
+ if (!result) {
219
+ return 'No result available';
220
+ }
221
+
222
+ let output = '';
223
+
224
+ // Add command info if metadata requested
225
+ if (includeMetadata) {
226
+ output += `Command: ${result.command}\n`;
227
+ output += `Working directory: ${result.workingDirectory}\n`;
228
+ output += `Duration: ${result.duration}ms\n`;
229
+ output += `Exit Code: ${result.exitCode}\n`;
230
+ if (result.signal) {
231
+ output += `Signal: ${result.signal}\n`;
232
+ }
233
+ output += '\n';
234
+ }
235
+
236
+ // Add stdout if present
237
+ if (result.stdout) {
238
+ if (includeMetadata) {
239
+ output += '--- STDOUT ---\n';
240
+ }
241
+ output += result.stdout;
242
+ if (includeMetadata && result.stderr) {
243
+ output += '\n';
244
+ }
245
+ }
246
+
247
+ // Add stderr if present
248
+ if (result.stderr) {
249
+ if (includeMetadata) {
250
+ if (result.stdout) output += '\n';
251
+ output += '--- STDERR ---\n';
252
+ } else if (result.stdout) {
253
+ output += '\n--- STDERR ---\n';
254
+ }
255
+ output += result.stderr;
256
+ }
257
+
258
+ // Add error message if failed and no stderr
259
+ if (!result.success && result.error && !result.stderr) {
260
+ if (output) output += '\n';
261
+ output += `Error: ${result.error}`;
262
+ }
263
+
264
+ // Add exit code for failed commands
265
+ if (!result.success && result.exitCode !== undefined && result.exitCode !== 0) {
266
+ if (output) output += '\n';
267
+ output += `Exit code: ${result.exitCode}`;
268
+ }
269
+
270
+ return output || (result.success ? 'Command completed successfully (no output)' : 'Command failed (no output)');
271
+ }
272
+
273
+ /**
274
+ * Validate execution options
275
+ * @param {Object} options - Options to validate
276
+ * @returns {Object} Validation result
277
+ */
278
+ export function validateExecutionOptions(options = {}) {
279
+ const errors = [];
280
+ const warnings = [];
281
+
282
+ // Check timeout
283
+ if (options.timeout !== undefined) {
284
+ if (typeof options.timeout !== 'number' || options.timeout < 0) {
285
+ errors.push('timeout must be a non-negative number');
286
+ } else if (options.timeout > 600000) { // 10 minutes
287
+ warnings.push('timeout is very high (>10 minutes)');
288
+ }
289
+ }
290
+
291
+ // Check maxBuffer
292
+ if (options.maxBuffer !== undefined) {
293
+ if (typeof options.maxBuffer !== 'number' || options.maxBuffer < 1024) {
294
+ errors.push('maxBuffer must be at least 1024 bytes');
295
+ } else if (options.maxBuffer > 100 * 1024 * 1024) { // 100MB
296
+ warnings.push('maxBuffer is very high (>100MB)');
297
+ }
298
+ }
299
+
300
+ // Check working directory
301
+ if (options.workingDirectory) {
302
+ if (typeof options.workingDirectory !== 'string') {
303
+ errors.push('workingDirectory must be a string');
304
+ } else if (!existsSync(options.workingDirectory)) {
305
+ errors.push(`workingDirectory does not exist: ${options.workingDirectory}`);
306
+ }
307
+ }
308
+
309
+ // Check environment
310
+ if (options.env && typeof options.env !== 'object') {
311
+ errors.push('env must be an object');
312
+ }
313
+
314
+ return {
315
+ valid: errors.length === 0,
316
+ errors,
317
+ warnings
318
+ };
319
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Simplified bash command permission checker (aligned with executor capabilities)
3
+ * @module agent/bashPermissions
4
+ */
5
+
6
+ import { DEFAULT_ALLOW_PATTERNS, DEFAULT_DENY_PATTERNS } from './bashDefaults.js';
7
+ import { parseCommand, isComplexCommand } from './bashCommandUtils.js';
8
+
9
+ /**
10
+ * Check if a pattern matches a parsed command
11
+ * @param {Object} parsedCommand - Parsed command with command and args
12
+ * @param {string} pattern - Pattern to match against (e.g., "git:status", "npm:*")
13
+ * @returns {boolean} True if pattern matches
14
+ */
15
+ function matchesPattern(parsedCommand, pattern) {
16
+ if (!parsedCommand || !pattern) return false;
17
+
18
+ const { command, args } = parsedCommand;
19
+ if (!command) return false;
20
+
21
+ // Split pattern into parts separated by ':'
22
+ const patternParts = pattern.split(':');
23
+ const commandName = patternParts[0];
24
+
25
+ // Check if command name matches (with wildcard support)
26
+ if (commandName === '*') {
27
+ // Wildcard matches any command
28
+ return true;
29
+ } else if (commandName !== command) {
30
+ // Command name doesn't match
31
+ return false;
32
+ }
33
+
34
+ // If only command name specified, it matches
35
+ if (patternParts.length === 1) {
36
+ return true;
37
+ }
38
+
39
+ // Check arguments
40
+ for (let i = 1; i < patternParts.length; i++) {
41
+ const patternArg = patternParts[i];
42
+ const argIndex = i - 1;
43
+
44
+ if (patternArg === '*') {
45
+ // Wildcard matches any argument (or no argument)
46
+ continue;
47
+ }
48
+
49
+ if (argIndex >= args.length) {
50
+ // Not enough arguments to match pattern
51
+ return false;
52
+ }
53
+
54
+ const actualArg = args[argIndex];
55
+ if (patternArg !== actualArg) {
56
+ // Argument doesn't match
57
+ return false;
58
+ }
59
+ }
60
+
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Check if any pattern in a list matches the command
66
+ * @param {Object} parsedCommand - Parsed command
67
+ * @param {string[]} patterns - Array of patterns to check
68
+ * @returns {boolean} True if any pattern matches
69
+ */
70
+ function matchesAnyPattern(parsedCommand, patterns) {
71
+ if (!patterns || patterns.length === 0) return false;
72
+ return patterns.some(pattern => matchesPattern(parsedCommand, pattern));
73
+ }
74
+
75
+ /**
76
+ * Bash permission checker for simple commands only
77
+ * Rejects complex shell constructs for security and alignment with executor
78
+ */
79
+ export class BashPermissionChecker {
80
+ /**
81
+ * Create a permission checker
82
+ * @param {Object} config - Configuration options
83
+ * @param {string[]} [config.allow] - Additional allow patterns
84
+ * @param {string[]} [config.deny] - Additional deny patterns
85
+ * @param {boolean} [config.disableDefaultAllow] - Disable default allow list
86
+ * @param {boolean} [config.disableDefaultDeny] - Disable default deny list
87
+ * @param {boolean} [config.debug] - Enable debug logging
88
+ */
89
+ constructor(config = {}) {
90
+ this.debug = config.debug || false;
91
+
92
+ // Build allow patterns
93
+ this.allowPatterns = [];
94
+ if (!config.disableDefaultAllow) {
95
+ this.allowPatterns.push(...DEFAULT_ALLOW_PATTERNS);
96
+ if (this.debug) {
97
+ console.log(`[BashPermissions] Added ${DEFAULT_ALLOW_PATTERNS.length} default allow patterns`);
98
+ }
99
+ }
100
+ if (config.allow && Array.isArray(config.allow)) {
101
+ this.allowPatterns.push(...config.allow);
102
+ if (this.debug) {
103
+ console.log(`[BashPermissions] Added ${config.allow.length} custom allow patterns:`, config.allow);
104
+ }
105
+ }
106
+
107
+ // Build deny patterns
108
+ this.denyPatterns = [];
109
+ if (!config.disableDefaultDeny) {
110
+ this.denyPatterns.push(...DEFAULT_DENY_PATTERNS);
111
+ if (this.debug) {
112
+ console.log(`[BashPermissions] Added ${DEFAULT_DENY_PATTERNS.length} default deny patterns`);
113
+ }
114
+ }
115
+ if (config.deny && Array.isArray(config.deny)) {
116
+ this.denyPatterns.push(...config.deny);
117
+ if (this.debug) {
118
+ console.log(`[BashPermissions] Added ${config.deny.length} custom deny patterns:`, config.deny);
119
+ }
120
+ }
121
+
122
+ if (this.debug) {
123
+ console.log(`[BashPermissions] Total patterns - Allow: ${this.allowPatterns.length}, Deny: ${this.denyPatterns.length}`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check if a simple command is allowed (rejects complex commands for security)
129
+ * @param {string} command - Command to check
130
+ * @returns {Object} Permission result
131
+ */
132
+ check(command) {
133
+ if (!command || typeof command !== 'string') {
134
+ return {
135
+ allowed: false,
136
+ reason: 'Invalid or empty command',
137
+ command: command
138
+ };
139
+ }
140
+
141
+ // First check if this is a complex command - reject immediately for security
142
+ if (isComplexCommand(command)) {
143
+ return {
144
+ allowed: false,
145
+ reason: 'Complex shell commands with pipes, operators, or redirections are not supported for security reasons',
146
+ command: command,
147
+ isComplex: true
148
+ };
149
+ }
150
+
151
+ // Parse the simple command
152
+ const parsed = parseCommand(command);
153
+
154
+ if (parsed.error) {
155
+ return {
156
+ allowed: false,
157
+ reason: parsed.error,
158
+ command: command
159
+ };
160
+ }
161
+
162
+ if (!parsed.command) {
163
+ return {
164
+ allowed: false,
165
+ reason: 'No valid command found',
166
+ command: command
167
+ };
168
+ }
169
+
170
+ if (this.debug) {
171
+ console.log(`[BashPermissions] Checking simple command: "${command}"`);
172
+ console.log(`[BashPermissions] Parsed: ${parsed.command} with args: [${parsed.args.join(', ')}]`);
173
+ }
174
+
175
+ // Check deny patterns first (deny takes precedence)
176
+ if (matchesAnyPattern(parsed, this.denyPatterns)) {
177
+ const matchedPatterns = this.denyPatterns.filter(pattern => matchesPattern(parsed, pattern));
178
+ return {
179
+ allowed: false,
180
+ reason: `Command matches deny pattern: ${matchedPatterns[0]}`,
181
+ command: command,
182
+ parsed: parsed,
183
+ matchedPatterns: matchedPatterns
184
+ };
185
+ }
186
+
187
+ // Check allow patterns
188
+ if (this.allowPatterns.length > 0) {
189
+ if (!matchesAnyPattern(parsed, this.allowPatterns)) {
190
+ return {
191
+ allowed: false,
192
+ reason: 'Command not in allow list',
193
+ command: command,
194
+ parsed: parsed
195
+ };
196
+ }
197
+ }
198
+
199
+ // Command passed all checks
200
+ const result = {
201
+ allowed: true,
202
+ command: command,
203
+ parsed: parsed,
204
+ isComplex: false
205
+ };
206
+
207
+ if (this.debug) {
208
+ console.log(`[BashPermissions] ALLOWED - command passed all checks`);
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ /**
215
+ * Get configuration summary
216
+ * @returns {Object} Configuration info
217
+ */
218
+ getConfig() {
219
+ return {
220
+ allowPatterns: this.allowPatterns.length,
221
+ denyPatterns: this.denyPatterns.length,
222
+ totalPatterns: this.allowPatterns.length + this.denyPatterns.length
223
+ };
224
+ }
225
+ }
226
+
227
+ // Export utility functions for testing
228
+ export { parseCommand, matchesPattern, matchesAnyPattern };