signalk-mcp-server
Version:
MCP server for SignalK marine data integration
181 lines • 6.49 kB
JavaScript
import ivm from 'isolated-vm';
/**
* V8 Isolate-based code execution sandbox
*
* Uses isolated-vm to execute agent-written code in a secure V8 isolate.
* Provides:
* - Memory isolation (default 128MB limit)
* - Execution timeout (default 30s)
* - No network access from sandbox
* - No filesystem access (except via bindings)
* - Console output capture
*
* Based on Cloudflare's Code Mode architecture:
* - Fresh isolate per execution (milliseconds startup)
* - Binding-based access to external systems (RPC-style)
* - No credentials exposed to sandbox code
*
* @example
* const sandbox = new IsolateSandbox();
* const result = await sandbox.execute(`
* console.log('Hello from isolate');
* return 42;
* `);
* console.log(result.logs); // ['Hello from isolate']
* console.log(result.result); // 42
*/
export class IsolateSandbox {
limits;
constructor(limits = {}) {
this.limits = {
memoryLimitMB: limits.memoryLimitMB || 128,
timeoutMs: limits.timeoutMs || 30000,
};
}
/**
* Execute code in a fresh V8 isolate
*
* @param code - JavaScript/TypeScript code to execute
* @param bindings - Optional bindings to inject into isolate context
* @returns ExecutionResult with output, logs, and timing
*
* @example
* // Simple execution
* const result = await sandbox.execute('2 + 2');
* console.log(result.result); // 4
*
* // With bindings
* const result = await sandbox.execute(
* 'await signalk.getVesselState()',
* { signalk: new SignalKBinding() }
* );
*/
async execute(code, bindings = {}) {
const startTime = Date.now();
const logs = [];
let isolate = null;
try {
// Create fresh V8 isolate with memory limit
isolate = new ivm.Isolate({
memoryLimit: this.limits.memoryLimitMB,
inspector: false, // No debugging access for security
});
// Create execution context
const context = await isolate.createContext();
// Inject console.log capture using applySync
const jail = context.global;
await jail.set('global', jail.derefInto());
await jail.set('_consoleLog', new ivm.Reference(function (...args) {
logs.push(args.map(arg => String(arg)).join(' '));
}));
await context.eval(`
globalThis.console = {
log: function(...args) {
_consoleLog.applySync(undefined, args);
}
};
`);
// Inject bindings (RPC-style access to external systems)
for (const [name, binding] of Object.entries(bindings)) {
await this.injectBinding(context, name, binding);
}
// Execute code with timeout
const script = await isolate.compileScript(code);
const result = await script.run(context, {
timeout: this.limits.timeoutMs,
promise: true, // Support async/await
});
// Copy result out of isolate (primitives only by default)
const finalResult = await this.extractResult(result);
return {
success: true,
result: finalResult,
logs,
executionTime: Date.now() - startTime,
};
}
catch (error) {
return {
success: false,
error: error.message || String(error),
logs,
executionTime: Date.now() - startTime,
};
}
finally {
// Cleanup: dispose isolate (memory freed)
if (isolate) {
isolate.dispose();
}
}
}
/**
* Inject a binding into the isolate context
* Bindings are objects with methods that can be called from agent code
*
* @private
*/
async injectBinding(context, name, binding) {
const jail = context.global;
// Get all methods from the binding object (including plain object properties)
const proto = Object.getPrototypeOf(binding);
const protoMethods = proto && proto !== Object.prototype
? Object.getOwnPropertyNames(proto).filter(prop => typeof binding[prop] === 'function' && prop !== 'constructor')
: [];
const ownMethods = Object.keys(binding).filter(prop => typeof binding[prop] === 'function');
const methods = [...new Set([...protoMethods, ...ownMethods])];
if (methods.length === 0) {
console.error(`Warning: No methods found for binding '${name}'`);
return;
}
// Create references for each method
for (const methodName of methods) {
const method = binding[methodName].bind(binding);
const refName = `_${name}_${methodName}`;
await jail.set(refName, new ivm.Reference(async (...args) => {
const result = await method(...args);
// Use ExternalCopy to transfer values across isolate boundary
return new ivm.ExternalCopy(result);
}));
}
// Create proxy object in isolate that calls the references
const methodProxies = methods.map(methodName => {
const refName = `_${name}_${methodName}`;
return ` ${methodName}: async function(...args) {
const externalCopy = await ${refName}.apply(undefined, args, {
arguments: { copy: true },
result: { promise: true }
});
return externalCopy.copy();
}`;
}).join(',\n');
await context.eval(`
globalThis.${name} = {
${methodProxies}
};
`);
}
/**
* Extract result from isolate
* Handles primitives, objects, and arrays
*
* @private
*/
async extractResult(result) {
if (result === null || result === undefined) {
return result;
}
// If it's an ivm.Reference, try to copy it
if (result && typeof result.copy === 'function') {
try {
return await result.copy();
}
catch {
// If copy fails, return string representation
return String(result);
}
}
return result;
}
}
//# sourceMappingURL=isolate-sandbox.js.map