Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .agent/contracts/node-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,44 @@ Bridge-provided randomness for global `crypto` APIs MUST delegate to host `node:
- **WHEN** host `node:crypto` randomness primitives are unavailable or fail
- **THEN** the bridge MUST throw a deterministic error matching the unsupported API format (`"<module>.<api> is not supported in sandbox"`) for the invoked randomness API and MUST NOT fall back to non-cryptographic randomness

### Requirement: Global WebCrypto Surface Matches The `crypto.webcrypto` Bridge
The bridge SHALL expose a single WebCrypto surface so global `crypto` APIs and `require('crypto').webcrypto` share the same object graph and constructor semantics.

#### Scenario: Sandboxed code compares global and module WebCrypto objects
- **WHEN** sandboxed code reads both `globalThis.crypto` and `require('crypto').webcrypto`
- **THEN** those references MUST point at the same WebCrypto object
- **AND** `crypto.subtle` MUST expose the same `SubtleCrypto` instance through both paths

#### Scenario: WebCrypto constructors stay non-user-constructible
- **WHEN** sandboxed code calls `new Crypto()`, `new SubtleCrypto()`, or `new CryptoKey()`
- **THEN** the bridge MUST throw a Node-compatible illegal-constructor `TypeError`
- **AND** prototype method receiver validation MUST reject detached calls with `ERR_INVALID_THIS`

### Requirement: Diffie-Hellman And ECDH Bridge Uses Host Node Crypto Objects
Bridge-provided `crypto` Diffie-Hellman and ECDH APIs SHALL delegate to host `node:crypto` objects so constructor validation, session state, encodings, and shared-secret derivation match Node.js semantics.

#### Scenario: Sandbox creates a Diffie-Hellman session
- **WHEN** sandboxed code calls `crypto.createDiffieHellman(...)`, `crypto.getDiffieHellman(...)`, or `crypto.createECDH(...)`
- **THEN** the bridge MUST construct the corresponding host `node:crypto` object
- **AND** subsequent method calls such as `generateKeys()`, `computeSecret()`, `getPublicKey()`, and `setPrivateKey()` MUST execute against that host object rather than an isolate-local reimplementation

#### Scenario: Sandbox uses stateless crypto.diffieHellman
- **WHEN** sandboxed code calls `crypto.diffieHellman({ privateKey, publicKey })`
- **THEN** the bridge MUST delegate to host `node:crypto.diffieHellman`
- **AND** the returned shared secret and thrown validation errors MUST preserve Node-compatible behavior

### Requirement: Crypto Stream Wrappers Preserve Transform Semantics And Validation Errors
Bridge-backed `crypto` hash and cipher wrappers SHALL remain compatible with Node stream semantics and MUST preserve Node-style validation error codes for callback-driven APIs.

#### Scenario: Sandbox hashes or encrypts data through stream piping
- **WHEN** sandboxed code uses `crypto.Hash`, `crypto.Cipheriv`, or `crypto.Decipheriv` as stream destinations or sources
- **THEN** those objects MUST be `stream.Transform` instances
- **AND** piping data through them MUST emit the same digest or ciphertext/plaintext bytes that the corresponding direct `update()`/`final()` calls would produce

#### Scenario: Sandbox calls pbkdf2 with invalid arguments
- **WHEN** sandboxed code calls `crypto.pbkdf2()` or `crypto.pbkdf2Sync()` with invalid callback, digest, password, salt, iteration, or key length arguments
- **THEN** the bridge MUST throw or surface Node-compatible `ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE` errors instead of plain untyped exceptions

### Requirement: Bridge FS Open Flag Translation Uses Named Constants
The bridge `fs` implementation MUST express string-flag translation using named open-flag constants (for example `O_WRONLY | O_CREAT | O_TRUNC`) aligned with Node `fs.constants` semantics, and MUST NOT rely on undocumented numeric literals.

Expand Down Expand Up @@ -160,3 +198,29 @@ The bridge global key registry consumed by host runtime setup, bridge modules, a
#### Scenario: Native V8 bridge registries stay aligned with async and sync lifecycle hooks
- **WHEN** bridge modules depend on a host bridge global via async `.apply(..., { result: { promise: true } })` or sync `.applySync(...)` semantics
- **THEN** the native V8 bridge function registries MUST expose a matching callable shape for that global (or an equivalent tested shim), and automated verification MUST cover the registry alignment

### Requirement: Dispatch-Multiplexed Bridge Errors Preserve Structured Metadata
Bridge globals routed through the `_loadPolyfill` dispatch multiplexer SHALL preserve host error metadata needed for Node-compatible assertions.

#### Scenario: Host bridge throws typed crypto validation error
- **WHEN** a dispatch-multiplexed bridge handler throws a host error with `name` and `code` (for example `TypeError` + `ERR_INVALID_ARG_VALUE`)
- **THEN** the sandbox-visible error MUST preserve that `name` and `code`
- **AND** the bridge MUST NOT collapse the error to a plain `Error` with only a message

### Requirement: HTTP Agent Bridge Preserves Node Pooling Semantics
Bridge-provided `http.Agent` behavior SHALL preserve the observable pooling state that Node.js userland and conformance tests inspect.

#### Scenario: Sandboxed code inspects agent bookkeeping
- **WHEN** sandboxed code uses `http.Agent` or `require('_http_agent').Agent`
- **THEN** the bridge MUST expose matching `Agent` constructors through both module paths
- **AND** `getName()`, `requests`, `sockets`, `freeSockets`, and `totalSocketCount` MUST reflect request queueing and socket reuse state with Node-compatible key shapes

#### Scenario: Keepalive sockets are reused or discarded
- **WHEN** sandboxed code enables `keepAlive` and reuses pooled HTTP connections
- **THEN** the bridge MUST mark reused requests via `request.reusedSocket`
- **AND** destroyed or remotely closed sockets MUST be removed from the pool instead of being reassigned to queued requests

#### Scenario: Total socket limits are configured
- **WHEN** sandboxed code constructs an `http.Agent` with `maxSockets`, `maxFreeSockets`, or `maxTotalSockets`
- **THEN** invalid argument types and ranges MUST throw Node-compatible `ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE` errors
- **AND** queued requests across origins MUST respect both per-origin and total socket limits
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

- use pnpm, vitest, and tsc for type checks
- use turbo for builds
- after changing `packages/core/isolate-runtime/src/inject/require-setup.ts` or Node bridge code that regenerates the isolate bundle, rebuild in this order: `pnpm --filter @secure-exec/nodejs build` then `pnpm --filter @secure-exec/core build`; the conformance runner executes built `dist` output, not just source files
- keep timeouts under 1 minute and avoid running full test suites unless necessary
- use one-line Conventional Commit messages; never add any co-authors (including agents)
- never mark work complete until typechecks pass and all tests pass in the current turn; if they fail, report the failing command and first concrete error
Expand Down Expand Up @@ -198,6 +199,11 @@ Follow the style in `packages/secure-exec/src/index.ts`.

## Documentation

- all public-facing docs (quickstart, guides, API reference, landing page, README) must focus on the **Node.js runtime** as the primary and default experience — do not lead with WasmVM, kernel internals, or multi-runtime concepts
- code examples in docs should use the `NodeRuntime` API (`runtime.run()`, `runtime.exec()`) as the default path; the kernel API (`createKernel`, `kernel.spawn()`) is for advanced multi-process use cases and should be presented as secondary
- keep documentation pages and their runnable example sources in sync: `docs/quickstart.mdx` must match `examples/kitchen-sink/src/`, and `docs/features/*.mdx` must match `examples/features/src/`
- when updating a doc snippet, update the corresponding example file and the docs/example verification scripts in the same change
- when converting runnable example code into documentation snippets, use public package imports like `from "secure-exec"` and `from "@secure-exec/typescript"` instead of repo-local source paths
- WasmVM and Python docs are experimental docs and must stay grouped under the `Experimental` section in `docs/docs.json`
- docs pages that must stay current with API changes:
- `docs/quickstart.mdx` — update when core setup flow changes
Expand Down
128 changes: 128 additions & 0 deletions docs-internal/arch/overview.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,133 @@
# Architecture Overview

## Architectural Model: Inverted VM

Traditional virtual machines (Firecracker, QEMU) place the OS **inside** the VM — a hypervisor
virtualizes hardware, and a guest kernel (Linux) runs on top:

```
Traditional: VM contains OS

┌────────────────────┐
│ Hypervisor / VM │ (Firecracker, QEMU, KVM)
│ │
│ ┌──────────────┐ │
│ │ Guest OS │ │ (Linux kernel)
│ │ │ │
│ │ ┌────────┐ │ │
│ │ │ Apps │ │ │ (ELF binaries)
│ │ └────────┘ │ │
│ └──────────────┘ │
└────────────────────┘
```

Secure Exec inverts this: the OS is the **outer** layer, and execution engines (V8, WASM) are
plugged **into** it. The kernel runs in the host process and mediates all I/O. There is no
hypervisor — the isolation boundary is the V8 isolate and WASM sandbox, not hardware virtualization:

```
Secure Exec: OS contains VMs

┌──────────────────────────────────────────────┐
│ Virtual OS (packages/core/kernel/) │
│ VFS, process table, FD table, │
│ sockets, pipes, signals, permissions │
│ │
│ ┌─────────────────┐ ┌───────────────────┐ │
│ │ V8 Isolate │ │ WASM Runtime │ │
│ │ (Node.js) │ │ (V8 WebAssembly)│ │
│ │ │ │ │ │
│ │ JS scripts │ │ POSIX binaries │ │
│ └─────────────────┘ └───────────────────┘ │
└──────────────────────────────────────────────┘
```

### Comparison: Containers vs MicroVMs vs Secure Exec

```
Container (Docker)

┌───────────────────────────────────────────┐
│ Host Linux Kernel (shared) │
│ │
│ ┌─────────────────┐ ┌────────────────┐ │
│ │ Namespace + │ │ Namespace + │ │
│ │ cgroup jail │ │ cgroup jail │ │
│ │ │ │ │ │
│ │ ┌───────────┐ │ │ ┌──────────┐ │ │
│ │ │ App 1 │ │ │ │ App 2 │ │ │
│ │ │ ELF bins │ │ │ │ ELF bins │ │ │
│ │ └───────────┘ │ │ └──────────┘ │ │
│ └─────────────────┘ └────────────────┘ │
└───────────────────────────────────────────┘
Kernel is shared. Kernel vuln = all containers escape.


MicroVM (Firecracker)

┌───────────────────────────────────────────┐
│ Host Linux Kernel │
│ │
│ ┌─────────────────┐ ┌────────────────┐ │
│ │ KVM / VT-x │ │ KVM / VT-x │ │
│ │ (hypervisor) │ │ (hypervisor) │ │
│ │ │ │ │ │
│ │ ┌────────────┐ │ │ ┌──────────┐ │ │
│ │ │ Guest │ │ │ │ Guest │ │ │
│ │ │ Linux │ │ │ │ Linux │ │ │
│ │ │ Kernel │ │ │ │ Kernel │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌──────┐ │ │ │ │ ┌──────┐ │ │ │
│ │ │ │ App │ │ │ │ │ │ App │ │ │ │
│ │ │ └──────┘ │ │ │ │ └──────┘ │ │ │
│ │ └────────────┘ │ │ └──────────┘ │ │
│ └─────────────────┘ └────────────────┘ │
└───────────────────────────────────────────┘
Each VM has its own kernel. Hypervisor vuln = escape.


Secure Exec

┌───────────────────────────────────────────┐
│ Host Process (Node.js / Browser) │
│ │
│ ┌─────────────────┐ ┌────────────────┐ │
│ │ Virtual OS │ │ Virtual OS │ │
│ │ (SEOS kernel) │ │ (SEOS kernel) │ │
│ │ │ │ │ │
│ │ ┌────────────┐ │ │ ┌──────────┐ │ │
│ │ │ V8 / WASM │ │ │ │ V8 / WASM│ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ JS / WASM │ │ │ │ JS / WASM│ │ │
│ │ │ programs │ │ │ │ programs │ │ │
│ │ └────────────┘ │ │ └──────────┘ │ │
│ └─────────────────┘ └────────────────┘ │
└───────────────────────────────────────────┘
Each instance has its own kernel. V8/WASM vuln = escape.
```

| | Container | MicroVM | Secure Exec |
|--------------------|----------------------|-----------------------|-----------------------|
| **Isolation** | Namespaces + cgroups | Hardware (VT-x/KVM) | V8 isolate + WASM |
| **Kernel** | Shared host kernel | Dedicated guest kernel| Virtual POSIX kernel |
| **Attack surface** | Host kernel syscalls | Hypervisor interface | JS/WASM sandbox |
| **Boot time** | ~100ms | ~125ms | <5ms |
| **Overhead** | Near-native | ~3-5% CPU/memory | V8/WASM overhead |
| **Runs in browser**| No | No | Yes |
| **Guest format** | ELF binaries | ELF binaries | JS scripts + WASM |
| **Escape risk** | Kernel vuln = escape | Hypervisor vuln = escape | V8 vuln = escape |

Key architectural differences:
- **Containers** share the host kernel — a kernel vulnerability lets every container escape. The kernel is the trust boundary and the attack surface simultaneously.
- **MicroVMs** run a dedicated guest kernel inside hardware virtualization. Stronger isolation (hypervisor boundary), but 100ms+ boot time and no browser support.
- **Secure Exec** provides its own virtual kernel in userspace. No shared kernel attack surface (the virtual kernel is per-instance), no hardware requirements, millisecond boot. The tradeoff is that isolation depends on V8/WASM sandbox correctness rather than hardware enforcement.

The WASI extensions (`native/wasmvm/crates/wasi-ext/`) bridge WASM syscalls into the OS kernel.
The Node.js bridge (`packages/nodejs/src/bridge/`) does the same for V8 isolate code. Both are
thin translation layers — the real implementation lives in the kernel.

## Package Map

```
Kernel-first API (createKernel + mount + exec)
packages/core/
Expand Down
4 changes: 4 additions & 0 deletions docs-internal/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Priority order is:

---

## Rename

- [ ] Rename `wasmvm` back to `seos` (Secure Exec OS) across the codebase (packages, directories, imports, docs)

docs-internal/proposal-kernel-consolidation.md
docs-internal/specs/custom-bindings.md
docs-internal/specs/cli-tool-e2e.md
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ createTypeScriptTools(options: TypeScriptToolsOptions)
| `runtimeDriverFactory` | `NodeRuntimeDriverFactory` | Creates the compiler sandbox runtime. |
| `memoryLimit` | `number` | Compiler sandbox isolate memory cap in MB. Default `512`. |
| `cpuTimeLimitMs` | `number` | Compiler sandbox CPU time budget in ms. |
| `compilerSpecifier` | `string` | Module specifier used to load the TypeScript compiler. Default `"/root/node_modules/typescript/lib/typescript.js"`. |
| `compilerSpecifier` | `string` | Module specifier used to load the TypeScript compiler. Default `"typescript"`. |

**Methods**

Expand Down
8 changes: 4 additions & 4 deletions docs/features/child-processes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ Sandboxed code can spawn child processes through the `CommandExecutor` interface

## Runnable example

Source file: `examples/features/src/child-processes.ts`

```ts
import {
NodeRuntime,
allowAllChildProcess,
createNodeDriver,
createNodeRuntimeDriverFactory,
} from "../../../packages/secure-exec/src/index.ts";
import type { CommandExecutor } from "../../../packages/secure-exec/src/types.ts";
} from "secure-exec";
import type { CommandExecutor } from "secure-exec";
import { spawn } from "node:child_process";

const commandExecutor: CommandExecutor = {
Expand Down Expand Up @@ -98,8 +100,6 @@ try {
}
```

Source: [examples/features/src/child-processes.ts](https://github.com/rivet-dev/secure-exec/blob/main/examples/features/src/child-processes.ts)

## Permission gating

Restrict which commands sandboxed code can spawn:
Expand Down
6 changes: 3 additions & 3 deletions docs/features/filesystem.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ secure-exec supports three filesystem backends. The system driver controls which

## Runnable example

Source file: `examples/features/src/filesystem.ts`

```ts
import {
NodeRuntime,
allowAllFs,
createInMemoryFileSystem,
createNodeDriver,
createNodeRuntimeDriverFactory,
} from "../../../packages/secure-exec/src/index.ts";
} from "secure-exec";

const filesystem = createInMemoryFileSystem();
const runtime = new NodeRuntime({
Expand Down Expand Up @@ -55,8 +57,6 @@ try {
}
```

Source: [examples/features/src/filesystem.ts](https://github.com/rivet-dev/secure-exec/blob/main/examples/features/src/filesystem.ts)

## OPFS (browser)

Persistent filesystem using the Origin Private File System API. This is the default for `createBrowserDriver()`.
Expand Down
Loading
Loading