codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
589 lines (503 loc) • 17 kB
text/typescript
import { z } from 'zod';
import { BaseTool } from './base-tool.js';
import { spawn, exec, ChildProcess } from 'child_process';
import { promisify } from 'util';
import { logger } from '../logger.js';
const execAsync = promisify(exec);
// SECURITY: Command whitelist and validation
const ALLOWED_COMMANDS = ['node', 'npm', 'git', 'ls', 'pwd', 'cat', 'grep', 'find', 'echo'];
const BLOCKED_PATTERNS = [
/rm\s+-rf/i, // Dangerous deletions
/chmod\s+777/i, // Permission changes
/sudo/i, // Privilege escalation
/su\s+/i, // User switching
/curl.*\|.*sh/i, // Remote code execution
/wget.*\|.*sh/i, // Remote code execution
/eval/i, // Code evaluation
/exec/i, // Code execution
/>\s*\/dev\//i, // Device access
/[;&|`$()]/, // Shell injection characters
];
function validateCommand(command: string): { isValid: boolean; reason?: string } {
if (!command || command.trim() === '') {
return { isValid: false, reason: 'Empty command' };
}
// Check against blocked patterns
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.test(command)) {
return { isValid: false, reason: `Command contains blocked pattern: ${pattern}` };
}
}
// Extract base command
const baseCommand = command.split(' ')[0];
// Check if base command is allowed
if (!ALLOWED_COMMANDS.includes(baseCommand)) {
return { isValid: false, reason: `Command '${baseCommand}' is not in allowed list` };
}
return { isValid: true };
}
interface ProcessSession {
id: string;
process: ChildProcess;
command: string;
startTime: number;
lastActivity: number;
outputBuffer: string[];
isInteractive: boolean;
}
/**
* Advanced Process Management Tool with session support
*/
export class AdvancedProcessTool extends BaseTool {
private sessions: Map<string, ProcessSession> = new Map();
private sessionCounter = 0;
constructor(private agentContext: { workingDirectory: string }) {
const parameters = z.object({
action: z.enum(['start', 'interact', 'read', 'list', 'kill', 'status']),
sessionId: z.string().optional().describe('Session ID for existing processes'),
command: z.string().optional().describe('Command to execute'),
input: z.string().optional().describe('Input to send to process'),
timeout: z.number().optional().default(30000).describe('Timeout in milliseconds'),
interactive: z.boolean().optional().default(false).describe('Start interactive session'),
environment: z.record(z.string()).optional().describe('Environment variables'),
workingDirectory: z.string().optional().describe('Working directory'),
});
super({
name: 'processManager',
description: 'Advanced process management with sessions, interaction, and monitoring',
category: 'Process Management',
parameters,
});
}
async execute(args: z.infer<typeof this.definition.parameters>): Promise<any> {
try {
switch (args.action) {
case 'start':
return await this.startProcess(args);
case 'interact':
return await this.interactWithProcess(args);
case 'read':
return await this.readProcessOutput(args);
case 'list':
return this.listSessions();
case 'kill':
return this.killProcess(args);
case 'status':
return this.getProcessStatus(args);
default:
return { error: `Unknown action: ${args.action}` };
}
} catch (error) {
return {
error: `Process management failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
private async startProcess(args: any): Promise<any> {
try {
const sessionId = `session_${++this.sessionCounter}_${Date.now()}`;
const cwd = args.workingDirectory || this.agentContext.workingDirectory;
// SECURITY: Validate command before execution
const validation = validateCommand(args.command);
if (!validation.isValid) {
logger.error(`🚨 SECURITY: Blocked dangerous command: ${args.command}`);
logger.error(`🚨 Reason: ${validation.reason}`);
return {
success: false,
sessionId: null,
error: `SECURITY BLOCKED: ${validation.reason}`,
output: '',
stderr: `Command blocked for security: ${validation.reason}`,
};
}
// Parse command and arguments
const [command, ...cmdArgs] = args.command.split(' ');
logger.info(`✅ SECURITY: Executing validated command: ${command}`);
const childProcess = spawn(command, cmdArgs, {
cwd,
env: { ...process.env, ...args.environment },
stdio: ['pipe', 'pipe', 'pipe'],
});
const session: ProcessSession = {
id: sessionId,
process: childProcess,
command: args.command,
startTime: Date.now(),
lastActivity: Date.now(),
outputBuffer: [],
isInteractive: args.interactive,
};
// Set up output handlers
this.setupOutputHandlers(session);
// Store session
this.sessions.set(sessionId, session);
// Handle process exit
childProcess.on('exit', code => {
session.outputBuffer.push(`Process exited with code: ${code}`);
if (!args.interactive) {
this.sessions.delete(sessionId);
}
});
// Wait for initial output or process to be ready
await this.waitForProcessReady(session, args.timeout);
return {
success: true,
sessionId,
command: args.command,
pid: childProcess.pid,
initialOutput: session.outputBuffer.slice(0, 10).join('\n'),
isRunning: !childProcess.killed,
interactive: args.interactive,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
private setupOutputHandlers(session: ProcessSession): void {
const maxBufferSize = 1000; // Keep last 1000 lines
session.process.stdout?.on('data', data => {
const lines = data
.toString()
.split('\n')
.filter((line: string) => line.trim());
session.outputBuffer.push(...lines);
session.lastActivity = Date.now();
// Keep buffer size manageable
if (session.outputBuffer.length > maxBufferSize) {
session.outputBuffer = session.outputBuffer.slice(-maxBufferSize);
}
});
session.process.stderr?.on('data', data => {
const lines = data
.toString()
.split('\n')
.filter((line: string) => line.trim());
const errorLines = lines.map((line: string) => `[ERROR] ${line}`);
session.outputBuffer.push(...errorLines);
session.lastActivity = Date.now();
if (session.outputBuffer.length > maxBufferSize) {
session.outputBuffer = session.outputBuffer.slice(-maxBufferSize);
}
});
session.process.on('error', error => {
session.outputBuffer.push(`[PROCESS ERROR] ${error.message}`);
session.lastActivity = Date.now();
});
}
private async waitForProcessReady(session: ProcessSession, timeout: number): Promise<void> {
return new Promise(resolve => {
const startTime = Date.now();
const checkReady = () => {
const elapsed = Date.now() - startTime;
if (elapsed >= timeout) {
resolve(); // Timeout reached
return;
}
if (session.outputBuffer.length > 0) {
resolve(); // Got some output
return;
}
if (session.process.killed) {
resolve(); // Process ended
return;
}
// Check again in 100ms
setTimeout(checkReady, 100);
};
checkReady();
});
}
private async interactWithProcess(args: any): Promise<any> {
const session = this.sessions.get(args.sessionId!);
if (!session) {
return { error: `Session not found: ${args.sessionId}` };
}
if (session.process.killed) {
return { error: 'Process has already exited' };
}
if (!args.input) {
return { error: 'Input is required for interaction' };
}
try {
// Send input to process
session.process.stdin?.write(args.input + '\n');
session.lastActivity = Date.now();
// Wait for response
const initialOutputLength = session.outputBuffer.length;
await this.waitForNewOutput(session, args.timeout || 5000, initialOutputLength);
// Get new output since interaction
const newOutput = session.outputBuffer.slice(initialOutputLength);
return {
success: true,
sessionId: args.sessionId,
input: args.input,
output: newOutput.join('\n'),
newLines: newOutput.length,
totalLines: session.outputBuffer.length,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
private async waitForNewOutput(
session: ProcessSession,
timeout: number,
initialLength: number
): Promise<void> {
return new Promise(resolve => {
const startTime = Date.now();
const checkForOutput = () => {
const elapsed = Date.now() - startTime;
if (elapsed >= timeout) {
resolve(); // Timeout
return;
}
if (session.outputBuffer.length > initialLength) {
resolve(); // Got new output
return;
}
if (session.process.killed) {
resolve(); // Process ended
return;
}
setTimeout(checkForOutput, 100);
};
checkForOutput();
});
}
private async readProcessOutput(args: any): Promise<any> {
const session = this.sessions.get(args.sessionId!);
if (!session) {
return { error: `Session not found: ${args.sessionId}` };
}
return {
sessionId: args.sessionId,
command: session.command,
isRunning: !session.process.killed,
output: session.outputBuffer.join('\n'),
totalLines: session.outputBuffer.length,
lastActivity: new Date(session.lastActivity).toISOString(),
uptime: Date.now() - session.startTime,
};
}
private listSessions(): any {
const sessions = Array.from(this.sessions.values()).map(session => ({
id: session.id,
command: session.command,
isRunning: !session.process.killed,
pid: session.process.pid,
uptime: Date.now() - session.startTime,
lastActivity: new Date(session.lastActivity).toISOString(),
outputLines: session.outputBuffer.length,
isInteractive: session.isInteractive,
}));
return {
totalSessions: sessions.length,
runningSessions: sessions.filter(s => s.isRunning).length,
sessions,
};
}
private killProcess(args: any): any {
const session = this.sessions.get(args.sessionId!);
if (!session) {
return { error: `Session not found: ${args.sessionId}` };
}
try {
if (!session.process.killed) {
session.process.kill('SIGTERM');
// Force kill after 5 seconds if still running
setTimeout(() => {
if (!session.process.killed) {
session.process.kill('SIGKILL');
}
}, 5000);
}
// Remove from sessions
this.sessions.delete(args.sessionId!);
return {
success: true,
sessionId: args.sessionId,
message: 'Process terminated',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
private getProcessStatus(args: any): any {
const session = this.sessions.get(args.sessionId!);
if (!session) {
return { error: `Session not found: ${args.sessionId}` };
}
return {
sessionId: args.sessionId,
command: session.command,
pid: session.process.pid,
isRunning: !session.process.killed,
startTime: new Date(session.startTime).toISOString(),
lastActivity: new Date(session.lastActivity).toISOString(),
uptime: Date.now() - session.startTime,
outputLines: session.outputBuffer.length,
recentOutput: session.outputBuffer.slice(-5).join('\n'),
isInteractive: session.isInteractive,
};
}
}
/**
* Memory Code Execution Tool (like in Desktop Commander)
*/
export class CodeExecutionTool extends BaseTool {
constructor(private agentContext: { workingDirectory: string }) {
const parameters = z.object({
language: z.enum(['python', 'javascript', 'nodejs', 'bash', 'powershell']),
code: z.string().describe('Code to execute'),
timeout: z.number().optional().default(30000).describe('Timeout in milliseconds'),
captureOutput: z.boolean().optional().default(true).describe('Capture and return output'),
environment: z.record(z.string()).optional().describe('Environment variables'),
});
super({
name: 'executeCode',
description: 'Execute code in memory without saving files (Python, Node.js, Bash)',
category: 'Code Execution',
parameters,
});
}
async execute(args: z.infer<typeof this.definition.parameters>): Promise<any> {
try {
switch (args.language) {
case 'python':
return await this.executePython(args);
case 'javascript':
case 'nodejs':
return await this.executeNodeJS(args);
case 'bash':
return await this.executeBash(args);
case 'powershell':
return await this.executePowerShell(args);
default:
return { error: `Unsupported language: ${args.language}` };
}
} catch (error) {
return {
error: `Code execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
private async executePython(args: any): Promise<any> {
try {
const result = await execAsync(`python -c "${args.code.replace(/"/g, '\\"')}"`, {
cwd: this.agentContext.workingDirectory,
timeout: args.timeout,
env: { ...process.env, ...args.environment },
});
return {
success: true,
language: 'python',
code: args.code,
output: result.stdout,
stderr: result.stderr,
executionTime: Date.now(),
};
} catch (error: any) {
return {
success: false,
language: 'python',
code: args.code,
error: error.message,
output: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.code,
};
}
}
private async executeNodeJS(args: any): Promise<any> {
try {
const result = await execAsync(`node -e "${args.code.replace(/"/g, '\\"')}"`, {
cwd: this.agentContext.workingDirectory,
timeout: args.timeout,
env: { ...process.env, ...args.environment },
});
return {
success: true,
language: 'nodejs',
code: args.code,
output: result.stdout,
stderr: result.stderr,
executionTime: Date.now(),
};
} catch (error: any) {
return {
success: false,
language: 'nodejs',
code: args.code,
error: error.message,
output: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.code,
};
}
}
private async executeBash(args: any): Promise<any> {
try {
const result = await execAsync(args.code, {
cwd: this.agentContext.workingDirectory,
timeout: args.timeout,
env: { ...process.env, ...args.environment },
shell: '/bin/bash',
});
return {
success: true,
language: 'bash',
code: args.code,
output: result.stdout,
stderr: result.stderr,
executionTime: Date.now(),
};
} catch (error: any) {
return {
success: false,
language: 'bash',
code: args.code,
error: error.message,
output: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.code,
};
}
}
private async executePowerShell(args: any): Promise<any> {
try {
const result = await execAsync(`powershell -Command "${args.code.replace(/"/g, '\\"')}"`, {
cwd: this.agentContext.workingDirectory,
timeout: args.timeout,
env: { ...process.env, ...args.environment },
});
return {
success: true,
language: 'powershell',
code: args.code,
output: result.stdout,
stderr: result.stderr,
executionTime: Date.now(),
};
} catch (error: any) {
return {
success: false,
language: 'powershell',
code: args.code,
error: error.message,
output: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.code,
};
}
}
}