@braedonsaunders/appkit-desk 0.7.0 → 0.8.0

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