Skip to main content

Target Node Version

22.x (derived from the @types/node 22.x validation baseline used by tests and type checks).

Architecture

secure-exec runs Node.js code inside a V8 isolate with a virtual kernel that mediates all system access. Nothing in the sandbox touches the host OS directly:
┌─────────────────────────────────────────────────┐
│  Sandbox (V8 Isolate)                           │
│                                                 │
│  User Code (require, fs, http, etc.)            │
│       │                                         │
│       ▼                                         │
│  Bridge Layer (polyfills + bridge modules)      │
│       │                                         │
│       ▼                                         │
│  Virtual Kernel                                 │
│  ├── VFS (virtual file system)                  │
│  ├── Process table (spawn, signals, exit)       │
│  ├── Network stack (TCP, HTTP, DNS, UDP)         │
│  └── Permissions engine (deny-by-default)       │
│       │                                         │
│       ▼                                         │
│  Host Adapters (embedder-provided)              │
└─────────────────────────────────────────────────┘


  Host OS (file system, network, etc.)
Key points:
  • All I/O routes through the virtual kernel. fs.readFile() goes through the VFS, http.request() goes through the network stack, child_process.spawn() goes through the process table. The kernel enforces permissions at every boundary.
  • Network calls are kernel-mediated. http.createServer() registers a virtual listener in the kernel’s network stack. http.request() to localhost routes through the kernel without touching real TCP — the kernel connects the virtual server to the virtual client directly. External requests go through the host adapter after permission checks.
  • The VFS is not the host file system. Files written by sandbox code live in the VFS (in-memory by default). The host file system is accessible only through explicit read-only overlays (e.g., node_modules) configured by the embedder.
  • Embedders provide host adapters that implement the actual I/O. A Node.js embedder provides real fs and net; a browser embedder provides fetch-based networking and no file system. The sandbox code doesn’t know or care which adapter backs the kernel.

Support Tiers

TierLabelMeaning
1BridgeRuntime implementation in secure-exec bridge modules.
2PolyfillBrowser-compatible polyfill package implementation.
3StubMinimal compatibility surface for lightweight usage and type/instance checks.
4Deferredrequire() succeeds, but APIs throw deterministic unsupported errors on call.
5UnsupportedNot implemented by design; require() throws immediately.
Unsupported API errors follow this format: "<module>.<api> is not supported in sandbox". Unsupported modules use: "<module> is not supported in sandbox".

Compatibility Matrix

ModuleTierStatus
fs1 (Bridge) + 4 (Deferred APIs)Implemented: readFile, writeFile, appendFile, open, read, write, close, readdir, mkdir, rmdir, rm, unlink, stat, lstat, rename, copyFile, exists, createReadStream, createWriteStream, writev, access, realpath, chmod, chown, link, symlink, readlink, truncate, utimes, cp, mkdtemp, opendir, glob, statfs, readv, fdatasync, fsync. Metadata-sensitive operations (stat, exists, readdir with withFileTypes) use metadata-native driver paths instead of content probing. rename delegates to driver semantics (atomic where supported; explicit limitation errors where not). Deferred watcher APIs: watch, watchFile, fs.promises.watch.
process1 (Bridge)Env access (permission-gated), cwd/chdir, exit semantics, timers, stdio, eventing, and basic usage/system metadata APIs.
os1 (Bridge)Platform/arch/version, user/system info, and os.constants.
child_process1 (Bridge) + 5 (fork)Implemented: spawn, spawnSync, exec, execSync, execFile, execFileSync; fork is intentionally unsupported.
http1 (Bridge)Implemented: request, get, createServer; bridged request/response/server classes and constants. Includes Agent with connection pooling (maxSockets, keepAlive), HTTP upgrade (101 Switching Protocols) handling, and trailer headers support.
https1 (Bridge)Implemented: request, get, createServer with the same contract as http, including Agent pooling, upgrade handling, and trailer headers.
http23 (Stub) + 5 (Full support)Provides compatibility classes (Http2ServerRequest, Http2ServerResponse); createServer and createSecureServer are unsupported.
dns1 (Bridge)Implemented: lookup, resolve, resolve4, resolve6, plus dns.promises variants.
module1 (Bridge)Implements createRequire, Module basics, and builtin resolution (require.resolve("fs") returns builtin identifiers).
timers1 (Bridge)setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate.
path2 (Polyfill)path-browserify; supports default and named ESM imports.
buffer2 (Polyfill)Polyfill via buffer.
url2 (Polyfill)Polyfill via whatwg-url and node-stdlib-browser shims.
events2 (Polyfill)Polyfill via events.
stream2 (Polyfill)Polyfill via readable-stream. stream/web subpath supported (Web Streams API: ReadableStream, WritableStream, TransformStream, etc.).
util2 (Polyfill)Polyfill via node-stdlib-browser.
assert2 (Polyfill)Polyfill via node-stdlib-browser.
querystring2 (Polyfill)Polyfill via node-stdlib-browser.
string_decoder2 (Polyfill)Polyfill via node-stdlib-browser.
zlib2 (Polyfill)Polyfill via node-stdlib-browser.
vm2 (Polyfill)Polyfill via node-stdlib-browser.
punycode2 (Polyfill)Polyfill via node-stdlib-browser.
crypto3 (Stub)Limited bridge/polyfill blend; getRandomValues() and randomUUID() use host node:crypto secure randomness, or throw deterministic unsupported errors if host entropy is unavailable; subtle.* methods throw deterministic unsupported errors.
tty2 (Polyfill)tty-browserify; isatty() returns false; ReadStream/WriteStream are compatibility constructors.
v83 (Stub)Pre-registered stub with mock heap stats and JSON-based serialize/deserialize.
constants2 (Polyfill)constants-browserify; os.constants remains available via os.
Fetch globals (fetch, Headers, Request, Response)1 (Bridge)Bridged via network bridge implementation.
async_hooks3 (Stub)AsyncLocalStorage (with run, enterWith, getStore, disable, exit), AsyncResource (with runInAsyncScope, emitDestroy), createHook (returns enable/disable no-ops), executionAsyncId, triggerAsyncId.
console1 (Bridge)Circular-safe bounded formatting via bridge shim; log, warn, error, info, debug, dir, time/timeEnd/timeLog, assert, clear, count/countReset, group/groupEnd, table, trace. Drop-by-default; consumers use onStdio hook for streaming.
diagnostics_channel3 (Stub)No-op channel(), tracingChannel(), Channel constructor; channels always report hasSubscribers: false; publish, subscribe, unsubscribe are no-ops. Provides Fastify compatibility.
Deferred modules (net, tls, readline, perf_hooks, worker_threads)4 (Deferred)require() returns stubs; APIs throw deterministic unsupported errors when called.
Unsupported modules (dgram, cluster, wasi, inspector, repl, trace_events, domain)5 (Unsupported)require() fails immediately with deterministic unsupported-module errors.

