@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.
- package/build/agent/ProbeAgent.js +12 -1
- package/build/agent/bashCommandUtils.js +200 -0
- package/build/agent/bashDefaults.js +202 -0
- package/build/agent/bashExecutor.js +319 -0
- package/build/agent/bashPermissions.js +228 -0
- package/build/agent/index.js +1523 -145
- package/build/agent/probeTool.js +9 -0
- package/build/agent/schemaUtils.js +89 -12
- package/build/agent/tools.js +13 -1
- package/build/index.js +6 -0
- package/build/tools/bash.js +216 -0
- package/build/tools/common.js +64 -0
- package/build/tools/index.js +6 -0
- package/cjs/agent/ProbeAgent.cjs +1479 -167
- package/cjs/index.cjs +1554 -176
- package/package.json +9 -2
- package/src/agent/ProbeAgent.js +12 -1
- package/src/agent/bashCommandUtils.js +200 -0
- package/src/agent/bashDefaults.js +202 -0
- package/src/agent/bashExecutor.js +319 -0
- package/src/agent/bashPermissions.js +228 -0
- package/src/agent/index.js +83 -2
- package/src/agent/probeTool.js +9 -0
- package/src/agent/schemaUtils.js +89 -12
- package/src/agent/tools.js +13 -1
- package/src/index.js +6 -0
- package/src/tools/bash.js +216 -0
- package/src/tools/common.js +64 -0
- package/src/tools/index.js +6 -0
|
@@ -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 };
|
package/src/agent/index.js
CHANGED
|
@@ -128,7 +128,15 @@ function parseArgs() {
|
|
|
128
128
|
traceConsole: false,
|
|
129
129
|
useStdin: false, // New flag to indicate stdin should be used
|
|
130
130
|
outline: false, // New flag to enable outline format
|
|
131
|
-
noMermaidValidation: false // New flag to disable mermaid validation
|
|
131
|
+
noMermaidValidation: false, // New flag to disable mermaid validation
|
|
132
|
+
// Bash tool configuration
|
|
133
|
+
enableBash: false,
|
|
134
|
+
bashAllow: null,
|
|
135
|
+
bashDeny: null,
|
|
136
|
+
bashTimeout: null,
|
|
137
|
+
bashWorkingDir: null,
|
|
138
|
+
disableDefaultBashAllow: false,
|
|
139
|
+
disableDefaultBashDeny: false
|
|
132
140
|
};
|
|
133
141
|
|
|
134
142
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -172,6 +180,20 @@ function parseArgs() {
|
|
|
172
180
|
config.outline = true;
|
|
173
181
|
} else if (arg === '--no-mermaid-validation') {
|
|
174
182
|
config.noMermaidValidation = true;
|
|
183
|
+
} else if (arg === '--enable-bash') {
|
|
184
|
+
config.enableBash = true;
|
|
185
|
+
} else if (arg === '--bash-allow' && i + 1 < args.length) {
|
|
186
|
+
config.bashAllow = args[++i];
|
|
187
|
+
} else if (arg === '--bash-deny' && i + 1 < args.length) {
|
|
188
|
+
config.bashDeny = args[++i];
|
|
189
|
+
} else if (arg === '--bash-timeout' && i + 1 < args.length) {
|
|
190
|
+
config.bashTimeout = args[++i];
|
|
191
|
+
} else if (arg === '--bash-working-dir' && i + 1 < args.length) {
|
|
192
|
+
config.bashWorkingDir = args[++i];
|
|
193
|
+
} else if (arg === '--no-default-bash-allow') {
|
|
194
|
+
config.disableDefaultBashAllow = true;
|
|
195
|
+
} else if (arg === '--no-default-bash-deny') {
|
|
196
|
+
config.disableDefaultBashDeny = true;
|
|
175
197
|
} else if (!arg.startsWith('--') && !config.question) {
|
|
176
198
|
// First non-flag argument is the question
|
|
177
199
|
config.question = arg;
|
|
@@ -223,6 +245,15 @@ Options:
|
|
|
223
245
|
--no-mermaid-validation Disable automatic mermaid diagram validation and fixing
|
|
224
246
|
--help, -h Show this help message
|
|
225
247
|
|
|
248
|
+
Bash Tool Options:
|
|
249
|
+
--enable-bash Enable bash command execution for system exploration
|
|
250
|
+
--bash-allow <patterns> Additional bash command patterns to allow (comma-separated)
|
|
251
|
+
--bash-deny <patterns> Additional bash command patterns to deny (comma-separated)
|
|
252
|
+
--no-default-bash-allow Disable default bash allow list (use only custom patterns)
|
|
253
|
+
--no-default-bash-deny Disable default bash deny list (use only custom patterns)
|
|
254
|
+
--bash-timeout <ms> Bash command timeout in milliseconds (default: 120000)
|
|
255
|
+
--bash-working-dir <path> Default working directory for bash commands
|
|
256
|
+
|
|
226
257
|
Environment Variables:
|
|
227
258
|
ANTHROPIC_API_KEY Anthropic Claude API key
|
|
228
259
|
OPENAI_API_KEY OpenAI GPT API key
|
|
@@ -621,6 +652,54 @@ async function main() {
|
|
|
621
652
|
}
|
|
622
653
|
}
|
|
623
654
|
|
|
655
|
+
// Process bash configuration
|
|
656
|
+
let bashConfig = null;
|
|
657
|
+
if (config.enableBash) {
|
|
658
|
+
bashConfig = {};
|
|
659
|
+
|
|
660
|
+
// Parse allow patterns
|
|
661
|
+
if (config.bashAllow) {
|
|
662
|
+
bashConfig.allow = config.bashAllow.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Parse deny patterns
|
|
666
|
+
if (config.bashDeny) {
|
|
667
|
+
bashConfig.deny = config.bashDeny.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Handle default list flags
|
|
671
|
+
if (config.disableDefaultBashAllow) {
|
|
672
|
+
bashConfig.disableDefaultAllow = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (config.disableDefaultBashDeny) {
|
|
676
|
+
bashConfig.disableDefaultDeny = true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Parse timeout
|
|
680
|
+
if (config.bashTimeout) {
|
|
681
|
+
const timeout = parseInt(config.bashTimeout, 10);
|
|
682
|
+
if (isNaN(timeout) || timeout < 1000) {
|
|
683
|
+
console.error('Error: Bash timeout must be a number >= 1000 milliseconds');
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
bashConfig.timeout = timeout;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Set working directory
|
|
690
|
+
if (config.bashWorkingDir) {
|
|
691
|
+
if (!existsSync(config.bashWorkingDir)) {
|
|
692
|
+
console.error(`Error: Bash working directory does not exist: ${config.bashWorkingDir}`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
bashConfig.workingDirectory = config.bashWorkingDir;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (config.verbose) {
|
|
699
|
+
console.log('Bash command execution enabled');
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
624
703
|
// Create and configure agent
|
|
625
704
|
const agentConfig = {
|
|
626
705
|
path: config.path,
|
|
@@ -632,7 +711,9 @@ async function main() {
|
|
|
632
711
|
tracer: appTracer,
|
|
633
712
|
outline: config.outline,
|
|
634
713
|
maxResponseTokens: config.maxResponseTokens,
|
|
635
|
-
disableMermaidValidation: config.noMermaidValidation
|
|
714
|
+
disableMermaidValidation: config.noMermaidValidation,
|
|
715
|
+
enableBash: config.enableBash,
|
|
716
|
+
bashConfig: bashConfig
|
|
636
717
|
};
|
|
637
718
|
|
|
638
719
|
const agent = new ProbeAgent(agentConfig);
|
package/src/agent/probeTool.js
CHANGED
|
@@ -201,6 +201,15 @@ export function createWrappedTools(baseTools) {
|
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Wrap bash tool
|
|
205
|
+
if (baseTools.bashTool) {
|
|
206
|
+
wrappedTools.bashToolInstance = wrapToolWithEmitter(
|
|
207
|
+
baseTools.bashTool,
|
|
208
|
+
'bash',
|
|
209
|
+
baseTools.bashTool.execute
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
204
213
|
return wrappedTools;
|
|
205
214
|
}
|
|
206
215
|
|