Skip to content
GitHub Get Started
Features

Runtime & Platform

Guest JavaScript runs with the full Node.js emulation surface: the process and Buffer globals, the node:* builtin modules, npm module resolution, and a virtualized Node process identity. This is what NodeRuntime boots, and today it is the only platform Secure Exec exposes.

Guest code runs inside a kernel-backed V8 isolate with zero host escapes. Even with the full Node surface available, every syscall (filesystem, network, child process) goes through the kernel, never the real host.

import { NodeRuntime } from "secure-exec";
const rt = await NodeRuntime.create();
try {
const probe = await rt.run<{
platform: string;
hasProcess: boolean;
hasBuffer: boolean;
nodeVersion: string;
sha256: string;
joinedPath: string;
}>(`
const { createHash } = await import("node:crypto");
const { join } = await import("node:path");
const sha256 = createHash("sha256").update("secure-exec").digest("hex");
globalThis.__return({
platform: typeof process !== "undefined" ? process.platform : "(no process)",
hasProcess: typeof process !== "undefined",
hasBuffer: typeof Buffer !== "undefined",
nodeVersion: typeof process !== "undefined" ? process.versions.node : "(none)",
sha256,
joinedPath: join("/home/user", "report.txt"),
});
`);
console.log(probe.value);
} finally {
await rt.dispose();
}

See Full Example

Output, the guest host environment as seen from inside the isolate:

{
platform: 'linux',
hasProcess: true,
hasBuffer: true,
nodeVersion: '22.0.0',
sha256: 'fa7ce60dac0cc1bfe7424a68e47ad3d712345cf936431bb147cd5f5de0371a4a',
joinedPath: '/home/user/report.txt'
}

Available to guest code on the default platform:

  • Node globals: process, Buffer, require, module, __dirname, __filename.
  • node:* builtins: fs, path, crypto, http, net, os, child_process, dns, and the rest of the Node standard library.
  • Node identity: virtualized process.versions (Node 22.0.0), process.platform (linux), execPath, and pid/ppid/uid/gid.
  • Web platform: fetch, URL, TextEncoder/TextDecoder, WebCrypto, structuredClone, Blob, AbortController.
  • Universal primitives: console, timers (setTimeout/setInterval/…), queueMicrotask.
  • Language + Wasm: the ECMAScript spec globals plus WebAssembly.

Every one of these is kernel-backed. fetch and node:net/node:http route through the kernel socket table (and are denied by default until you grant network); node:fs sees only the VM’s virtual filesystem; node:child_process spawns kernel-managed processes.

NodeRuntime.create() shapes what the guest sees before and after boot:

const rt = await NodeRuntime.create({
env: { API_BASE: "https://example.test" }, // guest process env
cwd: "/home/user", // default working dir
files: { "/root/data.json": '{"ok":true}' }, // seed VFS bytes
mounts: [ // project host dirs, Docker-style, lazy
{ guestPath: "/root/node_modules/typescript", hostPath: "/abs/typescript", readOnly: true },
],
permissions: { network: "allow" }, // merged over secure default (network denied)
tools: { // host capabilities the guest calls as commands
add: {
description: "Add two numbers",
inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
handler: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
},
},
loopbackExemptPorts: [3000], // let non-loopback connections reach this port
commandsDir: "/abs/wasm/commands", // override the WASM `sh`/coreutils dir
});
  • files copies bytes into the VFS up front; mounts reads host trees lazily through the VFS, so large node_modules are not copied as a blob.
  • permissions merges over a secure default (fs/childProcess/process/env allowed, network denied), so { network: "allow" } is enough to opt in.
  • tools auto-grants the tool scope when you set no tool policy; the guest invokes a tool by name with --json input over node:child_process.

After boot, the same surface is reachable on the live runtime:

