Skip to content

Commit 786c9db

Browse files
authored
Merge pull request #52 from rivet-dev/ralph/us-010-http-agent-compat
feat: [US-010] improve http.Agent bridge compatibility
2 parents a2cd01a + c7b4997 commit 786c9db

93 files changed

Lines changed: 10310 additions & 4719 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agent/contracts/node-bridge.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,44 @@ Bridge-provided randomness for global `crypto` APIs MUST delegate to host `node:
9494
- **WHEN** host `node:crypto` randomness primitives are unavailable or fail
9595
- **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
9696

97+
### Requirement: Global WebCrypto Surface Matches The `crypto.webcrypto` Bridge
98+
The bridge SHALL expose a single WebCrypto surface so global `crypto` APIs and `require('crypto').webcrypto` share the same object graph and constructor semantics.
99+
100+
#### Scenario: Sandboxed code compares global and module WebCrypto objects
101+
- **WHEN** sandboxed code reads both `globalThis.crypto` and `require('crypto').webcrypto`
102+
- **THEN** those references MUST point at the same WebCrypto object
103+
- **AND** `crypto.subtle` MUST expose the same `SubtleCrypto` instance through both paths
104+
105+
#### Scenario: WebCrypto constructors stay non-user-constructible
106+
- **WHEN** sandboxed code calls `new Crypto()`, `new SubtleCrypto()`, or `new CryptoKey()`
107+
- **THEN** the bridge MUST throw a Node-compatible illegal-constructor `TypeError`
108+
- **AND** prototype method receiver validation MUST reject detached calls with `ERR_INVALID_THIS`
109+
110+
### Requirement: Diffie-Hellman And ECDH Bridge Uses Host Node Crypto Objects
111+
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.
112+
113+
#### Scenario: Sandbox creates a Diffie-Hellman session
114+
- **WHEN** sandboxed code calls `crypto.createDiffieHellman(...)`, `crypto.getDiffieHellman(...)`, or `crypto.createECDH(...)`
115+
- **THEN** the bridge MUST construct the corresponding host `node:crypto` object
116+
- **AND** subsequent method calls such as `generateKeys()`, `computeSecret()`, `getPublicKey()`, and `setPrivateKey()` MUST execute against that host object rather than an isolate-local reimplementation
117+
118+
#### Scenario: Sandbox uses stateless crypto.diffieHellman
119+
- **WHEN** sandboxed code calls `crypto.diffieHellman({ privateKey, publicKey })`
120+
- **THEN** the bridge MUST delegate to host `node:crypto.diffieHellman`
121+
- **AND** the returned shared secret and thrown validation errors MUST preserve Node-compatible behavior
122+
123+
### Requirement: Crypto Stream Wrappers Preserve Transform Semantics And Validation Errors
124+
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.
125+
126+
#### Scenario: Sandbox hashes or encrypts data through stream piping
127+
- **WHEN** sandboxed code uses `crypto.Hash`, `crypto.Cipheriv`, or `crypto.Decipheriv` as stream destinations or sources
128+
- **THEN** those objects MUST be `stream.Transform` instances
129+
- **AND** piping data through them MUST emit the same digest or ciphertext/plaintext bytes that the corresponding direct `update()`/`final()` calls would produce
130+
131+
#### Scenario: Sandbox calls pbkdf2 with invalid arguments
132+
- **WHEN** sandboxed code calls `crypto.pbkdf2()` or `crypto.pbkdf2Sync()` with invalid callback, digest, password, salt, iteration, or key length arguments
133+
- **THEN** the bridge MUST throw or surface Node-compatible `ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE` errors instead of plain untyped exceptions
134+
97135
### Requirement: Bridge FS Open Flag Translation Uses Named Constants
98136
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.
99137

@@ -160,3 +198,29 @@ The bridge global key registry consumed by host runtime setup, bridge modules, a
160198
#### Scenario: Native V8 bridge registries stay aligned with async and sync lifecycle hooks
161199
- **WHEN** bridge modules depend on a host bridge global via async `.apply(..., { result: { promise: true } })` or sync `.applySync(...)` semantics
162200
- **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
201+
202+
### Requirement: Dispatch-Multiplexed Bridge Errors Preserve Structured Metadata
203+
Bridge globals routed through the `_loadPolyfill` dispatch multiplexer SHALL preserve host error metadata needed for Node-compatible assertions.
204+
205+
#### Scenario: Host bridge throws typed crypto validation error
206+
- **WHEN** a dispatch-multiplexed bridge handler throws a host error with `name` and `code` (for example `TypeError` + `ERR_INVALID_ARG_VALUE`)
207+
- **THEN** the sandbox-visible error MUST preserve that `name` and `code`
208+
- **AND** the bridge MUST NOT collapse the error to a plain `Error` with only a message
209+
210+
### Requirement: HTTP Agent Bridge Preserves Node Pooling Semantics
211+
Bridge-provided `http.Agent` behavior SHALL preserve the observable pooling state that Node.js userland and conformance tests inspect.
212+
213+
#### Scenario: Sandboxed code inspects agent bookkeeping
214+
- **WHEN** sandboxed code uses `http.Agent` or `require('_http_agent').Agent`
215+
- **THEN** the bridge MUST expose matching `Agent` constructors through both module paths
216+
- **AND** `getName()`, `requests`, `sockets`, `freeSockets`, and `totalSocketCount` MUST reflect request queueing and socket reuse state with Node-compatible key shapes
217+
218+
#### Scenario: Keepalive sockets are reused or discarded
219+
- **WHEN** sandboxed code enables `keepAlive` and reuses pooled HTTP connections
220+
- **THEN** the bridge MUST mark reused requests via `request.reusedSocket`
221+
- **AND** destroyed or remotely closed sockets MUST be removed from the pool instead of being reassigned to queued requests
222+
223+
#### Scenario: Total socket limits are configured
224+
- **WHEN** sandboxed code constructs an `http.Agent` with `maxSockets`, `maxFreeSockets`, or `maxTotalSockets`
225+
- **THEN** invalid argument types and ranges MUST throw Node-compatible `ERR_INVALID_ARG_TYPE` / `ERR_OUT_OF_RANGE` errors
226+
- **AND** queued requests across origins MUST respect both per-origin and total socket limits

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
- use pnpm, vitest, and tsc for type checks
5252
- use turbo for builds
53+
- 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
5354
- keep timeouts under 1 minute and avoid running full test suites unless necessary
5455
- use one-line Conventional Commit messages; never add any co-authors (including agents)
5556
- 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
@@ -198,6 +199,11 @@ Follow the style in `packages/secure-exec/src/index.ts`.
198199

199200
## Documentation
200201

202+
- 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
203+
- 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
204+
- 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/`
205+
- when updating a doc snippet, update the corresponding example file and the docs/example verification scripts in the same change
206+
- 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
201207
- WasmVM and Python docs are experimental docs and must stay grouped under the `Experimental` section in `docs/docs.json`
202208
- docs pages that must stay current with API changes:
203209
- `docs/quickstart.mdx` — update when core setup flow changes