ESM Execution Notes

  • exec(code, { filePath }) honors Node-style ESM classification for .mjs entrypoints and .js files under package.json "type": "module".
  • ESM package resolution uses import conditions, while require() and createRequire() keep require condition semantics in the same execution.
  • Built-in ESM imports such as node:fs and node:path support both default exports and named exports for supported APIs.
  • Dynamic import() preserves underlying missing-module, syntax, and evaluation failures instead of collapsing them into successful execution results.

Permanently Unsupported Features

Some Node.js features cannot be supported in secure-exec due to fundamental architectural constraints of the sandboxed V8 isolate. These are not planned for implementation.

Modules

ModuleReason
clusterRequires multi-process coordination with shared server handles and IPC. The sandbox runs a single V8 isolate — there is no process table to fork into.
worker_threadsRequires spawning OS threads with separate V8 isolates and shared memory (SharedArrayBuffer). The sandbox architecture is single-isolate-per-session.
vmvm.createContext() and vm.runInNewContext() require creating isolated V8 contexts within the same isolate. The sandbox provides one context. vm.runInThisContext() works (equivalent to eval).
inspectorExposes the V8 debugger protocol (breakpoints, heap snapshots, CPU profiling). This is a security surface that allows arbitrary code introspection and cannot be exposed in a sandbox.
domainDeprecated since Node.js v4 and removed from documentation. Not worth implementing — use async_hooks or structured error handling instead.
replInteractive read-eval-print loop requiring full terminal integration, tab completion, and command history. Not meaningful in a sandboxed execution context.
trace_eventsRequires V8 tracing infrastructure and file-based trace log output. Not available in the sandboxed isolate.
wasiWASI (WebAssembly System Interface) requires a separate WASM runtime within the V8 isolate. Not applicable — secure-exec IS the sandbox.

APIs

APIReason
child_process.fork()Creates a new Node.js process with IPC channel. Requires a real node binary and multi-process architecture. spawn/exec/execFile ARE supported.
process.execPathReturns path to the Node.js binary. The sandbox has no node binary on disk — code runs in an embedded V8 isolate, not a Node.js process.
V8 CLI flags (--expose-gc, --expose-internals, --harmony-*)The V8 isolate is pre-configured at sandbox creation time. Runtime flag injection is a security risk. --expose-gc may be supported in the future as a bridge call.
V8 snapshot APIs (v8.writeHeapSnapshot, v8.startupSnapshot)Require direct V8 C++ API access for heap serialization. Not exposed through the bridge.
Native addons (.node files)Require loading compiled shared libraries (.so/.dylib/.dll) into the process. The sandbox does not permit native code execution.

Behaviors

