// sandbox-worker.js — WebWorker entry point for isolated agent execution // Receives: { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } // Returns: { taskId, result, error, toolCalls, duration } let _vfs = null; let _apiProxyOrigin = null; // Signal ready immediately self.postMessage({ type: 'ready' }); self.addEventListener('message', async (e) => { const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data || {}; // Must have a taskId to respond to if (!taskId) return; const startTime = self.performance.now(); let toolCalls = 0; let result = ''; let error = null; try { // Rebuild VFS from snapshot _vfs = vfsSnapshot ? JSON.parse(vfsSnapshot) : { root: { type: 'dir', content: {} }, currentPath: [] }; _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null; // Parse the task for tool use const parts = (task || '').trim().split(/\s+/); const cmd = parts.length > 0 ? parts[0].toLowerCase() : ''; const commandWhitelist = allowedCommands || ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status']; if (cmd === 'ls' || cmd === 'list') { result = sandbox_ls(parts[1] || '/'); toolCalls++; } else if (cmd === 'cat' || cmd === 'read') { result = sandbox_cat(parts[1] || ''); toolCalls++; } else if (cmd === 'find' || cmd === 'search') { result = sandbox_find(parts.slice(1).join(' ') || ''); toolCalls++; } else if (cmd === 'grep') { result = sandbox_grep(parts.slice(1).join(' ')); toolCalls++; } else if (cmd === 'echo') { result = parts.slice(1).join(' '); toolCalls++; } else if (cmd === 'help') { result = 'Available commands: ' + commandWhitelist.join(', '); toolCalls++; } else if (cmd === 'status') { result = sandbox_status(); toolCalls++; } else if (capabilities && capabilities.includes('run_command') && commandWhitelist.includes(cmd)) { result = sandbox_run(cmd, parts.slice(1)); toolCalls++; } else { // If the task is descriptive (not a command), run smart handling if (capabilities && (capabilities.includes('list_files') || capabilities.includes('read_file'))) { const lower = (task || '').toLowerCase(); if (lower.includes('list') || lower.includes('file')) { const dir = parts.find(p => p.startsWith('/') || p.startsWith('.')) || '/'; result = sandbox_ls(dir); } else if (lower.includes('find') || lower.includes('search')) { const q = (task || '').replace(/find|search/i, '').trim(); result = sandbox_find(q || ''); } else if (lower.includes('count') || lower.includes('how many')) { result = sandbox_ls('/'); const count = (result.match(/\[file\]|\[dir\]/g) || []).length; result = 'Found ' + count + ' items in total.\n' + result; } else if (lower.includes('status') || lower.includes('health')) { result = sandbox_status(); } else { result = 'Task interpreted: "' + (task || '') + '"\nCapabilities: ' + (capabilities || []).join(', ') + '\nTip: Be more specific (e.g. "list files in /")'; } toolCalls++; } else { result = 'Task: "' + (task || '') + '"\nNo matching capability for command "' + cmd + '"'; } } } catch (e) { error = e.message || String(e); } const duration = ((self.performance.now() - startTime) / 1000).toFixed(2); self.postMessage({ taskId: taskId, result: result, error: error, toolCalls: toolCalls, duration: parseFloat(duration), vfsSnapshot: JSON.stringify(_vfs) }); }); // ─── Sandboxed VFS Operations ─── function sandbox_ls(path) { path = path || '/'; const entries = _vfs_get_entries(path); if (!entries) return 'ls: ' + path + ': No such directory'; const lines = entries.map(function(e) { const icon = e.type === 'dir' ? '[dir]' : '[file]'; const size = e.size !== undefined ? ' (' + e.size + 'B)' : ''; return icon + ' ' + e.name + size; }); return lines.join('\n') || '(empty)'; } function sandbox_cat(path) { const content = _vfs_get_content(path); if (content === null) return 'cat: ' + path + ': No such file'; if (typeof content === 'object') return 'cat: ' + path + ': Is a directory'; if (content.length > 8000) return content.substring(0, 8000) + '\n... (truncated at 8KB)'; return content; } function sandbox_find(query) { const results = []; const q = query ? query.toLowerCase() : ''; _vfs_walk('/', function(path, node) { if (node.type === 'file') { if (!q || path.toLowerCase().includes(q)) { results.push(path); } } }); if (results.length === 0) return 'No files found' + (query ? ' matching "' + query + '"' : ''); return results.join('\n'); } function sandbox_grep(query) { if (!query) return 'grep: missing pattern'; const q = query.toLowerCase(); const results = []; _vfs_walk('/', function(path, node) { if (node.type === 'file' && typeof node.content === 'string') { const lines = node.content.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].toLowerCase().includes(q)) { results.push(path + ':' + (i + 1) + ': ' + lines[i].trim().substring(0, 100)); } } } }); if (results.length === 0) return 'grep: no matches for "' + query + '"'; return results.slice(0, 50).join('\n') + (results.length > 50 ? '\n... (+' + (results.length - 50) + ' more)' : ''); } function sandbox_status() { let fileCount = 0, dirCount = 0; _vfs_walk('/', function(path, node) { if (node.type === 'file') fileCount++; else dirCount++; }); const vfsSize = JSON.stringify(_vfs).length; return 'Sandbox VFS Status:\n Files: ' + fileCount + '\n Directories: ' + dirCount + '\n VFS Size: ' + (vfsSize / 1024).toFixed(1) + 'KB'; } function sandbox_run(cmd, args) { return '[sandbox] ' + cmd + ' ' + args.join(' ') + '\n(Command execution limited in sandbox mode)'; } // ─── VFS Helpers ─── function _vfs_get_entries(path) { const dir = _vfs_resolve(path); if (!dir || dir.type !== 'dir') return null; return Object.entries(dir.content || {}).map(function(entry) { const name = entry[0]; const node = entry[1]; return { name: name, type: node.type || 'file', size: typeof node.content === 'string' ? node.content.length : undefined }; }).sort(function(a, b) { return a.name.localeCompare(b.name); }); } function _vfs_get_content(path) { const node = _vfs_resolve(path); if (!node || node.type === 'dir') return null; return node.content; } function _vfs_resolve(path) { if (!_vfs || !_vfs.root) return null; if (path === '/' || path === '') return _vfs.root; const parts = path.split('/').filter(function(p) { return p; }); let current = _vfs.root; for (let i = 0; i < parts.length; i++) { if (!current.content || !current.content[parts[i]]) return null; current = current.content[parts[i]]; } return current; } function _vfs_walk(path, cb) { const node = _vfs_resolve(path); if (!node) return; cb(path, node); if (node.type === 'dir' && node.content) { const names = Object.keys(node.content); for (let i = 0; i < names.length; i++) { _vfs_walk(path === '/' ? '/' + names[i] : path + '/' + names[i], cb); } } }