@braedonsaunders/appkit-desk 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/plan.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync } from 'node:fs';
3
+ import { existsSync, statSync } from 'node:fs';
4
4
  import { isIP } from 'node:net';
5
5
  import { arch as hostArch, release as hostRelease, tmpdir } from 'node:os';
6
- import { dirname, isAbsolute, join, resolve } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
7
7
  export const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor';
8
8
  export const DEFAULT_KVM_PATH = '/dev/kvm';
9
9
  export const DEFAULT_QEMU_IMG_PATH = '/usr/bin/qemu-img';
@@ -11,6 +11,12 @@ export const DEFAULT_RUNTIME_DIR = '/run/appkit-desk';
11
11
  export const DEFAULT_MEMORY_MB = 384;
12
12
  export const DEFAULT_VCPUS = 2;
13
13
  export const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet';
14
+ /**
15
+ * Where a Cloud Hypervisor desk looks for `virtiofsd` when `virtiofsdPath` is
16
+ * not set and it is not on `PATH`: Debian's and Fedora's package location.
17
+ */
18
+ export const DEFAULT_VIRTIOFSD_PATH = '/usr/libexec/virtiofsd';
19
+ export const DEFAULT_VIRTIOFSD_SANDBOX = 'namespace';
14
20
  /**
15
21
  * The guest network Virtualization.framework desks get from the helper's NAT:
16
22
  * the same addresses QEMU's user networking uses, so they are unsurprising in
@@ -26,6 +32,10 @@ export const VZ_GUEST_CID = 3;
26
32
  const MIN_DARWIN_MAJOR = 23;
27
33
  /** `sun_path` holds 104 bytes on Darwin, including the terminator. */
28
34
  const DARWIN_SOCKET_PATH_LIMIT = 103;
35
+ /** `sun_path` holds 108 bytes on Linux, including the terminator. */
36
+ const LINUX_SOCKET_PATH_LIMIT = 107;
37
+ /** Linux's virtio-fs tag field is 36 bytes; Virtualization.framework enforces the same. */
38
+ const SHARE_TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,35}$/;
29
39
  const DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
30
40
  const TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/;
31
41
  const MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i;
@@ -88,6 +98,7 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
88
98
  function buildCloudHypervisorPlan(options, dependencies) {
89
99
  const pathExists = dependencies.pathExists ?? existsSync;
90
100
  const deviceExists = dependencies.deviceExists ?? existsSync;
101
+ const isDirectory = dependencies.isDirectory ?? directoryExists;
91
102
  const deskId = cleanDeskId(options.deskId);
92
103
  const kvmPath = absolutePath(options.kvmPath ?? DEFAULT_KVM_PATH, 'kvmPath');
93
104
  if (!deviceExists(kvmPath)) {
@@ -140,6 +151,14 @@ function buildCloudHypervisorPlan(options, dependencies) {
140
151
  // guest instead of a missing snapshot out here.
141
152
  throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`);
142
153
  }
154
+ const shares = cloudHypervisorShares(cleanShares(options.shares, isDirectory), {
155
+ deskId,
156
+ runtimeDir,
157
+ virtiofsdPath: options.virtiofsdPath,
158
+ sandbox: options.virtiofsdSandbox ?? DEFAULT_VIRTIOFSD_SANDBOX,
159
+ pathExists,
160
+ searchPath: dependencies.searchPath ?? process.env.PATH ?? '',
161
+ });
143
162
  const create = pathExists(overlayPath)
144
163
  ? null
145
164
  : {
@@ -174,6 +193,11 @@ function buildCloudHypervisorPlan(options, dependencies) {
174
193
  // parameter exists for VMs whose tap was passed as a file descriptor, and
175
194
  // producing one from Node would mean an ioctl(TUNSETIFF) it cannot make.
176
195
  // Because `deskPlan` configures `--net tap=<name>`, there is nothing to pass.
196
+ //
197
+ // Shares are in the snapshotted config too, by socket path, with each
198
+ // daemon's state. What the snapshot cannot hold is a running `virtiofsd`, so
199
+ // the backend starts the same daemons on the same sockets before this VMM
200
+ // either way, and the VMM loads their state back into them.
177
201
  const args = restoring
178
202
  ? [
179
203
  '--api-socket', apiSocketPath,
@@ -189,10 +213,15 @@ function buildCloudHypervisorPlan(options, dependencies) {
189
213
  // Be explicit that the disk is raw: CH deprecated image-format auto-detection
190
214
  // and warns without image_type; there is never a qcow2 overlay here.
191
215
  '--disk', `path=${overlayPath},image_type=raw`,
192
- '--memory', `size=${memoryMb}M`,
216
+ // vhost-user devices map guest RAM into their daemon, so it must be
217
+ // shared memory; a desk without shares keeps private memory.
218
+ '--memory', `size=${memoryMb}M${shares.length === 0 ? '' : ',shared=on'}`,
193
219
  '--cpus', `boot=${vcpus}`,
194
220
  '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,
195
221
  '--net', `tap=${tapDevice},mac=${macAddress}`,
222
+ ...(shares.length === 0
223
+ ? []
224
+ : ['--fs', ...shares.map((share) => `tag=${share.tag},socket=${share.socketPath}`)]),
196
225
  '--serial', 'off',
197
226
  '--console', 'off',
198
227
  ];
@@ -205,6 +234,7 @@ function buildCloudHypervisorPlan(options, dependencies) {
205
234
  api: { socketPath: apiSocketPath },
206
235
  tap: { device: tapDevice, mac: macAddress },
207
236
  snapshot: { dir: snapshotDir, restoring },
237
+ shares,
208
238
  memoryMb,
209
239
  vcpus,
210
240
  kernelPath,
@@ -226,6 +256,9 @@ function buildVirtualizationPlan(options, dependencies) {
226
256
  if (options.tapDevice !== undefined) {
227
257
  throw new DeskError('tapDevice applies to Cloud Hypervisor desks; a Virtualization.framework desk has no TAP.');
228
258
  }
259
+ if (options.virtiofsdPath !== undefined || options.virtiofsdSandbox !== undefined) {
260
+ throw new DeskError('virtiofsdPath and virtiofsdSandbox apply to Cloud Hypervisor desks; Virtualization.framework serves shares itself.');
261
+ }
229
262
  const kernelPath = absolutePath(options.kernelPath, 'kernelPath');
230
263
  if (!pathExists(kernelPath)) {
231
264
  throw new DeskError(`Guest kernel does not exist: ${kernelPath}`);
@@ -258,6 +291,7 @@ function buildVirtualizationPlan(options, dependencies) {
258
291
  throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`);
259
292
  }
260
293
  const network = cleanGuestNetwork(options.guestNetwork ?? {});
294
+ const shares = cleanShares(options.shares, dependencies.isDirectory ?? directoryExists);
261
295
  return {
262
296
  hypervisor: 'virtualization',
263
297
  deskId,
@@ -273,6 +307,7 @@ function buildVirtualizationPlan(options, dependencies) {
273
307
  ...network,
274
308
  },
275
309
  snapshot: { dir: snapshotDir, restoring },
310
+ shares,
276
311
  memoryMb,
277
312
  vcpus,
278
313
  kernelPath,
@@ -281,6 +316,101 @@ function buildVirtualizationPlan(options, dependencies) {
281
316
  launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),
282
317
  };
283
318
  }
