|
| 1 | +--- |
| 2 | +title: Architecture |
| 3 | +description: How secure-exec components fit together across runtimes and environments. |
| 4 | +icon: "sitemap" |
| 5 | +--- |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +Every secure-exec sandbox has three layers: a **runtime** (public API), a **bridge** (isolation boundary), and a **system driver** (host capabilities). |
| 10 | + |
| 11 | +```mermaid |
| 12 | +flowchart TB |
| 13 | + subgraph Host["Host Process"] |
| 14 | + RT["Runtime<br/>(NodeRuntime / PythonRuntime)"] |
| 15 | + SD["System Driver<br/>(filesystem, network, processes, permissions)"] |
| 16 | + end |
| 17 | + subgraph Isolate["Sandbox"] |
| 18 | + UC["User Code"] |
| 19 | + BR["Bridge"] |
| 20 | + end |
| 21 | + RT --> SD |
| 22 | + RT --> Isolate |
| 23 | + UC -->|"capability request"| BR |
| 24 | + BR -->|"permission-gated call"| SD |
| 25 | +``` |
| 26 | + |
| 27 | +User code runs inside the sandbox and can only reach host capabilities through the bridge. The bridge enforces payload size limits on every transfer. The system driver wraps each capability in a permission check before executing it on the host. |
| 28 | + |
| 29 | +## Components |
| 30 | + |
| 31 | +### Runtime |
| 32 | + |
| 33 | +The public API. `NodeRuntime` and `PythonRuntime` are thin facades that accept a system driver and a runtime driver factory, then delegate all execution to the runtime driver. |
| 34 | + |
| 35 | +```ts |
| 36 | +const runtime = new NodeRuntime({ |
| 37 | + systemDriver: createNodeDriver(), |
| 38 | + runtimeDriverFactory: createNodeRuntimeDriverFactory(), |
| 39 | +}); |
| 40 | + |
| 41 | +await runtime.exec("console.log('hello')"); |
| 42 | +await runtime.run("export default 42"); |
| 43 | +runtime.dispose(); |
| 44 | +``` |
| 45 | + |
| 46 | +### System Driver |
| 47 | + |
| 48 | +A config object that bundles host capabilities. Deny-by-default. |
| 49 | + |
| 50 | +| Capability | What it provides | |
| 51 | +|---|---| |
| 52 | +| `filesystem` | Read/write/stat/mkdir operations | |
| 53 | +| `network` | fetch, DNS, HTTP | |
| 54 | +| `commandExecutor` | Child process spawning | |
| 55 | +| `permissions` | Per-capability allow/deny checks | |
| 56 | + |
| 57 | +Each capability is wrapped in a permission layer before the bridge can call it. Missing capabilities get deny-all stubs. |
| 58 | + |
| 59 | +### Bridge |
| 60 | + |
| 61 | +The narrow interface between the sandbox and the host. All privileged operations pass through the bridge. It serializes requests, enforces payload size limits, and routes calls to the appropriate system driver capability. |
| 62 | + |
| 63 | +### Runtime Driver |
| 64 | + |
| 65 | +Manages the actual execution environment. This is where the runtime-specific isolation mechanism lives. |
| 66 | + |
| 67 | +## Node Runtime |
| 68 | + |
| 69 | +On Node, the sandbox is a V8 isolate managed by `isolated-vm`. |
| 70 | + |
| 71 | +```mermaid |
| 72 | +flowchart TB |
| 73 | + subgraph Host["Host (Node.js process)"] |
| 74 | + NR["NodeRuntime"] |
| 75 | + NED["NodeExecutionDriver"] |
| 76 | + SD["System Driver"] |
| 77 | + MAFS["ModuleAccessFileSystem"] |
| 78 | + end |
| 79 | + subgraph ISO["V8 Isolate"] |
| 80 | + UC["User Code (CJS / ESM)"] |
| 81 | + BR["Bridge (ivm.Reference callbacks)"] |
| 82 | + MOD["Module Cache"] |
| 83 | + end |
| 84 | + NR --> NED |
| 85 | + NED --> ISO |
| 86 | + UC --> BR |
| 87 | + BR -->|"fs / net / process / crypto"| SD |
| 88 | + SD --> MAFS |
| 89 | +``` |
| 90 | + |
| 91 | +**Inside the isolate:** |
| 92 | +- User code runs as CJS or ESM (auto-detected from `package.json` `type` field) |
| 93 | +- Bridge globals are injected as `ivm.Reference` callbacks for fs, network, child_process, crypto, and timers |
| 94 | +- Compiled modules are cached per isolate |
| 95 | +- `Date.now()` and `performance.now()` return frozen values by default (timing mitigation) |
| 96 | +- `SharedArrayBuffer` is unavailable in freeze mode |
| 97 | + |
| 98 | +**Outside the isolate (host):** |
| 99 | +- `NodeExecutionDriver` creates contexts, compiles modules, and manages the isolate lifecycle |
| 100 | +- `ModuleAccessFileSystem` overlays host `node_modules` at `/app/node_modules` (read-only, blocks `.node` native addons, prevents symlink escapes) |
| 101 | +- System driver applies permission checks before every host operation |
| 102 | +- Bridge enforces payload size limits on all transfers (`ERR_SANDBOX_PAYLOAD_TOO_LARGE`) |
| 103 | + |
| 104 | +**Resource controls:** |
| 105 | +- `memoryLimit`: V8 isolate memory cap (default 128 MB) |
| 106 | +- `cpuTimeLimitMs`: CPU time budget (exit code 124 on exceeded) |
| 107 | +- `timingMitigation`: `"freeze"` (default) or `"off"` |
| 108 | + |
| 109 | +## Browser Runtime |
| 110 | + |
| 111 | +In the browser, the sandbox is a Web Worker. |
| 112 | + |
| 113 | +```mermaid |
| 114 | +flowchart TB |
| 115 | + subgraph Host["Host (browser main thread)"] |
| 116 | + NR["NodeRuntime"] |
| 117 | + BRD["BrowserRuntimeDriver"] |
| 118 | + SD["System Driver"] |
| 119 | + end |
| 120 | + subgraph WK["Web Worker"] |
| 121 | + UC["User Code (CJS / ESM)"] |
| 122 | + BR["Bridge (postMessage RPC)"] |
| 123 | + end |
| 124 | + NR --> BRD |
| 125 | + BRD -->|"postMessage"| WK |
| 126 | + UC --> BR |
| 127 | + BR -->|"postMessage"| SD |
| 128 | +``` |
| 129 | + |
| 130 | +**Inside the worker:** |
| 131 | +- User code runs as transformed CJS/ESM |
| 132 | +- Bridge globals are initialized from the worker init payload |
| 133 | +- Filesystem and network use permission-aware adapters |
| 134 | +- DNS operations return deterministic `ENOSYS` errors |
| 135 | + |
| 136 | +**Outside the worker (host):** |
| 137 | +- `BrowserRuntimeDriver` spawns workers, dispatches requests by ID, and correlates responses |
| 138 | +- `createBrowserDriver()` configures OPFS or in-memory filesystem and fetch-based networking |
| 139 | +- Node-only runtime options (like `memoryLimit`) are validated and rejected at creation time |
| 140 | + |
| 141 | +## Python Runtime |
| 142 | + |
| 143 | +The Python sandbox runs Pyodide in a Node Worker thread. |
| 144 | + |
| 145 | +```mermaid |
| 146 | +flowchart TB |
| 147 | + subgraph Host["Host (Node.js process)"] |
| 148 | + PR["PythonRuntime"] |
| 149 | + PRD["PyodideRuntimeDriver"] |
| 150 | + SD["System Driver"] |
| 151 | + end |
| 152 | + subgraph WK["Worker Thread"] |
| 153 | + PY["Pyodide (CPython via Emscripten)"] |
| 154 | + UC["User Code (Python)"] |
| 155 | + BR["Bridge (worker RPC)"] |
| 156 | + end |
| 157 | + PR --> PRD |
| 158 | + PRD -->|"worker messages"| WK |
| 159 | + UC --> BR |
| 160 | + BR -->|"worker RPC"| SD |
| 161 | +``` |
| 162 | + |
| 163 | +**Inside the worker:** |
| 164 | +- Pyodide loads once and keeps interpreter state warm across runs |
| 165 | +- Python code executes with access to bridged filesystem and network |
| 166 | +- stdio streams to the host via message events |
| 167 | + |
| 168 | +**Outside the worker (host):** |
| 169 | +- `PyodideRuntimeDriver` manages worker lifecycle and request correlation |
| 170 | +- Filesystem and network access goes through the same `SystemDriver` permission layer |
| 171 | +- On execution timeout, the worker state restarts for deterministic recovery |
| 172 | +- No `memoryLimit` or `timingMitigation` (Pyodide runs in a Worker, not a V8 isolate) |
| 173 | + |
| 174 | +## Permission Flow |
| 175 | + |
| 176 | +Every capability request follows the same path regardless of runtime: |
| 177 | + |
| 178 | +``` |
| 179 | +User Code -> Bridge -> Permission Check -> System Driver -> Host OS |
| 180 | +``` |
| 181 | + |
| 182 | +If the permission check denies the request, the bridge returns an error before the system driver is called. If no adapter is configured for a capability, a deny-all stub handles it. |
0 commit comments