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
JavaScript
/**
* 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