// module-sandbox.js — Sandboxed execution pool using WebWorkers // Commands: sandbox-status, sandbox-config, sandbox-run class SandboxModule { constructor() { this.commands = {}; this.terminal = null; this.workers = []; this.MAX_WORKERS = 4; this.MIN_WORKERS = 1; this.queue = []; this.activeTasks = {}; this._nextTaskId = 0; this._totalRuns = 0; this._totalErrors = 0; this._totalDuration = 0; this._fallbackMode = false; // true if WebWorkers unavailable this._workerReady = {}; } getHelpInfo() { return { 'sandbox-status': 'sandbox-status — Show sandbox pool health and task metrics', 'sandbox-config': 'sandbox-config — Tune: max-workers, min-workers, pool-timeout', 'sandbox-run': 'sandbox-run — Run a task in the sandbox (automatic routing)', }; } registerCommands() { this.commands['sandbox-status'] = () => this.showStatus(); this.commands['sandbox-config'] = (args) => this.setConfig(args); this.commands['sandbox-run'] = (args) => this.runInSandbox(args.join(' ')); } init(terminal) { this.terminal = terminal; this.workerUrl = '/spaces/newos4/modules/core/sandbox-worker.js'; // Detect Worker availability if (typeof Worker === 'undefined') { this._fallbackMode = true; this.terminal.print('⚠️ WebWorkers not available — sandbox using inline fallback', 'warning'); } else { this._initPool(); } this.terminal.print('🧪 Sandbox module loaded', 'info'); } _initPool() { // Start with MIN_WORKERS, grow on demand up to MAX_WORKERS for (let i = 0; i < this.MIN_WORKERS; i++) { this._spawnWorker(); } } _spawnWorker() { if (this.workers.length >= this.MAX_WORKERS) return null; try { const worker = new Worker(this.workerUrl); const id = `w${this.workers.length + 1}`; worker._sandboxId = id; worker._busy = false; worker.onmessage = (e) => this._handleResult(e.data, id); worker.onerror = (e) => { console.error(`[Sandbox] Worker ${id} error:`, e.message); this._totalErrors++; this._replaceWorker(id); }; this.workers.push(worker); this._workerReady[id] = true; return worker; } catch (e) { console.error('[Sandbox] Failed to spawn worker:', e); this._fallbackMode = true; return null; } } _replaceWorker(id) { const idx = this.workers.findIndex(w => w._sandboxId === id); if (idx !== -1) { this.workers[idx].terminate(); this.workers.splice(idx, 1); delete this._workerReady[id]; } // Re-spawn after a delay setTimeout(() => this._spawnWorker(), 1000); } _getIdleWorker() { for (const w of this.workers) { if (!w._busy) return w; } // Grow pool if under max if (this.workers.length < this.MAX_WORKERS) { const w = this._spawnWorker(); if (w) return w; } return null; } async runInSandbox(task, capabilities) { if (!task) return this.terminal.print('Usage: sandbox-run ', 'error'); if (this._fallbackMode) { return this._runInline(task, capabilities); } const worker = this._getIdleWorker(); if (!worker) { // Queue it return new Promise((resolve) => { this.queue.push({ task, capabilities, resolve }); this.terminal.print(`⏳ Task queued (${this.queue.length} waiting)`, 'info'); this._drainQueue(); }); } return this._executeOnWorker(worker, task, capabilities); } async _executeOnWorker(worker, task, capabilities) { const taskId = `task-${++this._nextTaskId}`; worker._busy = true; const startTime = performance.now(); return new Promise((resolve) => { const handler = (e) => { if (e.data && e.data.taskId === taskId) { worker.removeEventListener('message', handler); worker._busy = false; const duration = ((performance.now() - startTime) / 1000).toFixed(2); this._totalRuns++; this._totalDuration += parseFloat(duration); if (e.data.error) { this._totalErrors++; this.terminal.print(`❌ Sandbox task failed: ${e.data.error}`, 'error'); resolve({ error: e.data.error, duration }); } else { this.terminal.print(`✅ Sandbox task complete (${duration}s, ${e.data.toolCalls || 0} tools)`, 'success'); resolve({ result: e.data.result, duration, toolCalls: e.data.toolCalls || 0 }); } this._drainQueue(); } }; worker.addEventListener('message', handler); // Get VFS snapshot from terminal let vfsSnapshot = null; try { if (this.terminal && this.terminal.fs && this.terminal.fs.root) { vfsSnapshot = JSON.stringify({ root: this.terminal.fs.root, currentPath: this.terminal.fs.currentPath || [] }); } } catch (e) { console.warn('[Sandbox] Could not serialize VFS:', e.message); } const caps = capabilities || ['list_files', 'read_file']; const allowedCommands = ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status', 'touch', 'mkdir', 'rm', 'cp', 'mv']; worker.postMessage({ taskId, task, capabilities: caps, vfsSnapshot, allowedCommands, apiProxy: window.location.origin + '/api' }); // Timeout after 30 seconds setTimeout(() => { if (worker._busy) { worker._busy = false; this._totalErrors++; this.terminal.print(`⏰ Sandbox task ${taskId} timed out after 30s`, 'error'); resolve({ error: 'Timeout (30s)', duration: 30 }); this._drainQueue(); } }, 30000); }); } _handleResult(data, workerId) { // Handled via promise in _executeOnWorker // This is the fallback handler for tasks not tracked by promise if (data.error) { this.terminal.print(`[Sandbox ${workerId}] Error: ${data.error}`, 'error'); } } _drainQueue() { if (this.queue.length === 0) return; const next = this.queue.shift(); this.runInSandbox(next.task, next.capabilities).then(result => { next.resolve(result); }); } _runInline(task, capabilities) { // Fallback when WebWorkers unavailable — runs in main thread const startTime = performance.now(); const parts = task.trim().split(/\s+/); const cmd = parts[0].toLowerCase(); let result = ''; let toolCalls = 0; const fs = this.terminal ? this.terminal.fs : null; if (cmd === 'ls' && fs) { const path = parts[1] || '/'; const entries = fs.ls(path); if (!entries) return this.terminal.print(`ls: ${path}: No such directory`, 'error'); result = entries.map(e => `${e.type === 'dir' ? '[dir]' : '[file]'} ${e.name}`).join('\\n'); toolCalls++; } else if ((cmd === 'cat' || cmd === 'read') && fs) { const path = parts[1] || ''; const content = fs.readFile(path); if (content === null) return this.terminal.print(`cat: ${path}: No such file`, 'error'); result = typeof content === 'string' ? content.substring(0, 8000) : String(content); toolCalls++; } else if (cmd === 'find' && fs) { const q = parts.slice(1).join(' ') || ''; // Walk VFS const results = []; const walk = (p) => { const entries = fs.ls(p); if (!entries) return; for (const e of entries) { const full = p === '/' ? '/' + e.name : p + '/' + e.name; if (e.type === 'file' && (!q || full.toLowerCase().includes(q.toLowerCase()))) { results.push(full); } if (e.type === 'dir') walk(full); } }; walk('/'); result = results.join('\\n') || '(empty)'; toolCalls++; } else if (cmd === 'status') { let f = 0, d = 0; const walk = (p) => { const entries = fs ? fs.ls(p) : []; if (!entries) return; for (const e of entries) { if (e.type === 'file') f++; else { d++; walk(p === '/' ? '/' + e.name : p + '/' + e.name); } } }; walk('/'); result = `Files: ${f}, Dirs: ${d}`; toolCalls++; } else { result = `Task: "${task}" (inline fallback)`; } const duration = ((performance.now() - startTime) / 1000).toFixed(2); this._totalRuns++; this._totalDuration += parseFloat(duration); this.terminal.print(result, 'info'); return { result, duration, toolCalls }; } // ─── Config ─── setConfig(args) { if (args.length < 2) return this.terminal.print('Usage: sandbox-config ', 'error'); const key = args[0]; const val = args[1]; const configMap = { 'max-workers': () => { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 8) return 'Must be 1-8'; this.MAX_WORKERS = n; // Scale down if needed while (this.workers.length > n) { const w = this.workers.pop(); w.terminate(); } // Scale up for (let i = this.workers.length; i < Math.min(n, this.MIN_WORKERS); i++) { this._spawnWorker(); } return `max-workers set to ${n}`; }, 'min-workers': () => { const n = parseInt(val); if (isNaN(n) || n < 0 || n > this.MAX_WORKERS) return `Must be 0-${this.MAX_WORKERS}`; this.MIN_WORKERS = n; while (this.workers.length < n) this._spawnWorker(); return `min-workers set to ${n}`; }, }; if (!configMap[key]) { return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); } const msg = configMap[key](); this.terminal.print(`✅ ${msg}`, 'success'); } // ─── Status ─── showStatus() { const busy = this.workers.filter(w => w._busy).length; const idle = this.workers.length - busy; const avgDuration = this._totalRuns > 0 ? (this._totalDuration / this._totalRuns).toFixed(2) : 'N/A'; this.terminal.print('🧪 Sandbox Pool', 'header'); this.terminal.print(` Workers: ${this.workers.length}/${this.MAX_WORKERS} (${busy} busy, ${idle} idle)`, 'info'); this.terminal.print(` Mode: ${this._fallbackMode ? '⚠️ Inline fallback' : '✅ WebWorker'}`, 'info'); this.terminal.print(` Queue: ${this.queue.length} pending`, 'info'); this.terminal.print(` Total runs: ${this._totalRuns} (errors: ${this._totalErrors})`, 'info'); this.terminal.print(` Avg duration: ${avgDuration}s`, 'info'); } } // Required for module system if (typeof window !== 'undefined') { window.SandboxModule = SandboxModule; }