Sandboxed JavaScript code execution service for MemberJunction AI agents and workflows. Executes untrusted code in isolated worker processes using V8 isolates (isolated-vm) with a defense-in-depth security model.
Parent package: This package is part of the MemberJunction Actions Framework. For general Actions architecture, design philosophy, and the boundary between Actions and internal code, see the parent CLAUDE.md.
This package provides secure, isolated execution of JavaScript code for scenarios where AI agents or workflows need to generate and run code dynamically -- data analysis, transformations, calculations, and more. It uses a multi-layer security architecture to ensure that untrusted code cannot compromise system security or stability.
The service manages a pool of worker processes, each running code inside V8 isolates via the isolated-vm library. If a worker crashes due to a catastrophic error (e.g., V8 OOM), only that worker terminates and is automatically restarted -- the main application remains unaffected.
For comprehensive security analysis, threat model, and implementation details, see security-research.md.
npm install @memberjunction/code-execution
This package is server-side only (Node.js). It requires the isolated-vm native module, which compiles during installation.
flowchart TB
subgraph MainProcess["Main Application Process"]
CES["CodeExecutionService"]
WP["WorkerPool"]
Q["Request Queue"]
CES --> WP
WP --> Q
end
subgraph W1["Worker Process 1"]
IV1["isolated-vm Isolate"]
CTX1["V8 Context"]
SB1["Sandbox\n(user code)"]
IV1 --> CTX1 --> SB1
end
subgraph W2["Worker Process 2"]
IV2["isolated-vm Isolate"]
CTX2["V8 Context"]
SB2["Sandbox\n(user code)"]
IV2 --> CTX2 --> SB2
end
WP -- "IPC (JSON)" --> W1
WP -- "IPC (JSON)" --> W2
style MainProcess fill:#2d6a9f,stroke:#1a4971,color:#fff
style W1 fill:#2d8659,stroke:#1a5c3a,color:#fff
style W2 fill:#2d8659,stroke:#1a5c3a,color:#fff
style CES fill:#7c5295,stroke:#563a6b,color:#fff
style WP fill:#7c5295,stroke:#563a6b,color:#fff
style Q fill:#b8762f,stroke:#8a5722,color:#fff
style IV1 fill:#2d6a9f,stroke:#1a4971,color:#fff
style IV2 fill:#2d6a9f,stroke:#1a4971,color:#fff
style CTX1 fill:#b8762f,stroke:#8a5722,color:#fff
style CTX2 fill:#b8762f,stroke:#8a5722,color:#fff
style SB1 fill:#7c5295,stroke:#563a6b,color:#fff
style SB2 fill:#7c5295,stroke:#563a6b,color:#fff
The package uses a defense-in-depth approach with five independent security layers:
flowchart LR
Code["Untrusted\nCode"] --> L1
L1["Layer 1\nProcess\nIsolation"] --> L2["Layer 2\nV8 Isolate\n(isolated-vm)"]
L2 --> L3["Layer 3\nModule\nBlocking"]
L3 --> L4["Layer 4\nResource\nLimits"]
L4 --> L5["Layer 5\nLibrary\nAllowlist"]
L5 --> Exec["Safe\nExecution"]
style Code fill:#b8762f,stroke:#8a5722,color:#fff
style L1 fill:#2d6a9f,stroke:#1a4971,color:#fff
style L2 fill:#2d6a9f,stroke:#1a4971,color:#fff
style L3 fill:#2d6a9f,stroke:#1a4971,color:#fff
style L4 fill:#2d6a9f,stroke:#1a4971,color:#fff
style L5 fill:#2d6a9f,stroke:#1a4971,color:#fff
style Exec fill:#2d8659,stroke:#1a5c3a,color:#fff
| Layer | Mechanism | Protects Against |
|---|---|---|
| Process Isolation | Workers run in separate OS processes via child_process.fork() |
V8 catastrophic failures crashing the main app |
| V8 Isolates | Each execution runs in an isolated V8 context (isolated-vm) |
Sandbox escape, cross-execution data leakage |
| Module Blocking | Dangerous Node.js modules blocked (fs, http, child_process, etc.) |
Filesystem access, network requests, process spawning |
| Resource Limits | Configurable timeout and memory limits per execution | Denial-of-service via infinite loops or memory exhaustion |
| Library Allowlist | Only pre-vetted libraries available via require() |
Supply chain attacks, unsafe library usage |
flowchart TB
IDX["index.ts\n(public exports)"]
CES["CodeExecutionService\n(main entry point)"]
WP["WorkerPool\n(process management)"]
WKR["worker.ts\n(child process)"]
LIB["libraries/\n(allowlisted modules)"]
TYP["types/\n(interfaces)"]
IDX --> CES
IDX --> WP
IDX --> LIB
IDX --> TYP
CES --> WP
CES --> TYP
WP --> TYP
WKR --> TYP
WKR --> LIB
style IDX fill:#7c5295,stroke:#563a6b,color:#fff
style CES fill:#2d6a9f,stroke:#1a4971,color:#fff
style WP fill:#2d6a9f,stroke:#1a4971,color:#fff
style WKR fill:#2d8659,stroke:#1a5c3a,color:#fff
style LIB fill:#b8762f,stroke:#8a5722,color:#fff
style TYP fill:#b8762f,stroke:#8a5722,color:#fff
import { CodeExecutionService } from '@memberjunction/code-execution';
// Create service with optional worker pool configuration
const service = new CodeExecutionService({
poolSize: 2, // Number of worker processes (default: 2)
maxQueueSize: 100, // Max queued requests (default: 100)
maxCrashesPerWorker: 3, // Crashes before marking unhealthy (default: 3)
crashTimeWindow: 60000 // Time window for crash counting in ms (default: 60000)
});
// Initialize the worker pool (also auto-initializes on first execute)
await service.initialize();
// Execute code
const result = await service.execute({
code: `
const sum = input.values.reduce((a, b) => a + b, 0);
const average = sum / input.values.length;
output = { sum, average };
`,
language: 'javascript',
inputData: { values: [10, 20, 30, 40, 50] },
timeoutSeconds: 30, // Optional (default: 30)
memoryLimitMB: 128 // Optional (default: 128)
});
if (result.success) {
console.log(result.output); // { sum: 150, average: 30 }
console.log(result.logs); // Any console.log output from the code
} else {
console.error(result.error);
console.error(result.errorType); // 'TIMEOUT' | 'MEMORY_LIMIT' | 'SYNTAX_ERROR' | etc.
}
// Check worker pool health
const stats = service.getStats();
console.log('Active workers:', stats.activeWorkers);
console.log('Busy workers:', stats.busyWorkers);
console.log('Queue length:', stats.queueLength);
// Graceful shutdown (important for clean application exit)
await service.shutdown();
AI agents interact with the sandbox through the "Execute Code" action:
{
"type": "Action",
"action": {
"name": "Execute Code",
"params": {
"code": "const total = input.prices.reduce((sum, p) => sum + p, 0); output = total;",
"language": "javascript",
"inputData": "{\"prices\": [10, 20, 30]}"
}
}
}
The main entry point for executing sandboxed code.
constructor(options?: WorkerPoolOptions)
| Option | Type | Default | Description |
|---|---|---|---|
poolSize |
number |
2 |
Number of worker processes to maintain |
maxQueueSize |
number |
100 |
Maximum requests to queue before rejecting |
maxCrashesPerWorker |
number |
3 |
Crash count threshold before disabling a worker |
crashTimeWindow |
number |
60000 |
Time window (ms) for crash counting |
| Method | Returns | Description |
|---|---|---|
initialize() |
Promise<void> |
Start worker processes. Called automatically on first execute() if not called explicitly. |
execute(params) |
Promise<CodeExecutionResult> |
Execute code in a sandboxed worker. See CodeExecutionParams below. |
getStats() |
PoolStats |
Get current pool statistics (worker count, busy count, queue length). |
shutdown() |
Promise<void> |
Gracefully terminate all workers and reject pending requests. |
interface CodeExecutionParams {
code: string; // JavaScript code to execute
language: 'javascript'; // Currently only 'javascript' supported
inputData?: unknown; // Data available as the 'input' variable in sandbox
timeoutSeconds?: number; // Max execution time in seconds (default: 30)
memoryLimitMB?: number; // Memory limit per execution in MB (default: 128)
}
interface CodeExecutionResult {
success: boolean; // Whether execution completed without error
output?: unknown; // Value of the 'output' variable set by executed code
logs?: string[]; // Captured console output (log, error, warn, info)
error?: string; // Error message if execution failed
errorType?: 'TIMEOUT' | 'MEMORY_LIMIT' | 'SYNTAX_ERROR' | 'RUNTIME_ERROR' | 'SECURITY_ERROR';
executionTimeMs?: number; // Wall-clock execution duration in milliseconds
}
Manages the pool of worker processes. Typically used through CodeExecutionService, but can be instantiated directly for advanced use cases.
| Method | Returns | Description |
|---|---|---|
initialize() |
Promise<void> |
Fork worker processes and wait for them to signal ready. |
execute(params) |
Promise<CodeExecutionResult> |
Queue and execute a request in the next available worker. |
getStats() |
PoolStats |
Return totalWorkers, activeWorkers, busyWorkers, queueLength. |
shutdown() |
Promise<void> |
Gracefully kill all workers with SIGTERM, force-kill after 5 seconds. |
| Function | Returns | Description |
|---|---|---|
isModuleAllowed(name) |
boolean |
Check if a module name is in the allowlist. |
getAllowedModuleNames() |
string[] |
Return all allowed module names. |
getLibrarySource(name) |
string | null |
Return the JavaScript source for a library (inline or bundled). |
input variableoutput variableconsole.log, console.error, console.warn, console.info)JSON, Math, Date, Array, Object, String, Number, Boolean, RegExp, Errorfs, path modules blocked)http, https, net, axios blocked; fetch() API disabled)child_process, cluster blocked)os, process blocked)eval() or Function constructor (within user code scope)The sandbox provides a curated set of pre-vetted libraries accessible via require():
| Library | Type | Description |
|---|---|---|
lodash |
Inline subset | Array/collection/object utilities (chunk, groupBy, sortBy, pick, omit, get, set, sum, sumBy, mean, meanBy, cloneDeep, and more) |
date-fns |
Inline subset | Date manipulation (format, addDays, subDays, addMonths, differenceInDays, isAfter, isBefore, startOfDay, endOfDay, parseISO) |
mathjs |
Bundled full | Comprehensive mathematics library |
papaparse |
Bundled full | CSV parsing and generation |
jstat |
Bundled full | Statistical distributions, hypothesis testing, regression |
uuid |
Inline | UUID v4 generation |
validator |
Inline subset | String validation (isEmail, isURL, isNumeric, isAlpha, isAlphanumeric, isEmpty, isLength, isIn, matches) |
Libraries marked "Inline subset" are lightweight re-implementations of the most commonly used functions. Libraries marked "Bundled full" are the complete libraries loaded from pre-built JavaScript bundles.
The service classifies errors into five categories:
const result = await service.execute({
code: 'while(true) {}', // infinite loop
language: 'javascript'
});
if (!result.success) {
switch (result.errorType) {
case 'TIMEOUT':
// Code exceeded the configured timeout
break;
case 'MEMORY_LIMIT':
// Code exceeded the configured memory limit
break;
case 'SYNTAX_ERROR':
// Code has JavaScript syntax errors
break;
case 'RUNTIME_ERROR':
// Code threw an unhandled exception
break;
case 'SECURITY_ERROR':
// Code attempted a blocked operation (e.g., requiring 'fs')
break;
}
}
The worker pool implements automatic recovery with circuit breaker protection:
stateDiagram-v2
[*] --> Idle: Worker created
Idle --> Busy: Request assigned
Busy --> Idle: Execution complete
Busy --> Crashed: Process exit
Crashed --> Restarting: crashCount < max
Crashed --> Disabled: crashCount >= max
Restarting --> Idle: Worker ready
Disabled --> [*]: Removed from pool
const result = await service.execute({
code: `
const _ = require('lodash');
const metrics = {
totalSales: _.sumBy(input.sales, 'amount'),
avgSale: _.meanBy(input.sales, 'amount'),
topProducts: _.chain(input.sales)
.groupBy('product')
.map((items, product) => ({
product,
revenue: _.sumBy(items, 'amount')
}))
.sortBy(x => -x.revenue)
.take(5)
.value()
};
output = metrics;
`,
language: 'javascript',
inputData: {
sales: [
{ product: 'Widget', amount: 100 },
{ product: 'Gadget', amount: 200 },
{ product: 'Widget', amount: 150 }
]
}
});
const result = await service.execute({
code: `
const { format, addDays, differenceInDays } = require('date-fns');
const start = new Date(input.startDate);
const end = new Date(input.endDate);
output = {
duration: differenceInDays(end, start),
milestones: [
format(addDays(start, 30), 'yyyy-MM-dd'),
format(addDays(start, 60), 'yyyy-MM-dd'),
format(addDays(start, 90), 'yyyy-MM-dd')
]
};
`,
language: 'javascript',
inputData: {
startDate: '2025-01-01',
endDate: '2025-12-31'
}
});
const result = await service.execute({
code: `
const Papa = require('papaparse');
const parsed = Papa.parse(input.csvData, {
header: true,
dynamicTyping: true
});
const processed = parsed.data
.filter(row => row.status === 'active')
.map(row => ({
id: row.id,
name: row.name,
value: row.value * 1.1 // 10% markup
}));
output = processed;
`,
language: 'javascript',
inputData: {
csvData: 'id,name,value,status\\n1,Item A,100,active\\n2,Item B,200,inactive'
}
});
const result = await service.execute({
code: `
const math = require('mathjs');
const values = input.measurements;
output = {
mean: math.mean(values),
median: math.median(values),
std: math.std(values),
variance: math.variance(values),
min: math.min(values),
max: math.max(values)
};
`,
language: 'javascript',
inputData: {
measurements: [23.1, 24.5, 22.8, 25.0, 23.7, 24.2, 22.9]
}
});
CodeExecutionService instance and reuse it across your application. The worker pool is shared and handles concurrency internally.service.shutdown() during application shutdown to terminate workers cleanly and reject pending requests.timeoutSeconds based on expected workload. Short timeouts (5-10s) for simple calculations, longer (30-60s) for complex data processing.result.success before accessing result.output. Errors are returned in the result object, not thrown as exceptions.service.getStats() to monitor worker health and queue depth in production.console.log() statements in sandbox code for debugging. Output is captured in result.logs.async/await or Promises. All code must be synchronous.| Package | Purpose |
|---|---|
@memberjunction/core |
MJ core framework (logging via LogError, LogStatus) |
@memberjunction/global |
MJ global utilities |
isolated-vm |
V8 isolate-based sandboxing (native module) |
lodash |
Source for inline lodash subset |
date-fns |
Source for inline date-fns subset |
mathjs |
Bundled for sandbox use |
papaparse |
Bundled for sandbox use |
uuid |
Source for inline UUID implementation |
validator |
Source for inline validator subset |
async/await or Promises. Sandbox code must be synchronous.poolSize configuration or horizontal scaling for higher loads.MIT
Description
Sandboxed code execution service for MemberJunction