@torkbot/sandbox 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,199 +1,251 @@
1
1
  # Sandbox
2
2
 
3
- Sandbox is a TypeScript-first Node.js library for running work inside
4
- libkrun-backed microVMs with host-controlled filesystems and network policy.
3
+ Sandbox is a TypeScript-first Node.js library for running AI-agent work inside
4
+ libkrun-backed microVMs. It gives agent builders a small API for booting
5
+ isolated Linux workers, mounting host-controlled filesystems, persisting rootfs
6
+ mutations, and enforcing explicit network egress policy.
7
+
8
+ Use Sandbox when your agent needs to run tools, install packages, clone repos,
9
+ execute untrusted code, or call external APIs without handing the guest broad
10
+ host filesystem or network access.
11
+
12
+ Sandbox is designed for:
13
+
14
+ - agent runtimes that need disposable or durable Linux workspaces,
15
+ - coding agents that need real shells, compilers, package managers, and repo
16
+ checkouts,
17
+ - browser or data agents that need tightly-scoped outbound network access,
18
+ - hosted agent platforms that need per-task isolation with policy and audit
19
+ points in TypeScript,
20
+ - systems that need to broker credentials from the host without putting long
21
+ lived secrets inside the guest.
22
+
23
+ ## Example
24
+
25
+ This example creates a durable agent lane, mounts a host-controlled workspace,
26
+ allows DNS through Cloudflare, allows only GitHub API HTTP traffic, and injects
27
+ a short-lived GitHub token from the host before the request leaves the sandbox.
5
28
 
6
29
  ```ts
7
30
  import {
8
31
  defineSandbox,
9
32
  fs,
33
+ network,
10
34
  rootfs,
35
+ type SandboxBlockStore,
11
36
  } from "@torkbot/sandbox";
12
37
 
13
- const workspaceFs = fs.memory({
38
+ const workspace = fs.memory({
14
39
  files: {
15
- "/hello.txt": "hello from the host filesystem\n",
40
+ "/task.txt": "Summarize the current GitHub user\n",
16
41
  },
17
42
  });
18
43
 
44
+ const writableRootfs: SandboxBlockStore = new BlobBackedBlockStore({
45
+ bucket: "agent-rootfs-overlays",
46
+ keyPrefix: "lanes/github-worker",
47
+ });
48
+
49
+ const githubTokens = new GitHubInstallationTokenService({
50
+ installationId: 123456,
51
+ });
52
+
19
53
  const sandbox = defineSandbox({
20
- rootfs: rootfs.builtIn("alpine:3.23"),
54
+ rootfs: rootfs.cow({
55
+ base: rootfs.builtIn("alpine:3.23"),
56
+ writable: writableRootfs,
57
+ }),
21
58
  resources: {
22
- cpus: 2,
23
- memoryMiB: 2048,
59
+ cpus: 4,
60
+ memoryMiB: 4096,
24
61
  },
62
+ network: network.policy(async (conn) => {
63
+ // Let the guest resolve names, but answer DNS via an explicit resolver.
64
+ if (conn.matchDns("1.1.1.1")?.accept()) return;
65
+
66
+ // Only GitHub API HTTP(S) traffic gets HTTP middleware.
67
+ const github = conn.matchHttp("api.github.com");
68
+ if (!github) return;
69
+
70
+ // Keep the credential decision in host-controlled TypeScript.
71
+ if (!(await githubTokens.canServe(github))) return;
72
+
73
+ github.accept(async (request) => {
74
+ request.headers.set(
75
+ "authorization",
76
+ `Bearer ${await githubTokens.tokenForRequest(request)}`,
77
+ );
78
+ });
79
+ }),
25
80
  });
26
81
 
27
82
  await using lane = await sandbox.boot({
28
83
  mounts: {
29
- "/workspace": fs.virtual(workspaceFs),
84
+ "/workspace": fs.virtual(workspace),
30
85
  },
31
86
  cwd: "/workspace",
32
87
  });
33
88
 
34
- const result = await lane.exec("cat", ["hello.txt"]);
89
+ const result = await lane.exec("sh", [
90
+ "-lc",
91
+ "cat task.txt && curl -fsSL https://api.github.com/user",
92
+ ]);
35
93
 
36
94
  if (result.exitCode !== 0) {
37
95
  throw new Error(result.stderr);
38
96
  }
39
97
  ```
40
98
 
41
- ## Quick Start
99
+ The guest gets a normal Linux environment. The host keeps control over the
100
+ workspace contents, rootfs persistence, network decisions, and credential
101
+ injection.
42
102
 
43
- Create reusable machine configuration once, then boot one or more instances with
44
- the mounts each instance needs:
103
+ ## Quick Paths
45
104
 
46
- ```ts
47
- import {
48
- defineSandbox,
49
- fs,
50
- rootfs,
51
- } from "@torkbot/sandbox";
105
+ ### Run one isolated command
52
106
 
53
- const workspaceFs = fs.memory();
107
+ Use a built-in read-only rootfs and a memory-backed workspace:
108
+
109
+ ```ts
110
+ const workspace = fs.memory({
111
+ files: { "/hello.txt": "hello from the host\n" },
112
+ });
54
113
 
55
114
  const sandbox = defineSandbox({
56
115
  rootfs: rootfs.builtIn("alpine:3.23"),
57
116
  });
58
117
 
59
118
  await using lane = await sandbox.boot({
60
- mounts: {
61
- "/workspace": fs.virtual(workspaceFs),
62
- },
119
+ mounts: { "/workspace": fs.virtual(workspace) },
63
120
  cwd: "/workspace",
64
121
  });
65
122
 
66
- const result = await lane.exec("sh", ["-lc", "printf 'ok\\n'"], {
67
- env: { CI: "1" },
68
- });
69
-
70
- if (result.exitCode !== 0) {
71
- throw new Error(result.stderr);
72
- }
123
+ const result = await lane.exec("cat", ["hello.txt"]);
73
124
  ```
74
125
 
75
- The public API is split into three layers:
76
-
77
- - `defineSandbox(...)` describes reusable machine configuration.
78
- - `sandbox.boot(...)` creates a runtime instance with per-instance mounts.
79
- - `lane.exec(...)` runs buffered work inside the booted instance.
126
+ ### Give an agent a durable machine
80
127
 
81
- Expensive artifact preparation is intentionally outside `boot()`.
82
- `rootfs.builtIn("alpine:3.23")` selects a built-in rootfs artifact that must
83
- already be installed with Sandbox. It does not pull an image or build a rootfs
84
- at runtime.
85
-
86
- ## Durable, Policy-Controlled Instances
87
-
88
- Sandbox composes durable rootfs mutation with explicit network policy. In this
89
- example, dirty COW blocks are synchronized to blob storage, public HTTP(S)
90
- egress is allowed, and only GitHub API requests receive an installation token:
128
+ Use `rootfs.cow(...)` when package installs and rootfs mutations should survive
129
+ across boots. Sandbox owns the block-device protocol; your application owns the
130
+ storage backend.
91
131
 
92
132
  ```ts
93
- import {
94
- defineSandbox,
95
- network,
96
- rootfs,
97
- type SandboxBlockStore,
98
- } from "@torkbot/sandbox";
99
-
100
- const writableRootfs: SandboxBlockStore = new BlobSynchronizedCowBlockStore({
101
- bucket: "sandbox-rootfs-overlays",
102
- keyPrefix: "lanes/github-worker",
103
- });
104
-
105
- const githubTokens = new GitHubInstallationTokenService({
106
- installationId: 123456,
107
- });
108
-
109
133
  const sandbox = defineSandbox({
110
134
  rootfs: rootfs.cow({
111
135
  base: rootfs.builtIn("alpine:3.23"),
112
- writable: writableRootfs,
113
- }),
114
- resources: {
115
- cpus: 4,
116
- memoryMiB: 4096,
117
- },
118
- network: network.policy(async (conn) => {
119
- if (conn.matchDns("10.0.2.1")?.accept()) return;
120
-
121
- const github = conn.transport === "tcp"
122
- ? conn.matchHttp("api.github.com")
123
- : undefined;
124
- if (github) {
125
- github.accept(async (request) => {
126
- if (request.destination.hostname !== "api.github.com") return;
127
- request.headers.set(
128
- "authorization",
129
- `Bearer ${await githubTokens.tokenForRequest(request)}`,
130
- );
131
- });
132
- return;
133
- }
136
+ writable: blockStore,
134
137
  }),
135
138
  });