BehaviorReason
Real OS signals (SIGTERM, SIGUSR1, etc.)The sandbox is not an OS process — it’s a V8 isolate within a host process. There are no real POSIX signals to deliver. process.on('SIGINT') may be emulated in the future.
Real file system watchers (fs.watch, fs.watchFile, fs.promises.watch)The VFS (virtual file system) has no inotify/kqueue/FSEvents-equivalent primitive. These APIs fail fast with deterministic unsupported errors instead of hanging while waiting for events that the sandbox cannot produce.
Multi-context executionThe sandbox runs one V8 context per isolate. Features requiring context isolation (ShadowRealm, vm.createContext) cannot work.
QUIC protocolExperimental in Node.js, depends on tls + net + OpenSSL QUIC support. Not planned.

Tested Packages

The project-matrix test suite validates that real-world npm packages produce identical output in secure-exec and host Node.js. Each fixture is a black-box Node project with no sandbox-specific code.
PackageCategoryWhat It Tests
expressWeb FrameworkHTTP server, middleware, routing
fastifyWeb FrameworkAsync middleware, schema validation, plugins
nextWeb FrameworkReact SSR, module resolution, build tooling
viteBuild ToolESM, HMR server, plugin system
astroWeb FrameworkIsland architecture, SSR, multi-framework
honoWeb FrameworkESM imports, lightweight HTTP
axiosHTTP ClientHTTP client requests via fetch adapter, JSON APIs
node-fetchHTTP ClientFetch polyfill using http module, stream piping
dotenvConfigurationEnvironment variable loading, fs reads
semverUtilityVersion parsing and comparison
ssh2NetworkingSSH client/server, crypto, streams, events
ssh2-sftp-clientNetworkingSFTP client, file transfer APIs over SSH
pgDatabasePostgreSQL client, Pool/Client classes, type parsers
mysql2DatabaseMySQL client, connection/pool classes, escape/format utilities
ioredisDatabaseRedis client, Cluster, Command, pipeline/multi transaction APIs
drizzle-ormDatabaseORM schema definition, query building, ESM module graph
wsNetworkingWebSocket client/server, HTTP upgrade, events
jsonwebtokenCryptoJWT signing (HS256), verification, decode
bcryptjsCryptoPure JS password hashing and verification
chalkTerminalTerminal string styling, ANSI escape codes
lodash-esUtilityLarge ESM module resolution at scale
pinoLoggingStructured JSON logging, child loggers, serializers
uuidCryptoUUID generation (v4, v5), validation, version detection
yamlUtilityYAML parsing, stringifying, document API
zodValidationSchema definition, parsing, safe parse, transforms
rivetkitSDKLocal vendor package resolution
crypto (builtin)Cryptocrypto.randomBytes, randomUUID, getRandomValues
fs-metadata-renameFilesystemstat metadata, rename semantics
module-accessModule ResolutionNode modules overlay access
conditional-exportsModule ResolutionPackage exports field resolution
optional-depsModule ResolutionOptional dependency handling
peer-depsModule ResolutionPeer dependency resolution
transitive-depsModule ResolutionTransitive dependency chains
pnpm-layoutPackage Managerpnpm node_modules structure
npm-layoutPackage Managernpm flat node_modules layout
yarn-classic-layoutPackage ManagerYarn v1 node_modules layout
yarn-berry-layoutPackage ManagerYarn Berry PnP/node_modules layout
bun-layoutPackage ManagerBun node_modules layout
workspace-layoutPackage Managernpm workspace node_modules layout
sse-streamingNetworkingSSE server, chunked transfer-encoding, streaming reads
net-unsupported (fail)Error Handlingnet.createServer correctly errors
To request a new package be added to the test suite, open an issue.

Logging Behavior

  • console.log/warn/error are supported and serialize arguments with circular-safe bounded formatting.
  • exec()/run() results do not expose buffered stdout/stderr fields.
  • By default, secure-exec drops console emissions instead of buffering runtime-managed output.
  • Consumers that need logs should use the explicit onStdio hook to stream stdout/stderr events in emission order.

TypeScript Workflows

  • Core secure-exec runtimes execute JavaScript only.
  • Sandboxed TypeScript type checking and compilation belong in the separate @secure-exec/typescript package.

Node-Modules Overlay Behavior

  • Node runtime composes a read-only /app/node_modules overlay from <cwd>/node_modules (default cwd is host process.cwd(), configurable via moduleAccess.cwd).
  • Overlay reads are constrained to canonical paths under <cwd>/node_modules and fail closed on out-of-scope symlink/canonical escapes.
  • Writes and mutations under /app/node_modules/** are denied with EACCES.
  • Native addons (.node) are rejected in overlay-backed module loading.

Permission Model (Runtime/Bridge Scope)

  • This section describes the core runtime/bridge contract only.
  • Runtime permissions are deny-by-default for fs, network, childProcess, and env.
  • If a domain checker is not configured, operations fail with EACCES.
  • filterEnv strips environment variables unless permissions.env explicitly allows them.
  • Embedders can opt in via explicit permission policies such as allowAll, allowAllFs, allowAllNetwork, allowAllChildProcess, and allowAllEnv.
  • Driver-specific convenience defaults (for example, direct createNodeDriver(...) usage when adapters are provided without an explicit permissions policy) are implementation details and are not the canonical runtime/bridge security contract.