@synet/shell-exec
Version:
Safe shell command execution with Unit Architecture - foundation component for MetaDev
559 lines (554 loc) โข 20.7 kB
JavaScript
/**
* @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;