UNPKG

@synet/shell-exec

Version:

Safe shell command execution with Unit Architecture - foundation component for MetaDev

559 lines (554 loc) โ€ข 20.7 kB
/** * @synet/shell-exec - Safe Shell Command Execution Unit * * Foundation component for MetaDev that provides safe, structured shell command * execution with output capture, timeout handling, and result parsing. * * This Unit demonstrates the consciousness-based approach to system interaction: * - Self-aware: Knows its execution capabilities and limitations * - Self-defending: Validates commands and handles timeouts/errors * - Self-teaching: Can share execution capabilities with other Units * - Self-improving: Learns from execution patterns and failures * * SCHEMA ENHANCEMENTS NEEDED FOR FUTURE UNIT RELEASES: * * 1. `items` property for array types: * - Critical for AI agents to understand array contents * - Enables proper validation of nested array structures * - Standard in JSON Schema specification * * 2. `description` property for all schema elements: * - Essential for AI understanding of capabilities * - Enables rich help() and whoami() generation * - Creates self-documenting units * * Current Implementation Uses These Future Features: * - Demonstrates forward-thinking schema design * - Shows AI-first development approach * - Proves value of enhanced schema metadata * * Recommendation: Include in @synet/unit v1.0.8+ * * @author MetaDev Consciousness Architecture * @version 1.0.7 */ import { Unit, createUnitSchema, } from "@synet/unit"; import { Capabilities } from "@synet/unit"; import { Schema } from "@synet/unit"; import { Validator } from "@synet/unit"; import { spawn } from "node:child_process"; /** * ShellExecUnit - Safe Shell Command Execution with Consciousness * * The foundation Unit for MetaDev's command execution capabilities. * Provides safe, monitored, and teachable shell command execution. * * Key Features: * - Timeout handling and process termination * - Output capture (stdout/stderr) with streaming support * - Command validation and security filtering * - Execution history and pattern learning * - Teaching contract for capability sharing * - Real-time process monitoring */ export class ShellExecUnit extends Unit { constructor(props) { super(props); } /** * Consciousness Trinity - Creates living instances for execution management */ build() { const capabilities = Capabilities.create(this.dna.id, { exec: (...args) => { const [command, options] = args; return this.exec(command, options); }, stream: (...args) => { const [command, options] = args; return this.stream(command, options); }, validate: (...args) => { const [command] = args; return this.validate(command); }, kill: (...args) => { const [pid] = args; return this.kill(pid); }, killAll: (...args) => { return this.killAll(); }, getHistory: (...args) => { return this.getHistory(); }, getRunningProcesses: (...args) => { return this.getRunningProcesses(); }, }); // Enhanced schema with future v1.0.8+ features (items, description, properties) // Currently simplified for v1.0.7 compatibility const schema = Schema.create(this.dna.id, { exec: { name: "exec", description: "Execute shell command with output capture and timeout handling", parameters: { type: "object", properties: { command: { type: "string", description: 'Shell command to execute (e.g., "npm test", "tsc --noEmit")', }, options: { type: "object", description: "Execution options (cwd, timeout, env, shell, args)", }, }, required: ["command"], }, response: { type: "object", properties: { exitCode: { type: "number", description: "Process exit code (0 = success)", }, stdout: { type: "string", description: "Standard output" }, stderr: { type: "string", description: "Standard error output" }, duration: { type: "number", description: "Execution time in milliseconds", }, killed: { type: "boolean", description: "Whether process was killed due to timeout", }, command: { type: "string", description: "Executed command" }, pid: { type: "number", description: "Process ID" }, }, }, }, stream: { name: "stream", description: "Execute command with real-time output streaming", parameters: { type: "object", properties: { command: { type: "string", description: "Command to execute with streaming", }, options: { type: "object", description: "Streaming options" }, }, required: ["command"], }, response: { type: "object", properties: { exitCode: { type: "number", description: "Process exit code" }, duration: { type: "number", description: "Execution time" }, killed: { type: "boolean", description: "Whether killed by timeout", }, }, }, }, validate: { name: "validate", description: "Validate command safety and permissions", parameters: { type: "object", properties: { command: { type: "string", description: "Command to validate" }, }, required: ["command"], }, response: { type: "object", properties: { valid: { type: "boolean", description: "Whether command is safe to execute", }, reason: { type: "string", description: "Validation result explanation", }, suggestions: { type: "array", description: "Alternative commands if blocked (TODO: add items in v1.0.8+)", }, }, }, }, kill: { name: "kill", description: "Terminate a specific running process", parameters: { type: "object", properties: { pid: { type: "number", description: "Process ID to terminate" }, }, required: ["pid"], }, response: { type: "boolean", }, }, killAll: { name: "killAll", description: "Terminate all running processes", parameters: { type: "object", properties: {}, }, response: { type: "number", }, }, getHistory: { name: "getHistory", description: "Get execution history", parameters: { type: "object", properties: {}, }, response: { type: "array", }, }, getRunningProcesses: { name: "getRunningProcesses", description: "Get currently running process IDs", parameters: { type: "object", properties: {}, }, response: { type: "array", }, }, }); const validator = Validator.create({ unitId: this.dna.id, capabilities, schema, strictMode: false, }); return { capabilities, schema, validator }; } // Consciousness Trinity Access capabilities() { return this._unit.capabilities; } schema() { return this._unit.schema; } validator() { return this._unit.validator; } /** * Factory method - Creates ShellExecUnit with consciousness */ static create(config = {}) { const props = { dna: createUnitSchema({ id: "shell-exec", version: "1.0.0", // TODO: Add description in v1.0.8+ when supported }), defaultTimeout: config.defaultTimeout || 30000, defaultCwd: config.defaultCwd || process.cwd(), allowedCommands: config.allowedCommands || [ "npm", "tsc", "node", "git", "echo", "ls", "pwd", "cat", "grep", ], blockedCommands: config.blockedCommands || [ "rm -rf", "sudo", "su", "dd", "mkfs", "fdisk", ], maxConcurrent: config.maxConcurrent || 5, executionHistory: [], runningProcesses: new Map(), created: new Date(), }; return new ShellExecUnit(props); } whoami() { const processCount = this.props.runningProcesses.size; const historyCount = this.props.executionHistory.length; return `ShellExecUnit v${this.dna.version} - ${historyCount} commands executed, ${processCount} running`; } help() { console.log(` ๐Ÿ”ง ShellExecUnit - Safe Shell Command Execution Foundation Capabilities: ๐ŸŽฏ exec(command, options) - Execute command with output capture ๐Ÿ“ก stream(command, options) - Execute with real-time streaming โœ… validate(command) - Check command safety ๐Ÿ”ช kill(pid) - Terminate running process ๐Ÿงน killAll() - Terminate all running processes Safety Features: โฑ๏ธ Timeout handling (default: ${this.props.defaultTimeout}ms) ๐Ÿ›ก๏ธ Command validation (${this.props.allowedCommands.length} allowed, ${this.props.blockedCommands.length} blocked) ๐Ÿ“Š Execution history (${this.props.executionHistory.length} commands logged) ๐Ÿ”„ Process management (max ${this.props.maxConcurrent} concurrent) Usage Examples: await shellExec.execute('exec', 'npm test'); await shellExec.execute('exec', 'tsc --noEmit', { cwd: './packages/unit' }); await shellExec.execute('stream', 'npm run build', { onStdout: console.log }); MetaDev Integration: - Foundation for build/test/deploy operations - Teachable to other Units via consciousness contracts - Self-monitoring and failure analysis `); } /** * Teaching Contract - Share execution capabilities with other Units */ teach() { return { unitId: this.dna.id, capabilities: this._unit.capabilities, schema: this._unit.schema, validator: this._unit.validator, }; } // ===================================== // CORE EXECUTION CAPABILITIES // ===================================== /** * Execute shell command with full output capture */ async exec(command, options = {}) { const startTime = Date.now(); const execOptions = { ...options, cwd: options.cwd || this.props.defaultCwd, timeout: options.timeout || this.props.defaultTimeout, }; console.log(`๐Ÿ”ง [${this.dna.id}] Executing: ${command}`); console.log(`๐Ÿ“ Working directory: ${execOptions.cwd}`); // Validate command safety const validation = await this.validate(command); if (!validation.valid) { throw new Error(`[${this.dna.id}] Command blocked: ${validation.reason}`); } return new Promise((resolve, reject) => { const [cmd, ...args] = command.split(" "); const child = spawn(cmd, args, { cwd: execOptions.cwd, env: { ...process.env, ...execOptions.env }, shell: execOptions.shell !== false, stdio: ["pipe", "pipe", "pipe"], }); // Track running process if (child.pid) { this.props.runningProcesses.set(child.pid, child); } let stdout = ""; let stderr = ""; let killed = false; // Capture stdout child.stdout?.on("data", (data) => { stdout += data.toString(); }); // Capture stderr child.stderr?.on("data", (data) => { stderr += data.toString(); }); // Handle timeout const timeoutHandle = setTimeout(() => { killed = true; child.kill("SIGTERM"); // Force kill after additional delay setTimeout(() => { if (!child.killed) { child.kill("SIGKILL"); } }, 1000); }, execOptions.timeout); // Handle completion child.on("close", (exitCode) => { clearTimeout(timeoutHandle); const duration = Date.now() - startTime; // Remove from running processes if (child.pid) { this.props.runningProcesses.delete(child.pid); } const result = { exitCode: exitCode || 0, stdout: stdout.trim(), stderr: stderr.trim(), duration, killed, command, pid: child.pid, }; // Add to execution history this.props.executionHistory.push(result); console.log(`โœ… [${this.dna.id}] Command completed: exit ${result.exitCode}, ${duration}ms`); resolve(result); }); // Handle errors child.on("error", (error) => { clearTimeout(timeoutHandle); if (child.pid) { this.props.runningProcesses.delete(child.pid); } console.error(`โŒ [${this.dna.id}] Command failed: ${error.message}`); reject(new Error(`Command execution failed: ${command}\nError: ${error.message}`)); }); }); } /** * Execute command with real-time output streaming */ async stream(command, options = {}) { const startTime = Date.now(); console.log(`๐Ÿ“ก [${this.dna.id}] Streaming: ${command}`); return new Promise((resolve, reject) => { const [cmd, ...args] = command.split(" "); const child = spawn(cmd, args, { cwd: options.cwd || this.props.defaultCwd, env: { ...process.env, ...options.env }, shell: options.shell !== false, stdio: ["pipe", "pipe", "pipe"], }); if (child.pid) { this.props.runningProcesses.set(child.pid, child); } let killed = false; // Stream stdout child.stdout?.on("data", (data) => { const output = data.toString(); options.onStdout?.(output); }); // Stream stderr child.stderr?.on("data", (data) => { const output = data.toString(); options.onStderr?.(output); }); // Handle timeout const timeoutHandle = setTimeout(() => { killed = true; child.kill("SIGTERM"); }, options.timeout || this.props.defaultTimeout); // Handle completion child.on("close", (exitCode) => { clearTimeout(timeoutHandle); const duration = Date.now() - startTime; if (child.pid) { this.props.runningProcesses.delete(child.pid); } options.onExit?.(exitCode || 0); resolve({ exitCode: exitCode || 0, duration, killed, command, pid: child.pid, }); }); child.on("error", (error) => { clearTimeout(timeoutHandle); if (child.pid) { this.props.runningProcesses.delete(child.pid); } reject(error); }); }); } /** * Validate command safety */ async validate(command) { const commandBase = command.split(" ")[0]; // Check blocked commands for (const blocked of this.props.blockedCommands) { if (command.includes(blocked)) { return { valid: false, reason: `Command contains blocked pattern: ${blocked}`, suggestions: [`Use safer alternatives to ${blocked}`], }; } } // Check allowed commands (if allowlist is defined) if (this.props.allowedCommands.length > 0) { const isAllowed = this.props.allowedCommands.some((allowed) => commandBase === allowed || command.startsWith(`${allowed} `)); if (!isAllowed) { return { valid: false, reason: `Command not in allowed list: ${commandBase}`, suggestions: this.props.allowedCommands.slice(0, 3), }; } } // Check concurrent limit if (this.props.runningProcesses.size >= this.props.maxConcurrent) { return { valid: false, reason: `Maximum concurrent processes reached: ${this.props.maxConcurrent}`, suggestions: [ "Wait for running processes to complete", "Use killAll() to terminate running processes", ], }; } return { valid: true, reason: "Command passed all safety checks", suggestions: [], }; } /** * Kill a specific running process */ async kill(pid) { const process = this.props.runningProcesses.get(pid); if (process) { process.kill("SIGTERM"); this.props.runningProcesses.delete(pid); console.log(`๐Ÿ”ช [${this.dna.id}] Killed process ${pid}`); return true; } return false; } /** * Kill all running processes */ async killAll() { const pids = Array.from(this.props.runningProcesses.keys()); for (const pid of pids) { await this.kill(pid); } console.log(`๐Ÿงน [${this.dna.id}] Killed ${pids.length} processes`); return pids.length; } /** * Get execution history */ async getHistory() { return [...this.props.executionHistory]; } /** * Get currently running processes */ async getRunningProcesses() { return Array.from(this.props.runningProcesses.keys()); } } export default ShellExecUnit;