aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-sandbox.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-05 18:01:01 +0200
committerAgando <roduccomm@gmail.com>2026-06-05 18:01:01 +0200
commitb538577cd0a4292e4cd4ca1c9f9829fc7e133caa (patch)
tree04d76bf4f0d680cdb26b1a205481bc8628cd77b4 /modules/core/module-sandbox.js
parent5092f9a0ce02691dc476d7b64fa731a1518dd834 (diff)
auto: 2026-06-05_18:01 changes
Diffstat (limited to 'modules/core/module-sandbox.js')
-rw-r--r--modules/core/module-sandbox.js332
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