await rt.writeFile("/root/late.json", '{"added":"after boot"}');
const bytes = await rt.readFile("/root/late.json"); // Uint8Array
await rt.registerTools({ now: { description: "epoch ms", handler: () => Date.now() } });
  • rt.exec(code, options) runs an ESM program to completion, returns { stdout, stderr, exitCode }.
  • rt.run<T>(code, options) runs a program that calls globalThis.__return(value) and decodes it as result.value (plus stdout/stderr/exitCode).
  • rt.spawn(code, options) starts a long-running guest and returns a live NodeRuntimeProcess (pid, writeStdin, closeStdin, kill, wait, exitCode).
  • rt.fetch(port, input) drives an HTTP request from the host into a guest listener and returns { status, statusText, headers, body }, even with egress denied.
  • rt.waitForListener(query, options) / rt.findListener(query) resolve guest TCP listeners by port/host/path.

exec, run, and spawn accept the same per-run options:

const controller = new AbortController();
const result = await rt.exec(source, {
env: { EXTRA: "1" }, // merged over VM env
cwd: "/tmp",
stdin: "piped to the program", // string | Uint8Array
timeout: 5000, // kill after N ms
signal: controller.signal, // abort -> SIGTERM, call rejects AbortError
onStdout: (chunk) => process.stdout.write(chunk), // Uint8Array chunks, live
onStderr: (chunk) => process.stderr.write(chunk),
});

Host-to-guest dev-server pattern:

const server = await rt.spawn(`
import http from "node:http";
http.createServer((_, res) => res.end("ok")).listen(3000);
`);
const listener = await rt.waitForListener({ port: 3000 });
const res = await rt.fetch(listener.port ?? 3000, { path: "/" });
server.kill();
await server.wait();

The kernel models the guest host environment as a ladder, using esbuild’s vocabulary (node / browser / neutral) plus bare for the language-only tier esbuild has no name for. NodeRuntime always runs the top rung (node); the lower rungs are described here for context and are not currently selectable in Secure Exec (see the note below).

Capabilitynodebrowserneutralbare
Node globalsNoNoNo
node:* builtinsNoNoNo
Node identityNoNoNo
Web platformNoNo
Universal primitivesNo
Language + Wasm
  • node (the default): full Node.js compatibility. Nothing removed.
  • browser: a browser/Deno-like runtime. The Node surface is gone; web-standard globals remain. crypto is the WebCrypto object (crypto.subtle, crypto.getRandomValues), not the node:crypto module, so crypto.randomBytes/crypto.createHash are absent.
  • neutral: universal primitives only (console, timers, queueMicrotask) plus the language. No fetch/URL/WebCrypto, no Node.
  • bare: language only: the ECMAScript spec globals plus WebAssembly. No console, no timers, no fetch. The caller provides any host functionality the guest needs.

WebAssembly stays available on every tier, including bare. Compilation happens inside the isolate and is not a host escape.

An independent axis of the wire config (any combination with platform is valid). Like esbuild, the default is full Node resolution even for non-node platforms, which is exactly what NodeRuntime uses.

moduleResolutionimport "pkg"import "./x.js"node:*
node (default)
relativeNoNo
noneNoNoNo

(import "pkg" = bare/node_modules specifier; import "./x.js" = relative or absolute path; node:* follows the platform / allowedBuiltins.)

  • node: standard Node resolution: the node_modules ancestor walk, exports/imports/conditions, and realpath/symlink following.
  • relative: only relative and absolute paths resolve from the VFS; bare package specifiers and node:* builtins do not.
  • none: nothing resolves. Any import or require (including relative) fails. Produces a single self-contained entrypoint module, useful for locked-down evaluation of one script.

allowedBuiltins: restrict Node builtins (node platform only)

Section titled “allowedBuiltins: restrict Node builtins (node platform only)”

When platform is node, narrow which node:* builtin modules guest code may import:

  • omitted: the engine default allow-list (what NodeRuntime uses).
  • []: deny all builtins.
  • ["path", "fs"]: allow exactly these (a root name like fs also covers node:fs/promises).

allowedBuiltins is only meaningful under platform: "node"; the other platforms deny all node:* builtins regardless. Unknown builtin names are rejected.