This page documents experimental functionality. APIs, behavior, and docs may change without notice.
The Node runtime runs JavaScript code in isolated V8 isolates. It supports memory limits, CPU time budgets, and timing side-channel mitigation.
Kernel-first approach
Create a kernel and mount the Node runtime driver. This is the recommended approach for new projects.
import {
createKernel,
createInMemoryFileSystem,
createNodeRuntime,
} from "secure-exec";
const kernel = createKernel({
filesystem: createInMemoryFileSystem(),
});
await kernel.mount(createNodeRuntime());
const result = await kernel.exec("node -e \"console.log('hello')\"");
console.log(result.stdout); // "hello\n"
await kernel.dispose();
Runtime options
Pass options to createNodeRuntime() to configure V8 isolate behavior.
await kernel.mount(createNodeRuntime({
memoryLimit: 128, // 128 MB per isolate
}));
These exports are also available directly from @secure-exec/nodejs.
NodeRuntime (convenience class)
For direct code execution with typed return values, use the NodeRuntime class with a system driver and runtime driver factory.
import {
NodeRuntime,
createNodeDriver,
createNodeRuntimeDriverFactory,
} from "secure-exec";
const runtime = new NodeRuntime({
systemDriver: createNodeDriver(),
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
});
These exports are also available from @secure-exec/nodejs.
By default, all runtimes share a single V8 child process. You can pass a dedicated V8Runtime handle via createNodeRuntimeDriverFactory({ v8Runtime }) to control crash blast radius and resource partitioning. See Process Isolation for topology options and trade-offs.
exec vs run
NodeRuntime exposes two execution methods with different signatures and intended use cases:
// Process-style execution — observe stdout/stderr, set env/cwd/stdin
exec(code: string, options?: ExecOptions): Promise<ExecResult>
// Export-based evaluation — get computed values back
run<T>(code: string, filePath?: string): Promise<RunResult<T>>
| exec() | run() |
|---|
| Returns | { code, errorMessage? } | { code, errorMessage?, exports? } |
Per-call onStdio | Yes | No (use runtime-level hook) |
Per-call env / cwd / stdin | Yes | No |
| Best for | Side effects, CLI-style output, automation loops | Getting computed values back into the host |
When to use exec()
Use exec() when sandboxed code produces output you need to observe or when you need per-call control over the execution environment. This is the right choice for AI SDK tool loops, code interpreters, and any integration where the result is communicated through console.log rather than export.
// AI SDK tool loop — capture stdout from each step
for (const step of toolSteps) {
const result = await runtime.exec(step.code, {
onStdio: (e) => appendToToolResult(e.message),
env: { API_KEY: step.apiKey },
cwd: "/workspace",
});
if (result.code !== 0) handleError(result);
}
When to use run()
Use run() when sandboxed code exports a value you need in the host. The sandbox code uses export default or named exports, and the host reads them from result.exports.
// Evaluate a user-provided expression and get the result
const result = await runtime.run<{ default: number }>("export default 40 + 2");
console.log(result.exports?.default); // 42
If you find yourself parsing console.log output to extract a value, switch to run() with an export. If you need to watch a stream of output lines, switch to exec() with onStdio.
Capturing output
Console output is not buffered into the result. Use the onStdio hook to capture it.
The per-execution onStdio option is available on exec() only. To capture output from run(), set a runtime-level hook:
// Per-execution hook (exec only)
const logs: string[] = [];
await runtime.exec("console.log('hello'); console.error('oops')", {
onStdio: (event) => logs.push(`[${event.channel}] ${event.message}`),
});
// logs: ["[stdout] hello", "[stderr] oops"]
// Runtime-level hook (applies to both exec and run)
const runtime = new NodeRuntime({
systemDriver: createNodeDriver(),
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
onStdio: (event) => console.log(event.message),
});
Lifecycle
A single NodeRuntime instance is designed to be reused across many .exec() and .run() calls. Each call creates a fresh V8 isolate session internally, so per-execution state (module cache, budgets) is automatically reset while the underlying V8 process is reused efficiently.
// Recommended: create once, call many times, dispose at the end
const runtime = new NodeRuntime({
systemDriver: createNodeDriver(),
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
});
// AI SDK tool loop — each step reuses the same runtime
for (const step of toolSteps) {
const result = await runtime.exec(step.code, {
onStdio: (e) => log(e.message),
});
}
// Clean up when the session is over
runtime.dispose();
Do not dispose and recreate the runtime between sequential calls. Calling .exec() or .run() on a disposed runtime throws "NodeExecutionDriver has been disposed".
dispose() is synchronous and immediate — it kills active child processes and clears timers. Use terminate() (async) when you need to wait for graceful HTTP server shutdown before cleanup.
TypeScript workflows
NodeRuntime executes JavaScript only. For sandboxed TypeScript type checking or compilation, use the separate @secure-exec/typescript package. See TypeScript support.
Resource limits
You can cap memory and CPU time to prevent runaway code from exhausting host resources.
const runtime = new NodeRuntime({
systemDriver: createNodeDriver(),
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
memoryLimit: 128, // 128 MB isolate memory cap
cpuTimeLimitMs: 5000, // 5 second CPU budget
timingMitigation: "freeze", // freeze high-resolution timers (default)
});
Module loading
The sandbox provides a read-only overlay of the host’s node_modules at /app/node_modules. Sandboxed code can import or require() packages installed on the host without any additional configuration.
TypeScript support
TypeScript support lives in the companion @secure-exec/typescript package. It runs the TypeScript compiler inside a dedicated sandbox so untrusted inputs cannot consume host CPU or memory during type checking or compilation.
Shared setup:
import { createInMemoryFileSystem } from "@secure-exec/core";
import {
createNodeDriver,
createNodeRuntimeDriverFactory,
} from "@secure-exec/nodejs";
import { createTypeScriptTools } from "@secure-exec/typescript";
const filesystem = createInMemoryFileSystem();
const systemDriver = createNodeDriver({ filesystem });
const runtimeDriverFactory = createNodeRuntimeDriverFactory();
const ts = createTypeScriptTools({
systemDriver,
runtimeDriverFactory,
});
Type check source
Use typecheckSource(...) when you want to quickly validate AI-generated code before running it:
const result = await ts.typecheckSource({
filePath: "/root/snippet.ts",
sourceText: `
export function greet(name: string) {
return "hello " + missingValue;
}
`,
});
console.log(result.success); // false
console.log(result.diagnostics[0]?.message);
Type check project
Use typecheckProject(...) when you want to validate a full TypeScript project from the filesystem exposed by the system driver. That means tsconfig.json, source files, and any other project inputs are read from that sandbox filesystem view.
await filesystem.mkdir("/root/src");
await filesystem.writeFile(
"/root/tsconfig.json",
JSON.stringify({
compilerOptions: {
module: "nodenext",
moduleResolution: "nodenext",
target: "es2022",
},
include: ["src/**/*.ts"],
}),
);
await filesystem.writeFile(
"/root/src/index.ts",
"export const value: string = 123;\n",
);
const result = await ts.typecheckProject({ cwd: "/root" });
console.log(result.success); // false
console.log(result.diagnostics[0]?.message);
Compile source
Use compileSource(...) when you want to transpile one TypeScript source string and then hand the emitted JavaScript to NodeRuntime:
import { NodeRuntime } from "secure-exec";
const compiled = await ts.compileSource({
filePath: "/root/example.ts",
sourceText: "export default 40 + 2;",
compilerOptions: {
module: "esnext",
target: "es2022",
},
});
const runtime = new NodeRuntime({
systemDriver,
runtimeDriverFactory,
});
const execution = await runtime.run<{ default: number }>(
compiled.outputText ?? "",
"/root/example.js",
);
console.log(execution.exports?.default); // 42
Compile project
Use compileProject(...) when you want TypeScript to read a real project from the filesystem exposed by the system driver and write emitted files back into that same sandbox filesystem view.
import {
NodeRuntime,
} from "secure-exec";
await filesystem.mkdir("/root/src");
await filesystem.writeFile(
"/root/tsconfig.json",
JSON.stringify({
compilerOptions: {
module: "commonjs",
target: "es2022",
outDir: "/root/dist",
},
include: ["src/**/*.ts"],
}),
);
await filesystem.writeFile(
"/root/src/index.ts",
"export const answer: number = 42;\n",
);
await ts.compileProject({ cwd: "/root" });
const runtime = new NodeRuntime({
systemDriver,
runtimeDriverFactory,
});
const execution = await runtime.run<{ answer: number }>(
"export { default as answer } from './dist/index.js';",
"/root/entry.mjs",
);
console.log(execution.exports); // { answer: 42 }