How the MCP App Sandbox Isolates Untrusted MCP Applications in OpenWork
The OpenWork platform isolates untrusted MCP applications using Node.js VM contexts with strict timeouts, controlled global exposure, and event-driven API mediation to prevent host process contamination.
The OpenWork repository (different-ai/openwork) provides a secure execution environment for third-party Model Context Protocol (MCP) applications through a specialized sandbox implementation. Located at ee/apps/den-api/src/mcp-app-sandbox.ts, the MCPAppSandbox class creates a V8-isolated context that restricts memory access, enforces execution limits, and safely exposes host functionality without permitting arbitrary code execution.
Core Isolation Mechanisms
The sandbox implements a four-layer defense strategy using Node.js native VM capabilities to ensure untrusted code cannot escape its execution boundary.
VM Context Creation
The constructor initializes a completely isolated V8 context using createContext from the vm module. This context receives only explicitly whitelisted globals and a proxied console object that emits events rather than writing directly to stdout.
In ee/apps/den-api/src/mcp-app-sandbox.ts, the initialization code creates this restricted environment:
this.context = createContext({
console: {
log: (...args: any[]) => this.emit('log', ...args),
error: (...args: any[]) => this.emit('error', ...args),
},
...globals,
});
By default, the context contains no Node.js built-in modules (fs, http, process, etc.), eliminating filesystem and network access unless explicitly granted through the globals parameter.
Script Compilation and Caching
Before execution, the untrusted source code is compiled into a vm.Script object. This one-time compilation prevents repeated parsing overhead and ensures the code is syntactically valid before any execution occurs.
this.script = new Script(code, { filename: 'mcp-app.js' });
The filename parameter provides stack trace identification while the Script instance encapsulates the compiled bytecode for repeated runs within the same context.
Timeout Enforcement
The run() method executes the compiled script with a hard timeout defaulting to 2000 milliseconds. This prevents infinite loops and CPU exhaustion attacks from affecting the host process.
run() {
try {
this.script.runInContext(this.context, { timeout: this.timeout });
this.emit('finished');
} catch (e) {
this.emit('crash', e);
}
}
If execution exceeds the timeout, Node.js throws an error that the sandbox catches and emits as a crash event, allowing the host to handle the failure gracefully without terminating the entire application.
Safe API Exposure
The expose() method provides the only mechanism for untrusted code to interact with host capabilities. It injects wrapped functions into the sandbox context that emit events for monitoring while catching and sanitizing errors.
expose(name: string, fn: (...args: any[]) => any) {
this.context[name] = (...args: any[]) => {
try {
const result = fn(...args);
this.emit('api-call', { name, args, result });
return result;
} catch (e) {
this.emit('api-error', { name, args, error: e });
throw e;
}
};
}
This design ensures that all cross-boundary calls are logged via the api-call event, and exceptions in host code are captured as api-error events rather than crashing the sandbox or leaking stack traces.
Implementation in the Den API
The sandbox integrates with the broader OpenWork system through ee/apps/den-api/src/index.ts, which provides a convenience wrapper demonstrating typical usage patterns.
import { MCPAppSandbox } from './mcp-app-sandbox';
export function launchMCPApp(code: string) {
const sandbox = new MCPAppSandbox({ code });
// Expose a simple host API
sandbox.expose('fetchData', (url: string) => {
// In real implementation this would call the Den backend
return { url, data: 'mock' };
});
sandbox.run();
return sandbox;
}
This launcher creates the sandbox, exposes a controlled fetchData function, initiates execution, and returns the sandbox instance so callers can subscribe to lifecycle events.
Practical Usage Examples
Basic Sandbox Execution
Create and run an isolated MCP application with event monitoring:
import { MCPAppSandbox } from './mcp-app-sandbox';
const untrustedCode = `
console.log('Initializing MCP app');
const result = fetchData('https://api.example.com/data');
console.log('Received:', result);
`;
const sandbox = new MCPAppSandbox({
code: untrustedCode,
timeout: 3000
});
// Monitor sandbox output
sandbox.on('log', (...args) => console.log('[MCP]', ...args));
sandbox.on('error', (...args) => console.error('[MCP Error]', ...args));
sandbox.on('finished', () => console.log('Execution completed'));
// Expose safe host functionality
sandbox.expose('fetchData', (url: string) => {
// Validate and proxy the request
if (!url.startsWith('https://')) {
throw new Error('Only HTTPS URLs allowed');
}
return { url, data: 'sanitized-response' };
});
sandbox.run();
Handling Malicious or Hung Code
Enforce strict resource limits on potentially dangerous operations:
const suspiciousCode = `
while (true) {
console.log('Consuming CPU...');
}
`;
const sandbox = new MCPAppSandbox({
code: suspiciousCode,
timeout: 500 // 500ms limit
});
sandbox.on('crash', (error) => {
console.warn('Sandbox terminated:', error.message);
// Clean up resources, alert monitoring systems
});
sandbox.run();
// After 500ms, emits 'crash' event with timeout error
Custom Global Injection
Provide specific utilities while maintaining isolation:
const appCode = `
// Access custom utilities provided via globals
console.log('User:', userContext.id);
const hash = utils.sha256('sensitive-data');
`;
const sandbox = new MCPAppSandbox({
code: appCode,
globals: {
userContext: { id: 'user-123', role: 'analyst' },
utils: {
sha256: (data: string) => /* crypto implementation */
}
}
});
Summary
- VM Context Isolation: Uses
vm.createContext()to create a separate global environment without Node.js built-ins - Execution Time Limits: Enforces configurable timeouts (default 2000ms) via
script.runInContext()to prevent CPU exhaustion - Controlled API Surface: The
expose()method provides the only channel for host interaction, with automatic event logging - Event-Driven Monitoring: Extends
EventEmitterto provide visibility into logs, errors, completions, and API calls without breaking isolation - Pre-compilation: Scripts are compiled once and cached, improving performance for repeated executions
Frequently Asked Questions
How does the MCP app sandbox prevent filesystem access?
The sandbox removes all Node.js built-in modules from the execution context. Because the VM context created by createContext() only contains the explicitly provided console object and any user-defined globals, untrusted code cannot access require('fs'), require('path'), or other filesystem modules. This is enforced at the V8 engine level, making it impossible to bypass without modifying the host process.
Can the timeout be disabled for long-running MCP applications?
No, the timeout is mandatory for security. However, you can configure it up to a maximum value by passing the timeout option to the constructor (in milliseconds). The default is 2000ms, but you can extend this for legitimate long-running operations. Note that setting timeout: 0 or exceedingly high values increases vulnerability to denial-of-service attacks through infinite loops.
What happens when an exposed API function throws an error?
When a function registered via expose() throws an exception, the sandbox catches the error and emits an api-error event containing the function name, arguments, and error object. The exception is then re-thrown into the sandbox context so the untrusted code can handle it locally. This prevents error details from leaking to the host while still allowing the host to log and monitor failures.
Is the MCP app sandbox suitable for multi-tenant production environments?
While the sandbox provides strong isolation through VM contexts, it runs within the same Node.js process. For high-security multi-tenant environments, you should run each sandbox in a separate process or container, using the MCPAppSandbox as an additional defense layer. The current implementation is designed for trusted-execution scenarios where code is third-party but not actively malicious, focusing on preventing accidents and resource exhaustion rather than advanced sandbox escape techniques.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →