diff options
Diffstat (limited to 'modules/core/sandbox-worker.js')
| -rw-r--r-- | modules/core/sandbox-worker.js | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/modules/core/sandbox-worker.js b/modules/core/sandbox-worker.js new file mode 100644 index 0000000..5841582 --- /dev/null +++ b/modules/core/sandbox-worker.js @@ -0,0 +1,205 @@ +// 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; + +self.onmessage = async (e) => { + const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data; + _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null; + + const startTime = 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: [] }; + + // Log start + console.log(`[Sandbox ${taskId}] Starting task: "${task.substring(0, 60)}"`); + + // Parse the task for tool use + const toolResults = {}; + const commandWhitelist = allowedCommands || ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status']; + + // Execute basic commands + const parts = task.trim().split(/\s+/); + const cmd = parts[0].toLowerCase(); + + 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.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.includes('list_files') || capabilities.includes('read_file')) { + // Smart mode: interpret task + 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; + console.error(`[Sandbox ${taskId}] Error:`, e); + } + + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + + self.postMessage({ + taskId, + result, + error, + 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(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('/', (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('/', (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('/', (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(([name, node]) => ({ + name, + type: node.type || 'file', + size: typeof node.content === 'string' ? node.content.length : undefined + })).sort((a, b) => 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(p => p); + let current = _vfs.root; + for (const part of parts) { + if (!current.content || !current.content[part]) return null; + current = current.content[part]; + } + return current; +} + +function _vfs_walk(path, cb) { + const node = _vfs_resolve(path); + if (!node) return; + cb(path, node); + if (node.type === 'dir' && node.content) { + for (const [name, child] of Object.entries(node.content)) { + _vfs_walk(path === '/' ? '/' + name : path + '/' + name, cb); + } + } +}
\ No newline at end of file |