diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-05 18:01:01 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-05 18:01:01 +0200 |
| commit | b538577cd0a4292e4cd4ca1c9f9829fc7e133caa (patch) | |
| tree | 04d76bf4f0d680cdb26b1a205481bc8628cd77b4 /modules/core/module-sandbox.js | |
| parent | 5092f9a0ce02691dc476d7b64fa731a1518dd834 (diff) | |
auto: 2026-06-05_18:01 changes
Diffstat (limited to 'modules/core/module-sandbox.js')
| -rw-r--r-- | modules/core/module-sandbox.js | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/modules/core/module-sandbox.js b/modules/core/module-sandbox.js new file mode 100644 index 0000000..dc8dc76 --- /dev/null +++ b/modules/core/module-sandbox.js @@ -0,0 +1,332 @@ +// 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 <key> <value> — Tune: max-workers, min-workers, pool-timeout', + 'sandbox-run': 'sandbox-run <task> — 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 <task>', '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 <key> <value>', '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; +}
\ No newline at end of file |