@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
286 lines • 10.8 kB
JavaScript
/**
* 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