@braedonsaunders/appkit-desk 0.6.0 → 0.8.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.
Files changed (53) hide show
  1. package/README.md +83 -1
  2. package/backend.d.ts +28 -7
  3. package/backend.d.ts.map +1 -1
  4. package/backend.js +18 -521
  5. package/backend.js.map +1 -1
  6. package/events.d.ts +3 -0
  7. package/events.d.ts.map +1 -1
  8. package/events.js.map +1 -1
  9. package/host.d.ts +32 -1
  10. package/host.d.ts.map +1 -1
  11. package/host.js +141 -23
  12. package/host.js.map +1 -1
  13. package/index.d.ts +3 -0
  14. package/index.d.ts.map +1 -1
  15. package/index.js +3 -0
  16. package/index.js.map +1 -1
  17. package/machine.d.ts +145 -0
  18. package/machine.d.ts.map +1 -0
  19. package/machine.js +518 -0
  20. package/machine.js.map +1 -0
  21. package/network.d.ts +37 -0
  22. package/network.d.ts.map +1 -0
  23. package/network.js +173 -0
  24. package/network.js.map +1 -0
  25. package/package.json +2 -2
  26. package/plan.d.ts +129 -31
  27. package/plan.d.ts.map +1 -1
  28. package/plan.js +165 -9
  29. package/plan.js.map +1 -1
  30. package/port-relay.d.ts +13 -0
  31. package/port-relay.d.ts.map +1 -0
  32. package/port-relay.js +56 -0
  33. package/port-relay.js.map +1 -0
  34. package/virtualization.d.ts +16 -0
  35. package/virtualization.d.ts.map +1 -0
  36. package/virtualization.js +148 -0
  37. package/virtualization.js.map +1 -0
  38. package/vz/Package.swift +23 -0
  39. package/vz/Sources/DeskNet/Addresses.swift +106 -0
  40. package/vz/Sources/DeskNet/DHCP.swift +111 -0
  41. package/vz/Sources/DeskNet/Fence.swift +57 -0
  42. package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
  43. package/vz/Sources/DeskNet/Packets.swift +355 -0
  44. package/vz/Sources/DeskNet/Plumbing.swift +312 -0
  45. package/vz/Sources/DeskNet/Sockets.swift +221 -0
  46. package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
  47. package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
  48. package/vz/Sources/appkit-desk-vz/Helper.swift +505 -0
  49. package/vz/Sources/appkit-desk-vz/main.swift +38 -0
  50. package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
  51. package/vz/appkit-desk-vz.entitlements +8 -0
  52. package/vz/build.sh +23 -0
  53. package/vz/check/extract-boot.py +221 -0
package/plan.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { createHash } from 'node:crypto';
1
3
  import { existsSync } from 'node:fs';
4
+ import { isIP } from 'node:net';
5
+ import { arch as hostArch, release as hostRelease, tmpdir } from 'node:os';
2
6
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
7
  export const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor';
4
8
  export const DEFAULT_KVM_PATH = '/dev/kvm';
@@ -7,6 +11,21 @@ export const DEFAULT_RUNTIME_DIR = '/run/appkit-desk';
7
11
  export const DEFAULT_MEMORY_MB = 384;
8
12
  export const DEFAULT_VCPUS = 2;
9
13
  export const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet';
14
+ /**
15
+ * The guest network Virtualization.framework desks get from the helper's NAT:
16
+ * the same addresses QEMU's user networking uses, so they are unsurprising in
17
+ * a guest. Nothing outside the helper ever sees them.
18
+ */
19
+ export const DEFAULT_VZ_GUEST_ADDRESS = '10.0.2.15';
20
+ export const DEFAULT_VZ_GATEWAY_ADDRESS = '10.0.2.2';
21
+ export const DEFAULT_VZ_PREFIX_LENGTH = 24;
22
+ export const DEFAULT_VZ_DNS = ['1.1.1.1', '9.9.9.9'];
23
+ /** Virtualization.framework always gives the guest vsock context ID 3. */
24
+ export const VZ_GUEST_CID = 3;
25
+ /** The first Darwin release with Virtualization.framework save and restore (macOS 14). */
26
+ const MIN_DARWIN_MAJOR = 23;
27
+ /** `sun_path` holds 104 bytes on Darwin, including the terminator. */
28
+ const DARWIN_SOCKET_PATH_LIMIT = 103;
10
29
  const DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
11
30
  const TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/;
12
31
  const MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i;
@@ -23,14 +42,26 @@ export class DeskError extends Error {
23
42
  name = 'DeskError';
24
43
  }
25
44
  /**
26
- * Whether this host can run desks at all: Linux, a usable `/dev/kvm`, and the
27
- * Cloud Hypervisor binary. All three checks are injectable and the answer is
28
- * fail-closed — a host missing any of them gets no desk capability rather
29
- * than a degraded one.
45
+ * Whether this host can run desks at all. On Linux: a usable `/dev/kvm` and
46
+ * the Cloud Hypervisor binary. On macOS: Apple silicon, macOS 14 or later (the
47
+ * first with Virtualization.framework save and restore), and an existing
48
+ * `appkit-desk-vz` helper at `helperPath`. Signature and entitlement are checked
49
+ * by macOS on execution, not by this probe. Every check is injectable and the
50
+ * answer is fail-closed — a host missing any of them gets no desk capability
51
+ * rather than a degraded one.
30
52
  */
31
53
  export function isDeskSupported(options = {}) {
32
54
  const platform = options.platform ?? process.platform;
33
55
  const pathExists = options.pathExists ?? existsSync;
56
+ if (platform === 'darwin') {
57
+ const major = Number.parseInt((options.osRelease ?? hostRelease()).split('.')[0] ?? '', 10);
58
+ return ((options.arch ?? hostArch()) === 'arm64'
59
+ && Number.isInteger(major)
60
+ && major >= MIN_DARWIN_MAJOR
61
+ && options.helperPath !== undefined
62
+ && isAbsolute(options.helperPath)
63
+ && pathExists(options.helperPath));
64
+ }
34
65
  return (platform === 'linux'
35
66
  && pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)
36
67
  && pathExists(options.vmmPath ?? DEFAULT_VMM_PATH));
@@ -42,12 +73,19 @@ export function cleanDeskId(value) {
42
73
  }
43
74
  return value;
44
75
  }
45
- /**
46
- * Build the deterministic Cloud Hypervisor launch plan without spawning
47
- * anything. Fail closed: a missing `/dev/kvm`, VMM binary, kernel, base image,
48
- * or overlay directory throws instead of producing a plan that cannot boot.
49
- */
50
76
  export function buildDeskLaunchPlan(options, dependencies = {}) {
77
+ const hypervisor = options.hypervisor ?? 'cloud-hypervisor';
78
+ if (hypervisor === 'virtualization')
79
+ return buildVirtualizationPlan(options, dependencies);
80
+ if (hypervisor !== 'cloud-hypervisor') {
81
+ throw new DeskError(`Unknown hypervisor: ${String(hypervisor)}`);
82
+ }
83
+ if (options.guestNetwork !== undefined) {
84
+ throw new DeskError('guestNetwork applies to Virtualization.framework desks; a Cloud Hypervisor desk is networked with createDeskNetwork.');
85
+ }
86
+ return buildCloudHypervisorPlan(options, dependencies);
87
+ }
88
+ function buildCloudHypervisorPlan(options, dependencies) {
51
89
  const pathExists = dependencies.pathExists ?? existsSync;
52
90
  const deviceExists = dependencies.deviceExists ?? existsSync;
53
91
  const deskId = cleanDeskId(options.deskId);
@@ -159,6 +197,7 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
159
197
  '--console', 'off',
160
198
  ];
161
199
  return {
200
+ hypervisor: 'cloud-hypervisor',
162
201
  deskId,
163
202
  vmm: { command: vmmPath, args },
164
203
  overlay: { path: overlayPath, basePath: baseImagePath, create },
@@ -174,6 +213,123 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
174
213
  launcherIdentity,
175
214
  };
176
215
  }
