aboutsummaryrefslogtreecommitdiff
path: root/modules/core/sandbox-worker.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/sandbox-worker.js')
-rw-r--r--modules/core/sandbox-worker.js205
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