139
+ ```
136
140
 
137
- await using lane = await sandbox.boot({ cwd: "/workspace" });
141
+ Attach a writable COW block store to at most one running sandbox instance at a
142
+ time. Create one store per lane, or enforce exclusivity in your storage layer.
138
143
 
139
- await lane.exec("sh", [
140
- "-lc",
141
- "apk add --no-cache git curl && curl -fsSL https://api.github.com/user",
142
- ]);
144
+ ### Mount host-controlled data
145
+
146
+ Mounts are per boot. They are guest-visible paths backed by TypeScript
147
+ filesystem implementations, not host path passthrough.
148
+
149
+ ```ts
150
+ await using lane = await sandbox.boot({
151
+ mounts: {
152
+ "/workspace": fs.virtual(workspaceFs),
153
+ "/mnt/shared": fs.virtual(sharedFs),
154
+ },
155
+ cwd: "/workspace",
156
+ });
143
157
  ```
144
158
 
145
- ## API Overview
159
+ The built-in Alpine rootfs includes `/workspace`, `/tmp`, and `/mnt`. Mount
160
+ targets must already exist in the selected rootfs.
161
+
162
+ ### Control network egress
146
163
 
147
- ### Configuration
164
+ Networking is default-deny. Policy callbacks grant only the flows they accept:
148
165
 
149
166
  ```ts
150
- type SandboxDefinition = {
151
- rootfs: Rootfs;
152
- resources?: {
153
- cpus?: number;
154
- memoryMiB?: number;
155
- };
156
- network?: NetworkPolicy;
157
- };
167
+ const sandbox = defineSandbox({
168
+ rootfs: rootfs.builtIn("alpine:3.23"),
169
+ network: network.policy((conn) => {
170
+ if (conn.matchDns("1.1.1.1")?.accept()) return;
171
+
172
+ conn.matchHttp("api.example.com")?.accept((request) => {
173
+ request.headers.set("authorization", `Bearer ${apiToken}`);
174
+ });
175
+ }),
176
+ });
158
177
  ```
159
178
 
160
- `rootfs` selects the guest root filesystem. The first public rootfs source is
161
- the read-only built-in catalog:
179
+ Use `conn.accept()` for raw transport, and protocol match helpers when you want
180
+ Sandbox to handle protocol-specific semantics. `conn.matchHttp(...)` does not
181
+ trust the HTTP `Host` header; it uses trusted destination metadata and then
182
+ routes accepted traffic through HTTP-family enforcement.
183
+
184
+ ### Broker credentials from the host
185
+
186
+ Credential injection belongs in HTTP middleware, not in the guest filesystem or
187
+ environment:
162
188
 
163
189
  ```ts
164
- rootfs.builtIn("alpine:3.23");
190
+ const github = conn.matchHttp("api.github.com");
191
+ if (!github) return;
192
+
193
+ if (!(await policyManager.allow(github))) return;
194
+
195
+ github.accept(async (request) => {
196
+ request.headers.set(
197
+ "authorization",
198
+ `Bearer ${await policyManager.tokenFor(request)}`,
199
+ );
200
+ });
165
201
  ```
166
202
 
167
- `resources` controls the VM shape used by every instance booted from the
168
- definition. Omitted values use Sandbox defaults.
203
+ This lets the guest run ordinary tools such as `curl`, `git`, package managers,
204
+ or language CLIs while the host decides which outbound requests receive
205
+ credentials.
206
+
207
+ ## API Reference
208
+
209
+ ### `defineSandbox(options)`
210
+
211
+ Creates reusable machine configuration.
169
212
 
170
213
  ```ts