216
+ /**
217
+ * The Virtualization.framework plan. The same fail-closed checks on the guest
218
+ * assets; no KVM or VMM binary, because the backend owns the helper; and the
219
+ * overlay is an APFS clone (`cp -c`), which is to APFS what `--reflink=auto`
220
+ * is to XFS: instant and block-sharing. A volume that cannot clone must be
221
+ * replaced with one that supports it.
222
+ */
223
+ function buildVirtualizationPlan(options, dependencies) {
224
+ const pathExists = dependencies.pathExists ?? existsSync;
225
+ const deskId = cleanDeskId(options.deskId);
226
+ if (options.tapDevice !== undefined) {
227
+ throw new DeskError('tapDevice applies to Cloud Hypervisor desks; a Virtualization.framework desk has no TAP.');
228
+ }
229
+ const kernelPath = absolutePath(options.kernelPath, 'kernelPath');
230
+ if (!pathExists(kernelPath)) {
231
+ throw new DeskError(`Guest kernel does not exist: ${kernelPath}`);
232
+ }
233
+ const initramfsPath = options.initramfsPath === undefined ? null : absolutePath(options.initramfsPath, 'initramfsPath');
234
+ if (initramfsPath !== null && !pathExists(initramfsPath)) {
235
+ throw new DeskError(`Initramfs does not exist: ${initramfsPath}`);
236
+ }
237
+ const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath');
238
+ if (!pathExists(baseImagePath)) {
239
+ throw new DeskError(`Base image does not exist: ${baseImagePath}`);
240
+ }
241
+ const overlayPath = absolutePath(options.overlayPath, 'overlayPath');
242
+ const overlayDirectory = dirname(overlayPath);
243
+ if (!pathExists(overlayDirectory)) {
244
+ throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`);
245
+ }
246
+ const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb');
247
+ const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus');
248
+ const runtimeDir = absolutePath(options.runtimeDir ?? join(tmpdir(), 'appkit-desk'), 'runtimeDir');
249
+ // Keep Darwin's small sockaddr_un limit even for a maximum-length desk ID.
250
+ const socketId = createHash('sha256').update(deskId).digest('hex').slice(0, 20);
251
+ const vsockSocketPath = socketPath(join(runtimeDir, `d-${socketId}.vsock`));
252
+ const portsSocketPath = socketPath(join(runtimeDir, `d-${socketId}.ports`));
253
+ const snapshotDir = options.snapshotDir
254
+ ? absolutePath(options.snapshotDir, 'snapshotDir')
255
+ : join(overlayDirectory, `${deskId}.snapshot`);
256
+ const restoring = options.restore === true;
257
+ if (restoring && !pathExists(snapshotDir)) {
258
+ throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`);
259
+ }
260
+ const network = cleanGuestNetwork(options.guestNetwork ?? {});
261
+ return {
262
+ hypervisor: 'virtualization',
263
+ deskId,
264
+ overlay: {
265
+ path: overlayPath,
266
+ basePath: baseImagePath,
267
+ create: pathExists(overlayPath) ? null : { command: 'cp', args: ['-c', baseImagePath, overlayPath] },
268
+ },
269
+ vsock: { cid: VZ_GUEST_CID, socketPath: vsockSocketPath },
270
+ ports: { socketPath: portsSocketPath },
271
+ network: {
272
+ mac: cleanMacAddress(options.macAddress ?? deskMacAddress(deskId)),
273
+ ...network,
274
+ },
275
+ snapshot: { dir: snapshotDir, restoring },
276
+ memoryMb,
277
+ vcpus,
278
+ kernelPath,
279
+ initramfsPath,
280
+ kernelCmdline: options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE,
281
+ launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),
282
+ };
283
+ }
284
+ function cleanGuestNetwork(options) {
285
+ if (options.allowLan !== undefined && typeof options.allowLan !== 'boolean') {
286
+ throw new DeskError('guestNetwork.allowLan must be a boolean.');
287
+ }
288
+ const guestAddress = ipv4(options.guestAddress ?? DEFAULT_VZ_GUEST_ADDRESS, 'guestNetwork.guestAddress');
289
+ const gatewayAddress = ipv4(options.gatewayAddress ?? DEFAULT_VZ_GATEWAY_ADDRESS, 'guestNetwork.gatewayAddress');
290
+ const prefixLength = options.prefixLength ?? DEFAULT_VZ_PREFIX_LENGTH;
291
+ if (!Number.isInteger(prefixLength) || prefixLength < 8 || prefixLength > 30) {
292
+ throw new DeskError('guestNetwork.prefixLength must be an integer between 8 and 30.');
293
+ }
294
+ const mask = (0xffff_ffff << (32 - prefixLength)) >>> 0;
295
+ if (guestAddress === gatewayAddress
296
+ || ((ipv4Number(guestAddress) & mask) >>> 0) !== ((ipv4Number(gatewayAddress) & mask) >>> 0)) {
297
+ throw new DeskError("guestNetwork.guestAddress must be a different address in the gateway's subnet.");
298
+ }
299
+ const dns = [...(options.dns ?? DEFAULT_VZ_DNS)];
300
+ if (dns.length < 1 || dns.length > 3) {
301
+ throw new DeskError('guestNetwork.dns must list one to three IPv4 resolvers.');
302
+ }
303
+ return {
304
+ allowLan: options.allowLan ?? false,
305
+ guestAddress,
306
+ gatewayAddress,
307
+ prefixLength,
308
+ dns: dns.map((entry) => ipv4(entry, 'guestNetwork.dns')),
309
+ };
310
+ }
311
+ /** Stable per desk, so a restored guest finds the NIC it was saved with. */
312
+ function deskMacAddress(deskId) {
313
+ const digest = createHash('sha256').update(`appkit-desk:${deskId}`).digest();
314
+ const octets = [0x06, ...digest.subarray(0, 5)];
315
+ return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':');
316
+ }
317
+ function socketPath(path) {
318
+ if (Buffer.byteLength(path) > DARWIN_SOCKET_PATH_LIMIT) {
319
+ throw new DeskError(`Socket path ${path} is longer than the ${DARWIN_SOCKET_PATH_LIMIT} bytes macOS allows; `
320
+ + 'use a shorter runtimeDir or deskId.');
321
+ }
322
+ return path;
323
+ }
324
+ function ipv4(value, field) {
325
+ if (typeof value !== 'string' || isIP(value) !== 4) {
326
+ throw new DeskError(`${field} must be an IPv4 address.`);
327
+ }
328
+ return value;
329
+ }
330
+ function ipv4Number(address) {
331
+ return address.split('.').reduce((value, octet) => ((value << 8) | Number(octet)) >>> 0, 0);
332
+ }
177
333
  function defaultTapDevice(cid) {
178
334
  return `dsk${cid}`;
179
335
  }
