You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: update IPC architecture for CBOR codec and Bun support
Update internal arch overview, IPC serialization spec, and public
architecture docs to reflect the current binary framing with
runtime-dependent payload codec (V8 ValueSerializer for Node.js,
CBOR for Bun). Add performance comparison table and rationale.
Copy file name to clipboardExpand all lines: docs-internal/specs/v8-ipc-serialization.md
+61-10Lines changed: 61 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,7 @@ The envelope must be parseable **without a V8 isolate** because the Rust connect
33
33
|**V8 ValueSerializer**|`v8::ValueSerializer`|`node:v8.deserialize()`| Native | All V8 types (Date, Map, Set, RegExp, Error, circular refs, typed arrays) | None (built into V8 and Node.js) | Zero manual type conversion |
34
34
| MessagePack via rmpv (current) |`v8_to_rmpv()` → `rmpv::encode`|`@msgpack/msgpack decode()`| Native bin format | Primitives, arrays, objects, Uint8Array only |`rmpv`, `@msgpack/msgpack`| 130 lines of manual V8 type walking |
35
35
| JSON |`serde_json`|`JSON.parse()`| No (needs base64) | Primitives, arrays, objects | None | +33% overhead on binary, loses undefined/NaN/Infinity |
36
-
| CBOR (RFC 8949) |`ciborium`|`cbor-x`| Native | Similar to MessagePack |`ciborium`, `cbor-x`|No advantage over msgpack, less ecosystem|
36
+
| CBOR (RFC 8949) |`ciborium`|`cbor-x`| Native | Similar to MessagePack |`ciborium`, `cbor-x`|Used as Bun fallback codec (2× faster encode than JSON, binary-native)|
37
37
| Protocol Buffers |`prost`|`protobufjs`| Native | Schema-defined only |`prost`, `protobufjs`, codegen | Overkill — bridge args have no fixed schema |
38
38
| FlatBuffers |`flatbuffers`|`flatbuffers`| Zero-copy reads | Schema-defined only |`flatbuffers`, codegen | Zero-copy reads are compelling but schema requirement kills it for arbitrary JS values |
39
39
| bincode / postcard |`bincode` / `postcard`| Custom JS decoder | Native | Rust serde types |`bincode` / `postcard`| No JS library — would need a hand-written decoder |
@@ -247,16 +247,67 @@ The connection handler only needs to read **byte 5 through 5+N** (the session_id
247
247
| Deserialize IPC envelope |`rmp_serde::from_slice` (parses msgpack map, matches field names) | Read fixed offsets from buffer | No deserialization library |
248
248
| Binary data (1MB file) | V8 → rmpv::Binary → msgpack bin → msgpack envelope bin | V8 → ValueSerializer (includes backing store) | One fewer copy |
249
249
250
-
## Bun compatibility
250
+
## Bun compatibility (implemented)
251
251
252
-
Bun supports `v8.serialize()` / `v8.deserialize()` as a compatibility shim over JSC's serialization. The V8 wire format is a de facto standard. If a future host runtime doesn't support it, the binary header format allows swapping the payload serializer per-connection (add a version/capability byte to the Authenticate handshake).
252
+
Bun's `node:v8` module does **not** produce real V8 serialization format — it
253
+
emits a completely different binary encoding (appears MessagePack-like, not V8
254
+
`ValueSerializer` wire format). The Rust V8 sidecar's `ValueDeserializer`
255
+
rejects these payloads with "invalid header".
256
+
257
+
### Solution: CBOR codec for Bun
258
+
259
+
When the host runtime is Bun, the IPC payload codec switches from V8
260
+
ValueSerializer to **CBOR (RFC 8949)**:
261
+
262
+
-**JS side**: `cbor-x` for encode/decode (lazy-loaded only under Bun)
263
+
-**Rust side**: `ciborium` crate with manual `cbor_to_v8()` / `v8_to_cbor()`
264
+
converters in `bridge.rs`
265
+
-**Activation**: Host detects Bun via `typeof globalThis.Bun`, sets
266
+
`SECURE_EXEC_V8_CODEC=cbor` in the child process environment. Rust reads
Copy file name to clipboardExpand all lines: docs/architecture.mdx
+10-12Lines changed: 10 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -62,15 +62,15 @@ The narrow interface between the sandbox and the host. All privileged operations
62
62
63
63
## Node Runtime
64
64
65
-
On Node, the sandbox is a V8 isolate running in a **separate Rust process** (`@secure-exec/v8`). The host communicates with it over a Unix domain socket using length-prefixed MessagePack.
65
+
On Node.js and Bun, the sandbox is a V8 isolate running in a **separate Rust process** (`@secure-exec/v8`). The host communicates with it over a Unix domain socket using length-prefixed binary framing.
66
66
67
67
```mermaid
68
68
flowchart TB
69
69
subgraph Host["Host Process (Node.js / Bun)"]
70
70
NR["NodeRuntime"]
71
71
SD["System Driver"]
72
72
MAFS["ModuleAccessFileSystem"]
73
-
IPC_H["IPC Client<br/>(MessagePack over UDS)"]
73
+
IPC_H["IPC Client<br/>(binary framing over UDS)"]
74
74
end
75
75
subgraph Rust["V8 Runtime Process (Rust)"]
76
76
IPC_R["IPC Server"]
@@ -93,22 +93,20 @@ V8 has process-global state (`V8::Initialize`, signal handlers, ICU data) that m
93
93
94
94
### IPC protocol
95
95
96
-
All host↔sandbox communication uses **length-prefixed MessagePack** over a Unix domain socket:
96
+
All host↔sandbox communication uses **length-prefixed binary framing** over a Unix domain socket:
Message types are internally tagged (`{"type": "BridgeCall", ...}`). Binary data fields use MessagePack's native `bin` format — no base64 encoding.
102
+
The envelope is a fixed binary layout (no serialization library). Bridge function arguments and return values are serialized as opaque byte payloads inside the envelope using a runtime-dependent codec:
103
103
104
-
### Serialization layers
104
+
| Host runtime | Payload codec | Why |
105
+
|---|---|---|
106
+
|**Node.js**| V8 ValueSerializer (`node:v8`) | Built-in, handles all V8 types natively, zero dependencies |
107
+
|**Bun**| CBOR (`cbor-x` / `ciborium`) | Bun's `node:v8` module doesn't produce real V8 serialization format |
105
108
106
-
There are two MessagePack serialization layers:
107
-
108
-
1.**IPC envelope** — the outer message (`BridgeCall`, `BridgeResponse`, `ExecutionResult`, etc.) serialized with `rmp_serde` (Rust) / `@msgpack/msgpack` (JS).
109
-
2.**Bridge arguments/results** — function arguments and return values are separately MessagePack-encoded as opaque byte blobs (`args`, `result` fields) inside the IPC envelope. On the Rust side, V8 values are converted to/from `rmpv::Value` via native V8 API calls (`v8_to_rmpv` / `rmpv_to_v8`). On the JS host side, `@msgpack/msgpack``encode()`/`decode()` handles the inner payloads.
110
-
111
-
This double-encoding means the IPC framing layer doesn't need to understand bridge argument types — it just forwards opaque bytes.
109
+
CBOR was chosen over JSON for the Bun path because `cbor-x` encode is ~2× faster than `JSON.stringify` and supports binary payloads (`Uint8Array`/`Buffer`) natively without base64. The Rust sidecar detects the codec via the `SECURE_EXEC_V8_CODEC` environment variable set by the host at startup.
0 commit comments