UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

286 lines 10.8 kB
/** * Isolated VM Sandbox * @description Secure sandboxed execution environment using isolated-vm * @author Optimizely MCP Server * @version 1.0.0 */ import ivm from 'isolated-vm'; import { getLogger } from '../../logging/Logger.js'; export class IsolatedVMSandbox { logger = getLogger(); isolate; defaultOptions = { timeout: 30000, // 30 seconds memoryLimit: 128, // 128 MB inspector: true }; constructor(options) { const config = { ...this.defaultOptions, ...options }; this.logger.info({ memoryLimit: config.memoryLimit, inspector: config.inspector }, 'Creating isolated VM'); this.isolate = new ivm.Isolate({ memoryLimit: config.memoryLimit, inspector: config.inspector }); } /** * Execute plugin code in sandbox */ async execute(code, context, options) { const execOptions = { ...this.defaultOptions, ...options }; const startTime = Date.now(); const logs = []; this.logger.debug({ codeLength: code.length, hasPermissions: !!context.permissions }, 'Executing plugin code'); let vmContext = null; try { // Create a new context for this execution vmContext = await this.isolate.createContext(); const jail = vmContext.global; // Set up global object await jail.set('global', jail.derefInto()); // Inject safe APIs based on permissions await this.injectAPIs(jail, context, logs); // Inject context data (read-only copies) await this.injectContextData(jail, context); // Inject utilities await this.injectUtilities(jail, logs); // Wrap code in async function for better error handling const wrappedCode = ` (async function() { try { ${code} } catch (error) { return { success: false, error: error.message || 'Unknown error', stack: error.stack }; } })(); `; // Compile and run the code const script = await this.isolate.compileScript(wrappedCode); const resultHandle = await script.run(vmContext, { timeout: execOptions.timeout, promise: true }); // Extract result let result; if (resultHandle instanceof ivm.Reference) { result = await resultHandle.copy(); } else { result = resultHandle; } // Check if execution returned an error if (result && typeof result === 'object' && result.success === false) { throw new Error(result.error || 'Plugin execution failed'); } const executionTime = Date.now() - startTime; this.logger.info({ executionTime, logsCount: logs.length }, 'Plugin execution completed'); return { success: true, output: result, logs, metrics: { execution_time_ms: executionTime, memory_used_mb: await this.getMemoryUsage() } }; } catch (error) { const executionTime = Date.now() - startTime; this.logger.error({ error: error instanceof Error ? error.message : String(error), executionTime }, 'Plugin execution failed'); return { success: false, error: error instanceof Error ? error.message : String(error), logs, metrics: { execution_time_ms: executionTime } }; } finally { // Clean up context if (vmContext) { vmContext.release(); } } } /** * Inject safe APIs based on permissions */ async injectAPIs(jail, context, logs) { const apis = {}; // Entity operations (if permitted) if (context.permissions.read_entities && context.entityRouter) { apis.getEntity = this.createAsyncProxy(async (type, id) => { this.logger.debug({ type, id }, 'Plugin: getEntity'); return await context.entityRouter.getEntity({ entity_type: type, entity_id: id }); }); apis.listEntities = this.createAsyncProxy(async (type, filters) => { this.logger.debug({ type, filters }, 'Plugin: listEntities'); return await context.entityRouter.listEntities({ entity_type: type, ...filters }); }); } // Metrics access (if permitted) if (context.permissions.access_metrics && context.metricsProvider) { apis.getMetric = this.createAsyncProxy(async (metricName) => { this.logger.debug({ metricName }, 'Plugin: getMetric'); return await context.metricsProvider.getMetric(metricName); }); } // State management (if permitted) if (context.permissions.write_state && context.state) { apis.getState = this.createAsyncProxy(async (key) => { this.logger.debug({ key }, 'Plugin: getState'); return context.state.get(key); }); apis.setState = this.createAsyncProxy(async (key, value) => { this.logger.debug({ key, valueType: typeof value }, 'Plugin: setState'); context.state.set(key, value); return true; }); } // External requests (if permitted) if (context.permissions.external_requests) { apis.fetch = this.createAsyncProxy(async (url, options) => { this.logger.debug({ url, method: options?.method }, 'Plugin: fetch'); // Basic URL validation if (!url.startsWith('http://') && !url.startsWith('https://')) { throw new Error('Only HTTP(S) URLs are allowed'); } // Perform fetch with timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout try { const response = await fetch(url, { ...options, signal: controller.signal }); return { ok: response.ok, status: response.status, statusText: response.statusText, data: await response.text() }; } finally { clearTimeout(timeout); } }); } await jail.set('api', new ivm.Reference(apis)); } /** * Inject context data */ async injectContextData(jail, context) { // Parameters (read-only copy) if (context.parameters) { await jail.set('parameters', new ivm.ExternalCopy(context.parameters).copyInto()); } // Outputs (read-only copy) if (context.outputs) { await jail.set('outputs', new ivm.ExternalCopy(context.outputs).copyInto()); } } /** * Inject utility functions */ async injectUtilities(jail, logs) { // Console implementation const consoleObj = { log: (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); logs.push(`[LOG] ${message}`); }, error: (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); logs.push(`[ERROR] ${message}`); }, warn: (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); logs.push(`[WARN] ${message}`); }, info: (...args) => { const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); logs.push(`[INFO] ${message}`); } }; await jail.set('console', new ivm.Reference(consoleObj)); // JSON utilities await jail.set('JSON', new ivm.Reference({ parse: (str) => JSON.parse(str), stringify: (obj, replacer, space) => JSON.stringify(obj, replacer, space) })); // Math utilities await jail.set('Math', new ivm.Reference(Math)); // Date constructor await jail.set('Date', new ivm.Reference(Date)); // Basic utilities const utils = { // Sleep function sleep: this.createAsyncProxy(async (ms) => { if (ms > 10000) { throw new Error('Sleep duration cannot exceed 10 seconds'); } await new Promise(resolve => setTimeout(resolve, ms)); }), // UUID generator generateId: () => { return `id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }, // Type checking isArray: Array.isArray, isObject: (obj) => obj !== null && typeof obj === 'object' && !Array.isArray(obj) }; await jail.set('utils', new ivm.Reference(utils)); } /** * Create async proxy for functions */ createAsyncProxy(fn) { return new ivm.Reference(async function (...args) { try { const result = await fn(...args); return new ivm.ExternalCopy(result).copyInto(); } catch (error) { // Re-throw error in a way that crosses the boundary throw new Error(error instanceof Error ? error.message : 'Operation failed'); } }); } /** * Get memory usage of isolate */ async getMemoryUsage() { try { const heap = await this.isolate.getHeapStatistics(); return Math.round(heap.used_heap_size / (1024 * 1024)); // Convert to MB } catch { return 0; } } /** * Destroy the isolate */ async destroy() { this.logger.info('Destroying isolated VM'); this.isolate.dispose(); } } //# sourceMappingURL=IsolatedVMSandbox.js.map