docs-internal/arch/overview.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,133 @@
11
# Architecture Overview
22

3+
## Architectural Model: Inverted VM
4+
5+
Traditional virtual machines (Firecracker, QEMU) place the OS **inside** the VM — a hypervisor
6+
virtualizes hardware, and a guest kernel (Linux) runs on top:
7+
8+
```
9+
Traditional: VM contains OS
10+
11+
┌────────────────────┐
12+
│ Hypervisor / VM │ (Firecracker, QEMU, KVM)
13+
│ │
14+
│ ┌──────────────┐ │
15+
│ │ Guest OS │ │ (Linux kernel)
16+
│ │ │ │
17+
│ │ ┌────────┐ │ │
18+
│ │ │ Apps │ │ │ (ELF binaries)
19+
│ │ └────────┘ │ │
20+
│ └──────────────┘ │
21+
└────────────────────┘
22+
```
23+
24+
Secure Exec inverts this: the OS is the **outer** layer, and execution engines (V8, WASM) are
25+
plugged **into** it. The kernel runs in the host process and mediates all I/O. There is no
26+
hypervisor — the isolation boundary is the V8 isolate and WASM sandbox, not hardware virtualization:
27+
28+
```
29+
Secure Exec: OS contains VMs
30+
31+
┌──────────────────────────────────────────────┐
32+
│ Virtual OS (packages/core/kernel/) │
33+
│ VFS, process table, FD table, │
34+
│ sockets, pipes, signals, permissions │
35+
│ │
36+
│ ┌─────────────────┐ ┌───────────────────┐ │
37+
│ │ V8 Isolate │ │ WASM Runtime │ │
38+
│ │ (Node.js) │ │ (V8 WebAssembly)│ │
39+
│ │ │ │ │ │
40+
│ │ JS scripts │ │ POSIX binaries │ │
41+
│ └─────────────────┘ └───────────────────┘ │
42+
└──────────────────────────────────────────────┘
43+
```
44+
45+
### Comparison: Containers vs MicroVMs vs Secure Exec
46+
47+
```
48+
Container (Docker)
49+
50+
┌───────────────────────────────────────────┐
51+
│ Host Linux Kernel (shared) │
52+
│ │
53+
│ ┌─────────────────┐ ┌────────────────┐ │
54+
│ │ Namespace + │ │ Namespace + │ │
55+
│ │ cgroup jail │ │ cgroup jail │ │
56+
│ │ │ │ │ │
57+
│ │ ┌───────────┐ │ │ ┌──────────┐ │ │
58+
│ │ │ App 1 │ │ │ │ App 2 │ │ │
59+
│ │ │ ELF bins │ │ │ │ ELF bins │ │ │
60+
│ │ └───────────┘ │ │ └──────────┘ │ │
61+
│ └─────────────────┘ └────────────────┘ │
62+
└───────────────────────────────────────────┘
63+
Kernel is shared. Kernel vuln = all containers escape.
64+
65+
66+
MicroVM (Firecracker)
67+
68+
┌───────────────────────────────────────────┐
69+
│ Host Linux Kernel │
70+
│ │
71+
│ ┌─────────────────┐ ┌────────────────┐ │
72+
│ │ KVM / VT-x │ │ KVM / VT-x │ │
73+
│ │ (hypervisor) │ │ (hypervisor) │ │
74+
│ │ │ │ │ │
75+
│ │ ┌────────────┐ │ │ ┌──────────┐ │ │
76+
│ │ │ Guest │ │ │ │ Guest │ │ │
77+
│ │ │ Linux │ │ │ │ Linux │ │ │
78+
│ │ │ Kernel │ │ │ │ Kernel │ │ │
79+
│ │ │ │ │ │ │ │ │ │
80+
│ │ │ ┌──────┐ │ │ │ │ ┌──────┐ │ │ │
81+
│ │ │ │ App │ │ │ │ │ │ App │ │ │ │
82+
│ │ │ └──────┘ │ │ │ │ └──────┘ │ │ │
83+
│ │ └────────────┘ │ │ └──────────┘ │ │
84+
│ └─────────────────┘ └────────────────┘ │
85+
└───────────────────────────────────────────┘
86+
Each VM has its own kernel. Hypervisor vuln = escape.
87+
88+
89+
Secure Exec
90+
91+
┌───────────────────────────────────────────┐
92+
│ Host Process (Node.js / Browser) │
93+
│ │
94+
│ ┌─────────────────┐ ┌────────────────┐ │
95+
│ │ Virtual OS │ │ Virtual OS │ │
96+
│ │ (SEOS kernel) │ │ (SEOS kernel) │ │
97+
│ │ │ │ │ │
98+
│ │ ┌────────────┐ │ │ ┌──────────┐ │ │
99+
│ │ │ V8 / WASM │ │ │ │ V8 / WASM│ │ │
100+
│ │ │ │ │ │ │ │ │ │
101+
│ │ │ JS / WASM │ │ │ │ JS / WASM│ │ │
102+
│ │ │ programs │ │ │ │ programs │ │ │
103+
│ │ └────────────┘ │ │ └──────────┘ │ │
104+
│ └─────────────────┘ └────────────────┘ │
105+
└───────────────────────────────────────────┘
106+
Each instance has its own kernel. V8/WASM vuln = escape.
107+
```
108+
109+
| | Container | MicroVM | Secure Exec |
110+
|--------------------|----------------------|-----------------------|-----------------------|
111+
| **Isolation** | Namespaces + cgroups | Hardware (VT-x/KVM) | V8 isolate + WASM |
112+
| **Kernel** | Shared host kernel | Dedicated guest kernel| Virtual POSIX kernel |
113+
| **Attack surface** | Host kernel syscalls | Hypervisor interface | JS/WASM sandbox |
114+
| **Boot time** | ~100ms | ~125ms | <5ms |
115+
| **Overhead** | Near-native | ~3-5% CPU/memory | V8/WASM overhead |
116+
| **Runs in browser**| No | No | Yes |
117+
| **Guest format** | ELF binaries | ELF binaries | JS scripts + WASM |
118+
| **Escape risk** | Kernel vuln = escape | Hypervisor vuln = escape | V8 vuln = escape |
119+
120+
Key architectural differences:
121+
- **Containers** share the host kernel — a kernel vulnerability lets every container escape. The kernel is the trust boundary and the attack surface simultaneously.
122+
- **MicroVMs** run a dedicated guest kernel inside hardware virtualization. Stronger isolation (hypervisor boundary), but 100ms+ boot time and no browser support.
123+
- **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.
124+
125+
The WASI extensions (`native/wasmvm/crates/wasi-ext/`) bridge WASM syscalls into the OS kernel.
126+
The Node.js bridge (`packages/nodejs/src/bridge/`) does the same for V8 isolate code. Both are
127+
thin translation layers — the real implementation lives in the kernel.
128+
129+
## Package Map
130+
3131
```
4132
Kernel-first API (createKernel + mount + exec)
5133
packages/core/

docs-internal/todo.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Priority order is:
1212

1313
---
1414

15+
## Rename
16+
17+
- [ ] Rename `wasmvm` back to `seos` (Secure Exec OS) across the codebase (packages, directories, imports, docs)
18+
1519
docs-internal/proposal-kernel-consolidation.md
1620
docs-internal/specs/custom-bindings.md
1721
docs-internal/specs/cli-tool-e2e.md

docs/api-reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ createTypeScriptTools(options: TypeScriptToolsOptions)
136136
| `runtimeDriverFactory` | `NodeRuntimeDriverFactory` | Creates the compiler sandbox runtime. |
137137
| `memoryLimit` | `number` | Compiler sandbox isolate memory cap in MB. Default `512`. |
138138
| `cpuTimeLimitMs` | `number` | Compiler sandbox CPU time budget in ms. |
139-
| `compilerSpecifier` | `string` | Module specifier used to load the TypeScript compiler. Default `"/root/node_modules/typescript/lib/typescript.js"`. |
139+
| `compilerSpecifier` | `string` | Module specifier used to load the TypeScript compiler. Default `"typescript"`. |
140140

141141
**Methods**
142142

docs/features/child-processes.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ Sandboxed code can spawn child processes through the `CommandExecutor` interface
1212

1313
## Runnable example
1414

15+
Source file: `examples/features/src/child-processes.ts`
16+
1517
```ts
1618
import {
1719
NodeRuntime,
1820
allowAllChildProcess,
1921
createNodeDriver,
2022
createNodeRuntimeDriverFactory,
21-
} from "../../../packages/secure-exec/src/index.ts";
22-
import type { CommandExecutor } from "../../../packages/secure-exec/src/types.ts";
23+
} from "secure-exec";
24+
import type { CommandExecutor } from "secure-exec";
2325
import { spawn } from "node:child_process";
2426

