@machinen/runtime 0.1.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.
@@ -0,0 +1,2227 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+
3
+ interface VsockExecOptions {
4
+ /** How long to keep retrying the UDS connect. Default 30s. */
5
+ connectTimeoutMs?: number;
6
+ /** Poll interval in ms while retrying. Default 250. */
7
+ retryMs?: number;
8
+ /**
9
+ * Wall-clock ceiling for the spawned command. Default 5 minutes.
10
+ * Pass `null` (or `Infinity`) to disable — appropriate for
11
+ * long-running siblings (dev servers, file watchers, log tailers)
12
+ * that should live for the VM's lifetime. Mirrors `boot({ timeoutMs: null })`.
13
+ */
14
+ execTimeoutMs?: number | null;
15
+ /** Called with each stdout chunk as it arrives (pass-through tee). */
16
+ onStdout?: (chunk: Buffer) => void;
17
+ /** Called with each stderr chunk as it arrives (pass-through tee). */
18
+ onStderr?: (chunk: Buffer) => void;
19
+ }
20
+ interface VsockExecResult {
21
+ exitCode: number;
22
+ stdout: string;
23
+ stderr: string;
24
+ }
25
+ declare const VsockExec: {
26
+ /**
27
+ * Run `cmd` inside the guest via the exec-agent. The command string
28
+ * is passed verbatim to `sh -c` inside the guest, so shell syntax
29
+ * (pipes, redirection, env) works.
30
+ *
31
+ * Resolves once the agent returns an exit code. Rejects only on I/O
32
+ * failure or timeout — a non-zero exit is a normal result; the
33
+ * caller decides what to do.
34
+ *
35
+ * The TCP UDS → vsock bridge accepts host-side connections even
36
+ * before the guest has bound its side of port 1978 (the agent is
37
+ * started by the workload, which has to wait for the kernel to
38
+ * reach userspace). Such early connections get immediately reset
39
+ * when the bridge tries to forward them. We retry on those resets
40
+ * until the agent is actually listening.
41
+ */
42
+ /**
43
+ * @throws {ExecError} EXEC_AGENT_UNAVAILABLE (retryable) |
44
+ * EXEC_AGENT_TIMEOUT (retryable) | EXEC_PROTOCOL
45
+ */
46
+ readonly run: (udsPath: string, cmd: string, opts?: VsockExecOptions) => Promise<VsockExecResult>;
47
+ /**
48
+ * PTY-mode session against the exec-agent (#133). Bytes flow
49
+ * bidirectionally between `opts.stdin` (host keystrokes) and
50
+ * `opts.stdout` (workload's pty output); the returned handle's
51
+ * `.resize(cols, rows)` propagates window-size changes to the
52
+ * guest's `ioctl(TIOCSWINSZ)`, and `.cancel()` disconnects (the
53
+ * agent then closes its master fd, which sends SIGHUP to the
54
+ * workload's session and reaps the child).
55
+ *
56
+ * Resolves with `{ exitCode }` once the workload exits and the
57
+ * agent emits the X frame. The stdin listener attaches eagerly —
58
+ * the caller is responsible for putting the host terminal in raw
59
+ * mode beforehand (so Ctrl-C, arrows, etc. reach the guest as
60
+ * untranslated bytes) and restoring it after `result` settles.
61
+ *
62
+ * Connect retries are intentionally absent here: PTY sessions are
63
+ * always against an already-running VM whose agent is up. If the
64
+ * UDS isn't reachable on the first try, that's a real error worth
65
+ * surfacing — not a transient bring-up race like the `run()` path.
66
+ */
67
+ readonly startPty: (udsPath: string, cmd: string, opts: VsockExecPtyOptions) => VsockExecPtyHandle;
68
+ };
69
+ interface VsockExecPtyOptions {
70
+ /** Initial window size; the guest passes this to forkpty()'s winp. */
71
+ cols: number;
72
+ rows: number;
73
+ /**
74
+ * Host-side input source. Each `data` chunk is forwarded as an
75
+ * `I <n>\n<bytes>` frame. Caller wires `process.stdin` (in raw
76
+ * mode) here for an interactive shell.
77
+ */
78
+ stdin: Readable;
79
+ /**
80
+ * Host-side sink for PTY master output (`O <n>\n<bytes>` frames).
81
+ * Caller wires `process.stdout`.
82
+ */
83
+ stdout: Writable;
84
+ /** Connect timeout (ms). Default 5000 — agent should already be up. */
85
+ connectTimeoutMs?: number;
86
+ }
87
+ interface VsockExecPtyResult {
88
+ exitCode: number;
89
+ }
90
+ interface VsockExecPtyHandle {
91
+ /** Resolves with the workload's exit code once X arrives. */
92
+ readonly result: Promise<VsockExecPtyResult>;
93
+ /** Send a TIOCSWINSZ update. Hook from host's SIGWINCH. */
94
+ resize(cols: number, rows: number): void;
95
+ /** Disconnect; agent will SIGHUP the workload. */
96
+ cancel(): void;
97
+ }
98
+
99
+ interface ChunkLogEvent {
100
+ /**
101
+ * Where the chunk came from:
102
+ * - `guest-console` — kernel / PL011 console bytes (VMM stderr)
103
+ * - `exec-stdout` — stdout of an exec invocation
104
+ * - `exec-stderr` — stderr of an exec invocation
105
+ */
106
+ source: "guest-console" | "exec-stdout" | "exec-stderr";
107
+ /** Command string; set when `source` is `exec-stdout` or `exec-stderr`. */
108
+ cmd?: string;
109
+ /** Raw bytes as they arrive — not line-split, not decoded. */
110
+ chunk: Buffer;
111
+ }
112
+ interface PhaseLogEvent {
113
+ source: "phase";
114
+ /** Which runtime entry point produced these phases. */
115
+ kind: "boot" | "provision" | "snapshot" | "restore";
116
+ /** Phase name → wall-clock ms. Insertion order = timeline order. */
117
+ phases: ReadonlyMap<string, number>;
118
+ /** Wall-clock between PhaseTimer construction and flush. */
119
+ totalMs: number;
120
+ }
121
+ type LogEvent = ChunkLogEvent | PhaseLogEvent;
122
+ type OnLog = (evt: LogEvent) => void;
123
+
124
+ interface AttachOptions {
125
+ /**
126
+ * Look up a VM by the host pid of its VMM process. Kernel-unique
127
+ * while alive; mutually exclusive with `name`. Exactly one of
128
+ * `pid` / `name` is required.
129
+ */
130
+ pid?: number;
131
+ /** Look up a VM by the name passed to `boot({ name })`. */
132
+ name?: string;
133
+ /**
134
+ * Streaming log callback — fires for every byte of output from execs
135
+ * made through the returned handle. See #83. Guest kernel console is
136
+ * not available on attach handles (it belongs to the process that
137
+ * called `boot()`), so only `exec-stdout` / `exec-stderr` sources fire.
138
+ */
139
+ onLog?: OnLog;
140
+ }
141
+ /**
142
+ * Reconnect to a running VM registered by an earlier `boot()` call
143
+ * (possibly from a different process). Returns a `VmHandle` that can
144
+ * `exec()`, `snapshot()`, and `kill()` the remote VM via the vsock
145
+ * bridge the booter left behind.
146
+ *
147
+ * Attached handles have inert stream properties (`stdin`/`stdout`/
148
+ * `stderr` are empty `PassThrough`s) — those belong to the original
149
+ * booter. `output()`/`errorOutput()` resolve with the empty string.
150
+ * `wait()` polls the pid rather than listening for `exit`.
151
+ *
152
+ * @throws {RegistryError} REGISTRY_VM_NOT_FOUND
153
+ */
154
+ declare function attach(opts: AttachOptions): Promise<VmHandle>;
155
+
156
+ interface BootOptions {
157
+ /**
158
+ * Path to a rootfs tarball to boot from (e.g. the output of
159
+ * `provision()`, or `rootfs-debian-arm64.tar.gz` shipped in releases).
160
+ * Paired with `cmd` — both required, or neither (test-mode binary
161
+ * boots and snapshot-only restores both skip initramfs packing).
162
+ */
163
+ image?: string;
164
+ /**
165
+ * Command to run inside the guest. Packed into the synthesized
166
+ * `/machinen-config.json`. Paired with `image` — both required, or
167
+ * neither.
168
+ */
169
+ cmd?: string[];
170
+ /**
171
+ * Env vars exposed to the guest workload. Packed into the synthesized
172
+ * `/machinen-config.json`. Distinct from `vmmEnv`, which only affects
173
+ * the host-side VMM process.
174
+ */
175
+ env?: Record<string, string>;
176
+ /**
177
+ * Working directory for the guest cmd. Lands as `cwd` in the
178
+ * synthesized `/machinen-config.json`; `/init` calls `chdir()` to
179
+ * this path before exec'ing the cmd. Useful with `mount` /
180
+ * `liveMounts` to land directly inside the share (e.g.
181
+ * `guestCwd: "/mnt/workspace"`).
182
+ *
183
+ * Must be absolute. Throws `BOOT_CWD_INVALID` for relative paths or
184
+ * paths containing NULs. Same precedence as `cmd`/`env`: an
185
+ * image-baked `cwd` is overridden by this field when both are set.
186
+ */
187
+ guestCwd?: string;
188
+ /**
189
+ * Attach a scratch virtio-blk device (`/dev/vdb`, or `/dev/vda` on
190
+ * pre-#114 layouts) so this VM can be CRIU-snapshotted later via
191
+ * `vm.snapshot()`. Three forms:
192
+ *
193
+ * - `undefined` (default) — the runtime auto-allocates a per-boot
194
+ * ~8 GiB sparse scratch in `tmpdir()` and unlinks it on VM exit.
195
+ * Disk usage stays at zero until the guest writes; the upside is
196
+ * every booted VM is snapshotable without re-booting. See #50.
197
+ *
198
+ * - `'<path>'` — caller-managed file. Used as-is (must exist).
199
+ * Used by `restore()` to attach a tar archive of the bundle's
200
+ * CRIU images on `/dev/vdb`; the guest's
201
+ * `/sbin/machinen-restore` untars it and runs `criu restore`.
202
+ * The runtime synthesizes `cmd: ['/sbin/machinen-restore']` if
203
+ * no other cmd is given.
204
+ *
205
+ * - `false` — opt out entirely. No `/dev/vdb` attached. Use when
206
+ * you don't need snapshot capability and want to skip the
207
+ * (sparse, but still nonzero) inode allocation — typical for
208
+ * fast-cycling test boots.
209
+ */
210
+ snapshot?: string | false;
211
+ /**
212
+ * Boot the guest with the rootfs on a virtio-blk device (`/dev/vda`)
213
+ * instead of inflating the whole rootfs into a RAM-backed tmpfs via
214
+ * the initramfs. See #114.
215
+ *
216
+ * Default: `true` whenever `image` is set. The runtime materializes
217
+ * an ext4 image from `image` (cached at
218
+ * `~/.cache/machinen/rootfs/<sha256>.img`) and attaches it as the
219
+ * rootdisk; the guest's `/init` mounts + chroots into it before
220
+ * running the user cmd. Materialization needs `mke2fs` (or
221
+ * `mkfs.ext4`) on PATH — `brew install e2fsprogs` on macOS, the
222
+ * `e2fsprogs` package on Linux.
223
+ *
224
+ * - `string` — path to a pre-built ext4 `.img` file to attach
225
+ * directly. Skips the materialize step + cache.
226
+ * - `false` — opt out: keep the cpio-as-rootfs path. The whole
227
+ * rootfs lands in a tmpfs at boot (RAM scales ~8×
228
+ * with rootfs size). Mostly an escape hatch for
229
+ * tooling that doesn't need disk-backed semantics
230
+ * (e.g. `provision()` itself).
231
+ */
232
+ rootDisk?: boolean | string;
233
+ /**
234
+ * Absolute target size (bytes) for the materialized rootdisk image.
235
+ * Defaults to `max(2 GiB, treeBytes * 2.5)` — generous enough that
236
+ * boot-time `npm install -g <large package>` / `apt install ...`
237
+ * land without ENOSPC. Bump this for workloads that write more
238
+ * (e.g. 8 GiB for a build tree, 16 GiB for a model cache).
239
+ *
240
+ * The host file is sparse — unused capacity costs nothing on disk
241
+ * until the guest writes. The guest's online ext4 grow (in /init)
242
+ * resizes the on-disk filesystem to fill the file on every boot,
243
+ * so bumping this against an existing cached image works without
244
+ * a rematerialize.
245
+ *
246
+ * Ignored when `rootDisk` is a string path (the caller-provided
247
+ * image is taken as-is) or `rootDisk: false`. See #131.
248
+ */
249
+ rootDiskSizeBytes?: number;
250
+ /**
251
+ * Optional name to register this VM under (`attach({ name })`
252
+ * lookup key). Path-shaped strings ("worker/9012") are allowed.
253
+ * Names are unique while live — `boot()` throws
254
+ * `REGISTRY_NAME_IN_USE` if another VM already holds the name.
255
+ */
256
+ name?: string;
257
+ /**
258
+ * Bookkeeping: absolute path to the snapshot bundle this VM was
259
+ * forked from. Set by `restore({ snapDir })`; visible in
260
+ * `machinen ls`. Plain `boot()` leaves it undefined.
261
+ */
262
+ forkedFrom?: string;
263
+ /**
264
+ * A single host directory exposed to the guest as a writable
265
+ * filesystem rooted under `/mnt/<guest>/`. Guest writes survive
266
+ * snapshot/restore but never leak to the host source dir.
267
+ *
268
+ * Implementation (#272): the runtime builds a content-addressed
269
+ * read-only squashfs lower from `host` (cached in
270
+ * `~/.cache/machinen/mountdisk/`) and a per-VM ext4 sparse upper
271
+ * (4 GiB by default; bump via `mountDiskUpperSizeBytes`). Both
272
+ * files are fd-passed to the VMM, surfacing inside the guest as
273
+ * `/dev/vdc` (RO) and `/dev/vdd` (RW); /init layers them as a
274
+ * single overlayfs at `<guest>/`. The squashfs lower stays
275
+ * sealed for the VM's lifetime; writes go to the upper, which
276
+ * is reflinked into snapshot bundles so forks see prior writes
277
+ * without touching the source dir.
278
+ *
279
+ * Trade-off vs. `liveMount`: `mount` is copy-into-disk-image (no
280
+ * runtime channel back to the host source dir, snapshots cleanly,
281
+ * but writes don't propagate to the host); `liveMount` is a live
282
+ * vsock-FUSE pass-through (writes land on the host, doesn't survive
283
+ * snapshot/restore). Pick `mount` for inputs the guest may modify
284
+ * but the host shouldn't see; `liveMount` for shared scratch.
285
+ *
286
+ * See #64 (original `mount`), #78 (`liveMount`), #114 (rootdisk
287
+ * relocation; same shape), #272 (this overlay relocation).
288
+ */
289
+ mount?: {
290
+ host: string;
291
+ guest: string;
292
+ };
293
+ /**
294
+ * Absolute target size (bytes) for the per-VM ext4 RW upper of
295
+ * the `--mount` overlay (#272). Sparse, so unused capacity costs
296
+ * nothing on the host disk. Mirrors `rootDiskSizeBytes` (#131) —
297
+ * over-provision so the guest has plenty of room to write into
298
+ * the mount before hitting ENOSPC.
299
+ *
300
+ * Must be a positive multiple of 4096. Default 4 GiB.
301
+ */
302
+ mountDiskUpperSizeBytes?: number;
303
+ /**
304
+ * Internal: when set, skips the squashfs+ext4 materialization
305
+ * pipeline and uses pre-existing lower/upper files (typically the
306
+ * ones a snapshot bundle carries). Used by `restore()` to
307
+ * reconstruct the overlay without re-running `mksquashfs` on the
308
+ * host source dir (which may not exist on the restoring host).
309
+ *
310
+ * The runtime reflinks `upperPath` into a per-VM path so guest
311
+ * writes don't mutate the bundle in-place.
312
+ *
313
+ * @internal
314
+ */
315
+ _restoreMountDisk?: {
316
+ guest: string;
317
+ lowerPath: string;
318
+ upperPath: string;
319
+ };
320
+ /**
321
+ * Host directories exposed to the guest as live-share FUSE mounts
322
+ * (#78). Unlike `mount` (copy-once into the boot rootfs), these stay
323
+ * connected to the host: the guest reads on demand via a vsock FUSE
324
+ * relay, and nothing is copied at boot. `mode` defaults to `"rw"` —
325
+ * guest writes land on the host (#151, #156). Set `"ro"` for a
326
+ * one-way share (host caches, untrusted guests).
327
+ *
328
+ * Each guest path must live under `/mnt/` (same rule as `mount`).
329
+ * Repeatable; each entry gets its own vsock port.
330
+ *
331
+ * Snapshot / restore / fork (#273): liveMount has no guest-side
332
+ * state worth checkpointing — reads come from the host on demand,
333
+ * writes (in `"rw"`) land on the host immediately. The runtime
334
+ * unmounts each mount before CRIU dumps, then re-establishes a
335
+ * fresh window on the other side: for `vm.snapshot({ leaveRunning:
336
+ * true })` and `vm.fork()` the source's workload sees `/mnt/<guest>/`
337
+ * disappear for the dump duration (typically seconds, scales with
338
+ * memory size) before reappearing under fresh server state. Open
339
+ * fds across that window see EBADF on next syscall — same shape
340
+ * as "don't snapshot during a database write." Workloads that
341
+ * quiesce before snapshot are unaffected.
342
+ *
343
+ * Concurrent writes from multiple forks against the same host
344
+ * directory are no different from any other shared filesystem —
345
+ * the runtime re-establishes the window per-VM but doesn't
346
+ * coordinate writes between siblings. If two forks need
347
+ * non-overlapping write surfaces, point each at a distinct
348
+ * `host` path or use `mount` (copy-once, per-VM upper).
349
+ *
350
+ * Restore on a host where the recorded `host` path doesn't exist:
351
+ * fails loudly via `BOOT_MOUNT_HOST_NOT_FOUND`. Pass
352
+ * `restore({ liveMounts: [...] })` to override per-`guest` —
353
+ * each override entry's `guest` must match a recorded entry.
354
+ *
355
+ * Security note: a live-share mount gives a compromised guest a
356
+ * persistent channel back to the host filesystem. Containment keeps
357
+ * that bounded to the configured host root. `mount` (copy-once) has
358
+ * no such runtime channel and is strictly safer — prefer it for
359
+ * inputs you don't need write-through on.
360
+ */
361
+ liveMounts?: Array<{
362
+ host: string;
363
+ guest: string;
364
+ mode?: "ro" | "rw";
365
+ }>;
366
+ /**
367
+ * Host -> guest TCP port forwards installed via gvproxy's control
368
+ * API. Each entry maps `hostPort` on the host (bound to `hostAddr`,
369
+ * default `127.0.0.1`) to `guestPort` inside the guest.
370
+ */
371
+ portForward?: Array<{
372
+ hostPort: number;
373
+ guestPort: number;
374
+ hostAddr?: string;
375
+ }>;
376
+ /**
377
+ * Absolute or cwd-relative path to the VMM binary. Optional —
378
+ * if omitted, `boot()` resolves it via `resolveVmmBinary()`.
379
+ */
380
+ binary?: string;
381
+ /** Working directory for the VMM (for finding fixture files). */
382
+ cwd?: string;
383
+ /** Extra argv for the VMM. */
384
+ args?: string[];
385
+ /** Path to the guest kernel Image. Forwarded as `MACHINEN_KERNEL`. */
386
+ kernel?: string;
387
+ /** Path to the guest device-tree blob. Forwarded as `MACHINEN_DTB`. */
388
+ dtb?: string;
389
+ /**
390
+ * Guest RAM ceiling, in MiB (decimal integer; no unit suffixes). The
391
+ * VMM reads this as `MACHINEN_MEMORY` (#263 phase A). Defaults to
392
+ * `min(host_ram_mib / 2, 16384)` with a floor of 512 — sized for
393
+ * typical dev workloads while leaving the host responsive. The
394
+ * ceiling is approximately free until the guest touches a page (see
395
+ * `packages/microvm/docs/memory.md`), so over-provisioning costs
396
+ * little until phase B's balloon lands and lets it actually shrink.
397
+ *
398
+ * This is documented as a debug knob — most workloads should never
399
+ * need to set it.
400
+ */
401
+ memory?: number;
402
+ /**
403
+ * Wrap the VMM through the parent-death shim so it dies with this
404
+ * runtime process. Default true — the right answer for the common
405
+ * "boot, do work, exit" CLI flow.
406
+ *
407
+ * Set to false when the VMM is supposed to outlive the spawning
408
+ * process. `vm.fork()` (#216) sets this so the forked sibling
409
+ * survives `cli fork` returning. Without it, the kqueue-watching
410
+ * shim catches the CLI exit and SIGTERMs the fork mid-startup.
411
+ */
412
+ pdeathsig?: boolean;
413
+ /**
414
+ * Milliseconds to wait in `wait()` before giving up and rejecting.
415
+ * Defaults to 60s. Pass `null` to wait forever.
416
+ */
417
+ timeoutMs?: number | null;
418
+ /**
419
+ * Env passed to the VMM process on the host side (not exposed to the
420
+ * guest workload). Mostly for dev/test flags like `MACHINEN_BOOT_TEST`.
421
+ */
422
+ vmmEnv?: Record<string, string>;
423
+ /**
424
+ * Streaming log callback — fires for every byte of guest output:
425
+ * kernel console (VMM stderr) and every exec invocation made through
426
+ * the returned handle. See `LogEvent.source` to tell them apart. See
427
+ * #83. For per-call output-only tees on a single exec, use
428
+ * `vm.exec({ onStdout, onStderr })` instead.
429
+ */
430
+ onLog?: OnLog;
431
+ /**
432
+ * Detach the VMM from the runtime parent so the parent can exit
433
+ * while the VM keeps running (issue #150 phase 2). When set, `boot()`
434
+ * blocks only until the guest produces its first console byte
435
+ * (readiness signal) and then resolves a handle whose `.wait()` /
436
+ * `.output()` no longer reflect the live VM — the parent has unrefed
437
+ * the child and is free to exit.
438
+ *
439
+ * Forces `pdeathsig: false` (otherwise the parent's exit kills the
440
+ * VMM, defeating the purpose). Compatible with every other boot
441
+ * option: gvproxy + live-mount FUSE servers spawn as detached
442
+ * daemons wrapped through `pdeathsig --watch-pid <vmm>`, and `mount`
443
+ * (squashfs+ext4 overlay) is fd-passed to the VMM at spawn so the
444
+ * supervisor holds no live state afterwards.
445
+ *
446
+ * Cleanup of per-boot reflink disks, bundle dirs, and vsock UDS
447
+ * directories normally happens in the parent's `child.once("exit")`
448
+ * hook. After detach the parent is gone, so those leak until the
449
+ * follow-up `machinen gc` / `machinen stop` commands (PR2 of #150)
450
+ * land. Use `--detached` only when you understand that trade-off.
451
+ *
452
+ * Reattach with `attach({ name | pid })` from another process —
453
+ * the registry entry stays live, the vsock UDS is still listening.
454
+ */
455
+ detached?: boolean;
456
+ }
457
+ /**
458
+ * Boot a microVM and return a handle to interact with it.
459
+ *
460
+ * @throws {BootError} BOOT_VMM_MISSING | BOOT_VMM_PACKAGE_BROKEN |
461
+ * BOOT_IMAGE_NOT_FOUND | BOOT_SNAPSHOT_NOT_FOUND |
462
+ * BOOT_KERNEL_NOT_FOUND | BOOT_DTB_NOT_FOUND |
463
+ * BOOT_CMD_WITHOUT_IMAGE | BOOT_CMD_MISSING |
464
+ * BOOT_MOUNT_INVALID | BOOT_MOUNT_HOST_NOT_FOUND |
465
+ * BOOT_PORT_FORWARD_INVALID | BOOT_PORT_FORWARD_CONFLICT |
466
+ * BOOT_PORT_FORWARD_NO_GVPROXY | BOOT_PORT_FORWARD_IN_USE |
467
+ * BOOT_PACK_FAILED
468
+ */
469
+ declare function boot(opts?: BootOptions): Promise<VmHandle>;
470
+
471
+ /**
472
+ * A caller-provided `liveMounts` entry after validation, with the
473
+ * vsock port + host UDS path allocated. Threaded from `boot()` into
474
+ * the initramfs packer so the config and the host servers agree on
475
+ * ports and guest paths.
476
+ */
477
+ interface ResolvedLiveMount {
478
+ host: string;
479
+ guest: string;
480
+ port: number;
481
+ udsPath: string;
482
+ /**
483
+ * Per-mount stats file the detached helper writes its
484
+ * bytesServedOnPagesImg counter to. Lives next to `udsPath` under
485
+ * `vsockTempDir` so the supervisor's cleanupPaths sweep covers it.
486
+ */
487
+ statsPath: string;
488
+ mode: "ro" | "rw";
489
+ }
490
+ /**
491
+ * Build the synthesized `machinen-config.json` payload that /init
492
+ * reads at boot. Pure: takes the already-merged effective cmd/env
493
+ * plus the cwd inputs (user's guestCwd overrides image-baked cwd) and
494
+ * the live-mount ports.
495
+ *
496
+ * Exposed for tests; `synthesizeAndPackBundle` is the only production
497
+ * caller.
498
+ *
499
+ * @internal
500
+ */
501
+ declare function buildMachinenConfig(input: {
502
+ cmd: string[];
503
+ env: Record<string, string>;
504
+ guestCwd?: string;
505
+ imageCwd?: string;
506
+ liveMounts: ResolvedLiveMount[];
507
+ }): Record<string, unknown>;
508
+
509
+ /**
510
+ * Time-to-first-output-byte for a boot. Useful for measuring how
511
+ * much the snapshot path is (or isn't) buying us.
512
+ */
513
+ declare function measureFirstByte(vm: VmHandle): Promise<number>;
514
+
515
+ declare function autoSizeMemoryMib(hostBytes?: number): number;
516
+ declare function validateMemoryMib(mib: number): number;
517
+ /**
518
+ * Locate the VMM binary using the same lookup order as `@machinen/cli`:
519
+ * 1. `MACHINEN_VMM` env var (dev-mode override)
520
+ * 2. `require.resolve("@machinen/vmm-<arch>-<os>")` → `binary` export
521
+ *
522
+ * Callers can pass an explicit `binary` to `boot()` to bypass this.
523
+ *
524
+ * @throws {BootError} BOOT_VMM_MISSING | BOOT_VMM_PACKAGE_BROKEN
525
+ */
526
+ declare function resolveVmmBinary(): string;
527
+ /**
528
+ * Build the shell pipeline that `vm.writeFile()` ships through the
529
+ * exec-agent. Stays single-line so it works against the legacy EXEC
530
+ * opcode too (no need for the EXEC2 multi-line frame, which only newer
531
+ * agents understand).
532
+ *
533
+ * Encoding: contents go over the wire as base64 inside an `echo … |
534
+ * base64 -d` pipe, so any byte sequence (binary, newlines, quotes) is
535
+ * safe. `mkdir -p` runs first when `recursive` (the default).
536
+ *
537
+ * Returns a single cmd string. For payloads that would exceed Linux's
538
+ * `MAX_ARG_STRLEN` (128 KB per argv element) once shell-wrapped, use
539
+ * `buildWriteFileCmds` instead — `vm.writeFile()` does.
540
+ */
541
+ declare function buildWriteFileCmd(guestPath: string, contents: Buffer | string, opts?: WriteFileOptions): string;
542
+ /**
543
+ * Plan the cmd sequence `vm.writeFile()` issues for `contents`.
544
+ * Small payloads (base64 ≤ `WRITE_FILE_B64_CHUNK_BYTES`) collapse to a
545
+ * single cmd identical to `buildWriteFileCmd`'s output. Larger payloads
546
+ * stage the base64 to /tmp in append-chunks and then decode once at the
547
+ * end, so no individual cmd line approaches `MAX_ARG_STRLEN`.
548
+ */
549
+ declare function buildWriteFileCmds(guestPath: string, contents: Buffer | string, opts?: WriteFileOptions): string[];
550
+ declare function collect(stream: Readable, capBytes?: number): Promise<string>;
551
+
552
+ /**
553
+ * Shape of the optional `./machinen-config.json` baked into a rootfs
554
+ * tarball by `provision({ cmd, env })`. `boot()` reads it via
555
+ * `readImageConfig()` so callers don't need to re-pass `cmd`/`env` on
556
+ * every boot. `warmImageConfigCache()` accepts the same shape so a
557
+ * tarball-producing tool can pre-populate the lookup cache.
558
+ */
559
+ type ImageConfig = {
560
+ cmd?: string[];
561
+ env?: Record<string, string>;
562
+ cwd?: string;
563
+ };
564
+ /**
565
+ * Pre-populate the image-config cache for a freshly-written tarball.
566
+ * Lets `provision()` (and other tarball producers) skip the slow
567
+ * `tar -xzOf` lookup that the next `boot()` would otherwise pay —
568
+ * see #233. Best-effort: a missing/unwritable cache dir just falls
569
+ * back to the slow path on the next boot.
570
+ *
571
+ * Call AFTER the tarball is on disk (so size+mtime match what the
572
+ * cache key will be on read), passing exactly the config that was
573
+ * baked into the tarball's `./machinen-config.json` (or `null` when
574
+ * none was baked).
575
+ */
576
+ declare function warmImageConfigCache(imagePath: string, config: ImageConfig | null): void;
577
+
578
+ interface RestoreOptions extends Omit<BootOptions, "snapshot" | "image" | "cmd" | "name"> {
579
+ /**
580
+ * Snapshot bundle directory produced by `vm.snapshot()`.
581
+ * Must contain `img/<crius>` and `meta.json`.
582
+ */
583
+ snapDir: string;
584
+ /**
585
+ * Override the rootfs image used for the restore boot. Defaults
586
+ * to whatever caller passes through `image`-equivalent — but
587
+ * `restore()` always needs a base rootfs in the initramfs to
588
+ * carry /sbin/machinen-restore + criu. Most callers pass the
589
+ * release rootfs path here.
590
+ */
591
+ image?: string;
592
+ /**
593
+ * Optional explicit name for the restored VM. When omitted, the
594
+ * fork is auto-named `<sourceName>/<pid>` after spawn so it stays
595
+ * unique under the source's namespace.
596
+ */
597
+ name?: string;
598
+ /**
599
+ * Opt into lazy-pages restore — bundle is vsock-FUSE-mounted into
600
+ * the guest read-only and `criu restore --lazy-pages` faults pages
601
+ * on demand (#266). Default false: the runtime packs the CRIU
602
+ * image into a tar on `/dev/vdb`, the guest's
603
+ * `/sbin/machinen-restore` untars it into tmpfs, and CRIU does an
604
+ * eager load.
605
+ *
606
+ * Eager is still the default because lazy bundles a host-side FUSE
607
+ * server that doesn't compose with `--detach` (#150 phase 3). The
608
+ * historical second blocker — runaway free-page-reporting under
609
+ * lazy — is fixed in #290 by the in-tree kernel patch that stops
610
+ * the buddy allocator from clearing the Reported flag during a
611
+ * merge.
612
+ */
613
+ lazy?: boolean;
614
+ }
615
+ /**
616
+ * Restore a microVM from a snapshot bundle produced by
617
+ * `vm.snapshot({ outDir })`. Reads the bundle's `meta.json` to
618
+ * recover the source name, tars the CRIU image directory into a
619
+ * temporary archive, then `boot()`s with that archive attached as
620
+ * the scratch block device — the guest's `/sbin/machinen-restore`
621
+ * untars `/dev/vdb` into tmpfs and runs `criu restore` against the
622
+ * extracted images.
623
+ *
624
+ * The boot knobs:
625
+ *
626
+ * - `snapshot: <tar>` attaches the bundle archive as /dev/vdb
627
+ * - `name: <sourceName>/<pid>` auto-named fork (unless caller
628
+ * passed `name`)
629
+ * - `forkedFrom: <snapDir>` lineage for `machinen ls`
630
+ *
631
+ * Live-share mounts (#273): bundles created with active `liveMounts`
632
+ * carry only the `{guest, host, mode}` triples in `meta.liveMounts`
633
+ * — no bytes. By default `restore()` re-establishes each recorded
634
+ * mount as-is; the boot-time `existsSync(host)` check fails loudly
635
+ * (BOOT_MOUNT_HOST_NOT_FOUND) if the recorded host path is gone on
636
+ * the restoring host. Pass `liveMounts: [...]` to override per-
637
+ * `guest` (e.g. cross-host restore with remapped paths). Each
638
+ * override entry's `guest` MUST match a recorded one — the field is
639
+ * an override map, not an additive list. Bundles predating this
640
+ * field have `meta.liveMounts === undefined`; in that case
641
+ * `opts.liveMounts` is forwarded as-is for backward compatibility.
642
+ *
643
+ * @throws {BootError} BOOT_SNAPSHOT_NOT_FOUND if `<snapDir>/img/`
644
+ * is missing or empty.
645
+ * @throws {BootError} BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN if an entry in
646
+ * `opts.liveMounts` has a `guest` that doesn't appear in the
647
+ * bundle's `meta.liveMounts`.
648
+ */
649
+ declare function restore(opts: RestoreOptions): Promise<VmHandle>;
650
+
651
+ declare const _internal: {
652
+ collect: typeof collect;
653
+ CONSOLE_TAIL_BYTES: number;
654
+ validateMemoryMib: typeof validateMemoryMib;
655
+ };
656
+
657
+ interface VmHandle {
658
+ /**
659
+ * PID of the host-side VMM process — primary identifier across
660
+ * boot/attach. Kernel-unique while alive; reused after exit, so
661
+ * pass it to `attach({ pid })` while the VM is live (or use
662
+ * `--name` for a stable handle).
663
+ */
664
+ readonly pid: number;
665
+ /** Optional human-friendly name passed to `boot({ name })`. */
666
+ readonly name?: string;
667
+ readonly stdin: Writable;
668
+ readonly stdout: Readable;
669
+ readonly stderr: Readable;
670
+ /** Resolves when the VM process exits. Rejects on timeout. */
671
+ wait(): Promise<{
672
+ code: number | null;
673
+ signal: NodeJS.Signals | null;
674
+ }>;
675
+ /** Send SIGKILL to the VM. Resolves once it's really gone. */
676
+ kill(): Promise<void>;
677
+ /**
678
+ * Drop this host-side handle without killing the VMM. The VM keeps
679
+ * running and can be re-attached from another process. For locally-
680
+ * booted handles this closes captured streams; `wait()` and
681
+ * `exec()` become unreliable afterwards.
682
+ */
683
+ detach(): Promise<void>;
684
+ /**
685
+ * Buffer stdout until the process exits; return it as a UTF-8 string.
686
+ * Capped at ~1 MiB tail — long-running VMs keep only the most recent
687
+ * bytes (issue #150). Sufficient for kernel boot console + test
688
+ * assertions; not a full transcript.
689
+ */
690
+ output(): Promise<string>;
691
+ /** Same as `output()` but for stderr (where guest console lands). */
692
+ errorOutput(): Promise<string>;
693
+ /**
694
+ * Run a shell command inside the guest via the vsock exec-agent. Throws
695
+ * BootError on non-zero exit; callers who want to inspect failure
696
+ * should use `execRaw`.
697
+ *
698
+ * Requires the rootfs to have the exec-agent running on vsock port 1978
699
+ * (the standard debian base ships it). The vsock bridge is set up
700
+ * automatically by `boot()` unless the caller pre-set MACHINEN_VSOCK.
701
+ */
702
+ exec(cmd: string, opts?: VsockExecOptions): Promise<VsockExecResult>;
703
+ /** Like `exec()` but returns non-zero exit codes instead of throwing. */
704
+ execRaw(cmd: string, opts?: VsockExecOptions): Promise<VsockExecResult>;
705
+ /**
706
+ * Run a shell command inside a pseudoterminal. Bidirectional bytes
707
+ * flow between `opts.stdin` and `opts.stdout`; the returned handle's
708
+ * `.resize(cols, rows)` propagates window-size changes (hook your
709
+ * host's SIGWINCH).
710
+ *
711
+ * Caller is responsible for putting the host terminal in raw mode
712
+ * before calling and restoring it after `.result` settles — without
713
+ * raw mode, Ctrl-C / arrow keys / etc. won't reach the guest as
714
+ * untranslated bytes. See #133.
715
+ */
716
+ execPty(cmd: string, opts: VsockExecPtyOptions): VsockExecPtyHandle;
717
+ /**
718
+ * Write `contents` to `guestPath` inside the VM. Convenience over
719
+ * `vm.exec(...)` for the common "drop a config file from the host"
720
+ * case — no quoting/heredoc gymnastics, binary-safe via base64.
721
+ *
722
+ * Parent directories are created by default (`recursive: true`).
723
+ * Pass `mode` to set the file mode (octal, e.g. `0o755`).
724
+ * Pass `append: true` to append instead of overwrite.
725
+ *
726
+ * Best for small-to-medium files (configs, scripts) — the contents
727
+ * ride through a single vsock exec frame, so very large blobs are
728
+ * better handled with `--mount` / `VsockFiles.push`.
729
+ *
730
+ * Throws `ExecError` (`EXEC_NONZERO_EXIT`) if the underlying shell
731
+ * write fails (e.g. permissions, full disk, missing `base64`).
732
+ *
733
+ * @throws {ExecError} EXEC_VSOCK_UNAVAILABLE | EXEC_NONZERO_EXIT |
734
+ * EXEC_AGENT_UNAVAILABLE (retryable) | EXEC_AGENT_TIMEOUT (retryable)
735
+ */
736
+ writeFile(guestPath: string, contents: Buffer | string, opts?: WriteFileOptions): Promise<void>;
737
+ /**
738
+ * Freeze this VM with CRIU and write a snapshot bundle into
739
+ * `opts.outDir`. The bundle is a directory containing:
740
+ *
741
+ * <outDir>/img/ ← CRIU image files (pages-*.img,
742
+ * pagemap-*.img, core-*.img,
743
+ * dump.log, ...)
744
+ * <outDir>/meta.json ← source name + timestamp +
745
+ * optional mountDisk pointers
746
+ * <outDir>/mount-lower.sqfs ← squashfs RO lower (only when
747
+ * the source VM had `mount` set)
748
+ * <outDir>/mount-upper.img ← ext4 RW upper (only when
749
+ * the source VM had `mount` set)
750
+ *
751
+ * `mount-lower.sqfs` and `mount-upper.img` are reflinked from the
752
+ * runtime's per-VM materialization (#272), so on APFS / btrfs / xfs
753
+ * the snapshot is essentially free space-wise even for a large
754
+ * mount payload — blocks stay shared until either side writes.
755
+ *
756
+ * The caller must have booted the VM with a scratch disk (`snapshot:
757
+ * '<path>'` or default auto-allocation) so the guest had `/dev/vdb`
758
+ * to dump into; otherwise this throws `SNAPSHOT_NO_DISK`.
759
+ *
760
+ * Guest contract: the rootfs ships a dump helper callable via
761
+ * vsock exec — default `/sbin/machinen-dump`, override via
762
+ * `opts.dumpCmd`. The helper runs `criu dump --leave-running` and
763
+ * tars the resulting image set out on stdout, which the host
764
+ * extracts into `<outDir>/img/`. For destructive snapshots (default)
765
+ * the runtime then issues `/sbin/machinen-poweroff` over vsock to
766
+ * bring the VMM down; `opts.leaveRunning: true` skips that step
767
+ * and the source VM keeps running.
768
+ *
769
+ * `SNAPSHOT_TIMEOUT` if the dump exec doesn't return within
770
+ * `opts.timeoutMs`; `SNAPSHOT_DUMP_FAILED` if it returns non-zero
771
+ * or the streamed bundle is empty.
772
+ *
773
+ * Supported on both boot-owned and attach handles — attach uses
774
+ * the `diskPath` stored in the VM registry entry at boot time.
775
+ *
776
+ * By default the VM exits as part of the dump (CRIU kills the
777
+ * dumped tree on success). Pass `opts.leaveRunning: true` to keep
778
+ * the source VM alive — the workload resumes from the dump point
779
+ * and the bundle can be restored into a sibling VM (`vm.fork()`).
780
+ */
781
+ snapshot(opts: SnapshotOptions): Promise<SnapshotResult>;
782
+ /**
783
+ * Read the host's view of this VM's memory: the ceiling the VMM was
784
+ * sized at, the host RSS the VMM is currently holding, the bytes
785
+ * the virtio-balloon device has reported back to the host, and the
786
+ * count of lazy-restore pages the guest hasn't faulted in yet (#274).
787
+ *
788
+ * Pure read, no side effects. The numbers come from:
789
+ * - `ceiling` — captured at boot from the resolved
790
+ * `MACHINEN_MEMORY` env (fork: from the
791
+ * registry entry).
792
+ * - `hostRss` — `/proc/<vmm>/status:VmRSS` on Linux,
793
+ * `ps -o rss=` on Darwin. May be `null`
794
+ * if the VMM exited between calls.
795
+ * - `balloonInflated` — running total of bytes the balloon
796
+ * device has reclaimed via free-page
797
+ * reporting (`mmap MAP_FIXED` on the
798
+ * reported runs). Read out of the shared
799
+ * stats file the VMM mmaps at startup.
800
+ * `0` when the VMM was launched without
801
+ * `MACHINEN_STATS_FILE`.
802
+ * - `lazyPagesPending` — for forks restored lazily (#266), the
803
+ * count of pages the rewriter marked
804
+ * PE_LAZY at restore time minus pages
805
+ * served from `pages-*.img` over the
806
+ * FUSE mount since. `0` for eager
807
+ * restores and plain boots.
808
+ */
809
+ memoryStats(): Promise<MemoryStats>;
810
+ /**
811
+ * Snapshot this VM without killing it and immediately restore the
812
+ * bundle into a new sibling VM. Both source and fork keep running,
813
+ * independently addressable. See #216.
814
+ *
815
+ * Wraps `vm.snapshot({ leaveRunning: true })` + `restore()` with
816
+ * the safety defaults a fork wants:
817
+ * - `tcpKeep: false` (default) → the fork sees ECONNRESET on
818
+ * inherited TCP sockets, source keeps them. Set `tcpKeep: true`
819
+ * if you want both copies to share state (rarely correct).
820
+ * - `portForward: []` (default) → host ports are NOT inherited
821
+ * (they're global; source + fork would race). Pass new
822
+ * forwards explicitly.
823
+ *
824
+ * Returns a handle to the forked VM. The source VM is unaffected
825
+ * apart from being briefly frozen during `criu dump`.
826
+ *
827
+ * Bundle lifecycle: when `opts.outDir` is set, the bundle is kept
828
+ * and you can re-restore from it. When omitted, the bundle is
829
+ * written to a temp dir and removed when the fork exits.
830
+ */
831
+ fork(opts?: ForkOptions): Promise<VmHandle>;
832
+ }
833
+ /**
834
+ * Host-observable memory state for one VM (#274). All four fields are
835
+ * snapshots of "now" — call `memoryStats()` again to refresh.
836
+ */
837
+ interface MemoryStats {
838
+ /**
839
+ * Ceiling the VMM was sized at (MiB). The actual RSS climbs into
840
+ * this on demand and is reclaimed by the balloon (#263 phase B);
841
+ * the ceiling itself is fixed for the lifetime of the VM. `null`
842
+ * when the runtime didn't pick the value (caller pre-set
843
+ * `MACHINEN_MEMORY` via `vmmEnv`) — we won't honestly report a
844
+ * number we don't own.
845
+ */
846
+ ceilingMib: number | null;
847
+ /**
848
+ * Resident bytes the host kernel sees the VMM holding. `null`
849
+ * when the VMM has exited or `/proc/<pid>/status` / `ps` couldn't
850
+ * be read.
851
+ */
852
+ hostRssBytes: number | null;
853
+ /**
854
+ * Bytes the virtio-balloon device has reclaimed via free-page
855
+ * reporting since the VMM started. Strictly increases over the
856
+ * VMM's lifetime; if `hostRssBytes` is well below ceiling, balloon
857
+ * reclaim is the reason. Read out of the shared stats file the VMM
858
+ * writes via `MACHINEN_STATS_FILE`. `0` when the VMM was launched
859
+ * without that env var.
860
+ */
861
+ balloonInflatedBytes: number;
862
+ /**
863
+ * Pages the lazy-restore path (#266) has registered as PE_LAZY but
864
+ * the guest hasn't faulted in yet. Approximated as
865
+ * `entriesFlagged - bytesServedFromPagesImg / 4096`, clamped to
866
+ * `>= 0`. `0` for eager restores and plain (non-restored) boots.
867
+ */
868
+ lazyPagesPending: number;
869
+ }
870
+ interface WriteFileOptions {
871
+ /** Octal mode for the destination file (e.g. `0o755`). Default: leave as-is. */
872
+ mode?: number;
873
+ /** `mkdir -p` the parent directory before writing. Default: true. */
874
+ recursive?: boolean;
875
+ /** Append to the file instead of overwriting. Default: false. */
876
+ append?: boolean;
877
+ }
878
+ /**
879
+ * Options for `vm.snapshot(opts)`.
880
+ *
881
+ * Live-share mount note (#273): VMs booted with `liveMounts: [...]`
882
+ * are snapshottable. The runtime unmounts each FUSE mount before
883
+ * CRIU dumps and (for `leaveRunning: true`) re-establishes them
884
+ * after. Bytes are NOT captured into the bundle — only the host
885
+ * path / guest path / mode get recorded in `meta.liveMounts` so
886
+ * `restore()` can reconnect a live window on the other side. See
887
+ * the `liveMounts` doc on `BootOptions` for the full contract.
888
+ */
889
+ interface SnapshotOptions {
890
+ /**
891
+ * Directory the snapshot bundle is written to. Created if missing
892
+ * and required to be empty (or absent) so a previous snapshot
893
+ * can't be silently overwritten.
894
+ */
895
+ outDir: string;
896
+ /**
897
+ * Command to run in the guest to trigger the CRIU dump. Defaults to
898
+ * `/sbin/machinen-dump`.
899
+ */
900
+ dumpCmd?: string;
901
+ /**
902
+ * Wall-clock ceiling for the dump + shutdown. If the VMM hasn't exited
903
+ * in this window we SIGKILL it and fail. Default 90s.
904
+ */
905
+ timeoutMs?: number;
906
+ /**
907
+ * Streaming log callback — fires for every byte the dump emits
908
+ * (guest console + the dump exec). See #83. When both the snapshot
909
+ * call and `boot({ onLog })` have a callback set, both fire.
910
+ */
911
+ onLog?: OnLog;
912
+ /**
913
+ * Pass `--leave-running` to `criu dump` so the source workload
914
+ * survives the snapshot. The VMM stays up after the dump; success
915
+ * is signalled by the dump exec returning 0 instead of by VMM exit.
916
+ * Used by `vm.fork()` (#216).
917
+ *
918
+ * Default: false (current destructive snapshot behavior).
919
+ */
920
+ leaveRunning?: boolean;
921
+ /**
922
+ * Omit `--tcp-established` from `criu dump`. Restored sockets come
923
+ * back in CLOSED state — the workload sees ECONNRESET on first
924
+ * I/O, which is the right semantic when the dump is the source for
925
+ * a fork (otherwise both copies would race on the same connection
926
+ * state). See #216.
927
+ *
928
+ * Default: false (preserve TCP — current snapshot/restore behavior).
929
+ */
930
+ tcpClose?: boolean;
931
+ }
932
+ interface SnapshotResult {
933
+ /** Absolute path to the snapshot bundle directory. */
934
+ snapDir: string;
935
+ /** Absolute path to the CRIU image directory inside the bundle. */
936
+ imgDir: string;
937
+ /** Time from `snapshot()` entry to VMM exit, in milliseconds. */
938
+ elapsedMs: number;
939
+ /** Guest console output captured during the dump. */
940
+ consoleLog: string;
941
+ }
942
+ /**
943
+ * On-disk shape of the bundle's `meta.json`. Read by `restore()`
944
+ * to reconstruct the source VM's name when registering the fork.
945
+ */
946
+ interface SnapshotMeta {
947
+ /** Name passed to `boot({ name })` when the source VM was started. */
948
+ sourceName?: string;
949
+ /**
950
+ * Absolute path of the rootfs tarball the source VM was booted with
951
+ * (`boot({ image })` or its restored equivalent). `restore()` uses
952
+ * this as the default rootfs, so the same-host quickstart works
953
+ * without callers having to repeat the image path. Cross-host
954
+ * restores need either the path to resolve on the new host, or an
955
+ * explicit `image` override.
956
+ */
957
+ sourceImage?: string;
958
+ /** ms epoch when `vm.snapshot()` returned. */
959
+ snappedAt: number;
960
+ /**
961
+ * #272: when the source VM was booted with `mount: { host, guest }`,
962
+ * the snapshot bundle includes both halves of the overlay so a
963
+ * restore (same- or cross-host) can mount the same overlay without
964
+ * consulting the host source dir.
965
+ * - `guest`: absolute guest path the overlay mounts at.
966
+ * - `lower`: basename of the squashfs RO lower in the bundle dir.
967
+ * - `upper`: basename of the ext4 RW upper in the bundle dir.
968
+ */
969
+ mountDisk?: {
970
+ guest: string;
971
+ lower: string;
972
+ upper: string;
973
+ };
974
+ /**
975
+ * #273: live-share FUSE mounts (`liveMounts: [...]` at boot) the
976
+ * source VM had at snapshot time. Unlike `mountDisk`, no bytes are
977
+ * captured — `host` is the path on the host that was being live-
978
+ * shared, recorded so `restore()` can re-establish the same window
979
+ * on the restoring host. Each entry is the resolved config from the
980
+ * source's `resolveLiveMounts()`:
981
+ * - `guest`: absolute guest path the FUSE mount lands at.
982
+ * - `host`: absolute host path that was being shared.
983
+ * - `mode`: `"ro"` or `"rw"`, the share's write semantics.
984
+ *
985
+ * Restore policy: the bundle's recorded mounts are re-established
986
+ * verbatim by default. Pass `restore({ liveMounts })` to override
987
+ * per-guest `host`/`mode` — each override entry's `guest` must
988
+ * match a recorded entry, else BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN.
989
+ * Cross-host bundles where a recorded `host` doesn't exist on the
990
+ * restoring host fail loudly via the boot-time existence check —
991
+ * users remap with the override knob.
992
+ */
993
+ liveMounts?: Array<{
994
+ guest: string;
995
+ host: string;
996
+ mode: "ro" | "rw";
997
+ }>;
998
+ }
999
+ /**
1000
+ * Fork = `vm.snapshot({ leaveRunning: true })` + `restore(...)` rolled
1001
+ * into one call. The shape mirrors `RestoreOptions` (so anything you
1002
+ * could pass to `restore()` works on a fork) plus two fork-only knobs:
1003
+ * `outDir` (where to write the bundle) and `tcpKeep` (snapshot half).
1004
+ *
1005
+ * Notably this means `mount`, `liveMounts`, `env`, `guestCwd`, `memory`,
1006
+ * etc. are all settable on the fork — they take effect on the restored
1007
+ * sibling, not the source.
1008
+ *
1009
+ * `snapDir` is omitted because `vm.fork()` produces the bundle itself.
1010
+ * Re-included here are the fork-shaped docs for `name`, `portForward`,
1011
+ * `timeoutMs`, and `onLog` so call sites see the fork-specific defaults
1012
+ * instead of the boot/restore ones.
1013
+ */
1014
+ interface ForkOptions extends Omit<RestoreOptions, "snapDir"> {
1015
+ /**
1016
+ * If set, the snapshot bundle is written here and kept after the
1017
+ * fork exits — re-restore from this path to spawn another sibling.
1018
+ * If omitted, the bundle is written to a temp dir and removed
1019
+ * when the fork's VMM exits.
1020
+ */
1021
+ outDir?: string;
1022
+ /**
1023
+ * Default false: omit `--tcp-established` from the dump so the
1024
+ * fork sees ECONNRESET on sockets the source had open. Set true
1025
+ * to clone live TCP state into the fork (both VMs then race on
1026
+ * the same connection — only correct in narrow scenarios).
1027
+ */
1028
+ tcpKeep?: boolean;
1029
+ /**
1030
+ * Name for the forked VM. When omitted, restore()'s auto-naming
1031
+ * kicks in: `<sourceName>/<fork.pid>`.
1032
+ */
1033
+ name?: string;
1034
+ /**
1035
+ * Host→guest port forwards for the fork. NOT inherited from the
1036
+ * source — host ports are global and source + fork would race on
1037
+ * the same bind. Pass explicitly when the fork needs forwards.
1038
+ */
1039
+ portForward?: Array<{
1040
+ hostPort: number;
1041
+ guestPort: number;
1042
+ hostAddr?: string;
1043
+ }>;
1044
+ /**
1045
+ * Wall-clock ceiling for the restored fork's `wait()`. Defaults to
1046
+ * `null` (forever) — forks are typically long-lived sibling VMs and
1047
+ * interactive sessions can sit idle. Set a finite deadline if you
1048
+ * want the fork to be reaped after N ms of unresponsiveness. The
1049
+ * dump half uses `performSnapshot`'s own 90s default and isn't
1050
+ * configurable here.
1051
+ */
1052
+ timeoutMs?: number | null;
1053
+ /**
1054
+ * Streaming log callback for the snapshot half. Same shape as
1055
+ * `vm.snapshot({ onLog })`. Also used by the restore boot.
1056
+ */
1057
+ onLog?: OnLog;
1058
+ /**
1059
+ * Opt into lazy-pages restore for the fork — vsock-FUSE-mounted
1060
+ * bundle + `criu restore --lazy-pages`. Default false: the runtime
1061
+ * packs the CRIU image into a tar on `/dev/vdb` and the guest does
1062
+ * an eager load.
1063
+ *
1064
+ * Lazy keeps fork RSS proportional to the pages the sibling
1065
+ * actually touches, not the full snapshot size. Worth setting when
1066
+ * the source dumped a large heap that the fork will only sample.
1067
+ * Cannot combine with `--detach` (the lazy path needs the host's
1068
+ * FUSE server alive as long as the guest may fault, see #150
1069
+ * phase 3); the runtime falls back to eager in that case.
1070
+ */
1071
+ lazy?: boolean;
1072
+ /**
1073
+ * Backpressure gate (#274). Fraction of host total memory that must
1074
+ * be free before `vm.fork()` is allowed to proceed; if `MemAvailable`
1075
+ * (Linux) / `vm_stat free+speculative+purgeable` (Darwin) drops below
1076
+ * `totalmem() * threshold`, the fork is refused with
1077
+ * `FORK_MEMORY_BACKPRESSURE`. Mirrors the throw-immediately shape of
1078
+ * #267's port-conflict gate — caller decides whether to retry.
1079
+ *
1080
+ * Default 1% (`0.01`) — about 250 MiB on a 24 GiB host. The gate
1081
+ * exists to head off OOM kills, not to enforce a working-set
1082
+ * policy; bigger thresholds trip on real dev loops that boot
1083
+ * several VMs in sequence. Pass `0` to disable the gate entirely
1084
+ * (useful in tests or when you're knowingly running close to the
1085
+ * edge).
1086
+ */
1087
+ freeMemoryThreshold?: number;
1088
+ }
1089
+
1090
+ interface SandboxEntry {
1091
+ id: string;
1092
+ vm: VmHandle;
1093
+ scrollback: Buffer;
1094
+ readonly addedAt: number;
1095
+ }
1096
+ interface OnOutputListener {
1097
+ (chunk: Buffer, source: "stdout" | "stderr"): void;
1098
+ }
1099
+ /**
1100
+ * Registry of live sandboxes. Thread-safe in the sense that there's
1101
+ * only one runtime thread anyway; the class just bookkeeps handles +
1102
+ * their scrollback rings so the supervisor doesn't need to.
1103
+ */
1104
+ declare class Sandboxes {
1105
+ private readonly items;
1106
+ private readonly subs;
1107
+ /**
1108
+ * Maximum bytes retained per sandbox for replay on attach. The ring
1109
+ * keeps only the most recent chunk up to this limit — a reasonable
1110
+ * trade between "see enough context to know what's going on" and
1111
+ * "don't leak memory if the sandbox runs for hours."
1112
+ */
1113
+ readonly scrollbackBytes: number;
1114
+ constructor(opts?: {
1115
+ scrollbackBytes?: number;
1116
+ });
1117
+ add(id: string, vm: VmHandle): void;
1118
+ /** Remove a sandbox. Does not kill the VM — call `vm.kill()` first. */
1119
+ remove(id: string): void;
1120
+ list(): Array<{
1121
+ id: string;
1122
+ addedAt: number;
1123
+ }>;
1124
+ get(id: string): SandboxEntry | undefined;
1125
+ /** Write `data` to the sandbox's stdin. No-op if the id is unknown. */
1126
+ send(id: string, data: string | Buffer): boolean;
1127
+ /**
1128
+ * Subscribe to `id`'s output. Returns an unsubscribe function. The
1129
+ * listener fires only for NEW bytes produced after the subscription
1130
+ * — use `get(id).scrollback` to replay history if you want it.
1131
+ */
1132
+ onOutput(id: string, fn: OnOutputListener): () => void;
1133
+ private record;
1134
+ }
1135
+ interface SupervisorOptions {
1136
+ /** Registry to draw sandboxes from. */
1137
+ sandboxes: Sandboxes;
1138
+ /** Input byte stream. Defaults to `process.stdin`. */
1139
+ input?: NodeJS.ReadableStream;
1140
+ /** Output byte stream. Defaults to `process.stdout`. */
1141
+ output?: Writable;
1142
+ /** Prefix for slash-commands. Default `/`. */
1143
+ commandPrefix?: string;
1144
+ /**
1145
+ * Flip the terminal into raw mode while a sandbox is attached, and
1146
+ * restore it on detach. Enabled by default when `input` is a TTY.
1147
+ * Set to `false` in tests where `input` is a plain PassThrough.
1148
+ */
1149
+ rawTtyOnAttach?: boolean;
1150
+ /**
1151
+ * Forward SIGWINCH on the parent process (terminal resize) to any
1152
+ * attached sandbox that implements `.resize(cols, rows)`. Enabled
1153
+ * by default when `output` is a TTY.
1154
+ */
1155
+ forwardResize?: boolean;
1156
+ }
1157
+ /**
1158
+ * A minimal text-driven multiplexer. Runs until `.stop()` is called
1159
+ * or the input stream ends.
1160
+ *
1161
+ * Command surface when detached (lines prefixed with `/`):
1162
+ * /ls — list sandboxes and their state
1163
+ * /attach <id> — forward to/from the given sandbox
1164
+ * /help — show commands
1165
+ * /quit — stop the supervisor (does not kill sandboxes)
1166
+ *
1167
+ * When attached, bytes are piped verbatim to the sandbox's stdin.
1168
+ * Hit `Ctrl-] Ctrl-]` (two 0x1D bytes in a row) to detach.
1169
+ */
1170
+ declare class Supervisor {
1171
+ readonly sandboxes: Sandboxes;
1172
+ private readonly input;
1173
+ private readonly output;
1174
+ private readonly prefix;
1175
+ private readonly rawTtyOnAttach;
1176
+ private readonly forwardResize;
1177
+ private attachedId;
1178
+ private attachedUnsub;
1179
+ private lastGs;
1180
+ private stopped;
1181
+ private onEnd;
1182
+ private priorRawState;
1183
+ private winchHandler;
1184
+ constructor(opts: SupervisorOptions);
1185
+ /** Run until stopped. Resolves when input ends or stop() is called. */
1186
+ run(): Promise<void>;
1187
+ /** Programmatic stop (e.g. from a test). */
1188
+ stop(): void;
1189
+ /** Attach to `id`. Throws if id doesn't exist. */
1190
+ attach(id: string): void;
1191
+ detach(): void;
1192
+ private enterRawTty;
1193
+ private leaveRawTty;
1194
+ private installWinchHandler;
1195
+ private removeWinchHandler;
1196
+ private ingest;
1197
+ private handleCommand;
1198
+ private doLs;
1199
+ private bannerText;
1200
+ private print;
1201
+ }
1202
+
1203
+ interface PtyBootOptions {
1204
+ /** Absolute or cwd-relative path to the binary to fork. */
1205
+ binary: string;
1206
+ /** Extra env. Merged over process.env. */
1207
+ env?: Record<string, string>;
1208
+ cwd?: string;
1209
+ args?: string[];
1210
+ /** Initial terminal size. Defaults to 80x24. */
1211
+ cols?: number;
1212
+ rows?: number;
1213
+ /** TERM value. Default `xterm-256color` — the CC banner wants colors. */
1214
+ name?: string;
1215
+ }
1216
+ interface PtyVmHandle {
1217
+ readonly pid: number;
1218
+ readonly stdin: Writable;
1219
+ readonly stdout: Readable;
1220
+ /** Same stream as `stdout`. A pty merges stdout + stderr in the kernel. */
1221
+ readonly stderr: Readable;
1222
+ /** Tell the kernel the terminal is now `cols`x`rows`. Triggers SIGWINCH in the child. */
1223
+ resize(cols: number, rows: number): void;
1224
+ wait(): Promise<{
1225
+ code: number | null;
1226
+ signal: NodeJS.Signals | null;
1227
+ }>;
1228
+ kill(): Promise<void>;
1229
+ output(): Promise<string>;
1230
+ /** Alias of output() — a pty gives us one merged stream. */
1231
+ errorOutput(): Promise<string>;
1232
+ }
1233
+ /**
1234
+ * Fork `binary` under a new pty pair. The returned handle is wire-
1235
+ * compatible with `VmHandle` from index.ts so the existing Sandboxes
1236
+ * registry can hold it.
1237
+ */
1238
+ declare function bootPty(opts: PtyBootOptions): PtyVmHandle;
1239
+
1240
+ interface VsockWinsizeOptions {
1241
+ /** How long to keep retrying the UDS connect. Default 10s. */
1242
+ timeoutMs?: number;
1243
+ /** Poll interval in ms while retrying. Default 250. */
1244
+ retryMs?: number;
1245
+ }
1246
+ declare class VsockWinsize {
1247
+ private socket;
1248
+ private closed;
1249
+ private lastSent;
1250
+ private constructor();
1251
+ /**
1252
+ * Open a host Unix socket and keep retrying until the vsock bridge
1253
+ * + guest agent wire themselves up. Resolves once the TCP-like
1254
+ * connect completes — the agent may still be registering the
1255
+ * vsock listener on its side, but any bytes we send will be
1256
+ * buffered by the bridge's connection table.
1257
+ */
1258
+ static connect(udsPath: string, opts?: VsockWinsizeOptions): Promise<VsockWinsize>;
1259
+ /**
1260
+ * Send a new size. Idempotent against the most recent send — repeats
1261
+ * are dropped so a chatty SIGWINCH doesn't spam the bridge.
1262
+ */
1263
+ send(cols: number, rows: number): void;
1264
+ close(): void;
1265
+ }
1266
+
1267
+ interface VsockSecretsOptions {
1268
+ /** How long to keep retrying the UDS connect. Default 10s. */
1269
+ timeoutMs?: number;
1270
+ /** Poll interval in ms while retrying. Default 250. */
1271
+ retryMs?: number;
1272
+ }
1273
+ declare const VsockSecrets: {
1274
+ /**
1275
+ * Open the UDS the vsock bridge is listening on, push every
1276
+ * KEY=VALUE entry, close. Resolves once the write + close drain.
1277
+ *
1278
+ * Values must be single-line (no newlines). Keys must be valid
1279
+ * shell identifiers (letters/digits/underscore, no leading digit);
1280
+ * the guest agent skips entries that don't match.
1281
+ */
1282
+ readonly send: (udsPath: string, secrets: Record<string, string>, opts?: VsockSecretsOptions) => Promise<void>;
1283
+ };
1284
+
1285
+ interface VsockFilesOptions {
1286
+ /** How long to retry the UDS connect. Default 5s. */
1287
+ timeoutMs?: number;
1288
+ retryMs?: number;
1289
+ /** Forwarded to `tar --exclude=PATTERN`. Repeat per pattern. */
1290
+ excludes?: string[];
1291
+ }
1292
+ declare const VsockFiles: {
1293
+ /**
1294
+ * Stream `hostDir`'s contents into the guest at `guestPath`. Any
1295
+ * existing files at that path are overwritten (standard `tar -x`
1296
+ * semantics). If `guestPath` doesn't exist, the agent creates it.
1297
+ */
1298
+ readonly push: (udsPath: string, hostDir: string, guestPath: string, opts?: VsockFilesOptions) => Promise<void>;
1299
+ /**
1300
+ * Stream a tar of `guestPath` from the guest and untar into
1301
+ * `hostDir`. `hostDir` is created if missing.
1302
+ */
1303
+ readonly pull: (udsPath: string, guestPath: string, hostDir: string, opts?: VsockFilesOptions) => Promise<void>;
1304
+ };
1305
+
1306
+ interface ProvisionOptions {
1307
+ /**
1308
+ * Path to the base rootfs tarball to start from. Typically the
1309
+ * `rootfs-debian-arm64.tar.gz` produced by
1310
+ * `scripts/build-base-assets.sh` or shipped in a machinen release.
1311
+ *
1312
+ * Optional — when omitted, `provision()` resolves it via `resolveBaseRootfs()`
1313
+ * (MACHINEN_ASSETS_DIR env override, falling back to the `@machinen/cli`
1314
+ * cache at `~/.machinen/@machinen/runtime@<version>/bases/debian-arm64/`).
1315
+ */
1316
+ base?: string;
1317
+ /**
1318
+ * User-supplied provisioning steps. Runs inside the guest via vsock.
1319
+ */
1320
+ install: (vm: VmHandle) => Promise<void>;
1321
+ /**
1322
+ * Output path for the resulting rootfs tarball. Will be overwritten.
1323
+ * Consumed via `boot({ image: out })`.
1324
+ */
1325
+ out: string;
1326
+ /**
1327
+ * Default cmd baked into the image as `/machinen-config.json`.
1328
+ * When the image is later booted via `boot({ image })` without a
1329
+ * user-supplied `cmd`, the guest runs this. User-supplied `cmd` on
1330
+ * `boot()` still wins if provided.
1331
+ */
1332
+ cmd?: string[];
1333
+ /**
1334
+ * Default guest env baked into the image alongside `cmd`. Merged
1335
+ * with `boot({ env })` at boot time, with the caller's `env`
1336
+ * overriding on key collision.
1337
+ */
1338
+ env?: Record<string, string>;
1339
+ /**
1340
+ * Optional VMM binary path. Same lookup rules as `boot()` — if
1341
+ * omitted, resolves `@machinen/vmm-<arch>-<os>`.
1342
+ */
1343
+ binary?: string;
1344
+ /** Working directory. Defaults to process.cwd(). */
1345
+ cwd?: string;
1346
+ /**
1347
+ * Size of the scratch disk used to ferry the tarball from guest to
1348
+ * host. Must be larger than the expected post-install rootfs size.
1349
+ * Default: 1 GiB (sparse, so it doesn't actually take that space).
1350
+ */
1351
+ scratchDiskSizeBytes?: number;
1352
+ /**
1353
+ * Wall-clock ceiling for the whole build. If the install hook plus
1354
+ * the final archive + shutdown doesn't finish in this window, we
1355
+ * SIGKILL the VMM and fail. Default: 10 minutes.
1356
+ */
1357
+ timeoutMs?: number;
1358
+ /**
1359
+ * Extra env passed to the VMM process on the host side. Useful for
1360
+ * dev overrides like `MACHINEN_BOOT_TEST`. Distinct from `env`,
1361
+ * which bakes guest-workload env into the produced image.
1362
+ */
1363
+ vmmEnv?: Record<string, string>;
1364
+ /**
1365
+ * Path to the guest kernel. Optional — when omitted, `provision()`
1366
+ * resolves it via `resolveBaseKernel()` (MACHINEN_ASSETS_DIR override,
1367
+ * falling back to the `@machinen/cli` cache). Same semantics as
1368
+ * `boot({ kernel })` once resolved.
1369
+ */
1370
+ kernel?: string;
1371
+ /**
1372
+ * Path to the guest DTB. Optional — when omitted, resolved via
1373
+ * `resolveBaseDtb()` from the same fallback chain as `kernel`.
1374
+ */
1375
+ dtb?: string;
1376
+ /**
1377
+ * Streaming log callback — fires for every byte of guest output
1378
+ * during the build: guest kernel console, every `vm.exec()` call
1379
+ * the install hook makes, and the internal tar / poweroff execs.
1380
+ * See `LogEvent.source` to tell them apart. See #83.
1381
+ */
1382
+ onLog?: OnLog;
1383
+ }
1384
+ interface ProvisionResult {
1385
+ /** Absolute path to the output tarball. */
1386
+ imagePath: string;
1387
+ /** Size of the output tarball in bytes. */
1388
+ sizeBytes: number;
1389
+ /** Wall-clock time from build() entry to return. */
1390
+ elapsedMs: number;
1391
+ }
1392
+ /**
1393
+ * Resolve the path to the base rootfs tarball, in the same order
1394
+ * `provision()` itself does:
1395
+ *
1396
+ * 1. `explicit` — the caller-supplied path (resolved against `cwd`).
1397
+ * 2. `MACHINEN_ASSETS_DIR` env var — points at a directory laid out like
1398
+ * `scripts/build-base-assets.sh`'s output (contains
1399
+ * `rootfs-debian-arm64.tar.gz`). Same convention `@machinen/cli`
1400
+ * honors for local/dev builds.
1401
+ * 3. `@machinen/cli`'s on-disk cache at
1402
+ * `~/.machinen/@machinen/runtime@<version>/bases/debian-arm64/rootfs.tar.gz`.
1403
+ * Populated by running `machinen` once against the installed runtime.
1404
+ *
1405
+ * Throws `ProvisionError` with guidance if none of those turn up a file.
1406
+ * Exported so callers can pre-check or build their own tooling on it.
1407
+ *
1408
+ * @throws {ProvisionError} PROVISION_BASE_NOT_FOUND | PROVISION_ASSETS_DIR_INVALID
1409
+ */
1410
+ declare function resolveBaseRootfs(explicit?: string, cwd?: string): string;
1411
+ /**
1412
+ * Resolve the path to the guest kernel Image. Same fallback chain as
1413
+ * `resolveBaseRootfs`: explicit → `MACHINEN_ASSETS_DIR/Image-arm64` →
1414
+ * `@machinen/cli` cache at `<base>/Image`. Exported for callers that
1415
+ * want to pre-check or wire the path into `boot()`.
1416
+ *
1417
+ * @throws {ProvisionError} PROVISION_KERNEL_NOT_FOUND |
1418
+ * PROVISION_ASSETS_DIR_INVALID
1419
+ */
1420
+ declare function resolveBaseKernel(explicit?: string, cwd?: string): string;
1421
+ /**
1422
+ * Resolve the path to the guest DTB. Same fallback chain as
1423
+ * `resolveBaseRootfs`: explicit → `MACHINEN_ASSETS_DIR/virt-arm64.dtb` →
1424
+ * `@machinen/cli` cache at `<base>/virt.dtb`.
1425
+ *
1426
+ * @throws {ProvisionError} PROVISION_DTB_NOT_FOUND |
1427
+ * PROVISION_ASSETS_DIR_INVALID
1428
+ */
1429
+ declare function resolveBaseDtb(explicit?: string, cwd?: string): string;
1430
+ /**
1431
+ * Boot the base rootfs, run the user install hook, and freeze the
1432
+ * resulting filesystem state to a new tarball at `opts.out`.
1433
+ *
1434
+ * @throws {ProvisionError} PROVISION_BASE_NOT_FOUND |
1435
+ * PROVISION_KERNEL_NOT_FOUND | PROVISION_DTB_NOT_FOUND |
1436
+ * PROVISION_ASSETS_DIR_INVALID | PROVISION_INSTALL_HOOK_FAILED |
1437
+ * PROVISION_DISK_TOO_SMALL
1438
+ * @throws {BootError} see `boot()` — propagated from the inner boot
1439
+ */
1440
+ declare function provision(opts: ProvisionOptions): Promise<ProvisionResult>;
1441
+
1442
+ /** Default cache root: `~/.cache/machinen/rootfs`. */
1443
+ declare function rootfsImgCacheDir(): string;
1444
+ /**
1445
+ * Mark a cached rootfs image as "cleanly released" by writing the
1446
+ * sentinel that `ensureRootfsImage()` looks for on the next boot.
1447
+ * Called by the runtime after a VMM child exits without a signal —
1448
+ * an exit-code-only termination means the kernel had time to flush
1449
+ * and dismount the ext4 fs, so reusing the file is safe.
1450
+ *
1451
+ * No-op if the image doesn't exist (e.g. the runtime never
1452
+ * materialized one). Failures are swallowed: a missing marker just
1453
+ * means the next boot rebuilds from the tarball, which is wasteful
1454
+ * but never wrong.
1455
+ */
1456
+ declare function markRootfsImageClean(imgPath: string): void;
1457
+ interface EnsureRootfsImageOptions {
1458
+ /**
1459
+ * Override the cache directory. Default: `~/.cache/machinen/rootfs`.
1460
+ * Useful for tests.
1461
+ */
1462
+ cacheDir?: string;
1463
+ /**
1464
+ * Force re-materialization even if a cached image is already present.
1465
+ * Mostly for debugging the materializer.
1466
+ */
1467
+ force?: boolean;
1468
+ /**
1469
+ * Slack multiplier above the unpacked tarball size when sizing the
1470
+ * ext4 filesystem. Default: 2.5 — leaves enough room for the guest
1471
+ * to install a few hundred MB of packages on top of the base rootfs
1472
+ * before hitting ENOSPC. Sparse files cost nothing on disk until
1473
+ * written, so over-provisioning is essentially free; the trade-off
1474
+ * is a higher upper bound on physical disk use if the guest decides
1475
+ * to fill the filesystem.
1476
+ */
1477
+ sizeMultiplier?: number;
1478
+ /**
1479
+ * Minimum image size in bytes. The materializer enforces at least
1480
+ * this for small rootfs where the multiplier alone would leave
1481
+ * insufficient room for a real workload. Default: 2 GiB — boot-time
1482
+ * `npm install -g <large package>`, `apt install`, etc. land here
1483
+ * (#131). Sparse, so unused capacity is free.
1484
+ */
1485
+ minSizeBytes?: number;
1486
+ /**
1487
+ * Absolute target size in bytes. When set, overrides `sizeMultiplier`
1488
+ * and `minSizeBytes` entirely — fresh materializations get exactly
1489
+ * this size, cached `.img`s smaller than this are sparse-extended
1490
+ * (truncate(2)) so the next boot's online ext4 grow can fill them.
1491
+ * For the user-facing `boot({ rootDiskSizeBytes })` knob (#131).
1492
+ */
1493
+ sizeBytes?: number;
1494
+ /**
1495
+ * Sub-phase callback for the caller's PhaseTimer (#233 follow-up).
1496
+ * Fires for each measurable internal step: `sha256`, `e2fsck`,
1497
+ * `sparse-extend`, `tar-extract`, `mke2fs`, `gunzip-prebake`. The
1498
+ * caller typically does `phases.mark("<parent>.${name}", ms)` so
1499
+ * the breakdown shows up alongside the parent phase.
1500
+ */
1501
+ onPhase?: (name: string, ms: number) => void;
1502
+ }
1503
+ /**
1504
+ * Resolve `tarPath` to a cached ext4 `.img`, materializing it on first
1505
+ * call. Returns the absolute path to the cached image.
1506
+ *
1507
+ * Cache key: sha256 of the tarball. Same tarball → same image, even
1508
+ * across runs and processes. Concurrent callers do not race because
1509
+ * we materialize into a uniquely-named staging directory and atomically
1510
+ * rename into place — at worst two callers do redundant work; the
1511
+ * loser of the rename race re-checks and uses the winner's image.
1512
+ *
1513
+ * Lifecycle (#170): the returned path is handed back in the "in-use"
1514
+ * state (no `.ok` marker on disk). The caller is expected to invoke
1515
+ * `markRootfsImageClean(path)` once they're done — `boot()` does this
1516
+ * from its child-exit handler when the VMM exits without a signal,
1517
+ * `provision()` does it after cloning the image read-only. If the
1518
+ * marker is never recreated (caller crashed mid-write or simply
1519
+ * forgot), the next `ensureRootfsImage()` for the same tarball
1520
+ * treats the image as poisoned and rebuilds it.
1521
+ *
1522
+ * @throws {ProvisionError} ROOTFS_IMG_TOOL_MISSING (no e2fsprogs found)
1523
+ * | PROVISION_BASE_NOT_FOUND (tarball missing) |
1524
+ * PROVISION_INSTALL_HOOK_FAILED (tar / mke2fs failed)
1525
+ */
1526
+ declare function ensureRootfsImage(tarPath: string, opts?: EnsureRootfsImageOptions): string;
1527
+ /**
1528
+ * Resolve the mke2fs binary path using the same lookup order as
1529
+ * `ensureRootfsImage` itself: env override → bundled package → PATH →
1530
+ * Homebrew keg-only. Returns `undefined` when no binary is available
1531
+ * (callers should treat this as "skip the optimization", not an error).
1532
+ *
1533
+ * Exported so other tools that need to run mke2fs (e.g. `mountdisk-img`)
1534
+ * resolve the binary through the same lookup chain.
1535
+ */
1536
+ declare function resolveMke2fs(): string | undefined;
1537
+
1538
+ /** Default cache root: `~/.cache/machinen/mountdisk`. */
1539
+ declare function mountdiskImgCacheDir(): string;
1540
+ /**
1541
+ * Mark a cached squashfs lower as "cleanly released," same idiom as
1542
+ * `markRootfsImageClean()`. The lower is read-only inside the guest
1543
+ * so corruption is unlikely, but a host crash mid-write during the
1544
+ * initial mksquashfs would leave a truncated file in the cache.
1545
+ *
1546
+ * No-op when the image doesn't exist. Failures are swallowed.
1547
+ */
1548
+ declare function markMountDiskImageClean(imgPath: string): void;
1549
+ interface EnsureMountDiskImageOptions {
1550
+ /** Override the cache directory. Default: `~/.cache/machinen/mountdisk`. */
1551
+ cacheDir?: string;
1552
+ /** Force re-materialization. Mostly for debugging the materializer. */
1553
+ force?: boolean;
1554
+ /**
1555
+ * Sub-phase callback for the caller's PhaseTimer. Fires for each
1556
+ * measurable internal step: `manifest-hash`, `mksquashfs`,
1557
+ * `staging-rename`. The caller usually does
1558
+ * `phases.mark("<parent>.${name}", ms)`.
1559
+ */
1560
+ onPhase?: (name: string, ms: number) => void;
1561
+ }
1562
+ interface EnsureMountDiskImageResult {
1563
+ /** Absolute path to the cached squashfs lower. */
1564
+ lowerPath: string;
1565
+ /** Tree-manifest sha256 — also the cache key. Useful for tests. */
1566
+ key: string;
1567
+ }
1568
+ /**
1569
+ * Resolve `hostAbs` to a content-addressed squashfs lower image,
1570
+ * materializing it on first call. Returns the absolute path to the
1571
+ * cached `.sqfs`.
1572
+ *
1573
+ * Cache key: sha256 of a sorted manifest covering relpath, mode,
1574
+ * size, mtime_ns, and either the symlink target or the per-file
1575
+ * sha256. Same input tree → same image, even across runs and
1576
+ * processes. Concurrent callers don't race because we materialize
1577
+ * into a uniquely-named staging directory and atomically rename.
1578
+ *
1579
+ * Lifecycle (mirrors rootfs-img.ts): the returned path is in the
1580
+ * "in-use" state (no `.ok` marker on disk). The caller invokes
1581
+ * `markMountDiskImageClean(path)` once they're done.
1582
+ *
1583
+ * @throws {BootError} BOOT_MOUNTDISK_TOOL_MISSING when no mksquashfs
1584
+ * binary is found |
1585
+ * {ProvisionError} PROVISION_INSTALL_HOOK_FAILED when mksquashfs
1586
+ * exits non-zero |
1587
+ * {BootError} BOOT_MOUNT_HOST_NOT_FOUND when the source dir is
1588
+ * missing |
1589
+ * {BootError} BOOT_MOUNT_INVALID when the source dir isn't a
1590
+ * directory.
1591
+ */
1592
+ declare function ensureMountDiskImage(hostAbs: string, opts?: EnsureMountDiskImageOptions): EnsureMountDiskImageResult;
1593
+ interface EnsureMountDiskUpperOptions {
1594
+ /**
1595
+ * Target size in bytes. Default 4 GiB. Sparse, so unused capacity
1596
+ * costs nothing on the host disk. Mirrors `rootDiskSizeBytes` —
1597
+ * over-provision to give the guest room to write without
1598
+ * having to grow the file mid-VM.
1599
+ */
1600
+ sizeBytes?: number;
1601
+ }
1602
+ interface EnsureMountDiskUpperResult {
1603
+ /** Absolute path to the per-VM ext4 upper image. */
1604
+ upperPath: string;
1605
+ /** Size in bytes the file was allocated at. */
1606
+ sizeBytes: number;
1607
+ }
1608
+ /**
1609
+ * Materialize a per-VM ext4 RW upper image for the mount overlay.
1610
+ * Each call returns a fresh sparse file in `tmpdir()` — the upper is
1611
+ * specific to one VM and gets cleaned up alongside the per-boot
1612
+ * rootdisk reflink. Snapshots reflink the upper into the bundle so
1613
+ * writes survive snapshot/restore.
1614
+ *
1615
+ * Mirrors rootfs-img.ts's mke2fs lookup for the file-format step;
1616
+ * shares the same `BOOT_MOUNTDISK_TOOL_MISSING` failure mode if
1617
+ * mke2fs is unavailable (the runtime needs e2fsprogs anyway for the
1618
+ * rootdisk path, so this is rarely the failure that fires first).
1619
+ *
1620
+ * @throws {BootError} BOOT_MOUNTDISK_TOOL_MISSING when no mke2fs is
1621
+ * available |
1622
+ * {ProvisionError} PROVISION_INSTALL_HOOK_FAILED when mke2fs fails.
1623
+ */
1624
+ declare function ensureMountDiskUpper(opts?: EnsureMountDiskUpperOptions): EnsureMountDiskUpperResult;
1625
+ /**
1626
+ * Resolve the mksquashfs binary path using the same lookup order as
1627
+ * `ensureMountDiskImage` itself: env override → bundled package →
1628
+ * PATH → Homebrew opt prefix. Returns `undefined` when no binary is
1629
+ * available.
1630
+ */
1631
+ declare function resolveMksquashfs(): string | undefined;
1632
+
1633
+ /**
1634
+ * Default directory for `<pid>.boot.log` snapshots. Honors
1635
+ * `MACHINEN_DETACHED_LOG_DIR` so tests can scope writes to a tmpdir
1636
+ * without scribbling under `$HOME`.
1637
+ */
1638
+ declare function detachedLogRoot(): string;
1639
+ /** Path the next snapshot for `pid` will be written to. */
1640
+ declare function bootSnapshotPath(pid: number): string;
1641
+ /**
1642
+ * Atomically write the captured boot console to `path`. Best-effort:
1643
+ * a failure here must not block the detach — the VMM is already
1644
+ * running and the boot succeeded, so a missing snapshot is a
1645
+ * diagnostic loss, not a correctness issue. Returns `true` on
1646
+ * success, `false` if the write was skipped or failed.
1647
+ */
1648
+ declare function writeBootSnapshot(path: string, contents: string): boolean;
1649
+
1650
+ /** Result of `validatePid` — easy to switch on at the call site. */
1651
+ type PidStatus = "alive" | "dead" | "recycled";
1652
+ /**
1653
+ * Return whether the running process at `pid` is still our VMM.
1654
+ *
1655
+ * - `alive` — pid is alive AND the exe + start-time match.
1656
+ * - `dead` — kill(pid, 0) failed (gone or permission-denied,
1657
+ * either way unreachable).
1658
+ * - `recycled` — pid is alive but the process isn't ours (different
1659
+ * exe, or start time outside skew).
1660
+ *
1661
+ * Falls back to `alive` when the recorded entry lacks `vmmExe` /
1662
+ * `startedAt` (older entries from before PR2). Conservative on
1663
+ * purpose: the gc decision then leans on `kill(pid, 0)` alone, same
1664
+ * behaviour we had before.
1665
+ */
1666
+ declare function validatePid(pid: number, expected: {
1667
+ vmmExe?: string;
1668
+ startedAt?: number;
1669
+ }): PidStatus;
1670
+
1671
+ /** Per-entry record of what `runGc` did (or would do, with dryRun). */
1672
+ interface GcResult {
1673
+ pid: number;
1674
+ name?: string;
1675
+ status: PidStatus;
1676
+ /** Paths removed (or that would be removed under `dryRun`). */
1677
+ removedPaths: string[];
1678
+ /** Paths the gc tried to rm but couldn't (already gone, EPERM, …). */
1679
+ failedPaths: string[];
1680
+ /** True if the registry entry was (or would be) dropped. */
1681
+ registryRemoved: boolean;
1682
+ }
1683
+ interface RunGcOptions {
1684
+ /**
1685
+ * When true, list what would be cleaned without touching the disk
1686
+ * or registry. Used by `machinen gc --dry-run` and tests.
1687
+ */
1688
+ dryRun?: boolean;
1689
+ /**
1690
+ * Only act on this single entry (skip everything else in the
1691
+ * registry). Used by `machinen stop` after killing a specific VM.
1692
+ */
1693
+ pid?: number;
1694
+ }
1695
+ /**
1696
+ * Walk the registry; for each entry that's dead or pid-recycled,
1697
+ * remove its cleanupPaths + bootLog + registry entry. Returns one
1698
+ * result per entry processed (live entries are skipped silently).
1699
+ */
1700
+ declare function runGc(opts?: RunGcOptions): GcResult[];
1701
+
1702
+ interface RegistryEntry {
1703
+ /** PID of the VMM process on this host — primary key. */
1704
+ pid: number;
1705
+ /** Optional human-friendly name (from `boot({ name })`). Path-shaped allowed. */
1706
+ name?: string;
1707
+ /** Host-side vsock UDS the exec-agent is reachable on. */
1708
+ socketPath: string;
1709
+ /** Path to the image the VM was booted from (diagnostic only). */
1710
+ imagePath?: string;
1711
+ /**
1712
+ * Host-side path of the scratch disk attached to the guest. Used by
1713
+ * `attach().snapshot()` so an attach-owned handle can find the
1714
+ * guest-side scratch disk that backs the in-VM dump.
1715
+ */
1716
+ diskPath?: string;
1717
+ /**
1718
+ * Absolute path to the snapshot directory this VM was forked from
1719
+ * (set by `restore({ snapDir })`). Visible in `ls`; informational.
1720
+ */
1721
+ forkedFrom?: string;
1722
+ /**
1723
+ * Path to the one-shot boot-console snapshot written at detach time
1724
+ * (issue #150 phase 2). Only set on entries booted with
1725
+ * `--detached`; live post-detach console bytes are dropped on the
1726
+ * floor (the VMM ignores SIGPIPE), so this file is the only record
1727
+ * of the boot sequence on a detached VM.
1728
+ */
1729
+ bootLogPath?: string;
1730
+ /**
1731
+ * Per-boot artifacts that need to be removed when the VMM exits.
1732
+ * Today the in-process exit hook handles this for non-detached
1733
+ * boots. After detach (#150 phase 2) the parent is gone before the
1734
+ * VMM exits — `machinen gc` / `machinen stop` use this list to
1735
+ * clean up afterward. Each entry is an absolute path to either a
1736
+ * file (per-boot disk image) or a directory (bundle / vsock UDS).
1737
+ */
1738
+ cleanupPaths?: string[];
1739
+ /**
1740
+ * Absolute path to the VMM binary that was spawned. `machinen gc`
1741
+ * compares this against `/proc/<pid>/exe` (Linux) or `ps -o comm=`
1742
+ * (macOS) before treating an entry as live — without it, a recycled
1743
+ * pid that happens to belong to some other process would look alive
1744
+ * to `kill(pid, 0)` and the entry would be kept around forever.
1745
+ */
1746
+ vmmExe?: string;
1747
+ /**
1748
+ * PID of the gvproxy process spawned alongside this VMM (issue #150
1749
+ * phase 2 PR3). Recorded so `machinen stop` can SIGTERM gvproxy at
1750
+ * the same time as the VMM, and so `machinen gc` can validate /
1751
+ * reap it independently. Undefined when the VM was booted without
1752
+ * networking (no gvproxy binary, or `MACHINEN_NET_SOCKET` was
1753
+ * pre-set by the caller).
1754
+ */
1755
+ gvproxyPid?: number;
1756
+ /**
1757
+ * Absolute path to the gvproxy binary spawned for this VM. Used by
1758
+ * `machinen stop` for the same anti-recycling check the VMM gets
1759
+ * via `vmmExe` — we don't want to SIGTERM whatever process inherits
1760
+ * gvproxy's pid weeks later.
1761
+ */
1762
+ gvproxyExe?: string;
1763
+ /**
1764
+ * Host→guest port forwards configured at boot/fork time. Surfaced
1765
+ * in `machinen ls` so users can see which host port maps to which
1766
+ * VM without re-reading the launch command. Undefined when the VM
1767
+ * was booted without `-p` / `portForward: []`.
1768
+ */
1769
+ portForward?: Array<{
1770
+ hostPort: number;
1771
+ guestPort: number;
1772
+ hostAddr?: string;
1773
+ }>;
1774
+ /**
1775
+ * Guest RAM ceiling in MiB, as resolved by `boot()` (either the
1776
+ * caller's `memory:` option or `autoSizeMemoryMib()` for this host
1777
+ * — see #263 phase A). Surfaced in `machinen ls` (MEM column) and
1778
+ * read by `vm.memoryStats()` so callers can compare host RSS
1779
+ * against the ceiling without re-deriving it. Undefined when the
1780
+ * caller pre-set `MACHINEN_MEMORY` via `vmmEnv` and we never
1781
+ * computed our own.
1782
+ */
1783
+ memoryCeilingMib?: number;
1784
+ /**
1785
+ * Absolute path to the shared stats file the VMM writes balloon
1786
+ * counters to (#274). 16 bytes, mmaped MAP_SHARED on the VMM side
1787
+ * via `MACHINEN_STATS_FILE`. Persisted so an attach-owned handle
1788
+ * can read the same counters its boot-owned sibling sees. Undefined
1789
+ * for VMMs launched outside the runtime (which never received the
1790
+ * env var).
1791
+ */
1792
+ statsPath?: string;
1793
+ /**
1794
+ * Total pages the lazy-pages rewriter (#266) marked PE_LAZY when
1795
+ * the VM was restored. Set on restore-derived entries, undefined
1796
+ * for plain boots and eager restores. Surfaced via
1797
+ * `vm.memoryStats().lazyPagesPending`.
1798
+ */
1799
+ lazyPagesTotal?: number;
1800
+ /**
1801
+ * Absolute path under which the lazy-restore FUSE mount serves
1802
+ * `pages-*.img` reads. The mount-server tracks bytes served below
1803
+ * this prefix; `vm.memoryStats()` divides that by 4096 and
1804
+ * subtracts from `lazyPagesTotal` to derive `lazyPagesPending`.
1805
+ * Undefined when the VM wasn't lazy-restored.
1806
+ */
1807
+ lazyPagesMountRoot?: string;
1808
+ /**
1809
+ * #272: when the VM was booted with `mount: { host, guest }`, the
1810
+ * runtime materialized a squashfs RO lower + ext4 RW upper. Persist
1811
+ * those host paths so an attach-owned `vm.snapshot()` /
1812
+ * `vm.fork()` can reflink them into the snapshot bundle exactly
1813
+ * like the boot-owned handle does — without this, a CLI-side
1814
+ * `machinen snapshot <vm>` produces a bundle missing
1815
+ * `mount-lower.sqfs` / `mount-upper.img` and a later `restore`
1816
+ * silently boots without the overlay.
1817
+ */
1818
+ mountDisk?: {
1819
+ guest: string;
1820
+ lowerPath: string;
1821
+ upperPath: string;
1822
+ };
1823
+ /**
1824
+ * #273: live-share FUSE mounts (`liveMounts: [...]` at boot) the
1825
+ * VM was started with. Persisted so an attach-owned `vm.snapshot()`
1826
+ * / `vm.fork()` can record the same `meta.liveMounts` block in the
1827
+ * bundle and trigger /sbin/machinen-remount post-dump on
1828
+ * leaveRunning paths. Host UDS paths and vsock ports are NOT
1829
+ * recorded — those are the boot process's private state and aren't
1830
+ * useful to other processes (the owning process keeps the servers
1831
+ * listening through the dump, so attach reconnects without having
1832
+ * to bind anything).
1833
+ */
1834
+ liveMounts?: Array<{
1835
+ guest: string;
1836
+ host: string;
1837
+ mode: "ro" | "rw";
1838
+ }>;
1839
+ /**
1840
+ * #150 phase 3: pids + exes of the detached mount-server helpers
1841
+ * spawned alongside this VMM, one per live-mount. The helpers die
1842
+ * with the VMM via `pdeathsig --watch-pid` already, but `machinen
1843
+ * stop` SIGTERMs them up-front so the VMM exit hook doesn't race
1844
+ * with the helper's own pdeathsig-driven shutdown, and `machinen
1845
+ * gc` validates pid+exe to detect recycled pids the same way the
1846
+ * VMM and gvproxy entries do. Empty / undefined for VMs booted
1847
+ * without `liveMounts`.
1848
+ */
1849
+ liveMountServers?: Array<{
1850
+ pid: number;
1851
+ exe: string;
1852
+ }>;
1853
+ /** ms epoch when the entry was created. */
1854
+ startedAt: number;
1855
+ }
1856
+ /**
1857
+ * Absolute path to the registry root. Honors `MACHINEN_REGISTRY_DIR`
1858
+ * so tests can point at a scratch dir without stomping on real entries.
1859
+ */
1860
+ declare function registryRoot(): string;
1861
+ /**
1862
+ * List all registry entries whose pid is still alive. Prunes stale
1863
+ * entries (pid no longer alive) and orphaned name pins as a side
1864
+ * effect, so a crashed VMM doesn't leave a stuck record behind.
1865
+ */
1866
+ declare function list(): RegistryEntry[];
1867
+
1868
+ interface PackBundleOptions {
1869
+ /** Bundle directory with rootfs/ + machinen-config.json. */
1870
+ bundle: string;
1871
+ /** Path to the initramfs cpio to write. */
1872
+ out: string;
1873
+ /** Optional base rootfs tarball (rootfs-debian-arm64.tar.gz). */
1874
+ base?: string;
1875
+ /**
1876
+ * A single host directory copied into the guest between the base
1877
+ * tarball and the bundle's rootfs. Bundle files win on path
1878
+ * collisions. The caller is responsible for validating host exists
1879
+ * and is a directory, and that guest lives under `/mnt/`. See #64.
1880
+ */
1881
+ mount?: {
1882
+ host: string;
1883
+ guest: string;
1884
+ };
1885
+ /**
1886
+ * Extra env vars to merge into the bundle's machinen-config.json `env`
1887
+ * field before packing. The bundle's on-disk env wins on key collision
1888
+ * (same precedence as the mount overlay — bundle always gets the last
1889
+ * word). See #89.
1890
+ */
1891
+ env?: Record<string, string>;
1892
+ /** fnmatch patterns matched against each rootfs-relative path. */
1893
+ excludes?: string[];
1894
+ /** Optional path to the compiled /init. Default: ../microvm/test-fixtures/init relative to this file. */
1895
+ initPath?: string;
1896
+ /**
1897
+ * Optional host path to the compiled fuse-agent binary. When set,
1898
+ * the binary is injected at `/fuse-agent` (mode 0755) inside the
1899
+ * initramfs so /init can fork it per live-share mount. See #78.
1900
+ */
1901
+ fuseAgentPath?: string;
1902
+ /**
1903
+ * Optional path to the compiled /exec-agent. Default: same dir as
1904
+ * /init under packages/microvm/test-fixtures/. Used to override the
1905
+ * stale /exec-agent that may live in a re-provisioned base tarball.
1906
+ */
1907
+ execAgentPath?: string;
1908
+ }
1909
+ declare function packBundle(opts: PackBundleOptions): void;
1910
+ interface PackTinyBundleOptions {
1911
+ /** Bundle directory with machinen-config.json. The bundle's rootfs/ is ignored — the on-disk rootfs is on /dev/vda. */
1912
+ bundle: string;
1913
+ /** Path to the initramfs cpio to write. */
1914
+ out: string;
1915
+ /** Extra env merged into the bundle's machinen-config.json. Bundle keys win on collision. */
1916
+ env?: Record<string, string>;
1917
+ /**
1918
+ * Guest mountpoint for the `--mount` overlay (#272). When set, the
1919
+ * cpio carries `/etc/machinen-mountdisk-guest` with this path so
1920
+ * /init knows where to layer the squashfs+ext4 overlay after the
1921
+ * rootdisk pivot. The actual payload rides on virtio-blk slots 5+6,
1922
+ * not in the cpio. Must be an absolute path under `/mnt/`.
1923
+ */
1924
+ mountGuest?: string;
1925
+ /** Optional override for the compiled /init. Default: ../microvm/test-fixtures/init relative to this file. */
1926
+ initPath?: string;
1927
+ /** Optional path to the compiled fuse-agent; staged at /fuse-agent when set. */
1928
+ fuseAgentPath?: string;
1929
+ }
1930
+ /**
1931
+ * Build the tiny initramfs used by every user-facing boot() (#119).
1932
+ *
1933
+ * Layout:
1934
+ * /init compiled Zig init
1935
+ * /machinen-config.json cmd/env/cwd/liveMounts for /init
1936
+ * /etc/machinen-boot-epoch wall clock seed for the guest
1937
+ * /etc/machinen-mountdisk-guest optional, target dir for the
1938
+ * `--mount` overlay (#272). The
1939
+ * actual payload rides on virtio-
1940
+ * blk slots 5+6, not in the cpio.
1941
+ * /dev/console char node 5,1 — kernel needs it
1942
+ * before /init re-opens the console
1943
+ * /fuse-agent optional, only when liveMounts
1944
+ * /tmp sticky 1777
1945
+ *
1946
+ * No /lib/modules tree, no kmod, no /modules/*.ko, no Debian userland.
1947
+ * The custom kernel ships with virtio_*, ext4, vsock, squashfs, and
1948
+ * overlayfs built in (scripts/build-kernel-arm64.sh), so /init pivots
1949
+ * straight into /dev/vda without a finit_module pass.
1950
+ */
1951
+ declare function packTinyBundle(opts: PackTinyBundleOptions): void;
1952
+ interface PackRootfsOptions {
1953
+ rootfs: string;
1954
+ out: string;
1955
+ config?: string;
1956
+ excludes?: string[];
1957
+ initPath?: string;
1958
+ }
1959
+ declare function packRootfs(opts: PackRootfsOptions): void;
1960
+ interface PackMinimalOptions {
1961
+ out: string;
1962
+ initPath?: string;
1963
+ config?: string;
1964
+ }
1965
+ declare function packMinimal(opts: PackMinimalOptions): void;
1966
+ interface PackWorkspaceOptions {
1967
+ workspace: string;
1968
+ out: string;
1969
+ /** Directory name inside the cpio (default `workspace`). */
1970
+ mountpoint?: string;
1971
+ /** Basename-matched excludes. Default: DEFAULT_WORKSPACE_EXCLUDES. */
1972
+ excludes?: Iterable<string>;
1973
+ /** Max final size in MiB (default 500). Throws if exceeded. */
1974
+ maxMb?: number;
1975
+ }
1976
+ declare function packWorkspace(opts: PackWorkspaceOptions): void;
1977
+ /**
1978
+ * Invoked by the CLI shim at packages/microvm/test-fixtures/assets/mkinitramfs.ts.
1979
+ * Kept argv-compatible with the old Python script so shell fixtures
1980
+ * (smoke.sh, try.sh, handoff.sh) don't need deeper changes.
1981
+ */
1982
+ declare function cli(argv: string[]): void;
1983
+
1984
+ declare const ErrorCode: {
1985
+ readonly BOOT_VMM_MISSING: "BOOT_VMM_MISSING";
1986
+ readonly BOOT_VMM_PACKAGE_BROKEN: "BOOT_VMM_PACKAGE_BROKEN";
1987
+ readonly BOOT_IMAGE_NOT_FOUND: "BOOT_IMAGE_NOT_FOUND";
1988
+ readonly BOOT_SNAPSHOT_NOT_FOUND: "BOOT_SNAPSHOT_NOT_FOUND";
1989
+ readonly BOOT_KERNEL_NOT_FOUND: "BOOT_KERNEL_NOT_FOUND";
1990
+ readonly BOOT_DTB_NOT_FOUND: "BOOT_DTB_NOT_FOUND";
1991
+ readonly BOOT_CMD_WITHOUT_IMAGE: "BOOT_CMD_WITHOUT_IMAGE";
1992
+ readonly BOOT_CMD_MISSING: "BOOT_CMD_MISSING";
1993
+ readonly BOOT_CWD_INVALID: "BOOT_CWD_INVALID";
1994
+ readonly BOOT_MOUNT_INVALID: "BOOT_MOUNT_INVALID";
1995
+ readonly BOOT_MOUNT_HOST_NOT_FOUND: "BOOT_MOUNT_HOST_NOT_FOUND";
1996
+ readonly BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN: "BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN";
1997
+ readonly BOOT_PORT_FORWARD_INVALID: "BOOT_PORT_FORWARD_INVALID";
1998
+ readonly BOOT_PORT_FORWARD_CONFLICT: "BOOT_PORT_FORWARD_CONFLICT";
1999
+ readonly BOOT_PORT_FORWARD_NO_GVPROXY: "BOOT_PORT_FORWARD_NO_GVPROXY";
2000
+ readonly BOOT_PORT_FORWARD_IN_USE: "BOOT_PORT_FORWARD_IN_USE";
2001
+ readonly BOOT_PACK_FAILED: "BOOT_PACK_FAILED";
2002
+ readonly BOOT_TIMEOUT: "BOOT_TIMEOUT";
2003
+ readonly BOOT_DETACHED_READINESS_FAILED: "BOOT_DETACHED_READINESS_FAILED";
2004
+ readonly BOOT_MEMORY_INVALID: "BOOT_MEMORY_INVALID";
2005
+ readonly FORK_MEMORY_BACKPRESSURE: "FORK_MEMORY_BACKPRESSURE";
2006
+ readonly BOOT_MOUNTDISK_TOOL_MISSING: "BOOT_MOUNTDISK_TOOL_MISSING";
2007
+ readonly EXEC_VSOCK_UNAVAILABLE: "EXEC_VSOCK_UNAVAILABLE";
2008
+ readonly EXEC_AGENT_UNAVAILABLE: "EXEC_AGENT_UNAVAILABLE";
2009
+ readonly EXEC_AGENT_TIMEOUT: "EXEC_AGENT_TIMEOUT";
2010
+ readonly EXEC_NONZERO_EXIT: "EXEC_NONZERO_EXIT";
2011
+ readonly EXEC_PROTOCOL: "EXEC_PROTOCOL";
2012
+ readonly SNAPSHOT_NO_DISK: "SNAPSHOT_NO_DISK";
2013
+ readonly SNAPSHOT_DUMP_FAILED: "SNAPSHOT_DUMP_FAILED";
2014
+ readonly SNAPSHOT_TIMEOUT: "SNAPSHOT_TIMEOUT";
2015
+ readonly PROVISION_BASE_NOT_FOUND: "PROVISION_BASE_NOT_FOUND";
2016
+ readonly PROVISION_KERNEL_NOT_FOUND: "PROVISION_KERNEL_NOT_FOUND";
2017
+ readonly PROVISION_DTB_NOT_FOUND: "PROVISION_DTB_NOT_FOUND";
2018
+ readonly PROVISION_ASSETS_DIR_INVALID: "PROVISION_ASSETS_DIR_INVALID";
2019
+ readonly PROVISION_INSTALL_HOOK_FAILED: "PROVISION_INSTALL_HOOK_FAILED";
2020
+ readonly PROVISION_DISK_TOO_SMALL: "PROVISION_DISK_TOO_SMALL";
2021
+ readonly ROOTFS_IMG_TOOL_MISSING: "ROOTFS_IMG_TOOL_MISSING";
2022
+ readonly REGISTRY_VM_NOT_FOUND: "REGISTRY_VM_NOT_FOUND";
2023
+ readonly REGISTRY_NAME_IN_USE: "REGISTRY_NAME_IN_USE";
2024
+ readonly FILES_HOST_DIR_NOT_FOUND: "FILES_HOST_DIR_NOT_FOUND";
2025
+ readonly FILES_AGENT_UNAVAILABLE: "FILES_AGENT_UNAVAILABLE";
2026
+ readonly MOUNT_PATH_INVALID: "MOUNT_PATH_INVALID";
2027
+ readonly MOUNT_PATH_ESCAPE: "MOUNT_PATH_ESCAPE";
2028
+ readonly MOUNT_SERVER_BIN_MISSING: "MOUNT_SERVER_BIN_MISSING";
2029
+ readonly MOUNT_SERVER_SPAWN_FAILED: "MOUNT_SERVER_SPAWN_FAILED";
2030
+ readonly SECRETS_VALUE_INVALID: "SECRETS_VALUE_INVALID";
2031
+ readonly SECRETS_AGENT_UNAVAILABLE: "SECRETS_AGENT_UNAVAILABLE";
2032
+ readonly WINSIZE_AGENT_UNAVAILABLE: "WINSIZE_AGENT_UNAVAILABLE";
2033
+ readonly SANDBOX_ID_DUPLICATE: "SANDBOX_ID_DUPLICATE";
2034
+ readonly SANDBOX_ID_UNKNOWN: "SANDBOX_ID_UNKNOWN";
2035
+ readonly CACHE_BIND_FAILED: "CACHE_BIND_FAILED";
2036
+ readonly GVPROXY_NOT_FOUND: "GVPROXY_NOT_FOUND";
2037
+ readonly GVPROXY_EXPOSE_FAILED: "GVPROXY_EXPOSE_FAILED";
2038
+ readonly GVPROXY_PORT_IN_USE: "GVPROXY_PORT_IN_USE";
2039
+ readonly GVPROXY_INSTALL_FAILED: "GVPROXY_INSTALL_FAILED";
2040
+ readonly GVPROXY_SPAWN_FAILED: "GVPROXY_SPAWN_FAILED";
2041
+ readonly MKINITRAMFS_BUNDLE_INVALID: "MKINITRAMFS_BUNDLE_INVALID";
2042
+ readonly MKINITRAMFS_WORKSPACE_INVALID: "MKINITRAMFS_WORKSPACE_INVALID";
2043
+ readonly MKINITRAMFS_WORKSPACE_TOO_LARGE: "MKINITRAMFS_WORKSPACE_TOO_LARGE";
2044
+ readonly MKINITRAMFS_BASE_EXTRACT_FAILED: "MKINITRAMFS_BASE_EXTRACT_FAILED";
2045
+ readonly MKINITRAMFS_INIT_MISSING: "MKINITRAMFS_INIT_MISSING";
2046
+ readonly PARSE_FLAG_UNKNOWN: "PARSE_FLAG_UNKNOWN";
2047
+ readonly PARSE_FLAG_MISSING_VALUE: "PARSE_FLAG_MISSING_VALUE";
2048
+ readonly PARSE_FLAG_DUPLICATE: "PARSE_FLAG_DUPLICATE";
2049
+ readonly PARSE_FLAG_MALFORMED: "PARSE_FLAG_MALFORMED";
2050
+ readonly PARSE_PORT_INVALID: "PARSE_PORT_INVALID";
2051
+ };
2052
+ type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
2053
+ interface MachinenErrorOptions {
2054
+ /**
2055
+ * True if retrying the same call could plausibly succeed (transient
2056
+ * network blip, upstream fetch, vsock agent not listening yet). False
2057
+ * for misconfiguration (missing binary, bad mount path, invalid
2058
+ * port).
2059
+ */
2060
+ retryable?: boolean;
2061
+ /** Underlying error preserved via the standard `Error.cause` chain. */
2062
+ cause?: unknown;
2063
+ }
2064
+ /**
2065
+ * Base class for every error raised by @machinen/runtime and
2066
+ * @machinen/cli. Carries a flat `code`, a `retryable` hint, and the
2067
+ * underlying cause via the standard `Error.cause` mechanism.
2068
+ */
2069
+ declare class MachinenError extends Error {
2070
+ readonly code: ErrorCode;
2071
+ readonly retryable: boolean;
2072
+ constructor(code: ErrorCode, message: string, opts?: MachinenErrorOptions);
2073
+ }
2074
+ declare class BootError extends MachinenError {
2075
+ }
2076
+ declare class ExecError extends MachinenError {
2077
+ }
2078
+ declare class SnapshotError extends MachinenError {
2079
+ }
2080
+ declare class ProvisionError extends MachinenError {
2081
+ }
2082
+ declare class RegistryError extends MachinenError {
2083
+ }
2084
+ declare class FilesError extends MachinenError {
2085
+ }
2086
+ declare class MountError extends MachinenError {
2087
+ }
2088
+ declare class SecretsError extends MachinenError {
2089
+ }
2090
+ declare class WinsizeError extends MachinenError {
2091
+ }
2092
+ declare class SandboxError extends MachinenError {
2093
+ }
2094
+ declare class CacheError extends MachinenError {
2095
+ }
2096
+ declare class GvproxyError extends MachinenError {
2097
+ }
2098
+ declare class MkinitramfsError extends MachinenError {
2099
+ }
2100
+ declare class ParseError extends MachinenError {
2101
+ }
2102
+ /**
2103
+ * Narrowing type guard. Pass a specific `code` to check both identity
2104
+ * and discriminant in one call.
2105
+ */
2106
+ declare function isMachinenError(err: unknown, code?: ErrorCode): err is MachinenError;
2107
+ /**
2108
+ * Format a MachinenError for CLI stderr. Shows the code inline and walks
2109
+ * the `cause` chain. Used by the CLI's unified `handleError`; exported so
2110
+ * library callers can adopt the same format if they want to.
2111
+ */
2112
+ declare function formatMachinenError(err: MachinenError): string;
2113
+
2114
+ /**
2115
+ * Bytes of memory the OS reports as available right now. "Available"
2116
+ * is the loose union the kernel exposes:
2117
+ * - Linux → /proc/meminfo MemAvailable (post-3.14 kernels — every
2118
+ * distro machinen runs on). MemAvailable already accounts
2119
+ * for reclaimable slab + page-cache, so it's the right
2120
+ * answer for "could a new process allocate X bytes
2121
+ * without paging or OOM?".
2122
+ * - Darwin → vm_stat free + speculative + purgeable. Inactive is
2123
+ * excluded because it's dirty and needs a pageout, which
2124
+ * wouldn't help a fork that needs RAM right now.
2125
+ * - other → totalmem(). Soft-fail rather than block fork on a
2126
+ * platform we can't measure.
2127
+ */
2128
+ declare function readHostFreeBytes(): Promise<number>;
2129
+ /**
2130
+ * Total physical memory in bytes. Thin wrapper over `os.totalmem()`
2131
+ * exported alongside the free reader so tests and the backpressure
2132
+ * check pull both numbers from the same module.
2133
+ */
2134
+ declare function readHostTotalBytes(): number;
2135
+ /**
2136
+ * Default fraction of host memory we require to be free before
2137
+ * `vm.fork()` is allowed to proceed. The gate exists to keep a
2138
+ * runaway script from OOM-killing arbitrary host processes — not
2139
+ * to enforce a particular working-set policy. 1% on a 24 GiB host
2140
+ * = ~250 MiB, enough headroom for the lazy-restore criu spawn
2141
+ * (#266) plus a typical workload's UFFD page-in burst, while still
2142
+ * tripping early enough that a host with only a few hundred MiB
2143
+ * free fails fast instead of triggering the kernel OOM killer.
2144
+ *
2145
+ * Smoke-test rationale: a host running `pnpm smoke-tests` sees
2146
+ * five sequential VMs leave it with ~1 GiB free in steady state.
2147
+ * Anything stricter than this default trips on real-world dev
2148
+ * loops; anything looser stops being a meaningful gate.
2149
+ */
2150
+ declare const DEFAULT_FREE_MEMORY_THRESHOLD = 0.01;
2151
+ interface CheckForkBackpressureOptions {
2152
+ /**
2153
+ * Fraction of host total memory that must remain free for a fork
2154
+ * to proceed. Pass `0` (or any non-positive number) to disable the
2155
+ * gate entirely. Capped at `1` — `0.5` already means "refuse
2156
+ * unless half the host is free."
2157
+ */
2158
+ threshold: number;
2159
+ /** Pluggable for tests; defaults to {@link readHostFreeBytes}. */
2160
+ readFree?: () => Promise<number>;
2161
+ /** Pluggable for tests; defaults to {@link readHostTotalBytes}. */
2162
+ totalBytes?: number;
2163
+ }
2164
+ /**
2165
+ * Refuse a fork when the host is under memory pressure. Throws
2166
+ * `BootError("FORK_MEMORY_BACKPRESSURE")` when free < total *
2167
+ * threshold, modeled on the throw-immediately shape of #267's
2168
+ * `BOOT_PORT_FORWARD_IN_USE` gate. Caller is responsible for any
2169
+ * retry policy.
2170
+ */
2171
+ declare function checkForkBackpressure(opts: CheckForkBackpressureOptions): Promise<void>;
2172
+
2173
+ /** A pid plus the absolute path to its stats file (when available). */
2174
+ interface RssTarget {
2175
+ pid: number;
2176
+ /**
2177
+ * MACHINEN_STATS_FILE path for this VMM (registry entry's
2178
+ * `statsPath`). On Darwin we read `phys_footprint` from this file
2179
+ * in preference to `ps -o rss=`. Optional / undefined for arbitrary
2180
+ * pids that aren't machinen-managed; those fall back to ps.
2181
+ */
2182
+ statsPath?: string;
2183
+ }
2184
+ /** RSS bytes for one pid, or null if not readable. */
2185
+ declare function readHostRssBytes(pid: number, statsPath?: string): number | null;
2186
+ /**
2187
+ * Bulk variant for `machinen ls`: one syscall (Linux) or one
2188
+ * subprocess (Darwin) for every live VM, instead of N. Pids that
2189
+ * can't be read are simply absent from the result map — caller
2190
+ * decides whether to render "?" or skip the row.
2191
+ */
2192
+ declare function readHostRssBytesMulti(targets: ReadonlyArray<RssTarget | number>): Map<number, number>;
2193
+
2194
+ declare const STATS_FILE_SIZE = 24;
2195
+ interface BalloonCounters {
2196
+ /** Total bytes the balloon device has reclaimed via reporting. */
2197
+ bytesReported: number;
2198
+ /**
2199
+ * Total bytes the inflate queue has seen. We don't drive inflate
2200
+ * (`num_pages` stays 0), so this stays at 0 in well-behaved
2201
+ * deployments — non-zero means a buggy/hostile guest is pushing
2202
+ * pages into the balloon on its own.
2203
+ */
2204
+ bytesInflated: number;
2205
+ /**
2206
+ * Latest sample of this VMM's Darwin `phys_footprint` (the metric
2207
+ * that backs Activity Monitor's "Memory" column and excludes
2208
+ * `MADV_FREE_REUSABLE` pages). Refreshed every ~500 ms by a
2209
+ * sampler thread inside the VMM. Always 0 on Linux — there's no
2210
+ * Darwin-equivalent metric and the runtime reads
2211
+ * `/proc/<pid>/status:VmRSS` instead, which already reflects
2212
+ * `MADV_DONTNEED` reclaim.
2213
+ */
2214
+ hostPhysFootprintBytes: number;
2215
+ }
2216
+ /**
2217
+ * Read the balloon-stats file at `path`. Returns `null` when:
2218
+ * - the file is missing (VMM was launched without
2219
+ * `MACHINEN_STATS_FILE`, or the path is stale),
2220
+ * - it's shorter than `STATS_FILE_SIZE` (truncated mid-write — not
2221
+ * possible with the mmap'd writer, but defensive against an
2222
+ * out-of-band actor),
2223
+ * - it's unreadable (permissions, gone between stat and read).
2224
+ */
2225
+ declare function readBalloonStats(path: string): BalloonCounters | null;
2226
+
2227
+ export { type AttachOptions, type BalloonCounters, BootError, type BootOptions, CacheError, type CheckForkBackpressureOptions, type ChunkLogEvent, DEFAULT_FREE_MEMORY_THRESHOLD, type EnsureMountDiskImageOptions, type EnsureMountDiskImageResult, type EnsureMountDiskUpperOptions, type EnsureMountDiskUpperResult, type EnsureRootfsImageOptions, ErrorCode, ExecError, FilesError, type ForkOptions, type GcResult, GvproxyError, type ImageConfig, type LogEvent, MachinenError, type MachinenErrorOptions, type MemoryStats, MkinitramfsError, MountError, type OnLog, type OnOutputListener, type PackBundleOptions, type PackMinimalOptions, type PackRootfsOptions, type PackTinyBundleOptions, type PackWorkspaceOptions, ParseError, type PhaseLogEvent, type PidStatus, ProvisionError, type ProvisionOptions, type ProvisionResult, type PtyBootOptions, type PtyVmHandle, type RegistryEntry, RegistryError, type RestoreOptions, type RssTarget, type RunGcOptions, STATS_FILE_SIZE, type SandboxEntry, SandboxError, Sandboxes, SecretsError, SnapshotError, type SnapshotMeta, type SnapshotOptions, type SnapshotResult, Supervisor, type SupervisorOptions, type VmHandle, VsockExec, type VsockExecOptions, type VsockExecPtyHandle, type VsockExecPtyOptions, type VsockExecPtyResult, type VsockExecResult, VsockFiles, type VsockFilesOptions, VsockSecrets, type VsockSecretsOptions, VsockWinsize, type VsockWinsizeOptions, WinsizeError, type WriteFileOptions, _internal, attach, autoSizeMemoryMib, boot, bootPty, bootSnapshotPath, buildMachinenConfig, buildWriteFileCmd, buildWriteFileCmds, checkForkBackpressure, detachedLogRoot, ensureMountDiskImage, ensureMountDiskUpper, ensureRootfsImage, formatMachinenError, isMachinenError, list, markMountDiskImageClean, markRootfsImageClean, measureFirstByte, packBundle as mkinitramfsBundle, cli as mkinitramfsCli, packMinimal as mkinitramfsMinimal, packRootfs as mkinitramfsRootfs, packTinyBundle as mkinitramfsTinyBundle, packWorkspace as mkinitramfsWorkspace, mountdiskImgCacheDir, provision, readBalloonStats, readHostFreeBytes, readHostRssBytes, readHostRssBytesMulti, readHostTotalBytes, registryRoot, resolveBaseDtb, resolveBaseKernel, resolveBaseRootfs, resolveMke2fs, resolveMksquashfs, resolveVmmBinary, restore, rootfsImgCacheDir, runGc, validatePid, warmImageConfigCache, writeBootSnapshot };