171
- defineSandbox({
214
+ const sandbox = defineSandbox({
172
215
  rootfs: rootfs.builtIn("alpine:3.23"),
173
216
  resources: {
174
217
  cpus: 4,
175
218
  memoryMiB: 4096,
176
219
  },
220
+ network: network.policy((conn) => {
221
+ conn.matchDns("1.1.1.1")?.accept();
222
+ }),
177
223
  });
178
224
  ```
179
225
 
180
- Use `rootfs.cow(...)` when rootfs mutations should persist. The sandbox library
181
- owns the COW block-device contract; user-space owns the block store's
182
- durability, migration, and checkpoint policy. Built-in rootfs packages include
183
- one compressed QCOW2 image with an ext4 guest filesystem. `rootfs.builtIn(...)`
184
- mounts that image read-only in the guest; `rootfs.cow(...)` mounts the same base
185
- read-write through the host COW block store.
226
+ `defineSandbox(...)` does not boot a VM. It describes rootfs, resource, and
227
+ network policy defaults that can be reused across many boots.
228
+
229
+ ### `rootfs`
186
230
 
187
231
  ```ts
188
- defineSandbox({
189
- rootfs: rootfs.cow({
190
- base: rootfs.builtIn("alpine:3.23"),
191
- writable: laneBlockStore,
192
- }),
232
+ rootfs.builtIn("alpine:3.23");
233
+ ```
234
+
235
+ Selects a read-only built-in rootfs artifact. Built-in rootfs artifacts are
236
+ prepared at build or install time; Sandbox does not pull container images or
237
+ build root filesystems during `boot()`.
238
+
239
+ ```ts
240
+ rootfs.cow({
241
+ base: rootfs.builtIn("alpine:3.23"),
242
+ writable: blockStore,
193
243
  });
194
244
  ```
195
245
 
196
- The block store interface is intentionally storage-agnostic:
246
+ Mounts a built-in base rootfs through a writable copy-on-write block store.
247
+ Clean base-image blocks are served from the built-in artifact. Dirty blocks are
248
+ read lazily and flushed through your `SandboxBlockStore`.
197
249
 
198
250
  ```ts
199
251
  interface SandboxBlockStore {
@@ -211,240 +263,206 @@ interface SandboxBlockStore {
211
263
  }
212
264
  ```
213
265
 
214
- The `context.base` value identifies the exact built-in base image for this boot.
215
- The sandbox library passes it through to every block-store operation; user-space
216
- storage can use it to namespace blocks, reject mismatched snapshots, or migrate
217
- state. `list()` returns the block IDs currently present in the COW store. The
218
- Rust block backend reads that manifest once at boot, so clean base-image blocks
219
- are served without asking JavaScript. Dirty blocks are read lazily and writes are
220
- batched back through `write(...)` on flush.
266
+ The `context.base` value identifies the exact built-in base image for the boot,
267
+ so storage layers can namespace blocks, reject mismatched snapshots, or migrate
268
+ state.
221
269
 
222
- A writable COW block store must be attached to at most one running sandbox
223
- instance at a time. Concurrent sandboxes sharing the same writable store are
224
- undefined behavior; create one store per lane or enforce exclusivity in the
225
- storage driver.
270
+ ### `sandbox.boot(options)`
226
271
 
227
- `network` is optional. When omitted, egress is denied. A network policy receives
228
- connection requests and grants only the traffic it explicitly allows:
272
+ Boots a sandbox instance.
229
273
 
230
274
  ```ts
231
- const policy = network.policy(async (conn) => {
232
- if (conn.transport === "tcp" && conn.dst.isPublicInternet() && conn.dst.port === 443) {
233
- conn.accept();
234
- }
275
+ await using lane = await sandbox.boot({
276
+ mounts: {
277
+ "/workspace": fs.virtual(workspaceFs),
278
+ },
279
+ cwd: "/workspace",
235
280
  });
236
281
  ```
237
282
 
238
- `conn.accept()` grants the observed connection or flow at the transport layer.
239
- It does not classify the application protocol, does not enter HTTP middleware,
240
- and does not MITM TLS. `conn.acceptHttp(...)` is TCP-only and explicitly opts
241
- the flow into Sandbox's HTTP-family enforcement path. If the accepted flow is
242
- not actually HTTP or HTTPS, it fails closed.
283
+ Boot options are per instance. The same sandbox definition can be booted with
284
+ different mounts and working directories.
243
285
 
244
- ```ts
245
- const policy = network.policy(async (conn) => {
246
- const api = conn.transport === "tcp"
247
- ? conn.matchHttp("api.example.com")
248
- : undefined;
249
- if (!api) return;
286
+ ### Filesystems
250
287
 
251
- api.accept(async (request) => {
252
- request.headers.set(
253
- "authorization",
254
- `Bearer ${await credentialBroker.authorizationFor(request)}`,
255
- );
256
- });
288
+ ```ts
289
+ const workspaceFs = fs.memory({
290
+ files: {
291
+ "/README.md": "# Task\n",
292
+ },
257
293
  });
258
294
  ```
259
295
 
260
- Every TCP and UDP policy request carries source and destination IP-layer
261
- endpoints:
296
+ `fs.memory(...)` creates an in-memory POSIX filesystem.
262
297
 
263
298
  ```ts
264
- conn.src.ip;
265
- conn.src.port;
266
- conn.dst.ip;
267
- conn.dst.port;
299
+ const mount = fs.virtual(workspaceFs);
268
300
  ```
269
301
 
270
- Endpoint helpers classify logical address ranges without relying on hostnames:
271
- `isLoopback()`, `isPrivate()`, `isLinkLocal()`, `isMulticast()`,
272
- `isBroadcast()`, `isDocumentation()`, `isReserved()`, and
273
- `isPublicInternet()`. `transport` is the TCP/UDP discriminator. Sandbox does
274
- not expose a best-effort `conn.protocol` classifier.
302
+ `fs.virtual(...)` adapts a compatible JavaScript filesystem for guest mounts.
303
+ Sandbox mounts are host-implemented filesystems, not direct host directory
304
+ mounts.
275
305
 
276
- HTTP middleware receives trusted destination metadata separately from the
277
- request URL. `request.destination.hostname` is populated only when Sandbox can
278
- pin the destination IP to trusted connection metadata, such as its own DNS
279
- answer cache. IP-addressed requests still work under `acceptHttp(...)`, but they
280
- do not advertise a hostname. Do not use the HTTP `Host` header as authority for
281
- policy decisions.
306
+ ### Processes
282
307
 
283
- For common policy checks, protocol match helpers acquire a typed capability
284
- before accepting traffic:
308
+ ```ts
309
+ const result = await lane.exec("npm", ["test"], {
310
+ cwd: "/workspace",
311
+ env: { CI: "1" },
312
+ });
313
+ ```
314
+
315
+ `exec(...)` is the buffered process API. It returns after the command exits with
316
+ `exitCode`, `stdout`, and `stderr`.
317
+
318
+ ### Network Policy
285
319
 
286
320
  ```ts
287
- network.policy(async (conn) => {
288
- if (conn.matchDns("10.0.2.1")?.accept()) return;
321
+ const policy = network.policy(async (conn) => {
322
+ if (conn.matchDns("1.1.1.1")?.accept()) return;
289
323
 
290
- if (conn.transport !== "tcp") return;
291
- const http = conn.matchHttp((candidate) =>
292
- candidate.hostname === "api.github.com"
293
- );
294
- if (!http) return;
324
+ const api = conn.matchHttp("api.example.com");
325
+ if (!api) return;
295
326
 
296
- if (!(await policyManager.allow(http))) return;
327
+ if (!(await policyManager.allow(api))) return;
297
328
 
298
- http.accept((request) => policyManager.handle(request));
329
+ api.accept((request) => policyManager.handleHttp(request));
299
330
  });
300
331
  ```
301
332
 
302
- Deny remains the default. If the policy callback does not create a grant, the
303
- connection is blocked. The grants returned by `accept()` and `acceptHttp()` are
304
- reserved as future extension points for instance-local state, such as
305
- remembering a grant for a time window.
306
-
307
- The runtime uses this policy shape to keep the JavaScript boundary explicit.
308
- Native rules can be added under the same model later without changing the
309
- caller-facing API.
310
-
311
- ### Boot Options
333
+ Policy callbacks are default-deny. If the callback does not create a grant, the
334
+ connection is blocked.
312
335
 
313
- Mounts are per-instance because different sandbox instances often need
314
- different filesystems over the same reusable machine configuration:
336
+ Every policy event exposes IP-layer endpoints:
315
337
 
316
338
  ```ts
317
- await using lane = await sandbox.boot({
318
- mounts: {
319
- "/workspace": fs.virtual(workspaceFs),
320
- "/tmp": fs.virtual(privateFs),
321
- "/mnt": fs.virtual(sharedFs),
322
- },
323
- cwd: "/workspace",
324
- });
339
+ conn.src.ip;
340
+ conn.src.port;
341
+ conn.dst.ip;
342
+ conn.dst.port;
325
343
  ```
326
344
 
327
- Sandbox does not special-case `/workspace`. Mount paths are just guest-visible
328
- paths backed by user-supplied filesystems. The target path must already exist
329
- in the selected rootfs; the built-in Alpine rootfs includes `/workspace`,
330
- `/tmp`, and `/mnt`.
345
+ Endpoint helpers classify address ranges without relying on DNS, TLS, or HTTP:
331
346
 
332
- ### Filesystems
347
+ ```ts
348
+ conn.dst.isLoopback();
349
+ conn.dst.isPrivate();
350
+ conn.dst.isLinkLocal();
351
+ conn.dst.isMulticast();
352
+ conn.dst.isBroadcast();
353
+ conn.dst.isDocumentation();
354
+ conn.dst.isReserved();
355
+ conn.dst.isPublicInternet();
356
+ ```
333
357
 
334
- `fs.memory(...)` creates a real in-memory POSIX filesystem that can be mounted:
358
+ Transport and protocol helpers:
335
359
 
336
360
  ```ts
337
- const workspaceFs = fs.memory({
338
- files: {
339
- "/README.md": "# Example\n",
340
- },
341
- });
361
+ conn.accept(); // accept raw TCP or UDP transport
362
+ conn.matchDns("1.1.1.1"); // DNS over UDP or TCP, using 1.1.1.1 upstream
363
+ conn.matchTcp("203.0.113.10:5432");
364
+ conn.matchUdp("203.0.113.10:8125");
365
+ conn.matchHttp("api.example.com");
342
366
  ```
343
367
 
344
- `fs.virtual(...)` adapts any compatible user-space JavaScript filesystem to
345
- Sandbox mounts:
368
+ `conn.matchDns(...)` normalizes DNS over UDP and TCP. Passing a resolver spec
369
+ selects the upstream resolver used by `accept()`. Advanced policies can pass a
370
+ synchronous predicate and then provide explicit resolvers to `accept(...)`.
346
371
 
347
372
  ```ts
348
- const workspace = fs.virtual(workspaceFs);
373
+ const dns = conn.matchDns((candidate) => candidate.transport === "udp");
374
+ if (dns) {
375
+ dns.accept({ resolvers: ["1.1.1.1", "8.8.8.8"] });
376
+ }
349
377
  ```
350
378
 
351
- ### Processes
352
-
353
- `exec` is the simple buffered process API:
379
+ `conn.matchHttp(...)` acquires an HTTP capability from trusted destination
380
+ metadata. It does not inspect or trust the HTTP `Host` header. IP-addressed HTTP
381
+ requests can still be accepted through lower-level policy, but they do not
382
+ advertise a trusted hostname.
354
383
 
355
384
  ```ts
356
- const result = await lane.exec("npm", ["test"], {
357
- cwd: "/workspace",
358
- env: { CI: "1" },
359
- });
385
+ const http = conn.matchHttp((candidate) =>
386
+ candidate.hostname.endsWith(".example.com")
387
+ );
388
+
389
+ if (http) {
390
+ http.accept((request) => {
391
+ request.headers.set("x-agent-policy", "allowed");
392
+ });
393
+ }
360
394
  ```
361
395
 
362
- `exec` is intentionally small: it buffers stdout and stderr and returns when the
363
- process exits. Streaming stdin/stdout/stderr belongs in the future
364
- `lane.spawn(...)` API.
396
+ `http.accept(...)` enters Sandbox's HTTP-family enforcement path. If the matched
397
+ flow is not actually HTTP or HTTPS, it fails closed.
365
398
 
366
- ## Internal Architecture
399
+ ## Architecture Reference
367
400
 
368
- Sandbox hides the kernel, init, transport, and host helper behind a small
369
- TypeScript API:
401
+ Sandbox hides the kernel, init, transport, and host helper behind a TypeScript
402
+ API:
370
403
 
371
- - The runtime boots a libkrun-backed guest from a prebuilt rootfs artifact:
372
- a compressed QCOW2 image that contains an ext4 guest filesystem.
373
- - Kernel and init artifacts are implementation details owned by Sandbox.
404
+ - The runtime boots a libkrun-backed microVM from a prebuilt rootfs artifact.
405
+ - The built-in rootfs is a compressed QCOW2 image containing an ext4 guest
406
+ filesystem.
374
407
  - A signed `sandbox-host` helper owns the Node/Rust/libkrun boundary.
375
- - Guest control traffic uses an implicit fd-backed transport between the host
376
- and Sandbox init.
408
+ - Guest control traffic uses an fd-backed transport between the host and the
409
+ custom Sandbox init process.
377
410
  - Host-implemented virtual filesystems are mounted into the guest.
378
- - Rootfs mutation persistence is modeled as block-level copy-on-write rootfs,
379
- not as a guest-visible POSIX filesystem.
380
- - Network egress is default-deny. Native code should enforce fast-path policy
381
- decisions and delegate to JavaScript only when a policy callback is required.
382
- - HTTP request middleware is caller-provided JavaScript, but Sandbox owns the
383
- interception machinery and certificate plumbing.
384
- - When HTTP interception is enabled, the host generates the CA material and
385
- passes only the public CA certificate to Sandbox init. Init does not generate
386
- or manage certificates; the host exposes the supplied CA through an internal
387
- read-only virtiofs mount, then init installs it using the selected rootfs'
388
- native trust-store mechanism. Built-in rootfs launches use an ephemeral
389
- writable COW view for HTTP interception so init can update the guest trust
390
- store deterministically. If a rootfs does not provide a supported native
391
- trust-store installer, init fails closed.
392
-
393
- The intended boundary is that Sandbox knows how to launch, isolate, mount,
394
- intercept, and enforce. User-space owns artifact selection, filesystem
395
- durability, network policy state, confirmation flows, and credential brokering.
396
-
397
- ## Design Targets
398
-
399
- - no dynamic `libkrun` or `libkrunfw` dependency in the final host artifact,
400
- - a signed `sandbox-host` process for the Node/Rust host boundary,
401
- - custom guest init owned by this repo,
402
- - implicit fd-backed host control sockets owned by Sandbox,
403
- - avoid host filesystem coordination unless it is intrinsic to the artifact; prefer file descriptors, database handles, bytes, and async iterables over paths,
404
- - build-time rootfs shaping, with built-in rootfs artifacts selected by typed logical names at VM instantiation,
405
- - immutable rootfs by default, with copy-on-write rootfs supplied by a user-space block store when requested,
406
- - generic guest-visible mounts backed by the same user-space filesystem abstraction,
407
- - programmable virtual filesystems backed by TypeScript callbacks,
408
- - transparent HTTP interception with TypeScript request-header hooks,
409
- - default-deny outbound networking with JavaScript policy callbacks only where native rules cannot decide,
410
- - Rust-native or statically linkable networking components; sidecar network daemons are references, not default runtime dependencies,
411
- - macOS HVF entitlement signing verified as part of the integration test flow.
412
-
413
- ## Repository Layout
411
+ - Durable rootfs mutation is modeled as block-level copy-on-write storage, not
412
+ as a guest-visible POSIX filesystem.
413
+ - Network egress is default-deny and policy-controlled.
414
+ - HTTP request middleware is caller-provided JavaScript, while Sandbox owns
415
+ interception, forwarding, and certificate plumbing.
414
416
 
415
- - `src/`: TypeScript API consumed by Node.js callers.
416
- - `crates/sandbox-host`: signed VM-host helper used for macOS HVF launch.
417
- - `crates/sandbox`: Rust host implementation that owns the libkrun boundary and host services.
418
- - `crates/sandbox-init`: custom guest init used to configure the guest before supervising untrusted code.
419
- - `tests/e2e`: TypeScript e2e scenarios run directly by Node.js 24+ type stripping.
417
+ When HTTP interception is enabled, the host generates CA material and passes
418
+ only the public CA certificate to Sandbox init. Init installs that CA using the
419
+ selected rootfs' native trust-store mechanism. Built-in rootfs launches use an
420
+ ephemeral writable COW view for HTTP interception so trust-store installation is
421
+ deterministic. If a rootfs does not provide a supported trust-store installer,
422
+ init fails closed.
423
+
424
+ The intended boundary is:
420
425
 
421
- See [docs/architecture.md](docs/architecture.md) for the initial design.
426
+ - Sandbox owns launch, isolation, mounts, network interception, and enforcement.
427
+ - User-space owns artifact selection, filesystem durability, network policy
428
+ state, confirmation flows, audit logs, and credential brokering.
422
429
 
423
- Kernel artifacts are built separately from runtime VM creation. See [docs/kernel-build.md](docs/kernel-build.md) for the Docker-based `deps/libkrunfw` build entrypoint.
424
- See [docs/testing-strategy.md](docs/testing-strategy.md) for the integration and e2e verification plan.
430
+ See [docs/architecture.md](docs/architecture.md) for the design background,
431
+ [docs/kernel-build.md](docs/kernel-build.md) for kernel artifact builds, and
432
+ [docs/testing-strategy.md](docs/testing-strategy.md) for the integration and
433
+ e2e testing plan.
425
434
 
426
- ## Publishing
435
+ ## Platform Notes
427
436
 
428
- The npm package is published as `@torkbot/sandbox`. It does not use post-install scripts. The root package contains the TypeScript API and declares platform artifacts as optional dependencies:
437
+ The npm package is published as `@torkbot/sandbox`. It does not use
438
+ post-install scripts. The root package contains the TypeScript API and declares
439
+ platform artifacts as optional dependencies:
429
440
 
430
441
  - `@torkbot/sandbox-darwin-arm64`
431
442
  - `@torkbot/sandbox-linux-x64-gnu`
432
443
 
433
- Each platform package contains the `sandbox-host` helper and built-in rootfs artifacts for that target. Runtime artifact resolution only loads the installed optional dependency for the current platform. Local development uses the same layout by materializing the current platform package under `node_modules`.
444
+ Each platform package contains the `sandbox-host` helper and built-in rootfs
445
+ artifacts for that target. Runtime artifact resolution only loads the installed
446
+ optional dependency for the current platform. Local development uses the same
447
+ layout by materializing the current platform package under `node_modules`.
434
448
 
435
449
  ### macOS signing setup
436
450
 
437
- For now, the macOS `sandbox-host` artifact is not Developer ID signed or notarized. This is an explicit, possibly temporary workaround for publishing before this project has an Apple Developer account.
438
-
439
- macOS users must sign the installed helper locally before launching a VM:
451
+ For now, the macOS `sandbox-host` artifact is not Developer ID signed or
452
+ notarized. macOS users must sign the installed helper locally before launching a
453
+ VM:
440
454
 
441
455
  ```sh
442
456
  npx @torkbot/sandbox setup-macos
443
457
  ```
444
458
 
445
- This performs an ad-hoc local `codesign` with the `com.apple.security.hypervisor` entitlement required by Hypervisor.framework. It does not contact Apple and does not require an Apple Developer account. If a macOS user tries to launch a VM before running setup, Sandbox throws a runtime error that points back to this command.
459
+ This performs an ad-hoc local `codesign` with the
460
+ `com.apple.security.hypervisor` entitlement required by Hypervisor.framework. It
461
+ does not contact Apple and does not require an Apple Developer account. If a
462
+ macOS user tries to launch a VM before running setup, Sandbox throws a runtime
463
+ error that points back to this command.
446
464
 
447
- The release workflow verifies the tag, builds platform packages on their native runners, publishes the platform packages first, and then publishes the root package. That keeps the installable root package from pointing at missing optional artifacts while staying as close as npm allows to a single coordinated release operation.
465
+ ## Development
448
466
 
449
467
  Local release packaging sanity check:
450
468
 
@@ -452,8 +470,20 @@ Local release packaging sanity check:
452
470
  npm run release:pack
453
471
  ```
454
472
 
455
- After rebuilding local native artifacts, refresh the local optional package layout with:
473
+ After rebuilding local native artifacts, refresh the local optional package
474
+ layout with:
456
475
 
457
476
  ```sh
458
477
  npm run artifacts:link-current
459
478
  ```
479
+
480
+ Repository layout:
481
+
482
+ - `src/`: TypeScript API consumed by Node.js callers.
483
+ - `crates/sandbox`: Rust host implementation for libkrun, block storage,
484
+ network, HTTP, and VFS services.
485
+ - `crates/sandbox-host`: signed VM-host helper used for macOS HVF launch.
486
+ - `crates/sandbox-init`: custom guest init used to configure the guest before
487
+ supervising untrusted code.
488
+ - `tests/e2e`: TypeScript e2e scenarios run directly by Node.js 24+ type
489
+ stripping.