# How the MCP App Sandbox Isolates Untrusted MCP Applications in OpenWork

> Learn how the MCP app sandbox in OpenWork isolates untrusted apps using Node.js VM contexts, timeouts, and API mediation to ensure host process security.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-17

---

**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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/mcp-app-sandbox.ts), the initialization code creates this restricted environment:

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/index.ts), which provides a convenience wrapper demonstrating typical usage patterns.

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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 `EventEmitter` to 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.