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();}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'}Host environment (the node surface)
Section titled “Host environment (the node surface)”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(Node22.0.0),process.platform(linux),execPath, andpid/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.
Seeding the host environment
Section titled “Seeding the host environment”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});filescopies bytes into the VFS up front;mountsreads host trees lazily through the VFS, so largenode_modulesare not copied as a blob.permissionsmerges over a secure default (fs/childProcess/process/envallowed,networkdenied), so{ network: "allow" }is enough to opt in.toolsauto-grants thetoolscope when you set notoolpolicy; the guest invokes a tool by name with--jsoninput overnode: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"); // Uint8Arrayawait rt.registerTools({ now: { description: "epoch ms", handler: () => Date.now() } });Running and driving guest programs
Section titled “Running and driving guest programs”rt.exec(code, options)runs an ESM program to completion, returns{ stdout, stderr, exitCode }.rt.run<T>(code, options)runs a program that callsglobalThis.__return(value)and decodes it asresult.value(plusstdout/stderr/exitCode).rt.spawn(code, options)starts a long-running guest and returns a liveNodeRuntimeProcess(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 byport/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 platform ladder
Section titled “The platform ladder”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).
| Capability | node | browser | neutral | bare |
|---|---|---|---|---|
| Node globals | ✅ | No | No | No |
node:* builtins | ✅ | No | No | No |
| Node identity | ✅ | No | No | No |
| Web platform | ✅ | ✅ | No | No |
| Universal primitives | ✅ | ✅ | ✅ | No |
| 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.cryptois the WebCrypto object (crypto.subtle,crypto.getRandomValues), not thenode:cryptomodule, socrypto.randomBytes/crypto.createHashare absent.neutral: universal primitives only (console, timers,queueMicrotask) plus the language. Nofetch/URL/WebCrypto, no Node.bare: language only: the ECMAScript spec globals plusWebAssembly. Noconsole, no timers, nofetch. 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.
moduleResolution: how imports resolve
Section titled “moduleResolution: how imports resolve”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.
moduleResolution | import "pkg" | import "./x.js" | node:* |
|---|---|---|---|
node (default) | ✅ | ✅ | ✅ |
relative | No | ✅ | No |
none | No | No | No |
(import "pkg" = bare/node_modules specifier; import "./x.js" = relative or
absolute path; node:* follows the platform / allowedBuiltins.)
node: standard Node resolution: thenode_modulesancestor walk,exports/imports/conditions, andrealpath/symlink following.relative: only relative and absolute paths resolve from the VFS; bare package specifiers andnode:*builtins do not.none: nothing resolves. Anyimportorrequire(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
NodeRuntimeuses). []: deny all builtins.["path", "fs"]: allow exactly these (a root name likefsalso coversnode:fs/promises).
allowedBuiltins is only meaningful under platform: "node"; the other
platforms deny all node:* builtins regardless. Unknown builtin names are
rejected.