319
+ /** Validate shares as given: tags, absolute existing directories, explicit access. */
320
+ function cleanShares(shares, isDirectory) {
321
+ if (shares === undefined)
322
+ return [];
323
+ if (!Array.isArray(shares))
324
+ throw new DeskError('shares must be an array.');
325
+ const tags = new Set();
326
+ return shares.map((share, index) => {
327
+ const field = `shares[${index}]`;
328
+ if (typeof share !== 'object' || share === null)
329
+ throw new DeskError(`${field} must be an object.`);
330
+ if (typeof share.tag !== 'string' || !SHARE_TAG_PATTERN.test(share.tag)) {
331
+ throw new DeskError(`${field}.tag must be 1–36 lowercase letters, digits, or hyphens, starting with a letter or digit.`);
332
+ }
333
+ if (tags.has(share.tag))
334
+ throw new DeskError(`${field}.tag ${share.tag} is already used by another share.`);
335
+ tags.add(share.tag);
336
+ if (typeof share.path !== 'string')
337
+ throw new DeskError(`${field}.path must be an absolute path.`);
338
+ const path = absolutePath(share.path, `${field}.path`);
339
+ if (!isDirectory(path))
340
+ throw new DeskError(`${field}.path is not an existing directory: ${path}`);
341
+ if (typeof share.readOnly !== 'boolean')
342
+ throw new DeskError(`${field}.readOnly must be a boolean.`);
343
+ return { tag: share.tag, path, readOnly: share.readOnly };
344
+ });
345
+ }
346
+ /**
347
+ * One `virtiofsd` per share. `--cache=auto` lets the guest cache for a second
348
+ * at a time, so changes the host makes under a share reach running guests;
349
+ * `always` would let a guest keep serving a file the host has replaced.
350
+ * `--readonly` is the enforcement for a read-only share — the guest is never
351
+ * trusted to honour a read-only mount.
352
+ *
353
+ * The migration flags are for parking. Cloud Hypervisor carries the daemon's
354
+ * state — the files and directories the guest holds — inside a snapshot, and
355
+ * hands it to the fresh daemon on restore, which reopens each by path. A file
356
+ * that has no path by then (unlinked on the host, or by the guest while held
357
+ * open) becomes an error for the guest to see on that file alone, rather than
358
+ * a restore that fails and costs the whole desk its memory.
359
+ */
360
+ function cloudHypervisorShares(shares, context) {
361
+ if (shares.length === 0)
362
+ return [];
363
+ if (context.sandbox !== 'namespace' && context.sandbox !== 'chroot') {
364
+ throw new DeskError("virtiofsdSandbox must be 'namespace' or 'chroot'.");
365
+ }
366
+ const command = resolveVirtiofsd(context.virtiofsdPath, context.pathExists, context.searchPath);
367
+ return shares.map((share) => {
368
+ // A dot cannot appear in a tag, so `<desk>.<tag>` names one socket for
369
+ // exactly one desk and share even though desk IDs may contain hyphens.
370
+ const socketPath = join(context.runtimeDir, `desk-${context.deskId}.${share.tag}.fs.sock`);
371
+ if (Buffer.byteLength(socketPath) > LINUX_SOCKET_PATH_LIMIT) {
372
+ throw new DeskError(`Socket path ${socketPath} is longer than the ${LINUX_SOCKET_PATH_LIMIT} bytes Linux allows; `
373
+ + 'use a shorter runtimeDir, deskId, or share tag.');
374
+ }
375
+ return {
376
+ ...share,
377
+ socketPath,
378
+ daemon: {
379
+ command,
380
+ args: [
381
+ `--socket-path=${socketPath}`,
382
+ `--shared-dir=${share.path}`,
383
+ `--sandbox=${context.sandbox}`,
384
+ '--cache=auto',
385
+ '--migration-mode=find-paths',
386
+ '--migration-on-error=guest-error',
387
+ ...(share.readOnly ? ['--readonly'] : []),
388
+ ],
389
+ },
390
+ };
391
+ });
392
+ }
393
+ function resolveVirtiofsd(configured, pathExists, searchPath) {
394
+ if (configured !== undefined) {
395
+ const path = absolutePath(configured, 'virtiofsdPath');
396
+ if (!pathExists(path)) {
397
+ throw new DeskError(`virtiofsd was not found at ${path}. Install virtiofsd or configure virtiofsdPath.`);
398
+ }
399
+ return path;
400
+ }
401
+ const candidates = [
402
+ ...searchPath.split(delimiter).filter((entry) => isAbsolute(entry)).map((entry) => join(entry, 'virtiofsd')),
403
+ DEFAULT_VIRTIOFSD_PATH,
404
+ ];
405
+ const found = candidates.find((candidate) => pathExists(candidate));
406
+ if (found === undefined) {
407
+ throw new DeskError(`virtiofsd was not found on PATH or at ${DEFAULT_VIRTIOFSD_PATH}. Install virtiofsd 1.13 or later, or configure virtiofsdPath.`);
408
+ }
409
+ return found;
410
+ }
411
+ function directoryExists(path) {
412
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
413
+ }
284
414
  function cleanGuestNetwork(options) {
285
415
  if (options.allowLan !== undefined && typeof options.allowLan !== 'boolean') {
286
416
  throw new DeskError('guestNetwork.allowLan must be a boolean.');
package/plan.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAC1E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,2BAA2B,CAAA;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAA;AACxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AACrD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAA;AAC9B,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAA;AAC9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,WAAW,CAAA;AACnD,MAAM,CAAC,MAAM,0BAA0B,GAAG,UAAU,CAAA;AACpD,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAA;AAC1C,MAAM,CAAC,MAAM,cAAc,GAAsB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AACvE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAA;AAC7B,0FAA0F;AAC1F,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAC3B,sEAAsE;AACtE,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAEpC,MAAM,eAAe,GAAG,mCAAmC,CAAA;AAC3D,MAAM,kBAAkB,GAAG,kCAAkC,CAAA;AAC7D,MAAM,mBAAmB,GAAG,mCAAmC,CAAA;AAC/D,gEAAgE;AAChE,MAAM,aAAa,GAAG,CAAC,CAAA;AACvB,MAAM,aAAa,GAAG,WAAW,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAChB,IAAI,GAAW,WAAW,CAAA;CAC7C;AAgKD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,UAU5B,EAAE;IACJ,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAA;IACrD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,UAAU,CAAA;IACnD,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,SAAS,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;QAC3F,OAAO,CACL,CAAC,OAAO,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC,KAAK,OAAO;eACrC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;eACvB,KAAK,IAAI,gBAAgB;eACzB,OAAO,CAAC,UAAU,KAAK,SAAS;eAChC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC;eAC9B,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAClC,CAAA;IACH,CAAC;IACD,OAAO,CACL,QAAQ,KAAK,OAAO;WACjB,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;WAC/C,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC,CACnD,CAAA;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CACjB,sEAAsE,CACvE,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAwBD,MAAM,UAAU,mBAAmB,CACjC,OAA8B,EAC9B,eAA2C,EAAE;IAE7C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,kBAAkB,CAAA;IAC3D,IAAI,UAAU,KAAK,gBAAgB;QAAE,OAAO,uBAAuB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAC1F,IAAI,UAAU,KAAK,kBAAkB,EAAE,CAAC;QACtC,MAAM,IAAI,SAAS,CAAC,uBAAuB,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,SAAS,CACjB,sHAAsH,CACvH,CAAA;IACH,CAAC;IACD,OAAO,wBAAwB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AACxD,CAAC;AAED,SAAS,wBAAwB,CAC/B,OAA8B,EAC9B,YAAwC;IAExC,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,UAAU,CAAA;IACxD,MAAM,YAAY,GAAG,YAAY,CAAC,YAAY,IAAI,UAAU,CAAA;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,EAAE,SAAS,CAAC,CAAA;IAC5E,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,SAAS,CACjB,2BAA2B,OAAO,2CAA2C;cACzE,0CAA0C,CAC/C,CAAA;IACH,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,EAAE,SAAS,CAAC,CAAA;IAC5E,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CACjB,qCAAqC,OAAO,kDAAkD,CAC/F,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;IACjE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,gCAAgC,UAAU,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GACjB,OAAO,CAAC,aAAa,KAAK,SAAS;QACjC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IAC1D,IAAI,aAAa,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,SAAS,CAAC,6BAA6B,aAAa,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IAC1E,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,8BAA8B,aAAa,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;IACpE,MAAM,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7C,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,SAAS,CAAC,qCAAqC,gBAAgB,EAAE,CAAC,CAAA;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,IAAI,iBAAiB,EAAE,UAAU,CAAC,CAAA;IACnF,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,IAAI,aAAa,EAAE,OAAO,CAAC,CAAA;IACtE,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC3C,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,IAAI,mBAAmB,EAAE,YAAY,CAAC,CAAA;IACxF,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAA;IACjF,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAA;IACrF,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAA;IACrE,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACxE,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,MAAM,QAAQ,CAAC,CAAA;IAChE,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,MAAM,WAAW,CAAC,CAAA;IACjE,8EAA8E;IAC9E,2EAA2E;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW;QACrC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,MAAM,WAAW,CAAC,CAAA;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAA;IAC1C,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1C,2EAA2E;QAC3E,4EAA4E;QAC5E,2EAA2E;QAC3E,gDAAgD;QAChD,MAAM,IAAI,SAAS,CAAC,QAAQ,MAAM,kCAAkC,WAAW,EAAE,CAAC,CAAA;IACpF,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC;QACpC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC;YACE,sEAAsE;YACtE,8DAA8D;YAC9D,sEAAsE;YACtE,yEAAyE;YACzE,wEAAwE;YACxE,0EAA0E;YAC1E,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,CAAC,gBAAgB,EAAE,aAAa,EAAE,WAAW,CAAC;SACrD,CAAA;IAEL,2DAA2D;IAC3D,EAAE;IACF,2EAA2E;IAC3E,2EAA2E;IAC3E,6BAA6B;IAC7B,EAAE;IACF,4EAA4E;IAC5E,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,2EAA2E;IAC3E,wEAAwE;IACxE,2EAA2E;IAC3E,EAAE;IACF,yEAAyE;IACzE,sDAAsD;IACtD,EAAE;IACF,8EAA8E;IAC9E,wEAAwE;IACxE,0EAA0E;IAC1E,yEAAyE;IACzE,8EAA8E;IAC9E,MAAM,IAAI,GAAG,SAAS;QACpB,CAAC,CAAC;YACE,cAAc,EAAE,aAAa;YAC7B,WAAW,EAAE,qBAAqB,WAAW,eAAe;SAC7D;QACH,CAAC,CAAC;YACE,cAAc,EAAE,aAAa;YAC7B,UAAU,EAAE,UAAU;YACtB,yEAAyE;YACzE,qEAAqE;YACrE,GAAG,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;YACjE,WAAW,EAAE,aAAa;YAC1B,8EAA8E;YAC9E,qEAAqE;YACrE,QAAQ,EAAE,QAAQ,WAAW,iBAAiB;YAC9C,UAAU,EAAE,QAAQ,QAAQ,GAAG;YAC/B,QAAQ,EAAE,QAAQ,KAAK,EAAE;YACzB,SAAS,EAAE,OAAO,QAAQ,WAAW,eAAe,EAAE;YACtD,OAAO,EAAE,OAAO,SAAS,QAAQ,UAAU,EAAE;YAC7C,UAAU,EAAE,KAAK;YACjB,WAAW,EAAE,KAAK;SACnB,CAAA;IAEL,OAAO;QACL,UAAU,EAAE,kBAAkB;QAC9B,MAAM;QACN,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;QAC/B,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE;QAC/D,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE;QACrD,GAAG,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE;QAClC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE;QAC3C,QAAQ,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE;QACzC,QAAQ;QACR,KAAK;QACL,UAAU;QACV,aAAa;QACb,aAAa;QACb,gBAAgB;KACjB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAC9B,OAA8B,EAC9B,YAAwC;IAExC,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,UAAU,CAAA;IACxD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,0FAA0F,CAAC,CAAA;IACjH,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;IACjE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,gCAAgC,UAAU,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GACjB,OAAO,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IACnG,IAAI,aAAa,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,SAAS,CAAC,6BAA6B,aAAa,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IAC1E,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,8BAA8B,aAAa,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;IACpE,MAAM,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7C,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,SAAS,CAAC,qCAAqC,gBAAgB,EAAE,CAAC,CAAA;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,IAAI,iBAAiB,EAAE,UAAU,CAAC,CAAA;IACnF,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,IAAI,aAAa,EAAE,OAAO,CAAC,CAAA;IACtE,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,EAAE,YAAY,CAAC,CAAA;IAClG,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/E,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,QAAQ,QAAQ,CAAC,CAAC,CAAA;IAC3E,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,QAAQ,QAAQ,CAAC,CAAC,CAAA;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW;QACrC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,MAAM,WAAW,CAAC,CAAA;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAA;IAC1C,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,QAAQ,MAAM,kCAAkC,WAAW,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAA;IAC7D,OAAO;QACL,UAAU,EAAE,gBAAgB;QAC5B,MAAM;QACN,OAAO,EAAE;YACP,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE,aAAa;YACvB,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE;SACrG;QACD,KAAK,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE;QACzD,KAAK,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE;QACtC,OAAO,EAAE;YACP,GAAG,EAAE,eAAe,CAAC,OAAO,CAAC,UAAU,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;YAClE,GAAG,OAAO;SACX;QACD,QAAQ,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE;QACzC,QAAQ;QACR,KAAK;QACL,UAAU;QACV,aAAa;QACb,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,sBAAsB;QAC9D,gBAAgB,EAAE,qBAAqB,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAClE,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CACxB,OAAgC;IAEhC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5E,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAA;IACjE,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,wBAAwB,EAAE,2BAA2B,CAAC,CAAA;IACxG,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,0BAA0B,EAAE,6BAA6B,CAAC,CAAA;IAChH,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,wBAAwB,CAAA;IACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,IAAI,YAAY,GAAG,EAAE,EAAE,CAAC;QAC7E,MAAM,IAAI,SAAS,CAAC,gEAAgE,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,WAAW,IAAI,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,KAAK,CAAC,CAAA;IACvD,IACE,YAAY,KAAK,cAAc;WAC5B,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAC5F,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,gFAAgF,CAAC,CAAA;IACvG,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,cAAc,CAAC,CAAC,CAAA;IAChD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,SAAS,CAAC,yDAAyD,CAAC,CAAA;IAChF,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK;QACnC,YAAY;QACZ,cAAc;QACd,YAAY;QACZ,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;KACzD,CAAA;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,cAAc,CAAC,MAAc;IACpC,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,eAAe,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;IAC5E,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC/C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,IAAI,SAAS,CACjB,eAAe,IAAI,uBAAuB,wBAAwB,uBAAuB;cACrF,qCAAqC,CAC1C,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,KAAa;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,2BAA2B,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,UAAU,CAAC,OAAe;IACjC,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AAC7F,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,MAAM,GAAG,EAAE,CAAA;AACpB,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC,CAAA;IACrG,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa;IAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,aAAa,IAAI,KAAK,GAAG,aAAa,EAAE,CAAC;QAC/E,MAAM,IAAI,SAAS,CACjB,uCAAuC,aAAa,QAAQ,aAAa,GAAG,CAC7E,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,iFAAiF,CAAC,CAAA;IACxG,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,SAAS,CAAC,0CAA0C,KAAK,EAAE,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,QAA0C;IAE1C,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC/B,MAAM,GAAG,GAAG,aAAa,CAAA;IACzB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;QAC9E,MAAM,IAAI,SAAS,CAAC,yDAAyD,GAAG,GAAG,CAAC,CAAA;IACtF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;QAC9E,MAAM,IAAI,SAAS,CAAC,yDAAyD,GAAG,GAAG,CAAC,CAAA;IACtF,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAA;AACjD,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,KAAa;IAChD,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,4BAA4B,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,eAAe,CAAC,KAAa,EAAE,KAAa;IACnD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,8BAA8B,CAAC,CAAA;IAC7D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","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"]}
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, statSync } from 'node:fs'\nimport { isIP } from 'node:net'\nimport { arch as hostArch, release as hostRelease, tmpdir } from 'node:os'\nimport { delimiter, 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 * Where a Cloud Hypervisor desk looks for `virtiofsd` when `virtiofsdPath` is\n * not set and it is not on `PATH`: Debian's and Fedora's package location.\n */\nexport const DEFAULT_VIRTIOFSD_PATH = '/usr/libexec/virtiofsd'\nexport const DEFAULT_VIRTIOFSD_SANDBOX: DeskVirtiofsdSandbox = 'namespace'\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/** `sun_path` holds 108 bytes on Linux, including the terminator. */\nconst LINUX_SOCKET_PATH_LIMIT = 107\n/** Linux's virtio-fs tag field is 36 bytes; Virtualization.framework enforces the same. */\nconst SHARE_TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,35}$/\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\n/**\n * A host directory shared into the guest over virtio-fs. The guest mounts it\n * with `mount -t virtiofs <tag> <dir>`.\n */\nexport interface DeskShare {\n /** 1–36 lowercase letters, digits, or hyphens, starting with a letter or digit; unique per desk. */\n tag: string\n /** Absolute path of an existing host directory. */\n path: string\n /**\n * Refuse every write from the guest. Enforced by the host's file server, so\n * a guest cannot lift it by remounting the share read-write.\n */\n readOnly: boolean\n}\n\n/**\n * How `virtiofsd` confines itself to the shared directory. `namespace` (its\n * own default) pivots into the directory inside new mount, PID and network\n * namespaces, which needs CAP_SYS_ADMIN when run as root and unprivileged user\n * namespaces otherwise. `chroot` only chroots, which needs root and suits a\n * container that may not create namespaces. Both stop symlinks from escaping\n * the share; running unconfined is not offered.\n */\nexport type DeskVirtiofsdSandbox = 'namespace' | 'chroot'\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 /**\n * Host directories shared into the guest. Restoring a snapshot requires the\n * same shares the desk was parked with; see the backends.\n */\n shares?: readonly DeskShare[]\n /**\n * Cloud Hypervisor only: the `virtiofsd` binary (1.13 or later) that serves\n * each share. Defaults to the first `virtiofsd` on `PATH`, then\n * `/usr/libexec/virtiofsd`. Not needed, and not looked for, without shares.\n */\n virtiofsdPath?: string\n /** Cloud Hypervisor only: defaults to `namespace`. */\n virtiofsdSandbox?: DeskVirtiofsdSandbox\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 /** Host directories shared into the guest, in the order they were given. */\n shares: readonly DeskShare[]\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 * Each share with the `virtiofsd` that serves it. Every daemon must be\n * listening on its socket before the VMM starts, for a boot and for a\n * restore alike.\n */\n shares: readonly CloudHypervisorShare[]\n}\n\nexport interface CloudHypervisorShare extends DeskShare {\n /** The vhost-user socket the daemon listens on and the VMM connects to. */\n socketPath: string\n daemon: { command: string; args: 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 /** Whether a path is an existing directory; checked for every share. */\n isDirectory?: (path: string) => boolean\n /** Searched for `virtiofsd` when `virtiofsdPath` is not set; defaults to `process.env.PATH`. */\n searchPath?: string\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 isDirectory = dependencies.isDirectory ?? directoryExists\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 const shares = cloudHypervisorShares(cleanShares(options.shares, isDirectory), {\n deskId,\n runtimeDir,\n virtiofsdPath: options.virtiofsdPath,\n sandbox: options.virtiofsdSandbox ?? DEFAULT_VIRTIOFSD_SANDBOX,\n pathExists,\n searchPath: dependencies.searchPath ?? process.env.PATH ?? '',\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 //\n // Shares are in the snapshotted config too, by socket path, with each\n // daemon's state. What the snapshot cannot hold is a running `virtiofsd`, so\n // the backend starts the same daemons on the same sockets before this VMM\n // either way, and the VMM loads their state back into them.\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 // vhost-user devices map guest RAM into their daemon, so it must be\n // shared memory; a desk without shares keeps private memory.\n '--memory', `size=${memoryMb}M${shares.length === 0 ? '' : ',shared=on'}`,\n '--cpus', `boot=${vcpus}`,\n '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,\n '--net', `tap=${tapDevice},mac=${macAddress}`,\n ...(shares.length === 0\n ? []\n : ['--fs', ...shares.map((share) => `tag=${share.tag},socket=${share.socketPath}`)]),\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 shares,\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 if (options.virtiofsdPath !== undefined || options.virtiofsdSandbox !== undefined) {\n throw new DeskError(\n 'virtiofsdPath and virtiofsdSandbox apply to Cloud Hypervisor desks; Virtualization.framework serves shares itself.',\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 ? 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 const shares = cleanShares(options.shares, dependencies.isDirectory ?? directoryExists)\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 shares,\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline: options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE,\n launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),\n }\n}\n\n/** Validate shares as given: tags, absolute existing directories, explicit access. */\nfunction cleanShares(\n shares: readonly DeskShare[] | undefined,\n isDirectory: (path: string) => boolean,\n): DeskShare[] {\n if (shares === undefined) return []\n if (!Array.isArray(shares)) throw new DeskError('shares must be an array.')\n const tags = new Set<string>()\n return shares.map((share: DeskShare, index) => {\n const field = `shares[${index}]`\n if (typeof share !== 'object' || share === null) throw new DeskError(`${field} must be an object.`)\n if (typeof share.tag !== 'string' || !SHARE_TAG_PATTERN.test(share.tag)) {\n throw new DeskError(\n `${field}.tag must be 1–36 lowercase letters, digits, or hyphens, starting with a letter or digit.`,\n )\n }\n if (tags.has(share.tag)) throw new DeskError(`${field}.tag ${share.tag} is already used by another share.`)\n tags.add(share.tag)\n if (typeof share.path !== 'string') throw new DeskError(`${field}.path must be an absolute path.`)\n const path = absolutePath(share.path, `${field}.path`)\n if (!isDirectory(path)) throw new DeskError(`${field}.path is not an existing directory: ${path}`)\n if (typeof share.readOnly !== 'boolean') throw new DeskError(`${field}.readOnly must be a boolean.`)\n return { tag: share.tag, path, readOnly: share.readOnly }\n })\n}\n\n/**\n * One `virtiofsd` per share. `--cache=auto` lets the guest cache for a second\n * at a time, so changes the host makes under a share reach running guests;\n * `always` would let a guest keep serving a file the host has replaced.\n * `--readonly` is the enforcement for a read-only share — the guest is never\n * trusted to honour a read-only mount.\n *\n * The migration flags are for parking. Cloud Hypervisor carries the daemon's\n * state — the files and directories the guest holds — inside a snapshot, and\n * hands it to the fresh daemon on restore, which reopens each by path. A file\n * that has no path by then (unlinked on the host, or by the guest while held\n * open) becomes an error for the guest to see on that file alone, rather than\n * a restore that fails and costs the whole desk its memory.\n */\nfunction cloudHypervisorShares(\n shares: readonly DeskShare[],\n context: {\n deskId: string\n runtimeDir: string\n virtiofsdPath: string | undefined\n sandbox: DeskVirtiofsdSandbox\n pathExists: (path: string) => boolean\n searchPath: string\n },\n): CloudHypervisorShare[] {\n if (shares.length === 0) return []\n if (context.sandbox !== 'namespace' && context.sandbox !== 'chroot') {\n throw new DeskError(\"virtiofsdSandbox must be 'namespace' or 'chroot'.\")\n }\n const command = resolveVirtiofsd(context.virtiofsdPath, context.pathExists, context.searchPath)\n return shares.map((share) => {\n // A dot cannot appear in a tag, so `<desk>.<tag>` names one socket for\n // exactly one desk and share even though desk IDs may contain hyphens.\n const socketPath = join(context.runtimeDir, `desk-${context.deskId}.${share.tag}.fs.sock`)\n if (Buffer.byteLength(socketPath) > LINUX_SOCKET_PATH_LIMIT) {\n throw new DeskError(\n `Socket path ${socketPath} is longer than the ${LINUX_SOCKET_PATH_LIMIT} bytes Linux allows; `\n + 'use a shorter runtimeDir, deskId, or share tag.',\n )\n }\n return {\n ...share,\n socketPath,\n daemon: {\n command,\n args: [\n `--socket-path=${socketPath}`,\n `--shared-dir=${share.path}`,\n `--sandbox=${context.sandbox}`,\n '--cache=auto',\n '--migration-mode=find-paths',\n '--migration-on-error=guest-error',\n ...(share.readOnly ? ['--readonly'] : []),\n ],\n },\n }\n })\n}\n\nfunction resolveVirtiofsd(\n configured: string | undefined,\n pathExists: (path: string) => boolean,\n searchPath: string,\n): string {\n if (configured !== undefined) {\n const path = absolutePath(configured, 'virtiofsdPath')\n if (!pathExists(path)) {\n throw new DeskError(`virtiofsd was not found at ${path}. Install virtiofsd or configure virtiofsdPath.`)\n }\n return path\n }\n const candidates = [\n ...searchPath.split(delimiter).filter((entry) => isAbsolute(entry)).map((entry) => join(entry, 'virtiofsd')),\n DEFAULT_VIRTIOFSD_PATH,\n ]\n const found = candidates.find((candidate) => pathExists(candidate))\n if (found === undefined) {\n throw new DeskError(\n `virtiofsd was not found on PATH or at ${DEFAULT_VIRTIOFSD_PATH}. Install virtiofsd 1.13 or later, or configure virtiofsdPath.`,\n )\n }\n return found\n}\n\nfunction directoryExists(path: string): boolean {\n return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true\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"]}
@@ -1 +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"}
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,CA2G9F"}
package/virtualization.js CHANGED
@@ -5,7 +5,7 @@ import { dirname, isAbsolute, join } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { DeskError } from './plan.js';
7
7
  import { GUEST_AGENT_VSOCK_PORT } from './protocol.js';
8
- import { attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput, errorMessage, runToCompletion, terminate, watchExit, } from './machine.js';
8
+ import { assertShareManifest, attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput, errorMessage, runToCompletion, terminate, watchExit, writeShareManifest, } from './machine.js';
9
9
  export function createVirtualizationBackend(options) {
10
10
  if (options.consoleLogPath && !isAbsolute(options.consoleLogPath))
11
11
  throw new DeskError('consoleLogPath must be an absolute path.');
@@ -25,6 +25,12 @@ export function createVirtualizationBackend(options) {
25
25
  throw new DeskError('Virtualization requires macOS.');
26
26
  if (plan.overlay.create)
27
27
  await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`);
28
+ // A saved machine restores only into the device list it was saved with.
29
+ if (plan.snapshot.restoring) {
30
+ await assertShareManifest(plan.snapshot.dir, plan.shares).catch((error) => {
31
+ throw new DeskError(`Virtualization desk ${plan.deskId} cannot be restored: ${errorMessage(error)}`);
32
+ });
33
+ }
28
34
  await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 });
29
35
  const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity });
30
36
  const exit = watchExit(child);
@@ -102,6 +108,7 @@ export function createVirtualizationBackend(options) {
102
108
  ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),
103
109
  disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,
104
110
  vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,
111
+ shares: plan.shares.map(({ tag, path, readOnly }) => ({ tag, path, readOnly })),
105
112
  ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),
106
113
  network: { ...plan.network, macAddress: plan.network.mac },
107
114
  });
@@ -130,6 +137,7 @@ export function createVirtualizationBackend(options) {
130
137
  try {
131
138
  await call('save', { path: join(dir, 'state') });
132
139
  await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 });
140
+ await writeShareManifest(dir, plan.shares);
133
141
  }
134
142
  finally {
135
143
  await stop();
@@ -1 +1 @@
1
- {"version":3,"file":"virtualization.js","sourceRoot":"","sources":["../src/virtualization.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport type { Duplex } from 'node:stream'\nimport type { DeskBackend, DeskProcessLauncher } from './backend'\nimport { DeskError } from './plan'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput,\n errorMessage, runToCompletion, terminate, watchExit,\n} from './machine'\n\n/** A signed helper is supplied by the host; it is not downloaded or executed at install time. */\nexport interface VirtualizationBackendOptions {\n helperPath: string\n launcher?: DeskProcessLauncher\n connect?: (socketPath: string) => Duplex\n connectTimeoutMs?: number\n /** Existing file for the guest serial console; omit to disable it. */\n consoleLogPath?: string\n requestTimeoutMs?: number\n guestAgentPort?: number\n platform?: NodeJS.Platform\n}\n\nexport function createVirtualizationBackend(options: VirtualizationBackendOptions): DeskBackend {\n if (options.consoleLogPath && !isAbsolute(options.consoleLogPath)) throw new DeskError('consoleLogPath must be an absolute path.')\n if (!isAbsolute(options.helperPath)) throw new DeskError('helperPath must be an absolute path.')\n const launcher: DeskProcessLauncher = options.launcher ?? ((command, args, { identity }) => spawn(command, [...args], {\n stdio: ['pipe', 'pipe', 'pipe'], uid: identity?.uid, gid: identity?.gid,\n }))\n const timeout = options.connectTimeoutMs ?? 30_000\n return {\n hypervisor: 'virtualization',\n vmmPath: options.helperPath,\n async boot(plan, hooks) {\n if (plan.hypervisor !== 'virtualization') throw new DeskError('Virtualization backend requires a virtualization plan.')\n if ((options.platform ?? process.platform) !== 'darwin') throw new DeskError('Virtualization requires macOS.')\n if (plan.overlay.create) await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`)\n await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 })\n const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity })\n const exit = watchExit(child)\n const log = drainOutput(child, 8192, ['stderr'])\n if (!child.stdin || !child.stdout) {\n await terminate(child, exit, 2000)\n throw new DeskError('Virtualization helper requires piped stdin and stdout.')\n }\n const input = child.stdin\n // A helper exit may close stdin while a write is in flight.\n input.on('error', () => undefined)\n const pending = new Map<string, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>()\n const lines = createInterface({ input: child.stdout })\n lines.on('line', (line) => {\n let message: Record<string, unknown>\n try { message = JSON.parse(line) as Record<string, unknown> } catch { return }\n if (!message || typeof message !== 'object' || typeof message.id !== 'string') return\n const entry = pending.get(message.id)\n if (!entry) return\n pending.delete(message.id); clearTimeout(entry.timer)\n if (message.ok === true && message.result && typeof message.result === 'object') entry.resolve(message.result as Record<string, unknown>)\n else entry.reject(new DeskError(String(message.error ?? 'Helper request failed.')))\n })\n void exit.promise.then(() => {\n for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new DeskError(`Virtualization helper exited: ${log.tail()}`)) }\n pending.clear(); lines.close()\n })\n function call(op: string, fields: Record<string, unknown> = {}): Promise<Record<string, unknown>> {\n if (exit.done) return Promise.reject(new DeskError('Virtualization helper exited.'))\n const id = randomUUID()\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => { pending.delete(id); reject(new DeskError(`Virtualization helper ${op} timed out.`)) }, timeout)\n pending.set(id, { resolve, reject, timer })\n input.write(`${JSON.stringify({ ...fields, id, op })}\\n`, (error) => {\n if (error) { const entry = pending.get(id); if (entry) { clearTimeout(entry.timer); pending.delete(id); reject(error) } }\n })\n })\n }\n const stop = async () => {\n try { if (!exit.done) await call('stop') } finally { await terminate(child, exit, 2000) }\n }\n try {\n const identifierPath = join(plan.snapshot.dir, 'machine-id')\n const identifier = plan.snapshot.restoring ? await readFile(identifierPath, 'utf8') : undefined\n const boot = await call('boot', {\n kernel: plan.kernelPath, initrd: plan.initramfsPath, cmdline: plan.kernelCmdline,\n ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),\n disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,\n vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,\n ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),\n network: { ...plan.network, macAddress: plan.network.mac },\n })\n if (typeof boot.machineIdentifier !== 'string') throw new DeskError('Helper did not return a machine identifier.')\n const machineIdentifier = boot.machineIdentifier\n if (plan.snapshot.restoring) {\n if (!hooks?.onRestored) throw new DeskError('Restore requires snapshot invalidation before resume.')\n await hooks.onRestored()\n await call('resume')\n }\n const connectAgent = (abandoned?: () => boolean) => connectWithRetry({\n connect: options.connect ?? defaultConnect, now: Date.now, socketPath: plan.vsock.socketPath,\n port: options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT, timeoutMs: timeout, retryDelayMs: 100,\n attemptTimeoutMs: 5000, exited: exit, abandoned,\n })\n const stream = await connectAgent()\n return createGuestMachine({\n deskId: plan.deskId, exit, stream, reconnect: connectAgent, reconnectWindowMs: timeout,\n now: Date.now, idFactory: randomUUID, requestTimeoutMs: options.requestTimeoutMs ?? 30_000,\n stop,\n connect: (port) => attemptHandshake(options.connect ?? defaultConnect, plan.ports.socketPath, port, 5000),\n ...(boot.saveRestore === true ? {\n park: async (dir: string) => {\n try {\n await call('save', { path: join(dir, 'state') })\n await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 })\n } finally { await stop() }\n },\n } : {}),\n })\n } catch (error) {\n await terminate(child, exit, 2000)\n throw new DeskError(`Virtualization desk ${plan.deskId} failed: ${errorMessage(error)}${log.tail() ? ` (${log.tail()})` : ''}`)\n }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"virtualization.js","sourceRoot":"","sources":["../src/virtualization.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport type { Duplex } from 'node:stream'\nimport type { DeskBackend, DeskProcessLauncher } from './backend'\nimport { DeskError } from './plan'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n assertShareManifest, attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput,\n errorMessage, runToCompletion, terminate, watchExit, writeShareManifest,\n} from './machine'\n\n/** A signed helper is supplied by the host; it is not downloaded or executed at install time. */\nexport interface VirtualizationBackendOptions {\n helperPath: string\n launcher?: DeskProcessLauncher\n connect?: (socketPath: string) => Duplex\n connectTimeoutMs?: number\n /** Existing file for the guest serial console; omit to disable it. */\n consoleLogPath?: string\n requestTimeoutMs?: number\n guestAgentPort?: number\n platform?: NodeJS.Platform\n}\n\nexport function createVirtualizationBackend(options: VirtualizationBackendOptions): DeskBackend {\n if (options.consoleLogPath && !isAbsolute(options.consoleLogPath)) throw new DeskError('consoleLogPath must be an absolute path.')\n if (!isAbsolute(options.helperPath)) throw new DeskError('helperPath must be an absolute path.')\n const launcher: DeskProcessLauncher = options.launcher ?? ((command, args, { identity }) => spawn(command, [...args], {\n stdio: ['pipe', 'pipe', 'pipe'], uid: identity?.uid, gid: identity?.gid,\n }))\n const timeout = options.connectTimeoutMs ?? 30_000\n return {\n hypervisor: 'virtualization',\n vmmPath: options.helperPath,\n async boot(plan, hooks) {\n if (plan.hypervisor !== 'virtualization') throw new DeskError('Virtualization backend requires a virtualization plan.')\n if ((options.platform ?? process.platform) !== 'darwin') throw new DeskError('Virtualization requires macOS.')\n if (plan.overlay.create) await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`)\n // A saved machine restores only into the device list it was saved with.\n if (plan.snapshot.restoring) {\n await assertShareManifest(plan.snapshot.dir, plan.shares).catch((error: unknown) => {\n throw new DeskError(`Virtualization desk ${plan.deskId} cannot be restored: ${errorMessage(error)}`)\n })\n }\n await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 })\n const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity })\n const exit = watchExit(child)\n const log = drainOutput(child, 8192, ['stderr'])\n if (!child.stdin || !child.stdout) {\n await terminate(child, exit, 2000)\n throw new DeskError('Virtualization helper requires piped stdin and stdout.')\n }\n const input = child.stdin\n // A helper exit may close stdin while a write is in flight.\n input.on('error', () => undefined)\n const pending = new Map<string, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>()\n const lines = createInterface({ input: child.stdout })\n lines.on('line', (line) => {\n let message: Record<string, unknown>\n try { message = JSON.parse(line) as Record<string, unknown> } catch { return }\n if (!message || typeof message !== 'object' || typeof message.id !== 'string') return\n const entry = pending.get(message.id)\n if (!entry) return\n pending.delete(message.id); clearTimeout(entry.timer)\n if (message.ok === true && message.result && typeof message.result === 'object') entry.resolve(message.result as Record<string, unknown>)\n else entry.reject(new DeskError(String(message.error ?? 'Helper request failed.')))\n })\n void exit.promise.then(() => {\n for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new DeskError(`Virtualization helper exited: ${log.tail()}`)) }\n pending.clear(); lines.close()\n })\n function call(op: string, fields: Record<string, unknown> = {}): Promise<Record<string, unknown>> {\n if (exit.done) return Promise.reject(new DeskError('Virtualization helper exited.'))\n const id = randomUUID()\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => { pending.delete(id); reject(new DeskError(`Virtualization helper ${op} timed out.`)) }, timeout)\n pending.set(id, { resolve, reject, timer })\n input.write(`${JSON.stringify({ ...fields, id, op })}\\n`, (error) => {\n if (error) { const entry = pending.get(id); if (entry) { clearTimeout(entry.timer); pending.delete(id); reject(error) } }\n })\n })\n }\n const stop = async () => {\n try { if (!exit.done) await call('stop') } finally { await terminate(child, exit, 2000) }\n }\n try {\n const identifierPath = join(plan.snapshot.dir, 'machine-id')\n const identifier = plan.snapshot.restoring ? await readFile(identifierPath, 'utf8') : undefined\n const boot = await call('boot', {\n kernel: plan.kernelPath, initrd: plan.initramfsPath, cmdline: plan.kernelCmdline,\n ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),\n disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,\n vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,\n shares: plan.shares.map(({ tag, path, readOnly }) => ({ tag, path, readOnly })),\n ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),\n network: { ...plan.network, macAddress: plan.network.mac },\n })\n if (typeof boot.machineIdentifier !== 'string') throw new DeskError('Helper did not return a machine identifier.')\n const machineIdentifier = boot.machineIdentifier\n if (plan.snapshot.restoring) {\n if (!hooks?.onRestored) throw new DeskError('Restore requires snapshot invalidation before resume.')\n await hooks.onRestored()\n await call('resume')\n }\n const connectAgent = (abandoned?: () => boolean) => connectWithRetry({\n connect: options.connect ?? defaultConnect, now: Date.now, socketPath: plan.vsock.socketPath,\n port: options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT, timeoutMs: timeout, retryDelayMs: 100,\n attemptTimeoutMs: 5000, exited: exit, abandoned,\n })\n const stream = await connectAgent()\n return createGuestMachine({\n deskId: plan.deskId, exit, stream, reconnect: connectAgent, reconnectWindowMs: timeout,\n now: Date.now, idFactory: randomUUID, requestTimeoutMs: options.requestTimeoutMs ?? 30_000,\n stop,\n connect: (port) => attemptHandshake(options.connect ?? defaultConnect, plan.ports.socketPath, port, 5000),\n ...(boot.saveRestore === true ? {\n park: async (dir: string) => {\n try {\n await call('save', { path: join(dir, 'state') })\n await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 })\n await writeShareManifest(dir, plan.shares)\n } finally { await stop() }\n },\n } : {}),\n })\n } catch (error) {\n await terminate(child, exit, 2000)\n throw new DeskError(`Virtualization desk ${plan.deskId} failed: ${errorMessage(error)}${log.tail() ? ` (${log.tail()})` : ''}`)\n }\n },\n }\n}\n"]}