2527
const commandExecutor: CommandExecutor = {
@@ -98,8 +100,6 @@ try {
98100
}
99101
```
100102

101-
Source: [examples/features/src/child-processes.ts](https://github.com/rivet-dev/secure-exec/blob/main/examples/features/src/child-processes.ts)
102-
103103
## Permission gating
104104

105105
Restrict which commands sandboxed code can spawn:

docs/features/filesystem.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ secure-exec supports three filesystem backends. The system driver controls which
1212

1313
## Runnable example
1414

15+
Source file: `examples/features/src/filesystem.ts`
16+
1517
```ts
1618
import {
1719
NodeRuntime,
1820
allowAllFs,
1921
createInMemoryFileSystem,
2022
createNodeDriver,
2123
createNodeRuntimeDriverFactory,
22-
} from "../../../packages/secure-exec/src/index.ts";
24+
} from "secure-exec";
2325

2426
const filesystem = createInMemoryFileSystem();
2527
const runtime = new NodeRuntime({
@@ -55,8 +57,6 @@ try {
5557
}
5658
```
5759

58-
Source: [examples/features/src/filesystem.ts](https://github.com/rivet-dev/secure-exec/blob/main/examples/features/src/filesystem.ts)
59-
6060
## OPFS (browser)
6161

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

0 commit comments

Comments
 (0)