package/plan.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\n\nexport const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor'\nexport const DEFAULT_KVM_PATH = '/dev/kvm'\nexport const DEFAULT_QEMU_IMG_PATH = '/usr/bin/qemu-img'\nexport const DEFAULT_RUNTIME_DIR = '/run/appkit-desk'\nexport const DEFAULT_MEMORY_MB = 384\nexport const DEFAULT_VCPUS = 2\nexport const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet'\n\nconst DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/\nconst TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/\nconst MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i\n/** vsock CIDs 0–2 are reserved (hypervisor, loopback, host). */\nconst MIN_GUEST_CID = 3\nconst MAX_GUEST_CID = 0xffff_fffe\n\n/**\n * Every failure this package raises. The `name` is annotated as `string`\n * rather than left to infer its literal so a subclass can narrow the meaning\n * of a failure — see `DeskRequestFateUnknownError` — without callers losing\n * the one `instanceof DeskError` they already check.\n */\nexport class DeskError extends Error {\n override readonly name: string = 'DeskError'\n}\n\nexport interface DeskLauncherIdentity {\n /** Host/container UID used to invoke the VMM and image tooling. */\n uid: number\n /** Host/container GID used to invoke the VMM and image tooling. */\n gid: number\n}\n\nexport interface DeskLaunchPlanOptions {\n deskId: string\n vmmPath?: string\n kvmPath?: string\n qemuImgPath?: string\n /** Absolute path of the kernel image the microVM boots. */\n kernelPath: string\n /**\n * Absolute path of an initramfs. Required whenever the kernel loads its\n * root-filesystem or virtio drivers as modules — a stock distro cloud kernel\n * panics with \"VFS: Unable to mount root fs\" if it boots without one. Omit\n * only for a kernel with virtio_blk/ext4 built in.\n */\n initramfsPath?: string\n /** Absolute path of the golden raw base image every desk clones. */\n baseImagePath: string\n /** Absolute path of this desk's copy-on-write overlay (a raw reflink clone). */\n overlayPath: string\n memoryMb?: number\n vcpus?: number\n /** Guest vsock context ID; must be unique among resident desks. */\n vsockCid: number\n /** Directory holding the vsock and API control sockets. */\n runtimeDir?: string\n /** Host TAP device name; derived from the CID when omitted. */\n tapDevice?: string\n /** Guest MAC address; derived from the CID when omitted. */\n macAddress?: string\n kernelCmdline?: string\n launcherIdentity?: DeskLauncherIdentity\n /**\n * Where this desk's memory snapshot lives. Defaults beside the overlay, which\n * is the volume sized for desk state — a snapshot is roughly `memoryMb` of\n * guest RAM, so it does not belong on a runtime tmpfs.\n */\n snapshotDir?: string\n /**\n * Resume from that snapshot instead of booting the kernel.\n *\n * A restored VM continues with its process tree intact, which is the whole\n * point of parking with memory rather than shutting down: an agent's in-guest\n * daemon survives. Refused when the snapshot is not there, rather than falling\n * back to a cold boot — a silent cold boot is how a desk loses the state it\n * was promised to have kept.\n */\n restore?: boolean\n}\n\n/**\n * The complete Cloud Hypervisor invocation as inspectable data. Nothing here\n * has been spawned: tests, audit logs, and readiness screens can assert on the\n * exact argv a desk would boot with.\n */\nexport interface DeskLaunchPlan {\n deskId: string\n vmm: { command: string; args: string[] }\n overlay: {\n path: string\n basePath: string\n /** The overlay-creation step, or `null` when the overlay already exists. */\n create: { command: string; args: string[] } | null\n }\n vsock: { cid: number; socketPath: string }\n api: { socketPath: string }\n tap: { device: string; mac: string }\n /** Where a memory snapshot of this desk is written, and read back from. */\n snapshot: { dir: string; restoring: boolean }\n memoryMb: number\n vcpus: number\n kernelPath: string\n /** Resolved initramfs path, or `null` when the kernel needs none. */\n initramfsPath: string | null\n kernelCmdline: string\n launcherIdentity?: DeskLauncherIdentity\n}\n\n/**\n * Whether this host can run desks at all: Linux, a usable `/dev/kvm`, and the\n * Cloud Hypervisor binary. All three checks are injectable and the answer is\n * fail-closed — a host missing any of them gets no desk capability rather\n * than a degraded one.\n */\nexport function isDeskSupported(options: {\n platform?: NodeJS.Platform\n kvmPath?: string\n vmmPath?: string\n pathExists?: (path: string) => boolean\n} = {}): boolean {\n const platform = options.platform ?? process.platform\n const pathExists = options.pathExists ?? existsSync\n return (\n platform === 'linux'\n && pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)\n && pathExists(options.vmmPath ?? DEFAULT_VMM_PATH)\n )\n}\n\n/** Validate a desk identifier, which is used in socket paths and device names. */\nexport function cleanDeskId(value: string): string {\n if (!DESK_ID_PATTERN.test(value)) {\n throw new DeskError(\n 'deskId must be 1–64 letters, numbers, dots, underscores, or hyphens.',\n )\n }\n return value\n}\n\n/**\n * Build the deterministic Cloud Hypervisor launch plan without spawning\n * anything. Fail closed: a missing `/dev/kvm`, VMM binary, kernel, base image,\n * or overlay directory throws instead of producing a plan that cannot boot.\n */\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies: {\n pathExists?: (path: string) => boolean\n deviceExists?: (path: string) => boolean\n } = {},\n): DeskLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deviceExists = dependencies.deviceExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n const kvmPath = absolutePath(options.kvmPath ?? DEFAULT_KVM_PATH, 'kvmPath')\n if (!deviceExists(kvmPath)) {\n throw new DeskError(\n `KVM is not available at ${kvmPath}. Desks require hardware virtualization; `\n + 'there is no software-emulation fallback.',\n )\n }\n const vmmPath = absolutePath(options.vmmPath ?? DEFAULT_VMM_PATH, 'vmmPath')\n if (!pathExists(vmmPath)) {\n throw new DeskError(\n `Cloud Hypervisor was not found at ${vmmPath}. Install cloud-hypervisor or configure vmmPath.`,\n )\n }\n const kernelPath = absolutePath(options.kernelPath, 'kernelPath')\n if (!pathExists(kernelPath)) {\n throw new DeskError(`Guest kernel does not exist: ${kernelPath}`)\n }\n const initramfsPath =\n options.initramfsPath === undefined\n ? null\n : absolutePath(options.initramfsPath, 'initramfsPath')\n if (initramfsPath !== null && !pathExists(initramfsPath)) {\n throw new DeskError(`Initramfs does not exist: ${initramfsPath}`)\n }\n const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath')\n if (!pathExists(baseImagePath)) {\n throw new DeskError(`Base image does not exist: ${baseImagePath}`)\n }\n const overlayPath = absolutePath(options.overlayPath, 'overlayPath')\n const overlayDirectory = dirname(overlayPath)\n if (!pathExists(overlayDirectory)) {\n throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`)\n }\n const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb')\n const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus')\n const vsockCid = cleanCid(options.vsockCid)\n const runtimeDir = absolutePath(options.runtimeDir ?? DEFAULT_RUNTIME_DIR, 'runtimeDir')\n const tapDevice = cleanTapDevice(options.tapDevice ?? defaultTapDevice(vsockCid))\n const macAddress = cleanMacAddress(options.macAddress ?? defaultMacAddress(vsockCid))\n const kernelCmdline = options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE\n const launcherIdentity = cleanLauncherIdentity(options.launcherIdentity)\n const vsockSocketPath = join(runtimeDir, `desk-${deskId}.vsock`)\n const apiSocketPath = join(runtimeDir, `desk-${deskId}.api.sock`)\n // Beside the overlay by default: a snapshot is the guest's RAM, so it belongs\n // on the volume provisioned for desk state rather than on a runtime tmpfs.\n const snapshotDir = options.snapshotDir\n ? absolutePath(options.snapshotDir, 'snapshotDir')\n : join(overlayDirectory, `${deskId}.snapshot`)\n const restoring = options.restore === true\n if (restoring && !pathExists(snapshotDir)) {\n // Refused rather than silently cold-booted. A caller asking to restore has\n // been told this desk kept its memory; quietly booting a fresh kernel would\n // lose the process tree it was relying on and look like a crash inside the\n // guest instead of a missing snapshot out here.\n throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`)\n }\n\n const create = pathExists(overlayPath)\n ? null\n : {\n // Cloud Hypervisor cannot follow disk backing chains: a qcow2 overlay\n // over a qcow2/raw base is rejected with UnsupportedFeature /\n // MaxNestingDepthExceeded. So the overlay is a plain raw file, cloned\n // from the raw base with `cp --reflink=auto` — an instant, block-sharing\n // copy-on-write clone on XFS/Btrfs and a graceful full-copy fallback on\n // ext4. Either way CH boots the resulting raw file with no backing chain.\n command: 'cp',\n args: ['--reflink=auto', baseImagePath, overlayPath],\n }\n\n // A restoring VMM is handed the snapshot and nothing else.\n //\n // The snapshot carries the whole machine config — memory size, cpus, disk,\n // vsock, net — so passing `--kernel`/`--disk` alongside `--restore` is not\n // redundant, it is rejected.\n //\n // `resume=false` is deliberate, and it is the difference between this being\n // safe and being a data-loss bug. A memory snapshot is only valid against the\n // disk as it stood when the snapshot was taken. Resume the guest in this same\n // spawn and it starts writing immediately, so for as long as the snapshot\n // still exists there are two valid-looking pasts for one desk — and a host\n // that died in that window would, on the next start, replay this memory\n // against a disk that had moved on. That is corruption, not a stale cache.\n //\n // So the guest comes back PAUSED, the caller destroys the snapshot while\n // nothing can write, and only then is the VM resumed.\n //\n // The tap is reopened BY NAME from the snapshotted config. That is the detail\n // that makes this possible at all: Cloud Hypervisor's `net_fds` restore\n // parameter exists for VMs whose tap was passed as a file descriptor, and\n // producing one from Node would mean an ioctl(TUNSETIFF) it cannot make.\n // Because `deskPlan` configures `--net tap=<name>`, there is nothing to pass.\n const args = restoring\n ? [\n '--api-socket', apiSocketPath,\n '--restore', `source_url=file://${snapshotDir},resume=false`,\n ]\n : [\n '--api-socket', apiSocketPath,\n '--kernel', kernelPath,\n // A modular distro kernel needs its initramfs to bring up virtio_blk and\n // mount root; omitted only for a kernel with those drivers built in.\n ...(initramfsPath === null ? [] : ['--initramfs', initramfsPath]),\n '--cmdline', kernelCmdline,\n // Be explicit that the disk is raw: CH deprecated image-format auto-detection\n // and warns without image_type; there is never a qcow2 overlay here.\n '--disk', `path=${overlayPath},image_type=raw`,\n '--memory', `size=${memoryMb}M`,\n '--cpus', `boot=${vcpus}`,\n '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,\n '--net', `tap=${tapDevice},mac=${macAddress}`,\n '--serial', 'off',\n '--console', 'off',\n ]\n\n return {\n deskId,\n vmm: { command: vmmPath, args },\n overlay: { path: overlayPath, basePath: baseImagePath, create },\n vsock: { cid: vsockCid, socketPath: vsockSocketPath },\n api: { socketPath: apiSocketPath },\n tap: { device: tapDevice, mac: macAddress },\n snapshot: { dir: snapshotDir, restoring },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline,\n launcherIdentity,\n }\n}\n\nfunction defaultTapDevice(cid: number): string {\n return `dsk${cid}`\n}\n\nfunction defaultMacAddress(cid: number): string {\n // Locally administered, unicast prefix; the low four octets carry the CID so\n // the address is stable per desk and never collides across resident desks.\n const octets = [0x06, 0x00, (cid >>> 24) & 0xff, (cid >>> 16) & 0xff, (cid >>> 8) & 0xff, cid & 0xff]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction cleanCid(value: number): number {\n if (!Number.isInteger(value) || value < MIN_GUEST_CID || value > MAX_GUEST_CID) {\n throw new DeskError(\n `vsockCid must be an integer between ${MIN_GUEST_CID} and ${MAX_GUEST_CID}.`,\n )\n }\n return value\n}\n\nfunction cleanTapDevice(value: string): string {\n if (!TAP_DEVICE_PATTERN.test(value)) {\n throw new DeskError('tapDevice must be 1–15 characters of letters, numbers, underscores, or hyphens.')\n }\n return value\n}\n\nfunction cleanMacAddress(value: string): string {\n if (!MAC_ADDRESS_PATTERN.test(value)) {\n throw new DeskError(`macAddress is not a valid MAC address: ${value}`)\n }\n return value.toLowerCase()\n}\n\nexport function cleanLauncherIdentity(\n identity: DeskLauncherIdentity | undefined,\n): DeskLauncherIdentity | undefined {\n if (!identity) return undefined\n const max = 2_147_483_647\n if (!Number.isInteger(identity.uid) || identity.uid < 1 || identity.uid > max) {\n throw new DeskError(`launcherIdentity.uid must be an integer between 1 and ${max}.`)\n }\n if (!Number.isInteger(identity.gid) || identity.gid < 0 || identity.gid > max) {\n throw new DeskError(`launcherIdentity.gid must be an integer between 0 and ${max}.`)\n }\n return { uid: identity.uid, gid: identity.gid }\n}\n\nfunction absolutePath(value: string, field: string): string {\n if (!value || !isAbsolute(value)) {\n throw new DeskError(`${field} must be an absolute path.`)\n }\n return resolve(value)\n}\n\nfunction positiveInteger(value: number, field: string): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new DeskError(`${field} must be a positive integer.`)\n }\n return value\n}\n"]}
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer'\nimport { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { isIP } from 'node:net'\nimport { arch as hostArch, release as hostRelease, tmpdir } from 'node:os'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\n\nexport const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor'\nexport const DEFAULT_KVM_PATH = '/dev/kvm'\nexport const DEFAULT_QEMU_IMG_PATH = '/usr/bin/qemu-img'\nexport const DEFAULT_RUNTIME_DIR = '/run/appkit-desk'\nexport const DEFAULT_MEMORY_MB = 384\nexport const DEFAULT_VCPUS = 2\nexport const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet'\n/**\n * The guest network Virtualization.framework desks get from the helper's NAT:\n * the same addresses QEMU's user networking uses, so they are unsurprising in\n * a guest. Nothing outside the helper ever sees them.\n */\nexport const DEFAULT_VZ_GUEST_ADDRESS = '10.0.2.15'\nexport const DEFAULT_VZ_GATEWAY_ADDRESS = '10.0.2.2'\nexport const DEFAULT_VZ_PREFIX_LENGTH = 24\nexport const DEFAULT_VZ_DNS: readonly string[] = ['1.1.1.1', '9.9.9.9']\n/** Virtualization.framework always gives the guest vsock context ID 3. */\nexport const VZ_GUEST_CID = 3\n/** The first Darwin release with Virtualization.framework save and restore (macOS 14). */\nconst MIN_DARWIN_MAJOR = 23\n/** `sun_path` holds 104 bytes on Darwin, including the terminator. */\nconst DARWIN_SOCKET_PATH_LIMIT = 103\n\nconst DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/\nconst TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/\nconst MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i\n/** vsock CIDs 0–2 are reserved (hypervisor, loopback, host). */\nconst MIN_GUEST_CID = 3\nconst MAX_GUEST_CID = 0xffff_fffe\n\n/**\n * Every failure this package raises. The `name` is annotated as `string`\n * rather than left to infer its literal so a subclass can narrow the meaning\n * of a failure — see `DeskRequestFateUnknownError` — without callers losing\n * the one `instanceof DeskError` they already check.\n */\nexport class DeskError extends Error {\n override readonly name: string = 'DeskError'\n}\n\nexport interface DeskLauncherIdentity {\n /** Host/container UID used to invoke the VMM and image tooling. */\n uid: number\n /** Host/container GID used to invoke the VMM and image tooling. */\n gid: number\n}\n\n/**\n * Which hypervisor a plan is for: Cloud Hypervisor over KVM on Linux, or\n * Apple's Virtualization.framework through the `appkit-desk-vz` helper on an\n * Apple silicon Mac.\n */\nexport type DeskHypervisor = 'cloud-hypervisor' | 'virtualization'\n\n/**\n * The guest network of a Virtualization.framework desk. The helper runs a\n * userspace NAT: the guest reaches the public internet and nothing else unless\n * `allowLan` is set — no RFC 1918 space, link-local, CGNAT, or the Mac itself.\n */\nexport interface DeskGuestNetworkOptions {\n /** Let the guest reach the Mac's LAN, link-local and CGNAT space, and the Mac. */\n allowLan?: boolean\n /** The address the helper's DHCP server leases the guest. */\n guestAddress?: string\n /** The helper's own address on the link: the guest's router. */\n gatewayAddress?: string\n prefixLength?: number\n /** Resolvers the guest is handed; the first also answers DNS sent to the gateway. */\n dns?: readonly string[]\n}\n\nexport interface DeskLaunchPlanOptions {\n deskId: string\n /** Defaults to `cloud-hypervisor`. */\n hypervisor?: DeskHypervisor\n /** Cloud Hypervisor only: the VMM binary. */\n vmmPath?: string\n /** Cloud Hypervisor only: the KVM device. */\n kvmPath?: string\n qemuImgPath?: string\n /** Absolute path of the kernel image the microVM boots. */\n kernelPath: string\n /**\n * Absolute path of an initramfs. Required whenever the kernel loads its\n * root-filesystem or virtio drivers as modules — a stock distro cloud kernel\n * panics with \"VFS: Unable to mount root fs\" if it boots without one. Omit\n * only for a kernel with virtio_blk/ext4 built in.\n */\n initramfsPath?: string\n /** Absolute path of the golden raw base image every desk clones. */\n baseImagePath: string\n /** Absolute path of this desk's copy-on-write overlay (a raw reflink clone). */\n overlayPath: string\n memoryMb?: number\n vcpus?: number\n /**\n * Guest vsock context ID; must be unique among resident desks. Cloud\n * Hypervisor only — a Virtualization.framework guest is always CID 3, and\n * its vsock is private to its helper.\n */\n vsockCid: number\n /**\n * Directory holding the desk's control sockets. Defaults to\n * `/run/appkit-desk` for Cloud Hypervisor and `<os tmpdir>/appkit-desk` for\n * Virtualization.framework.\n */\n runtimeDir?: string\n /** Cloud Hypervisor only: host TAP device name; derived from the CID when omitted. */\n tapDevice?: string\n /**\n * Guest MAC address. Derived from the CID for Cloud Hypervisor, and from the\n * desk ID for Virtualization.framework, whose restore needs the same NIC.\n */\n macAddress?: string\n /** Virtualization.framework only: the helper's NAT network and fence. */\n guestNetwork?: DeskGuestNetworkOptions\n kernelCmdline?: string\n launcherIdentity?: DeskLauncherIdentity\n /**\n * Where this desk's memory snapshot lives. Defaults beside the overlay, which\n * is the volume sized for desk state — a snapshot is roughly `memoryMb` of\n * guest RAM, so it does not belong on a runtime tmpfs.\n */\n snapshotDir?: string\n /**\n * Resume from that snapshot instead of booting the kernel.\n *\n * A restored VM continues with its process tree intact, which is the whole\n * point of parking with memory rather than shutting down: an agent's in-guest\n * daemon survives. Refused when the snapshot is not there, rather than falling\n * back to a cold boot — a silent cold boot is how a desk loses the state it\n * was promised to have kept.\n */\n restore?: boolean\n}\n\n/** What every plan carries, whichever hypervisor runs it. */\nexport interface DeskLaunchPlanBase {\n deskId: string\n overlay: {\n path: string\n basePath: string\n /** The overlay-creation step, or `null` when the overlay already exists. */\n create: { command: string; args: string[] } | null\n }\n /**\n * The host Unix socket the guest's vsock is reached through, with Cloud\n * Hypervisor's hybrid-vsock preamble: write `CONNECT <port>\\n`, read\n * `OK <n>\\n`, then the stream is the guest connection.\n */\n vsock: { cid: number; socketPath: string }\n /** Where a memory snapshot of this desk is written, and read back from. */\n snapshot: { dir: string; restoring: boolean }\n memoryMb: number\n vcpus: number\n kernelPath: string\n /** Resolved initramfs path, or `null` when the kernel needs none. */\n initramfsPath: string | null\n kernelCmdline: string\n launcherIdentity?: DeskLauncherIdentity\n}\n\n/**\n * The complete Cloud Hypervisor invocation as inspectable data. Nothing here\n * has been spawned: tests, audit logs, and readiness screens can assert on the\n * exact argv a desk would boot with.\n */\nexport interface CloudHypervisorLaunchPlan extends DeskLaunchPlanBase {\n hypervisor: 'cloud-hypervisor'\n vmm: { command: string; args: string[] }\n api: { socketPath: string }\n tap: { device: string; mac: string }\n}\n\n/**\n * A Virtualization.framework desk as inspectable data: what the helper is\n * asked to boot. The helper path belongs to the backend, not the plan.\n */\nexport interface VirtualizationLaunchPlan extends DeskLaunchPlanBase {\n hypervisor: 'virtualization'\n /**\n * The host Unix socket guest TCP ports are reached through, with the same\n * preamble as `vsock`: `CONNECT <tcp port>\\n` answered by `OK <n>\\n` once\n * the guest accepts, or a close with nothing written when it refuses.\n */\n ports: { socketPath: string }\n network: {\n mac: string\n allowLan: boolean\n guestAddress: string\n gatewayAddress: string\n prefixLength: number\n dns: string[]\n }\n}\n\nexport type DeskLaunchPlan = CloudHypervisorLaunchPlan | VirtualizationLaunchPlan\n\n/**\n * Whether this host can run desks at all. On Linux: a usable `/dev/kvm` and\n * the Cloud Hypervisor binary. On macOS: Apple silicon, macOS 14 or later (the\n * first with Virtualization.framework save and restore), and an existing\n * `appkit-desk-vz` helper at `helperPath`. Signature and entitlement are checked\n * by macOS on execution, not by this probe. Every check is injectable and the\n * answer is fail-closed — a host missing any of them gets no desk capability\n * rather than a degraded one.\n */\nexport function isDeskSupported(options: {\n platform?: NodeJS.Platform\n kvmPath?: string\n vmmPath?: string\n /** macOS: the signed `appkit-desk-vz` helper. Required there. */\n helperPath?: string\n arch?: string\n /** The kernel release, as `os.release()` reports it. */\n osRelease?: string\n pathExists?: (path: string) => boolean\n} = {}): boolean {\n const platform = options.platform ?? process.platform\n const pathExists = options.pathExists ?? existsSync\n if (platform === 'darwin') {\n const major = Number.parseInt((options.osRelease ?? hostRelease()).split('.')[0] ?? '', 10)\n return (\n (options.arch ?? hostArch()) === 'arm64'\n && Number.isInteger(major)\n && major >= MIN_DARWIN_MAJOR\n && options.helperPath !== undefined\n && isAbsolute(options.helperPath)\n && pathExists(options.helperPath)\n )\n }\n return (\n platform === 'linux'\n && pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)\n && pathExists(options.vmmPath ?? DEFAULT_VMM_PATH)\n )\n}\n\n/** Validate a desk identifier, which is used in socket paths and device names. */\nexport function cleanDeskId(value: string): string {\n if (!DESK_ID_PATTERN.test(value)) {\n throw new DeskError(\n 'deskId must be 1–64 letters, numbers, dots, underscores, or hyphens.',\n )\n }\n return value\n}\n\nexport interface DeskLaunchPlanDependencies {\n pathExists?: (path: string) => boolean\n deviceExists?: (path: string) => boolean\n}\n\n/**\n * Build the deterministic launch plan without spawning anything. Fail closed:\n * a missing `/dev/kvm` or VMM binary (Cloud Hypervisor), kernel, base image,\n * or overlay directory throws instead of producing a plan that cannot boot.\n */\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions & { hypervisor: 'virtualization' },\n dependencies?: DeskLaunchPlanDependencies,\n): VirtualizationLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions & { hypervisor?: 'cloud-hypervisor' },\n dependencies?: DeskLaunchPlanDependencies,\n): CloudHypervisorLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies?: DeskLaunchPlanDependencies,\n): DeskLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies = {},\n): DeskLaunchPlan {\n const hypervisor = options.hypervisor ?? 'cloud-hypervisor'\n if (hypervisor === 'virtualization') return buildVirtualizationPlan(options, dependencies)\n if (hypervisor !== 'cloud-hypervisor') {\n throw new DeskError(`Unknown hypervisor: ${String(hypervisor)}`)\n }\n if (options.guestNetwork !== undefined) {\n throw new DeskError(\n 'guestNetwork applies to Virtualization.framework desks; a Cloud Hypervisor desk is networked with createDeskNetwork.',\n )\n }\n return buildCloudHypervisorPlan(options, dependencies)\n}\n\nfunction buildCloudHypervisorPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies,\n): CloudHypervisorLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deviceExists = dependencies.deviceExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n const kvmPath = absolutePath(options.kvmPath ?? DEFAULT_KVM_PATH, 'kvmPath')\n if (!deviceExists(kvmPath)) {\n throw new DeskError(\n `KVM is not available at ${kvmPath}. Desks require hardware virtualization; `\n + 'there is no software-emulation fallback.',\n )\n }\n const vmmPath = absolutePath(options.vmmPath ?? DEFAULT_VMM_PATH, 'vmmPath')\n if (!pathExists(vmmPath)) {\n throw new DeskError(\n `Cloud Hypervisor was not found at ${vmmPath}. Install cloud-hypervisor or configure vmmPath.`,\n )\n }\n const kernelPath = absolutePath(options.kernelPath, 'kernelPath')\n if (!pathExists(kernelPath)) {\n throw new DeskError(`Guest kernel does not exist: ${kernelPath}`)\n }\n const initramfsPath =\n options.initramfsPath === undefined\n ? null\n : absolutePath(options.initramfsPath, 'initramfsPath')\n if (initramfsPath !== null && !pathExists(initramfsPath)) {\n throw new DeskError(`Initramfs does not exist: ${initramfsPath}`)\n }\n const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath')\n if (!pathExists(baseImagePath)) {\n throw new DeskError(`Base image does not exist: ${baseImagePath}`)\n }\n const overlayPath = absolutePath(options.overlayPath, 'overlayPath')\n const overlayDirectory = dirname(overlayPath)\n if (!pathExists(overlayDirectory)) {\n throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`)\n }\n const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb')\n const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus')\n const vsockCid = cleanCid(options.vsockCid)\n const runtimeDir = absolutePath(options.runtimeDir ?? DEFAULT_RUNTIME_DIR, 'runtimeDir')\n const tapDevice = cleanTapDevice(options.tapDevice ?? defaultTapDevice(vsockCid))\n const macAddress = cleanMacAddress(options.macAddress ?? defaultMacAddress(vsockCid))\n const kernelCmdline = options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE\n const launcherIdentity = cleanLauncherIdentity(options.launcherIdentity)\n const vsockSocketPath = join(runtimeDir, `desk-${deskId}.vsock`)\n const apiSocketPath = join(runtimeDir, `desk-${deskId}.api.sock`)\n // Beside the overlay by default: a snapshot is the guest's RAM, so it belongs\n // on the volume provisioned for desk state rather than on a runtime tmpfs.\n const snapshotDir = options.snapshotDir\n ? absolutePath(options.snapshotDir, 'snapshotDir')\n : join(overlayDirectory, `${deskId}.snapshot`)\n const restoring = options.restore === true\n if (restoring && !pathExists(snapshotDir)) {\n // Refused rather than silently cold-booted. A caller asking to restore has\n // been told this desk kept its memory; quietly booting a fresh kernel would\n // lose the process tree it was relying on and look like a crash inside the\n // guest instead of a missing snapshot out here.\n throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`)\n }\n\n const create = pathExists(overlayPath)\n ? null\n : {\n // Cloud Hypervisor cannot follow disk backing chains: a qcow2 overlay\n // over a qcow2/raw base is rejected with UnsupportedFeature /\n // MaxNestingDepthExceeded. So the overlay is a plain raw file, cloned\n // from the raw base with `cp --reflink=auto` — an instant, block-sharing\n // copy-on-write clone on XFS/Btrfs and a graceful full-copy fallback on\n // ext4. Either way CH boots the resulting raw file with no backing chain.\n command: 'cp',\n args: ['--reflink=auto', baseImagePath, overlayPath],\n }\n\n // A restoring VMM is handed the snapshot and nothing else.\n //\n // The snapshot carries the whole machine config — memory size, cpus, disk,\n // vsock, net — so passing `--kernel`/`--disk` alongside `--restore` is not\n // redundant, it is rejected.\n //\n // `resume=false` is deliberate, and it is the difference between this being\n // safe and being a data-loss bug. A memory snapshot is only valid against the\n // disk as it stood when the snapshot was taken. Resume the guest in this same\n // spawn and it starts writing immediately, so for as long as the snapshot\n // still exists there are two valid-looking pasts for one desk — and a host\n // that died in that window would, on the next start, replay this memory\n // against a disk that had moved on. That is corruption, not a stale cache.\n //\n // So the guest comes back PAUSED, the caller destroys the snapshot while\n // nothing can write, and only then is the VM resumed.\n //\n // The tap is reopened BY NAME from the snapshotted config. That is the detail\n // that makes this possible at all: Cloud Hypervisor's `net_fds` restore\n // parameter exists for VMs whose tap was passed as a file descriptor, and\n // producing one from Node would mean an ioctl(TUNSETIFF) it cannot make.\n // Because `deskPlan` configures `--net tap=<name>`, there is nothing to pass.\n const args = restoring\n ? [\n '--api-socket', apiSocketPath,\n '--restore', `source_url=file://${snapshotDir},resume=false`,\n ]\n : [\n '--api-socket', apiSocketPath,\n '--kernel', kernelPath,\n // A modular distro kernel needs its initramfs to bring up virtio_blk and\n // mount root; omitted only for a kernel with those drivers built in.\n ...(initramfsPath === null ? [] : ['--initramfs', initramfsPath]),\n '--cmdline', kernelCmdline,\n // Be explicit that the disk is raw: CH deprecated image-format auto-detection\n // and warns without image_type; there is never a qcow2 overlay here.\n '--disk', `path=${overlayPath},image_type=raw`,\n '--memory', `size=${memoryMb}M`,\n '--cpus', `boot=${vcpus}`,\n '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,\n '--net', `tap=${tapDevice},mac=${macAddress}`,\n '--serial', 'off',\n '--console', 'off',\n ]\n\n return {\n hypervisor: 'cloud-hypervisor',\n deskId,\n vmm: { command: vmmPath, args },\n overlay: { path: overlayPath, basePath: baseImagePath, create },\n vsock: { cid: vsockCid, socketPath: vsockSocketPath },\n api: { socketPath: apiSocketPath },\n tap: { device: tapDevice, mac: macAddress },\n snapshot: { dir: snapshotDir, restoring },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline,\n launcherIdentity,\n }\n}\n\n/**\n * The Virtualization.framework plan. The same fail-closed checks on the guest\n * assets; no KVM or VMM binary, because the backend owns the helper; and the\n * overlay is an APFS clone (`cp -c`), which is to APFS what `--reflink=auto`\n * is to XFS: instant and block-sharing. A volume that cannot clone must be\n * replaced with one that supports it.\n */\nfunction buildVirtualizationPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies,\n): VirtualizationLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n if (options.tapDevice !== undefined) {\n throw new DeskError('tapDevice applies to Cloud Hypervisor desks; a Virtualization.framework desk has no TAP.')\n }\n const kernelPath = absolutePath(options.kernelPath, 'kernelPath')\n if (!pathExists(kernelPath)) {\n throw new DeskError(`Guest kernel does not exist: ${kernelPath}`)\n }\n const initramfsPath =\n options.initramfsPath === undefined ? null : absolutePath(options.initramfsPath, 'initramfsPath')\n if (initramfsPath !== null && !pathExists(initramfsPath)) {\n throw new DeskError(`Initramfs does not exist: ${initramfsPath}`)\n }\n const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath')\n if (!pathExists(baseImagePath)) {\n throw new DeskError(`Base image does not exist: ${baseImagePath}`)\n }\n const overlayPath = absolutePath(options.overlayPath, 'overlayPath')\n const overlayDirectory = dirname(overlayPath)\n if (!pathExists(overlayDirectory)) {\n throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`)\n }\n const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb')\n const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus')\n const runtimeDir = absolutePath(options.runtimeDir ?? join(tmpdir(), 'appkit-desk'), 'runtimeDir')\n // Keep Darwin's small sockaddr_un limit even for a maximum-length desk ID.\n const socketId = createHash('sha256').update(deskId).digest('hex').slice(0, 20)\n const vsockSocketPath = socketPath(join(runtimeDir, `d-${socketId}.vsock`))\n const portsSocketPath = socketPath(join(runtimeDir, `d-${socketId}.ports`))\n const snapshotDir = options.snapshotDir\n ? absolutePath(options.snapshotDir, 'snapshotDir')\n : join(overlayDirectory, `${deskId}.snapshot`)\n const restoring = options.restore === true\n if (restoring && !pathExists(snapshotDir)) {\n throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`)\n }\n const network = cleanGuestNetwork(options.guestNetwork ?? {})\n return {\n hypervisor: 'virtualization',\n deskId,\n overlay: {\n path: overlayPath,\n basePath: baseImagePath,\n create: pathExists(overlayPath) ? null : { command: 'cp', args: ['-c', baseImagePath, overlayPath] },\n },\n vsock: { cid: VZ_GUEST_CID, socketPath: vsockSocketPath },\n ports: { socketPath: portsSocketPath },\n network: {\n mac: cleanMacAddress(options.macAddress ?? deskMacAddress(deskId)),\n ...network,\n },\n snapshot: { dir: snapshotDir, restoring },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline: options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE,\n launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),\n }\n}\n\nfunction cleanGuestNetwork(\n options: DeskGuestNetworkOptions,\n): Omit<VirtualizationLaunchPlan['network'], 'mac'> {\n if (options.allowLan !== undefined && typeof options.allowLan !== 'boolean') {\n throw new DeskError('guestNetwork.allowLan must be a boolean.')\n }\n const guestAddress = ipv4(options.guestAddress ?? DEFAULT_VZ_GUEST_ADDRESS, 'guestNetwork.guestAddress')\n const gatewayAddress = ipv4(options.gatewayAddress ?? DEFAULT_VZ_GATEWAY_ADDRESS, 'guestNetwork.gatewayAddress')\n const prefixLength = options.prefixLength ?? DEFAULT_VZ_PREFIX_LENGTH\n if (!Number.isInteger(prefixLength) || prefixLength < 8 || prefixLength > 30) {\n throw new DeskError('guestNetwork.prefixLength must be an integer between 8 and 30.')\n }\n const mask = (0xffff_ffff << (32 - prefixLength)) >>> 0\n if (\n guestAddress === gatewayAddress\n || ((ipv4Number(guestAddress) & mask) >>> 0) !== ((ipv4Number(gatewayAddress) & mask) >>> 0)\n ) {\n throw new DeskError(\"guestNetwork.guestAddress must be a different address in the gateway's subnet.\")\n }\n const dns = [...(options.dns ?? DEFAULT_VZ_DNS)]\n if (dns.length < 1 || dns.length > 3) {\n throw new DeskError('guestNetwork.dns must list one to three IPv4 resolvers.')\n }\n return {\n allowLan: options.allowLan ?? false,\n guestAddress,\n gatewayAddress,\n prefixLength,\n dns: dns.map((entry) => ipv4(entry, 'guestNetwork.dns')),\n }\n}\n\n/** Stable per desk, so a restored guest finds the NIC it was saved with. */\nfunction deskMacAddress(deskId: string): string {\n const digest = createHash('sha256').update(`appkit-desk:${deskId}`).digest()\n const octets = [0x06, ...digest.subarray(0, 5)]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction socketPath(path: string): string {\n if (Buffer.byteLength(path) > DARWIN_SOCKET_PATH_LIMIT) {\n throw new DeskError(\n `Socket path ${path} is longer than the ${DARWIN_SOCKET_PATH_LIMIT} bytes macOS allows; `\n + 'use a shorter runtimeDir or deskId.',\n )\n }\n return path\n}\n\nfunction ipv4(value: string, field: string): string {\n if (typeof value !== 'string' || isIP(value) !== 4) {\n throw new DeskError(`${field} must be an IPv4 address.`)\n }\n return value\n}\n\nfunction ipv4Number(address: string): number {\n return address.split('.').reduce((value, octet) => ((value << 8) | Number(octet)) >>> 0, 0)\n}\n\nfunction defaultTapDevice(cid: number): string {\n return `dsk${cid}`\n}\n\nfunction defaultMacAddress(cid: number): string {\n // Locally administered, unicast prefix; the low four octets carry the CID so\n // the address is stable per desk and never collides across resident desks.\n const octets = [0x06, 0x00, (cid >>> 24) & 0xff, (cid >>> 16) & 0xff, (cid >>> 8) & 0xff, cid & 0xff]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction cleanCid(value: number): number {\n if (!Number.isInteger(value) || value < MIN_GUEST_CID || value > MAX_GUEST_CID) {\n throw new DeskError(\n `vsockCid must be an integer between ${MIN_GUEST_CID} and ${MAX_GUEST_CID}.`,\n )\n }\n return value\n}\n\nfunction cleanTapDevice(value: string): string {\n if (!TAP_DEVICE_PATTERN.test(value)) {\n throw new DeskError('tapDevice must be 1–15 characters of letters, numbers, underscores, or hyphens.')\n }\n return value\n}\n\nfunction cleanMacAddress(value: string): string {\n if (!MAC_ADDRESS_PATTERN.test(value)) {\n throw new DeskError(`macAddress is not a valid MAC address: ${value}`)\n }\n return value.toLowerCase()\n}\n\nexport function cleanLauncherIdentity(\n identity: DeskLauncherIdentity | undefined,\n): DeskLauncherIdentity | undefined {\n if (!identity) return undefined\n const max = 2_147_483_647\n if (!Number.isInteger(identity.uid) || identity.uid < 1 || identity.uid > max) {\n throw new DeskError(`launcherIdentity.uid must be an integer between 1 and ${max}.`)\n }\n if (!Number.isInteger(identity.gid) || identity.gid < 0 || identity.gid > max) {\n throw new DeskError(`launcherIdentity.gid must be an integer between 0 and ${max}.`)\n }\n return { uid: identity.uid, gid: identity.gid }\n}\n\nfunction absolutePath(value: string, field: string): string {\n if (!value || !isAbsolute(value)) {\n throw new DeskError(`${field} must be an absolute path.`)\n }\n return resolve(value)\n}\n\nfunction positiveInteger(value: number, field: string): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new DeskError(`${field} must be a positive integer.`)\n }\n return value\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { Duplex } from 'node:stream';
2
+ /** A local endpoint backed by a running desk's TCP connection. */
3
+ export interface DeskPortListener {
4
+ host: '127.0.0.1';
5
+ port: number;
6
+ /** Close the listener and every active connection. Safe to call repeatedly. */
7
+ close(): Promise<void>;
8
+ }
9
+ /** Expose one guest TCP port to local HTTP, WebSocket or other TCP clients. */
10
+ export declare function listenDeskPort(desk: {
11
+ connect(port: number): Promise<Duplex>;
12
+ }, port: number): Promise<DeskPortListener>;
13
+ //# sourceMappingURL=port-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-relay.d.ts","sourceRoot":"","sources":["../src/port-relay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,+EAA+E;AAC/E,wBAAsB,cAAc,CAClC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,EAChD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAsC3B"}
package/port-relay.js ADDED
@@ -0,0 +1,56 @@
1
+ import { createServer } from 'node:net';
2
+ import { DeskError } from './plan.js';
3
+ /** Expose one guest TCP port to local HTTP, WebSocket or other TCP clients. */
4
+ export async function listenDeskPort(desk, port) {
5
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
6
+ throw new DeskError('Guest TCP port must be between 1 and 65535.');
7
+ const clients = new Set();
8
+ const guests = new Set();
9
+ let closed = false;
10
+ let closing;
11
+ const server = createServer((client) => {
12
+ if (closed) {
13
+ client.destroy();
14
+ return;
15
+ }
16
+ clients.add(client);
17
+ client.once('close', () => clients.delete(client));
18
+ client.on('error', () => client.destroy());
19
+ void Promise.resolve().then(() => desk.connect(port)).then((guest) => {
20
+ if (closed || client.destroyed) {
21
+ guest.destroy();
22
+ return;
23
+ }
24
+ guests.add(guest);
25
+ guest.once('close', () => { guests.delete(guest); client.destroy(); });
26
+ guest.on('error', () => { guest.destroy(); client.destroy(); });
27
+ client.once('close', () => guest.destroy());
28
+ client.pipe(guest).pipe(client);
29
+ }, () => client.destroy());
30
+ });
31
+ await new Promise((resolve, reject) => {
32
+ const onError = (error) => reject(error);
33
+ server.once('error', onError);
34
+ server.listen(0, '127.0.0.1', () => { server.off('error', onError); resolve(); });
35
+ });
36
+ // Errors after listen must not become unhandled process errors.
37
+ server.on('error', () => { void close(); });
38
+ const address = server.address();
39
+ if (!address || typeof address === 'string') {
40
+ server.close();
41
+ throw new DeskError('Could not determine desk relay address.');
42
+ }
43
+ function close() {
44
+ if (closing)
45
+ return closing;
46
+ closed = true;
47
+ for (const client of clients)
48
+ client.destroy();
49
+ for (const guest of guests)
50
+ guest.destroy();
51
+ closing = new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
52
+ return closing;
53
+ }
54
+ return { host: '127.0.0.1', port: address.port, close };
55
+ }
56
+ //# sourceMappingURL=port-relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-relay.js","sourceRoot":"","sources":["../src/port-relay.ts"],"names":[],"mappings":"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","sourcesContent":["import { createServer, type Server, type Socket } from 'node:net'\nimport type { Duplex } from 'node:stream'\nimport { DeskError } from './plan'\n\n/** A local endpoint backed by a running desk's TCP connection. */\nexport interface DeskPortListener {\n host: '127.0.0.1'\n port: number\n /** Close the listener and every active connection. Safe to call repeatedly. */\n close(): Promise<void>\n}\n\n/** Expose one guest TCP port to local HTTP, WebSocket or other TCP clients. */\nexport async function listenDeskPort(\n desk: { connect(port: number): Promise<Duplex> },\n port: number,\n): Promise<DeskPortListener> {\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new DeskError('Guest TCP port must be between 1 and 65535.')\n const clients = new Set<Socket>()\n const guests = new Set<Duplex>()\n let closed = false\n let closing: Promise<void> | undefined\n const server: Server = createServer((client) => {\n if (closed) { client.destroy(); return }\n clients.add(client)\n client.once('close', () => clients.delete(client))\n client.on('error', () => client.destroy())\n void Promise.resolve().then(() => desk.connect(port)).then((guest) => {\n if (closed || client.destroyed) { guest.destroy(); return }\n guests.add(guest)\n guest.once('close', () => { guests.delete(guest); client.destroy() })\n guest.on('error', () => { guest.destroy(); client.destroy() })\n client.once('close', () => guest.destroy())\n client.pipe(guest).pipe(client)\n }, () => client.destroy())\n })\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error) => reject(error)\n server.once('error', onError)\n server.listen(0, '127.0.0.1', () => { server.off('error', onError); resolve() })\n })\n // Errors after listen must not become unhandled process errors.\n server.on('error', () => { void close() })\n const address = server.address()\n if (!address || typeof address === 'string') { server.close(); throw new DeskError('Could not determine desk relay address.') }\n function close(): Promise<void> {\n if (closing) return closing\n closed = true\n for (const client of clients) client.destroy()\n for (const guest of guests) guest.destroy()\n closing = new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))\n return closing\n }\n return { host: '127.0.0.1', port: address.port, close }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { Duplex } from 'node:stream';
2
+ import type { DeskBackend, DeskProcessLauncher } from './backend.js';
3
+ /** A signed helper is supplied by the host; it is not downloaded or executed at install time. */
4
+ export interface VirtualizationBackendOptions {
5
+ helperPath: string;
6
+ launcher?: DeskProcessLauncher;
7
+ connect?: (socketPath: string) => Duplex;
8
+ connectTimeoutMs?: number;
9
+ /** Existing file for the guest serial console; omit to disable it. */
10
+ consoleLogPath?: string;
11
+ requestTimeoutMs?: number;
12
+ guestAgentPort?: number;
13
+ platform?: NodeJS.Platform;
14
+ }
15
+ export declare function createVirtualizationBackend(options: VirtualizationBackendOptions): DeskBackend;
16
+ //# sourceMappingURL=virtualization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtualization.d.ts","sourceRoot":"","sources":["../src/virtualization.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAQjE,iGAAiG;AACjG,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAC3B;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,4BAA4B,GAAG,WAAW,CAmG9F"}
@@ -0,0 +1,148 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join } from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import { DeskError } from './plan.js';
7
+ import { GUEST_AGENT_VSOCK_PORT } from './protocol.js';
8
+ import { attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput, errorMessage, runToCompletion, terminate, watchExit, } from './machine.js';
9
+ export function createVirtualizationBackend(options) {
10
+ if (options.consoleLogPath && !isAbsolute(options.consoleLogPath))
11
+ throw new DeskError('consoleLogPath must be an absolute path.');
12
+ if (!isAbsolute(options.helperPath))
13
+ throw new DeskError('helperPath must be an absolute path.');
14
+ const launcher = options.launcher ?? ((command, args, { identity }) => spawn(command, [...args], {
15
+ stdio: ['pipe', 'pipe', 'pipe'], uid: identity?.uid, gid: identity?.gid,
16
+ }));
17
+ const timeout = options.connectTimeoutMs ?? 30_000;
18
+ return {
19
+ hypervisor: 'virtualization',
20
+ vmmPath: options.helperPath,
21
+ async boot(plan, hooks) {
22
+ if (plan.hypervisor !== 'virtualization')
23
+ throw new DeskError('Virtualization backend requires a virtualization plan.');
24
+ if ((options.platform ?? process.platform) !== 'darwin')
25
+ throw new DeskError('Virtualization requires macOS.');
26
+ if (plan.overlay.create)
27
+ await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`);
28
+ await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 });
29
+ const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity });
30
+ const exit = watchExit(child);
31
+ const log = drainOutput(child, 8192, ['stderr']);
32
+ if (!child.stdin || !child.stdout) {
33
+ await terminate(child, exit, 2000);
34
+ throw new DeskError('Virtualization helper requires piped stdin and stdout.');
35
+ }
36
+ const input = child.stdin;
37
+ // A helper exit may close stdin while a write is in flight.
38
+ input.on('error', () => undefined);
39
+ const pending = new Map();
40
+ const lines = createInterface({ input: child.stdout });
41
+ lines.on('line', (line) => {
42
+ let message;
43
+ try {
44
+ message = JSON.parse(line);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ if (!message || typeof message !== 'object' || typeof message.id !== 'string')
50
+ return;
51
+ const entry = pending.get(message.id);
52
+ if (!entry)
53
+ return;
54
+ pending.delete(message.id);
55
+ clearTimeout(entry.timer);
56
+ if (message.ok === true && message.result && typeof message.result === 'object')
57
+ entry.resolve(message.result);
58
+ else
59
+ entry.reject(new DeskError(String(message.error ?? 'Helper request failed.')));
60
+ });
61
+ void exit.promise.then(() => {
62
+ for (const entry of pending.values()) {
63
+ clearTimeout(entry.timer);
64
+ entry.reject(new DeskError(`Virtualization helper exited: ${log.tail()}`));
65
+ }
66
+ pending.clear();
67
+ lines.close();
68
+ });
69
+ function call(op, fields = {}) {
70
+ if (exit.done)
71
+ return Promise.reject(new DeskError('Virtualization helper exited.'));
72
+ const id = randomUUID();
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => { pending.delete(id); reject(new DeskError(`Virtualization helper ${op} timed out.`)); }, timeout);
75
+ pending.set(id, { resolve, reject, timer });
76
+ input.write(`${JSON.stringify({ ...fields, id, op })}\n`, (error) => {
77
+ if (error) {
78
+ const entry = pending.get(id);
79
+ if (entry) {
80
+ clearTimeout(entry.timer);
81
+ pending.delete(id);
82
+ reject(error);
83
+ }
84
+ }
85
+ });
86
+ });
87
+ }
88
+ const stop = async () => {
89
+ try {
90
+ if (!exit.done)
91
+ await call('stop');
92
+ }
93
+ finally {
94
+ await terminate(child, exit, 2000);
95
+ }
96
+ };
97
+ try {
98
+ const identifierPath = join(plan.snapshot.dir, 'machine-id');
99
+ const identifier = plan.snapshot.restoring ? await readFile(identifierPath, 'utf8') : undefined;
100
+ const boot = await call('boot', {
101
+ kernel: plan.kernelPath, initrd: plan.initramfsPath, cmdline: plan.kernelCmdline,
102
+ ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),
103
+ disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,
104
+ vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,
105
+ ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),
106
+ network: { ...plan.network, macAddress: plan.network.mac },
107
+ });
108
+ if (typeof boot.machineIdentifier !== 'string')
109
+ throw new DeskError('Helper did not return a machine identifier.');
110
+ const machineIdentifier = boot.machineIdentifier;
111
+ if (plan.snapshot.restoring) {
112
+ if (!hooks?.onRestored)
113
+ throw new DeskError('Restore requires snapshot invalidation before resume.');
114
+ await hooks.onRestored();
115
+ await call('resume');
116
+ }
117
+ const connectAgent = (abandoned) => connectWithRetry({
118
+ connect: options.connect ?? defaultConnect, now: Date.now, socketPath: plan.vsock.socketPath,
119
+ port: options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT, timeoutMs: timeout, retryDelayMs: 100,
120
+ attemptTimeoutMs: 5000, exited: exit, abandoned,
121
+ });
122
+ const stream = await connectAgent();
123
+ return createGuestMachine({
124
+ deskId: plan.deskId, exit, stream, reconnect: connectAgent, reconnectWindowMs: timeout,
125
+ now: Date.now, idFactory: randomUUID, requestTimeoutMs: options.requestTimeoutMs ?? 30_000,
126
+ stop,
127
+ connect: (port) => attemptHandshake(options.connect ?? defaultConnect, plan.ports.socketPath, port, 5000),
128
+ ...(boot.saveRestore === true ? {
129
+ park: async (dir) => {
130
+ try {
131
+ await call('save', { path: join(dir, 'state') });
132
+ await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 });
133
+ }
134
+ finally {
135
+ await stop();
136
+ }
137
+ },
138
+ } : {}),
139
+ });
140
+ }
141
+ catch (error) {
142
+ await terminate(child, exit, 2000);
143
+ throw new DeskError(`Virtualization desk ${plan.deskId} failed: ${errorMessage(error)}${log.tail() ? ` (${log.tail()})` : ''}`);
144
+ }
145
+ },
146
+ };
147
+ }
148
+ //# sourceMappingURL=virtualization.js.map