UNPKG

@every-env/cli

Version:

Multi-agent orchestrator for AI-powered development workflows

185 lines 7.46 kB
import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { logger } from '../utils/logger.js'; import { validateCommand, sanitizeArgs, createSafeEnvironment, RESOURCE_LIMITS } from '../utils/security.js'; export class BaseAgent extends EventEmitter { config; options; process; startTime; stdout = []; stderr = []; outputSize = 0; processTimeout; constructor(config, options = {}) { super(); this.config = config; this.options = options; } /** * Stops the running process gracefully */ async stop() { if (this.process && !this.process.killed) { logger.warn(`Stopping process for agent: ${this.config.id}`); // Clear timeout if (this.processTimeout) { clearTimeout(this.processTimeout); } // Try graceful shutdown first this.process.kill('SIGTERM'); // Give process 1 second to cleanup await new Promise(resolve => setTimeout(resolve, 1000)); // Force kill if still running if (!this.process.killed) { logger.warn(`Force killing process for agent: ${this.config.id}`); this.process.kill('SIGKILL'); } } } async spawnProcess(command, args, prompt) { // Validate command is allowed validateCommand(command); // Sanitize arguments const safeArgs = sanitizeArgs(args); return new Promise((resolve, reject) => { // For Node.js scripts, we need to prepend Node flags let actualCommand = command; let actualArgs = safeArgs; // Check if we're spawning a Node script const isNodeScript = command === 'node' || command === process.execPath || args[0]?.endsWith('.js') || args[0]?.endsWith('.ts'); if (isNodeScript && command !== process.execPath) { // Use the same Node binary as parent actualCommand = process.execPath; // Prepend Node flags from parent process actualArgs = [...process.execArgv, ...args]; } // Create safe environment for child process const childEnv = createSafeEnvironment({ ...this.options.env, ...this.config.env, }); this.process = spawn(actualCommand, actualArgs, { cwd: this.options.workingDir || this.config.workingDir || process.cwd(), env: childEnv, stdio: this.options.stdio || ['pipe', 'pipe', 'pipe'], }); logger.debug(`[${this.config.id}] Process spawned:`, { pid: this.process.pid, command: actualCommand, args: actualArgs, cwd: this.options.workingDir || this.config.workingDir || process.cwd() }); // Handle process events this.setupProcessHandlers(); // Send prompt based on mode this.sendPrompt(prompt); // Set timeout with resource limits const timeout = this.options.timeout || this.config.timeout || RESOURCE_LIMITS.PROCESS_TIMEOUT; this.processTimeout = setTimeout(() => { if (this.process && !this.process.killed) { logger.warn(`[${this.config.id}] Process timeout after ${timeout}ms`); this.process.kill('SIGTERM'); // Give process time to cleanup setTimeout(() => { if (this.process && !this.process.killed) { this.process.kill('SIGKILL'); } }, 1000); reject(new Error(`Process timeout after ${timeout}ms`)); } }, timeout); this.process.on('close', (code) => { logger.debug(`[${this.config.id}] Process closed with code:`, { code }); if (this.processTimeout) { clearTimeout(this.processTimeout); } resolve(code || 0); }); this.process.on('error', (error) => { logger.error(`[${this.config.id}] Process error:`, { error: error.message }); reject(error); }); // Log when stdin is closed this.process.stdin?.on('close', () => { logger.debug(`[${this.config.id}] stdin closed`); }); }); } setupProcessHandlers() { if (!this.process) return; this.process.stdout?.on('data', (data) => { const text = data.toString(); this.outputSize += data.length; // Check output size limit if (this.outputSize > RESOURCE_LIMITS.MAX_OUTPUT_SIZE) { logger.error(`[${this.config.id}] Output size limit exceeded: ${this.outputSize} bytes`); if (this.process && !this.process.killed) { this.process.kill('SIGTERM'); } return; } this.stdout.push(text); logger.debug(`[${this.config.id}] stdout:`, { output: text.trim() }); this.emit('stdout', text); }); this.process.stderr?.on('data', (data) => { const text = data.toString(); this.outputSize += data.length; // Check output size limit if (this.outputSize > RESOURCE_LIMITS.MAX_OUTPUT_SIZE) { logger.error(`[${this.config.id}] Output size limit exceeded: ${this.outputSize} bytes`); if (this.process && !this.process.killed) { this.process.kill('SIGTERM'); } return; } this.stderr.push(text); logger.debug(`[${this.config.id}] stderr:`, { output: text.trim() }); this.emit('stderr', text); }); } sendPrompt(prompt) { if (!this.process) return; logger.debug(`[${this.config.id}] Sending prompt via ${this.config.promptMode}`, { promptLength: prompt.length, promptMode: this.config.promptMode }); switch (this.config.promptMode) { case 'stdin': logger.debug(`[${this.config.id}] Writing prompt to stdin`); this.process.stdin?.write(prompt); this.process.stdin?.end(); break; case 'env': // Prompt already in environment // Still need to close stdin so process doesn't hang this.process.stdin?.end(); break; case 'flag': case 'arg': // Prompt passed as argument // Still need to close stdin so process doesn't hang this.process.stdin?.end(); break; } } buildResult(status, outputFile, error) { const duration = this.startTime ? Date.now() - this.startTime : 0; return { agentId: this.config.id, status, outputFile, duration, stdout: this.stdout, stderr: this.stderr, error, }; } } //# sourceMappingURL=base-agent.js.map