UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

601 lines (516 loc) โ€ข 16.7 kB
import { Sandbox } from '@e2b/code-interpreter'; import { logger } from '../logger.js'; /** * Execution result from E2B sandbox */ export interface ExecutionResult { success: boolean; output?: string; error?: string; executionTime: number; resourceUsage?: ResourceUsage; files?: string[]; } /** * Resource usage metrics */ export interface ResourceUsage { memory: number; cpu: number; diskSpace: number; networkRequests: number; } /** * E2B Sandbox wrapper for session management */ export interface E2BSandbox { id: string; sandbox: Sandbox; createdAt: Date; lastUsed: Date; resourceLimits: ResourceLimits; } /** * Resource limits configuration */ export interface ResourceLimits { memory?: string; cpu?: string; diskSpace?: string; timeout?: number; } /** * Core E2B Service for managing sandboxed code execution * * This service provides secure, isolated code execution capabilities * using E2B sandboxes, replacing the unsafe direct execution methods. */ export class E2BService { private sandboxPool: Map<string, E2BSandbox> = new Map(); private config: E2BConfiguration; private isInitialized: boolean = false; private cleanupTimer: NodeJS.Timeout | null = null; constructor(config?: Partial<E2BConfiguration>) { this.config = { apiKey: process.env.E2B_API_KEY || '', defaultEnvironment: 'base', sessionTimeout: 3600000, // 1 hour maxConcurrentSessions: 10, resourceLimits: { memory: '512MB', cpu: '0.5', diskSpace: '1GB', timeout: 30000, // 30 seconds }, ...config, }; if (!this.config.apiKey) { logger.warn('E2B API key not found. Code execution will be disabled.'); } } /** * Initialize the E2B service */ async initialize(): Promise<void> { try { if (!this.config.apiKey) { throw new Error('E2B API key is required'); } // Test connection by creating a temporary sandbox await this.testConnection(); this.isInitialized = true; logger.info('โœ… E2B service initialized successfully'); // Start cleanup timer for expired sessions this.startCleanupTimer(); } catch (error) { logger.error('โŒ Failed to initialize E2B service:', error); throw error; } } /** * Test E2B connection by creating and destroying a sandbox */ private async testConnection(): Promise<void> { try { const testSandbox = await Sandbox.create({ apiKey: this.config.apiKey, }); const result = await testSandbox.runCode('print("E2B connection test successful")'); if (!result.text || !result.text.includes('successful')) { throw new Error('E2B connection test failed'); } // Note: E2B sandboxes auto-cleanup, no explicit close needed logger.info('๐Ÿ”— E2B connection test passed'); } catch (error) { throw new Error( `E2B connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Create a new sandbox for a session */ async createSandbox(sessionId: string, environment?: string): Promise<E2BSandbox> { if (!this.isInitialized) { throw new Error('E2B service not initialized'); } if (this.sandboxPool.has(sessionId)) { logger.warn(`Sandbox already exists for session ${sessionId}, returning existing`); return this.sandboxPool.get(sessionId)!; } if (this.sandboxPool.size >= this.config.maxConcurrentSessions) { // Clean up old sessions to make room await this.cleanupOldestSession(); } try { logger.info(`๐Ÿš€ Creating E2B sandbox for session: ${sessionId}`); const startTime = Date.now(); const sandbox = await Sandbox.create({ apiKey: this.config.apiKey, }); const e2bSandbox: E2BSandbox = { id: sessionId, sandbox, createdAt: new Date(), lastUsed: new Date(), resourceLimits: this.config.resourceLimits, }; this.sandboxPool.set(sessionId, e2bSandbox); const creationTime = Date.now() - startTime; logger.info(`โœ… Sandbox created for session ${sessionId} in ${creationTime}ms`); return e2bSandbox; } catch (error) { logger.error(`โŒ Failed to create sandbox for session ${sessionId}:`, error); throw new Error( `Failed to create E2B sandbox: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Get existing sandbox for a session */ async getSandbox(sessionId: string): Promise<E2BSandbox | null> { const sandbox = this.sandboxPool.get(sessionId); if (sandbox) { // Update last used timestamp sandbox.lastUsed = new Date(); return sandbox; } return null; } /** * Get or create sandbox for a session */ async getOrCreateSandbox(sessionId: string): Promise<E2BSandbox> { const existing = await this.getSandbox(sessionId); if (existing) { return existing; } return await this.createSandbox(sessionId); } /** * Execute code in a sandbox */ async executeCode( sessionId: string, code: string, language: string = 'python' ): Promise<ExecutionResult> { const startTime = Date.now(); try { const e2bSandbox = await this.getOrCreateSandbox(sessionId); const { sandbox } = e2bSandbox; logger.info(`๐Ÿ”ง Executing ${language} code in session ${sessionId}`); logger.debug(`Code to execute: ${code.substring(0, 100)}${code.length > 100 ? '...' : ''}`); // Execute the code based on language const result = await this.executeByLanguage(sandbox, code, language); const executionTime = Date.now() - startTime; const executionResult: ExecutionResult = { success: !result.error, output: result.text || (result.logs ? result.logs.stdout.join('\n') : ''), error: result.error?.name || (result.logs ? result.logs.stderr.join('\n') : undefined), executionTime, files: result.results?.map((r: any) => r.filename).filter(Boolean), }; logger.info( `โœ… Code execution completed in ${executionTime}ms - Success: ${executionResult.success}` ); if (executionResult.error) { logger.warn(`Execution error: ${executionResult.error}`); } return executionResult; } catch (error) { const executionTime = Date.now() - startTime; logger.error(`โŒ Code execution failed for session ${sessionId}:`, error); return { success: false, error: error instanceof Error ? error.message : 'Unknown execution error', executionTime, }; } } /** * Execute code based on language type */ private async executeByLanguage(sandbox: Sandbox, code: string, language: string): Promise<any> { switch (language.toLowerCase()) { case 'python': case 'py': return await sandbox.runCode(code); case 'javascript': case 'js': { // Convert to Python execution of JavaScript-like logic where possible // Note: E2B primarily supports Python, so we convert simple JS to Python const pythonCode = this.convertJSToPython(code); return await sandbox.runCode(pythonCode); } case 'bash': case 'shell': { // Execute bash commands safely const bashCode = ` import subprocess import sys try: result = subprocess.run(${JSON.stringify(code)}, shell=True, capture_output=True, text=True, timeout=30) if result.stdout: print(result.stdout) if result.stderr: print(result.stderr, file=sys.stderr) sys.exit(result.returncode) except subprocess.TimeoutExpired: print("Command timed out", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Execution error: {e}", file=sys.stderr) sys.exit(1) `; return await sandbox.runCode(bashCode); } default: throw new Error(`Unsupported language: ${language}`); } } /** * Convert simple JavaScript to Python (basic conversion) */ private convertJSToPython(jsCode: string): string { // Basic JS to Python conversion for simple operations const pythonCode = jsCode .replace(/console\.log\(/g, 'print(') .replace(/let\s+/g, '') .replace(/const\s+/g, '') .replace(/var\s+/g, '') .replace(/===\s/g, '== ') .replace(/!==\s/g, '!= ') .replace(/true/g, 'True') .replace(/false/g, 'False') .replace(/null/g, 'None'); return pythonCode; } /** * Upload file to sandbox */ async uploadFile(sessionId: string, filePath: string, content: string): Promise<void> { try { const e2bSandbox = await this.getOrCreateSandbox(sessionId); // Use Python to write the file const writeCode = ` with open(${JSON.stringify(filePath)}, 'w') as f: f.write(${JSON.stringify(content)}) print(f"File written to {${JSON.stringify(filePath)}}") `; await e2bSandbox.sandbox.runCode(writeCode); logger.info(`๐Ÿ“„ File uploaded to sandbox ${sessionId}: ${filePath}`); } catch (error) { logger.error(`โŒ Failed to upload file to session ${sessionId}:`, error); throw error; } } /** * Download file from sandbox */ async downloadFile(sessionId: string, filePath: string): Promise<string> { try { const e2bSandbox = await this.getSandbox(sessionId); if (!e2bSandbox) { throw new Error(`No sandbox found for session ${sessionId}`); } // Use Python to read the file const readCode = ` try: with open(${JSON.stringify(filePath)}, 'r') as f: content = f.read() print(content) except FileNotFoundError: print(f"ERROR: File not found: {${JSON.stringify(filePath)}}") except Exception as e: print(f"ERROR: {e}") `; const result = await e2bSandbox.sandbox.runCode(readCode); if (result.text && result.text.startsWith('ERROR:')) { throw new Error(result.text); } logger.info(`๐Ÿ“ฅ File downloaded from sandbox ${sessionId}: ${filePath}`); return result.text || ''; } catch (error) { logger.error(`โŒ Failed to download file from session ${sessionId}:`, error); throw error; } } /** * Destroy a sandbox and clean up resources */ async destroySandbox(sessionId: string): Promise<void> { try { const e2bSandbox = this.sandboxPool.get(sessionId); if (e2bSandbox) { // Note: E2B sandboxes auto-cleanup, just remove from pool this.sandboxPool.delete(sessionId); logger.info(`๐Ÿ—‘๏ธ Sandbox destroyed for session: ${sessionId}`); } } catch (error) { logger.error(`โŒ Failed to destroy sandbox for session ${sessionId}:`, error); // Still remove from pool even if cleanup failed this.sandboxPool.delete(sessionId); } } /** * Clean up the oldest session to make room for new ones */ private async cleanupOldestSession(): Promise<void> { if (this.sandboxPool.size === 0) return; let oldestSession: string | null = null; let oldestTime = Date.now(); for (const [sessionId, sandbox] of this.sandboxPool.entries()) { if (sandbox.lastUsed.getTime() < oldestTime) { oldestTime = sandbox.lastUsed.getTime(); oldestSession = sessionId; } } if (oldestSession) { logger.info(`๐Ÿงน Cleaning up oldest session: ${oldestSession}`); await this.destroySandbox(oldestSession); } } /** * Start cleanup timer for expired sessions */ private startCleanupTimer(): void { this.cleanupTimer = setInterval(async () => { await this.cleanupExpiredSessions(); }, 300000); // Clean up every 5 minutes } /** * Stop cleanup timer and cleanup all resources */ async shutdown(): Promise<void> { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } // Destroy all active sandboxes const destroyPromises = Array.from(this.sandboxPool.keys()).map(sessionId => this.destroySandbox(sessionId) ); await Promise.allSettled(destroyPromises); this.sandboxPool.clear(); this.isInitialized = false; logger.info('โœ… E2B service shut down successfully'); } /** * Clean up expired sessions */ async cleanupExpiredSessions(): Promise<void> { const now = Date.now(); const expiredSessions: string[] = []; for (const [sessionId, sandbox] of this.sandboxPool.entries()) { const timeSinceLastUse = now - sandbox.lastUsed.getTime(); if (timeSinceLastUse > this.config.sessionTimeout) { expiredSessions.push(sessionId); } } if (expiredSessions.length > 0) { logger.info(`๐Ÿงน Cleaning up ${expiredSessions.length} expired sessions`); for (const sessionId of expiredSessions) { await this.destroySandbox(sessionId); } } } /** * Install package in a sandbox */ async installPackage( sessionId: string, packageName: string, language: 'python' | 'javascript' = 'python' ): Promise<ExecutionResult> { try { const e2bSandbox = await this.getOrCreateSandbox(sessionId); let installCommand: string; if (language === 'python') { installCommand = ` import subprocess import sys try: result = subprocess.run([sys.executable, '-m', 'pip', 'install', '${packageName}'], capture_output=True, text=True, timeout=60) if result.returncode == 0: print(f"Successfully installed {packageName}") print(result.stdout) else: print(f"Failed to install {packageName}") print(result.stderr) raise Exception(f"Installation failed: {result.stderr}") except subprocess.TimeoutExpired: raise Exception("Package installation timed out") except Exception as e: raise Exception(f"Installation error: {str(e)}") `; } else { // JavaScript package installation via npm (if available in sandbox) installCommand = ` import subprocess import sys try: result = subprocess.run(['npm', 'install', '${packageName}'], capture_output=True, text=True, timeout=60) if result.returncode == 0: print(f"Successfully installed {packageName}") print(result.stdout) else: print(f"Failed to install {packageName}") print(result.stderr) raise Exception(f"Installation failed: {result.stderr}") except subprocess.TimeoutExpired: raise Exception("Package installation timed out") except Exception as e: raise Exception(f"Installation error: {str(e)}") `; } const result = await e2bSandbox.sandbox.runCode(installCommand); return { success: !result.error, output: result.text || '', error: result.error?.name, executionTime: 0, // Not tracking this for package installation }; } catch (error) { logger.error(`โŒ Failed to install package ${packageName} in session ${sessionId}:`, error); return { success: false, error: error instanceof Error ? error.message : 'Unknown package installation error', executionTime: 0, }; } } /** * Get list of active session IDs */ getActiveSessions(): string[] { return Array.from(this.sandboxPool.keys()); } /** * Get service statistics */ getStats(): E2BServiceStats { return { isInitialized: this.isInitialized, activeSessions: this.sandboxPool.size, maxConcurrentSessions: this.config.maxConcurrentSessions, totalSessionsCreated: this.sandboxPool.size, // This could be tracked more accurately averageSessionAge: this.calculateAverageSessionAge(), }; } /** * Calculate average session age */ private calculateAverageSessionAge(): number { if (this.sandboxPool.size === 0) return 0; const now = Date.now(); let totalAge = 0; for (const sandbox of this.sandboxPool.values()) { totalAge += now - sandbox.createdAt.getTime(); } return totalAge / this.sandboxPool.size; } } /** * E2B service configuration */ export interface E2BConfiguration { apiKey: string; defaultEnvironment: string; sessionTimeout: number; maxConcurrentSessions: number; resourceLimits: ResourceLimits; } /** * E2B service statistics */ export interface E2BServiceStats { isInitialized: boolean; activeSessions: number; maxConcurrentSessions: number; totalSessionsCreated: number; averageSessionAge: number; }