@every-env/cli
Version:
Multi-agent orchestrator for AI-powered development workflows
185 lines • 7.46 kB
JavaScript
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