UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

298 lines (280 loc) 9.06 kB
/** * SandboxWorker - Execute code in isolated Web Worker * * Provides safe execution environment for PCard logic. * Supports multiple runtimes: * - JavaScript: Native execution via Function() * - Python: Execution via Pyodide (WebAssembly Python) * * @see https://pyodide.org/en/stable/ */ /** * Worker code as inline string (will be converted to Blob URL) * Supports both JavaScript and Python (via Pyodide) execution */ const WORKER_CODE = ` // Multi-runtime sandboxed execution environment let pyodide = null; let pyodideLoading = null; // Pyodide CDN URL - can be overridden via message const PYODIDE_CDN = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/'; // Load Pyodide on demand (lazy loading) async function loadPyodide() { if (pyodide) return pyodide; if (pyodideLoading) return pyodideLoading; pyodideLoading = (async () => { // Import Pyodide from CDN importScripts(PYODIDE_CDN + 'pyodide.js'); pyodide = await self.loadPyodide({ indexURL: PYODIDE_CDN, }); console.log('[SandboxWorker] Pyodide loaded successfully'); return pyodide; })(); return pyodideLoading; } self.onmessage = async function(e) { const request = e.data; try { if (request.method === 'pcard.execute') { const { pcard, context } = request.params; const runtime = pcard.runtime || 'javascript'; let result; if (runtime === 'python' || runtime === 'py') { result = await executePython(pcard.code, pcard.input, context); } else { result = await executeJavaScript(pcard.code, pcard.input, context); } self.postMessage({ jsonrpc: '2.0', id: request.id, result }); } else if (request.method === 'pcard.verify') { const { expectedOutput, actualOutput } = request.params; const matches = JSON.stringify(expectedOutput) === JSON.stringify(actualOutput); self.postMessage({ jsonrpc: '2.0', id: request.id, result: { verified: matches } }); } else if (request.method === 'runtime.preload') { // Preload Pyodide for faster first execution const runtime = request.params?.runtime || 'python'; if (runtime === 'python' || runtime === 'py') { await loadPyodide(); self.postMessage({ jsonrpc: '2.0', id: request.id, result: { loaded: true, runtime: 'python' } }); } else { self.postMessage({ jsonrpc: '2.0', id: request.id, result: { loaded: true, runtime: 'javascript' } }); } } else { throw new Error('Method not found: ' + request.method); } } catch (error) { self.postMessage({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: error.message || 'Execution error' } }); } }; // Execute JavaScript code in sandbox async function executeJavaScript(code, input, context) { const fn = new Function('input', 'context', code); return fn(input, context || {}); } // Execute Python code via Pyodide async function executePython(code, input, context) { const py = await loadPyodide(); // Convert input to Python dict py.globals.set('input', py.toPy(input || {})); py.globals.set('context', py.toPy(context || {})); // Wrap code to capture result const wrappedCode = \` import json # Make input accessible as variables _input = input.to_py() if hasattr(input, 'to_py') else input _context = context.to_py() if hasattr(context, 'to_py') else context # Standard variable names for CLM a = _input.get('a') if isinstance(_input, dict) else None b = _input.get('b') if isinstance(_input, dict) else None values = _input.get('values') if isinstance(_input, dict) else None result = None \${code} # Return result result \`; try { const pyResult = await py.runPythonAsync(wrappedCode); // Convert Python result to JavaScript return pyResult?.toJs ? pyResult.toJs({ dict_converter: Object.fromEntries }) : pyResult; } finally { // Clean up globals py.globals.delete('input'); py.globals.delete('context'); } } `; /** * SandboxWorker - Manages Web Worker for isolated execution * * Supports multiple runtimes: * - javascript/js: Native JS execution * - python/py: Python via Pyodide (WebAssembly) */ export class SandboxWorker { worker = null; pythonLoaded = false; pendingRequests = new Map(); defaultTimeout = 5000; // 5 seconds /** * Initialize the sandbox worker */ async init() { const blob = new Blob([WORKER_CODE], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); this.worker = new Worker(workerUrl); this.worker.onmessage = this.handleMessage.bind(this); this.worker.onerror = this.handleError.bind(this); // Clean up blob URL URL.revokeObjectURL(workerUrl); } requestCounter = 0; /** * Execute code in sandbox * @param code - Code to execute * @param input - Input data for the code * @param context - Optional execution context * @param runtime - Runtime to use: 'javascript' (default) or 'python' */ async execute(code, input, context, runtime = 'javascript') { if (!this.worker) { throw new Error('Worker not initialized. Call init() first.'); } const request = { jsonrpc: '2.0', id: ++this.requestCounter, method: 'pcard.execute', params: { pcard: { code, input, runtime }, context } }; return this.sendRequest(request); } /** * Preload a runtime for faster first execution * Useful for Python which requires loading Pyodide (~10MB) * @param runtime - Runtime to preload: 'python' or 'javascript' */ async preloadRuntime(runtime = 'python') { if (!this.worker) { throw new Error('Worker not initialized. Call init() first.'); } const request = { jsonrpc: '2.0', id: ++this.requestCounter, method: 'runtime.preload', params: { runtime } }; const result = await this.sendRequest(request); if (runtime === 'python' || runtime === 'py') { this.pythonLoaded = true; } return result; } /** * Check if Python runtime is loaded */ isPythonLoaded() { return this.pythonLoaded; } /** * Verify output matches expected * Note: Uses internal JSON-RPC format with direct values */ async verify(hash, expectedOutput, actualOutput) { if (!this.worker) { throw new Error('Worker not initialized. Call init() first.'); } const request = { jsonrpc: '2.0', id: ++this.requestCounter, method: 'pcard.verify', params: { hash, expectedOutput, actualOutput } }; return this.sendRequest(request); } /** * Send request and wait for response */ sendRequest(request) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(request.id); reject(new Error(`Request ${request.id} timed out`)); }, this.defaultTimeout); this.pendingRequests.set(request.id, { resolve, reject, timeout }); this.worker.postMessage(request); }); } /** * Handle worker message */ handleMessage(event) { const response = event.data; const pending = this.pendingRequests.get(response.id); if (pending) { clearTimeout(pending.timeout); this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(`Error ${response.error.code}: ${response.error.message}`)); } else { pending.resolve(response.result); } } } /** * Handle worker error */ handleError(event) { console.error('Worker error:', event.message); // Reject all pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error(`Worker error: ${event.message}`)); } this.pendingRequests.clear(); } /** * Terminate the worker */ terminate() { if (this.worker) { // Reject pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error('Worker terminated')); } this.pendingRequests.clear(); this.worker.terminate(); this.worker = null; } } /** * Set timeout for requests */ setTimeout(ms) { this.defaultTimeout = ms; } } //# sourceMappingURL=SandboxWorker.js.map