UNPKG

visaire

Version:

A fully autonomous agentic CLI tool for interacting with large language models (Claude, Gemini, GPT) that can execute multi-step tasks automatically

554 lines (476 loc) 14.9 kB
const { spawn, exec } = require('child_process'); const { promisify } = require('util'); const path = require('path'); const fs = require('fs-extra'); const execAsync = promisify(exec); /** * Enhanced execution tool with security controls and comprehensive command handling */ class ExecTool { constructor(options = {}) { this.logger = options.logger; this.security = options.security || {}; this.allowedCommands = options.allowedCommands || this.security.allowedCommands || [ 'npm', 'node', 'git', 'ls', 'pwd', 'cat', 'echo', 'mkdir', 'touch', 'grep', 'find', 'curl', 'wget', 'which', 'whereis', 'ps', 'sleep' ]; this.blockedCommands = options.blockedCommands || this.security.blockedCommands || [ 'rm', 'rmdir', 'del', 'format', 'fdisk', 'mkfs', 'dd', 'sudo', 'su', 'chmod', 'chown', 'passwd', 'shutdown', 'reboot', 'halt', 'init' ]; this.maxExecutionTime = options.maxExecutionTime || this.security.maxExecutionTime || 30000; // 30 seconds this.maxOutputSize = options.maxOutputSize || this.security.maxOutputSize || 1048576; // 1MB this.workingDirectory = process.cwd(); } /** * Get tool metadata */ getMetadata() { return { name: 'exec', version: '2.0.0', description: 'Enhanced command execution with security controls', capabilities: [ 'execute_command', 'install_package', 'run_script', 'spawn_process', 'check_command', 'get_environment' ], security: { allowedCommands: this.allowedCommands, blockedCommands: this.blockedCommands, maxExecutionTime: this.maxExecutionTime, maxOutputSize: this.maxOutputSize } }; } /** * Validate action before execution */ async validateAction(action) { const validation = { valid: true, errors: [], warnings: [] }; if (action.parameters && action.parameters[0]) { const command = action.parameters[0]; const safetyCheck = this.isCommandSafe(command); if (!safetyCheck.safe) { validation.valid = false; validation.errors.push(safetyCheck.reason); } if (safetyCheck.warnings) { validation.warnings.push(...safetyCheck.warnings); } } return validation; } /** * Main execution method */ async execute(method, ...args) { if (!this[method] || typeof this[method] !== 'function') { throw new Error(`Method ${method} not found in ExecTool`); } const startTime = Date.now(); try { const result = await this[method](...args); if (this.logger) { this.logger.logPerformance(`exec:${method}`, Date.now() - startTime, { args: args.length, success: true }); } return result; } catch (error) { if (this.logger) { this.logger.logPerformance(`exec:${method}`, Date.now() - startTime, { args: args.length, success: false, error: error.message }); } throw error; } } /** * Execute shell command */ async executeCommand(command, options = {}) { try { // Security check const safetyCheck = this.isCommandSafe(command); if (!safetyCheck.safe) { throw new Error(`Command blocked: ${safetyCheck.reason}`); } const execOptions = { cwd: options.cwd || this.workingDirectory, timeout: options.timeout || this.maxExecutionTime, maxBuffer: options.maxBuffer || this.maxOutputSize, env: { ...process.env, ...options.env } }; if (this.logger) { this.logger.info('Executing command', { command, options: execOptions }); } const startTime = Date.now(); const { stdout, stderr } = await execAsync(command, execOptions); const executionTime = Date.now() - startTime; const result = { success: true, command, stdout: stdout.toString(), stderr: stderr.toString(), executionTime, exitCode: 0 }; if (this.logger) { this.logger.info('Command executed successfully', { command, executionTime, outputLength: result.stdout.length }); } return result; } catch (error) { const result = { success: false, command, error: error.message, stdout: error.stdout ? error.stdout.toString() : '', stderr: error.stderr ? error.stderr.toString() : '', exitCode: error.code || 1, signal: error.signal }; if (this.logger) { this.logger.error('Command execution failed', { command, error: error.message, exitCode: result.exitCode }); } return result; } } /** * Install npm package */ async installPackage(packageName, options = {}) { try { // Validate package name if (!this.isValidPackageName(packageName)) { throw new Error(`Invalid package name: ${packageName}`); } const installType = options.dev ? '--save-dev' : '--save'; const version = options.version ? `@${options.version}` : ''; const command = `npm install ${installType} ${packageName}${version}`; if (this.logger) { this.logger.info('Installing package', { packageName, options }); } const result = await this.executeCommand(command, { timeout: options.timeout || 120000, // 2 minutes for package installation ...options }); if (result.success) { // Verify installation const verifyResult = await this.verifyPackageInstallation(packageName); result.verified = verifyResult.installed; result.version = verifyResult.version; } return result; } catch (error) { if (this.logger) { this.logger.error('Package installation failed', { packageName, error: error.message }); } throw error; } } /** * Run npm script */ async runScript(scriptName, options = {}) { try { // Check if package.json exists const packageJsonPath = path.join(this.workingDirectory, 'package.json'); if (!await fs.pathExists(packageJsonPath)) { throw new Error('package.json not found in current directory'); } // Read package.json to verify script exists const packageJson = await fs.readJson(packageJsonPath); if (!packageJson.scripts || !packageJson.scripts[scriptName]) { throw new Error(`Script '${scriptName}' not found in package.json`); } const command = `npm run ${scriptName}`; if (this.logger) { this.logger.info('Running npm script', { scriptName, command: packageJson.scripts[scriptName] }); } return await this.executeCommand(command, options); } catch (error) { if (this.logger) { this.logger.error('Script execution failed', { scriptName, error: error.message }); } throw error; } } /** * Spawn long-running process */ async spawnProcess(command, args = [], options = {}) { return new Promise((resolve, reject) => { try { // Security check const fullCommand = `${command} ${args.join(' ')}`; const safetyCheck = this.isCommandSafe(fullCommand); if (!safetyCheck.safe) { reject(new Error(`Command blocked: ${safetyCheck.reason}`)); return; } const spawnOptions = { cwd: options.cwd || this.workingDirectory, env: { ...process.env, ...options.env }, stdio: options.stdio || 'pipe' }; if (this.logger) { this.logger.info('Spawning process', { command, args, options: spawnOptions }); } const child = spawn(command, args, spawnOptions); let stdout = ''; let stderr = ''; // Collect output if (child.stdout) { child.stdout.on('data', (data) => { stdout += data.toString(); if (stdout.length > this.maxOutputSize) { child.kill('SIGTERM'); reject(new Error('Output size limit exceeded')); } }); } if (child.stderr) { child.stderr.on('data', (data) => { stderr += data.toString(); }); } // Handle process completion child.on('close', (code, signal) => { const result = { success: code === 0, command: `${command} ${args.join(' ')}`, stdout, stderr, exitCode: code, signal, pid: child.pid }; if (this.logger) { this.logger.info('Process completed', { command, exitCode: code, signal, outputLength: stdout.length }); } resolve(result); }); child.on('error', (error) => { if (this.logger) { this.logger.error('Process spawn failed', { command, error: error.message }); } reject(error); }); // Set timeout const timeout = options.timeout || this.maxExecutionTime; setTimeout(() => { if (!child.killed) { child.kill('SIGTERM'); reject(new Error(`Process timed out after ${timeout}ms`)); } }, timeout); } catch (error) { reject(error); } }); } /** * Check if command exists */ async checkCommand(command) { try { const result = await this.executeCommand(`which ${command}`, { timeout: 5000 }); return { exists: result.success && result.stdout.trim().length > 0, path: result.stdout.trim(), command }; } catch (error) { return { exists: false, error: error.message, command }; } } /** * Get environment information */ async getEnvironment() { try { const nodeVersion = await this.executeCommand('node --version'); const npmVersion = await this.executeCommand('npm --version'); const gitVersion = await this.executeCommand('git --version'); return { success: true, node: { version: nodeVersion.stdout.trim(), available: nodeVersion.success }, npm: { version: npmVersion.stdout.trim(), available: npmVersion.success }, git: { version: gitVersion.stdout.trim(), available: gitVersion.success }, platform: process.platform, arch: process.arch, cwd: this.workingDirectory, env: { NODE_ENV: process.env.NODE_ENV, PATH: process.env.PATH } }; } catch (error) { if (this.logger) { this.logger.error('Failed to get environment info', { error: error.message }); } throw error; } } /** * Check if command is safe to execute */ isCommandSafe(command) { const result = { safe: true, reason: '', warnings: [] }; // Extract base command const baseCommand = command.trim().split(' ')[0]; // Check blocked commands for (const blocked of this.blockedCommands) { if (command.toLowerCase().includes(blocked.toLowerCase())) { result.safe = false; result.reason = `Blocked command detected: ${blocked}`; return result; } } // Check if command is in allowed list (if allowedCommands is restrictive) if (this.allowedCommands.length > 0) { const isAllowed = this.allowedCommands.some(allowed => baseCommand.toLowerCase() === allowed.toLowerCase() || command.toLowerCase().startsWith(allowed.toLowerCase()) ); if (!isAllowed) { result.safe = false; result.reason = `Command not in allowed list: ${baseCommand}`; return result; } } // Check for dangerous patterns const dangerousPatterns = [ /rm\s+-rf/i, />\s*\/dev\/null/i, /;\s*rm/i, /&&\s*rm/i, /\|\s*rm/i, /sudo/i, /su\s/i ]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { result.warnings.push(`Potentially dangerous pattern detected: ${pattern.source}`); } } // Check for shell injection attempts const injectionPatterns = [ /[;&|`$()]/, /\$\{/, /\$\(/ ]; for (const pattern of injectionPatterns) { if (pattern.test(command)) { result.warnings.push(`Potential shell injection pattern: ${pattern.source}`); } } return result; } /** * Validate npm package name */ isValidPackageName(packageName) { // Basic npm package name validation const packageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; return packageNameRegex.test(packageName); } /** * Verify package installation */ async verifyPackageInstallation(packageName) { try { const command = `npm list ${packageName} --depth=0 --json`; const result = await this.executeCommand(command, { timeout: 10000 }); if (result.success) { try { const listData = JSON.parse(result.stdout); const dependencies = listData.dependencies || {}; if (dependencies[packageName]) { return { installed: true, version: dependencies[packageName].version }; } } catch (parseError) { // Fallback to simple check } } return { installed: false }; } catch (error) { return { installed: false, error: error.message }; } } /** * Configure security settings */ configureSecurity(settings) { if (settings.allowedCommands) { this.allowedCommands = settings.allowedCommands; } if (settings.blockedCommands) { this.blockedCommands = settings.blockedCommands; } if (settings.maxExecutionTime) { this.maxExecutionTime = settings.maxExecutionTime; } if (settings.maxOutputSize) { this.maxOutputSize = settings.maxOutputSize; } if (this.logger) { this.logger.info('Exec tool security configured', settings); } } /** * Set working directory */ setWorkingDirectory(directory) { if (fs.existsSync(directory)) { this.workingDirectory = path.resolve(directory); if (this.logger) { this.logger.info('Working directory changed', { directory: this.workingDirectory }); } } else { throw new Error(`Directory does not exist: ${directory}`); } } } module.exports = ExecTool;