@holoscript/holosystem 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1292 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import {
4
+ copyFileSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ rmSync,
11
+ } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { dirname, join, relative, resolve, sep } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ export const HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.vm-launch-plan.v1';
17
+ export const HOLOSYSTEM_VM_EXECUTOR_SCHEMA = 'holoscript.holosystem.vm-executor.v1';
18
+ export const HOLOSYSTEM_VM_ASSET_SCHEMA = 'holoscript.holosystem.vm-asset.v1';
19
+ export const HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA = 'holoscript.holosystem.vm-launch-receipt.v1';
20
+ export const HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.whpx-vm-launch-plan.v1';
21
+ export const HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA =
22
+ 'holoscript.holosystem.whpx-vm-launch-receipt.v1';
23
+ export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA =
24
+ 'holoscript.holosystem.whpx-sandboxed-vm-launch-plan.v1';
25
+ export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA =
26
+ 'holoscript.holosystem.whpx-sandboxed-vm-launch-receipt.v1';
27
+ export const HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL =
28
+ 'holoscript.holosystem.windows-sandbox-launch.v1';
29
+ export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA =
30
+ 'holoscript.holosystem.windows-sandbox-launcher.v1';
31
+
32
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
33
+ const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
34
+ const MAX_RUNTIME_FILES = 1024;
35
+ const MAX_RUNTIME_FILE_BYTES = 128 * 1024 * 1024;
36
+ const MAX_RUNTIME_BYTES = 512 * 1024 * 1024;
37
+ const MAX_KERNEL_BYTES = 128 * 1024 * 1024;
38
+ const MAX_INITRD_BYTES = 512 * 1024 * 1024;
39
+ const MAX_CONSOLE_BYTES = 1024 * 1024;
40
+ const EXPECTED_EXIT_CODE = 33;
41
+ const SANDBOX_LAUNCHER_BINARY = 'holosystem-sandbox-launcher.exe';
42
+ const SANDBOX_KIND = 'windows-low-integrity-job-v1';
43
+ const SANDBOX_PROCESS_MEMORY_BYTES = 512 * 1024 * 1024;
44
+ const SANDBOX_PROTOCOL_BYTES = 3 * 1024 * 1024;
45
+ const SANDBOX_LAUNCHER_PATH = fileURLToPath(
46
+ new URL(`../native/windows-x64/${SANDBOX_LAUNCHER_BINARY}`, import.meta.url)
47
+ );
48
+ export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST = hashBytes(
49
+ readFileSync(SANDBOX_LAUNCHER_PATH)
50
+ );
51
+ const TCG_POLICY = Object.freeze({
52
+ accelerator: 'tcg',
53
+ userConfiguration: 'disabled',
54
+ defaultDevices: 'disabled',
55
+ network: 'none',
56
+ display: 'none',
57
+ monitor: 'none',
58
+ usb: 'disabled',
59
+ reboot: 'disabled',
60
+ processEnvironment: 'minimal',
61
+ guestSignal: 'serial-digest-and-debug-exit',
62
+ diagnostics: 'none',
63
+ });
64
+ const WHPX_POLICY = Object.freeze({
65
+ accelerator: 'whpx',
66
+ userConfiguration: 'disabled',
67
+ defaultDevices: 'disabled',
68
+ network: 'none',
69
+ display: 'none',
70
+ monitor: 'none',
71
+ usb: 'disabled',
72
+ reboot: 'disabled',
73
+ processEnvironment: 'minimal',
74
+ guestSignal: 'serial-digest-and-debug-exit',
75
+ diagnostics: 'pinned-digest',
76
+ });
77
+ const WHPX_SANDBOXED_POLICY = Object.freeze({
78
+ ...WHPX_POLICY,
79
+ hostProcess: SANDBOX_KIND,
80
+ token: 'disable-max-privilege-low-integrity',
81
+ job: 'pre-resume-kill-on-close-active-process-memory-ui',
82
+ writableTemp: 'low-integrity-private-snapshot',
83
+ });
84
+ const BOUNDARIES = Object.freeze([
85
+ 'guest-artifact-provenance',
86
+ 'hardware-hypervisor-acceleration',
87
+ 'host-crash-dump-custody',
88
+ 'host-os-and-emulator-correctness',
89
+ 'host-process-isolation',
90
+ 'measured-boot-and-firmware',
91
+ 'qemu-runtime-supply-chain',
92
+ 'side-channel-resistance',
93
+ ]);
94
+ const TCG_ADAPTER = Object.freeze({
95
+ accelerator: 'tcg',
96
+ accelerationName: 'qemu-tcg',
97
+ hardwareBacked: false,
98
+ planSchema: HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA,
99
+ receiptSchema: HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA,
100
+ policy: TCG_POLICY,
101
+ requiresDiagnosticsDigest: false,
102
+ });
103
+ const WHPX_ADAPTER = Object.freeze({
104
+ accelerator: 'whpx',
105
+ accelerationName: 'qemu-whpx',
106
+ hardwareBacked: true,
107
+ planSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA,
108
+ receiptSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
109
+ policy: WHPX_POLICY,
110
+ requiresDiagnosticsDigest: true,
111
+ hostSandboxed: false,
112
+ });
113
+ const WHPX_SANDBOXED_ADAPTER = Object.freeze({
114
+ accelerator: 'whpx',
115
+ accelerationName: 'qemu-whpx',
116
+ hardwareBacked: true,
117
+ planSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA,
118
+ receiptSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA,
119
+ policy: WHPX_SANDBOXED_POLICY,
120
+ requiresDiagnosticsDigest: true,
121
+ hostSandboxed: true,
122
+ });
123
+
124
+ function isRecord(value) {
125
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
126
+ }
127
+
128
+ function issue(issues, code, path, message) {
129
+ issues.push({ code, path, message });
130
+ }
131
+
132
+ function hashBytes(value) {
133
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
134
+ }
135
+
136
+ function hashJson(value) {
137
+ return hashBytes(JSON.stringify(value));
138
+ }
139
+
140
+ function lexical(left, right) {
141
+ return left < right ? -1 : left > right ? 1 : 0;
142
+ }
143
+
144
+ function validId(value) {
145
+ return typeof value === 'string' && /^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(value);
146
+ }
147
+
148
+ function portableRuntimePath(value) {
149
+ return (
150
+ typeof value === 'string' &&
151
+ value.length > 0 &&
152
+ value.length <= 512 &&
153
+ !value.includes('\\') &&
154
+ !value.includes('\0') &&
155
+ !value.startsWith('/') &&
156
+ !/^[A-Za-z]:/u.test(value) &&
157
+ !/(?:^|\/)\.\.?(?:\/|$)/u.test(value) &&
158
+ !/[\r\n]/u.test(value)
159
+ );
160
+ }
161
+
162
+ function knownKeys(value, allowed, path, issues) {
163
+ if (!isRecord(value)) return;
164
+ for (const key of Object.keys(value)) {
165
+ if (!allowed.has(key)) {
166
+ issue(
167
+ issues,
168
+ 'vm-launch-field-unknown',
169
+ path ? `${path}.${key}` : key,
170
+ 'Field is not part of the declarative machine-VM launch vocabulary.'
171
+ );
172
+ }
173
+ }
174
+ }
175
+
176
+ function inspectDigest(value, path, issues) {
177
+ if (!DIGEST_PATTERN.test(value || '')) {
178
+ issue(issues, 'vm-launch-digest-invalid', path, 'Value must be a lowercase SHA-256 digest.');
179
+ }
180
+ }
181
+
182
+ function normalizedPlan(plan) {
183
+ return {
184
+ schema: plan.schema,
185
+ id: plan.id,
186
+ host: { os: plan.host.os, architecture: plan.host.architecture },
187
+ executor: {
188
+ kind: plan.executor.kind,
189
+ binary: plan.executor.binary,
190
+ binaryDigest: plan.executor.binaryDigest,
191
+ runtimeDigest: plan.executor.runtimeDigest,
192
+ },
193
+ target: {
194
+ architecture: plan.target.architecture,
195
+ machine: plan.target.machine,
196
+ accelerator: plan.target.accelerator,
197
+ },
198
+ guest: {
199
+ kernelDigest: plan.guest.kernelDigest,
200
+ initrdDigest: plan.guest.initrdDigest,
201
+ expectedConsoleDigest: plan.guest.expectedConsoleDigest,
202
+ expectedDiagnosticsDigest: plan.guest.expectedDiagnosticsDigest || null,
203
+ },
204
+ resources: {
205
+ memoryMiB: plan.resources.memoryMiB,
206
+ cpus: plan.resources.cpus,
207
+ timeoutSeconds: plan.resources.timeoutSeconds,
208
+ },
209
+ sandbox: plan.sandbox
210
+ ? { kind: plan.sandbox.kind, launcherDigest: plan.sandbox.launcherDigest }
211
+ : null,
212
+ launches: plan.launches,
213
+ };
214
+ }
215
+
216
+ function inspectVmLaunchPlanForAdapter(plan, adapter) {
217
+ const issues = [];
218
+ if (!isRecord(plan)) {
219
+ issue(issues, 'vm-launch-plan-invalid', '$', 'VM launch plan must be a JSON object.');
220
+ return { ready: false, issues };
221
+ }
222
+
223
+ knownKeys(
224
+ plan,
225
+ new Set([
226
+ 'schema',
227
+ 'id',
228
+ 'host',
229
+ 'executor',
230
+ 'target',
231
+ 'guest',
232
+ 'resources',
233
+ ...(adapter.hostSandboxed ? ['sandbox'] : []),
234
+ 'launches',
235
+ ]),
236
+ '',
237
+ issues
238
+ );
239
+ if (plan.schema !== adapter.planSchema) {
240
+ issue(issues, 'vm-launch-schema-mismatch', 'schema', `Expected ${adapter.planSchema}.`);
241
+ }
242
+ if (!validId(plan.id)) {
243
+ issue(issues, 'vm-launch-id-invalid', 'id', 'Launch id must be a portable identifier.');
244
+ }
245
+
246
+ knownKeys(plan.host, new Set(['os', 'architecture']), 'host', issues);
247
+ if (plan.host?.os !== 'windows' || plan.host?.architecture !== 'amd64') {
248
+ issue(
249
+ issues,
250
+ 'vm-launch-host-unsupported',
251
+ 'host',
252
+ 'This tracer supports only a Windows AMD64 host.'
253
+ );
254
+ }
255
+
256
+ knownKeys(
257
+ plan.executor,
258
+ new Set(['kind', 'binary', 'binaryDigest', 'runtimeDigest']),
259
+ 'executor',
260
+ issues
261
+ );
262
+ if (plan.executor?.kind !== 'qemu-system' || plan.executor?.binary !== EXECUTOR_BINARY) {
263
+ issue(
264
+ issues,
265
+ 'vm-launch-executor-unsupported',
266
+ 'executor',
267
+ `This tracer supports only ${EXECUTOR_BINARY}.`
268
+ );
269
+ }
270
+ inspectDigest(plan.executor?.binaryDigest, 'executor.binaryDigest', issues);
271
+ inspectDigest(plan.executor?.runtimeDigest, 'executor.runtimeDigest', issues);
272
+
273
+ knownKeys(plan.target, new Set(['architecture', 'machine', 'accelerator']), 'target', issues);
274
+ if (plan.target?.architecture !== 'amd64' || plan.target?.machine !== 'q35') {
275
+ issue(
276
+ issues,
277
+ 'vm-launch-target-unsupported',
278
+ 'target',
279
+ 'This tracer supports only the explicit AMD64 q35 machine target.'
280
+ );
281
+ }
282
+ if (plan.target?.accelerator !== adapter.accelerator) {
283
+ issue(
284
+ issues,
285
+ 'vm-launch-accelerator-unsupported',
286
+ 'target.accelerator',
287
+ `This adapter requires the explicit ${adapter.accelerator} accelerator and never falls back.`
288
+ );
289
+ }
290
+
291
+ knownKeys(
292
+ plan.guest,
293
+ new Set([
294
+ 'kernelDigest',
295
+ 'initrdDigest',
296
+ 'expectedConsoleDigest',
297
+ ...(adapter.requiresDiagnosticsDigest ? ['expectedDiagnosticsDigest'] : []),
298
+ ]),
299
+ 'guest',
300
+ issues
301
+ );
302
+ inspectDigest(plan.guest?.kernelDigest, 'guest.kernelDigest', issues);
303
+ inspectDigest(plan.guest?.initrdDigest, 'guest.initrdDigest', issues);
304
+ inspectDigest(plan.guest?.expectedConsoleDigest, 'guest.expectedConsoleDigest', issues);
305
+ if (adapter.requiresDiagnosticsDigest) {
306
+ inspectDigest(plan.guest?.expectedDiagnosticsDigest, 'guest.expectedDiagnosticsDigest', issues);
307
+ }
308
+
309
+ knownKeys(plan.resources, new Set(['memoryMiB', 'cpus', 'timeoutSeconds']), 'resources', issues);
310
+ if (
311
+ plan.resources?.memoryMiB !== 128 ||
312
+ plan.resources?.cpus !== 1 ||
313
+ !Number.isInteger(plan.resources?.timeoutSeconds) ||
314
+ plan.resources.timeoutSeconds < 5 ||
315
+ plan.resources.timeoutSeconds > 120
316
+ ) {
317
+ issue(
318
+ issues,
319
+ 'vm-launch-limit-invalid',
320
+ 'resources',
321
+ 'Resources require 128 MiB, one CPU, and a timeout from 5 to 120 seconds.'
322
+ );
323
+ }
324
+ if (plan.launches !== 2) {
325
+ issue(
326
+ issues,
327
+ 'vm-launch-count-invalid',
328
+ 'launches',
329
+ 'Exactly two clean launches are required for a deterministic receipt.'
330
+ );
331
+ }
332
+
333
+ if (adapter.hostSandboxed) {
334
+ knownKeys(plan.sandbox, new Set(['kind', 'launcherDigest']), 'sandbox', issues);
335
+ if (plan.sandbox?.kind !== SANDBOX_KIND) {
336
+ issue(
337
+ issues,
338
+ 'vm-launch-sandbox-unsupported',
339
+ 'sandbox.kind',
340
+ `This adapter requires ${SANDBOX_KIND}.`
341
+ );
342
+ }
343
+ inspectDigest(plan.sandbox?.launcherDigest, 'sandbox.launcherDigest', issues);
344
+ }
345
+
346
+ return { ready: issues.length === 0, issues };
347
+ }
348
+
349
+ export function inspectVmLaunchPlan(plan) {
350
+ return inspectVmLaunchPlanForAdapter(plan, TCG_ADAPTER);
351
+ }
352
+
353
+ export function inspectWhpxVmLaunchPlan(plan) {
354
+ return inspectVmLaunchPlanForAdapter(plan, WHPX_ADAPTER);
355
+ }
356
+
357
+ export function inspectWhpxSandboxedVmLaunchPlan(plan) {
358
+ return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
359
+ }
360
+
361
+ function scanRuntime(directory, issues) {
362
+ const files = [];
363
+ let totalBytes = 0;
364
+ let stopped = false;
365
+
366
+ function visit(current) {
367
+ if (stopped) return;
368
+ let entries;
369
+ try {
370
+ entries = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
371
+ lexical(left.name, right.name)
372
+ );
373
+ } catch {
374
+ issue(issues, 'vm-executor-unreadable', 'executorDirectory', 'Runtime could not be read.');
375
+ stopped = true;
376
+ return;
377
+ }
378
+
379
+ for (const entry of entries) {
380
+ if (stopped) return;
381
+ const absolute = join(current, entry.name);
382
+ const portable = relative(directory, absolute).split(sep).join('/');
383
+ let stats;
384
+ try {
385
+ stats = lstatSync(absolute);
386
+ } catch {
387
+ issue(
388
+ issues,
389
+ 'vm-executor-unreadable',
390
+ `runtime.files.${portable}`,
391
+ 'A runtime entry could not be inspected.'
392
+ );
393
+ continue;
394
+ }
395
+ if (stats.isSymbolicLink()) {
396
+ issue(
397
+ issues,
398
+ 'vm-executor-link-forbidden',
399
+ `runtime.files.${portable}`,
400
+ 'Symbolic links and reparse-point indirection are not executor inputs.'
401
+ );
402
+ continue;
403
+ }
404
+ if (stats.isDirectory()) {
405
+ visit(absolute);
406
+ continue;
407
+ }
408
+ if (!stats.isFile()) {
409
+ issue(
410
+ issues,
411
+ 'vm-executor-type-forbidden',
412
+ `runtime.files.${portable}`,
413
+ 'Only regular runtime files are accepted.'
414
+ );
415
+ continue;
416
+ }
417
+ if (!portableRuntimePath(portable)) {
418
+ issue(
419
+ issues,
420
+ 'vm-executor-path-invalid',
421
+ `runtime.files.${portable}`,
422
+ 'Runtime paths must be portable relative paths.'
423
+ );
424
+ continue;
425
+ }
426
+ if (stats.size > MAX_RUNTIME_FILE_BYTES) {
427
+ issue(
428
+ issues,
429
+ 'vm-executor-file-too-large',
430
+ `runtime.files.${portable}`,
431
+ 'A runtime file exceeds the per-file limit.'
432
+ );
433
+ continue;
434
+ }
435
+ let content;
436
+ try {
437
+ content = readFileSync(absolute);
438
+ } catch {
439
+ issue(
440
+ issues,
441
+ 'vm-executor-unreadable',
442
+ `runtime.files.${portable}`,
443
+ 'A runtime file could not be read.'
444
+ );
445
+ continue;
446
+ }
447
+ totalBytes += content.length;
448
+ files.push({ path: portable, bytes: content.length, digest: hashBytes(content) });
449
+ if (files.length > MAX_RUNTIME_FILES) {
450
+ issue(
451
+ issues,
452
+ 'vm-executor-file-limit',
453
+ 'runtime.files',
454
+ 'Runtime exceeds the file-count limit.'
455
+ );
456
+ stopped = true;
457
+ } else if (totalBytes > MAX_RUNTIME_BYTES) {
458
+ issue(
459
+ issues,
460
+ 'vm-executor-too-large',
461
+ 'runtime.files',
462
+ 'Runtime exceeds the aggregate byte limit.'
463
+ );
464
+ stopped = true;
465
+ }
466
+ }
467
+ }
468
+
469
+ visit(directory);
470
+ return files.sort((left, right) => lexical(left.path, right.path));
471
+ }
472
+
473
+ export function inspectVmExecutor({ executorDirectory } = {}) {
474
+ const issues = [];
475
+ const directory = typeof executorDirectory === 'string' ? resolve(executorDirectory) : null;
476
+ let directoryReady = false;
477
+ try {
478
+ const stats = lstatSync(directory);
479
+ directoryReady = stats.isDirectory() && !stats.isSymbolicLink();
480
+ } catch {
481
+ directoryReady = false;
482
+ }
483
+ if (!directoryReady) {
484
+ issue(
485
+ issues,
486
+ 'vm-executor-directory-invalid',
487
+ 'executorDirectory',
488
+ 'Executor input must be a real caller-owned directory.'
489
+ );
490
+ }
491
+
492
+ const files = directoryReady ? scanRuntime(directory, issues) : [];
493
+ const binary = files.find((entry) => entry.path === EXECUTOR_BINARY);
494
+ if (!binary) {
495
+ issue(
496
+ issues,
497
+ 'vm-executor-binary-missing',
498
+ 'runtime.binary',
499
+ `Runtime must contain ${EXECUTOR_BINARY} at its root.`
500
+ );
501
+ }
502
+ const envelope = { schema: HOLOSYSTEM_VM_EXECUTOR_SCHEMA, binary: EXECUTOR_BINARY, files };
503
+ return {
504
+ schema: HOLOSYSTEM_VM_EXECUTOR_SCHEMA,
505
+ ready: issues.length === 0,
506
+ binary: EXECUTOR_BINARY,
507
+ binaryDigest: binary?.digest || null,
508
+ digest: issues.length === 0 ? hashJson(envelope) : null,
509
+ files,
510
+ summary: {
511
+ files: files.length,
512
+ bytes: files.reduce((total, file) => total + file.bytes, 0),
513
+ },
514
+ issues,
515
+ };
516
+ }
517
+
518
+ export function inspectVmLaunchAsset({ assetPath, kind } = {}) {
519
+ const issues = [];
520
+ if (!['kernel', 'initrd'].includes(kind)) {
521
+ issue(issues, 'vm-asset-kind-invalid', 'kind', 'Asset kind must be kernel or initrd.');
522
+ }
523
+ const absolute = typeof assetPath === 'string' ? resolve(assetPath) : null;
524
+ let content = null;
525
+ try {
526
+ const stats = lstatSync(absolute);
527
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
528
+ const maximum = kind === 'kernel' ? MAX_KERNEL_BYTES : MAX_INITRD_BYTES;
529
+ if (stats.size > maximum) {
530
+ issue(issues, 'vm-asset-too-large', 'assetPath', 'Guest asset exceeds its byte limit.');
531
+ } else {
532
+ content = readFileSync(absolute);
533
+ }
534
+ } catch {
535
+ issue(
536
+ issues,
537
+ 'vm-asset-file-invalid',
538
+ 'assetPath',
539
+ 'Guest asset must be a readable regular caller-owned file.'
540
+ );
541
+ }
542
+ return {
543
+ schema: HOLOSYSTEM_VM_ASSET_SCHEMA,
544
+ ready: issues.length === 0,
545
+ kind: ['kernel', 'initrd'].includes(kind) ? kind : null,
546
+ digest: content && issues.length === 0 ? hashBytes(content) : null,
547
+ bytes: content && issues.length === 0 ? content.length : null,
548
+ issues,
549
+ };
550
+ }
551
+
552
+ export function inspectWindowsVmSandboxLauncher() {
553
+ const issues = [];
554
+ let content = null;
555
+ try {
556
+ const stats = lstatSync(SANDBOX_LAUNCHER_PATH);
557
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
558
+ content = readFileSync(SANDBOX_LAUNCHER_PATH);
559
+ } catch {
560
+ issue(
561
+ issues,
562
+ 'vm-launch-sandbox-launcher-invalid',
563
+ 'sandbox.launcher',
564
+ 'The packaged Windows sandbox launcher must be a readable regular file.'
565
+ );
566
+ }
567
+ const digest = content ? hashBytes(content) : null;
568
+ if (digest && digest !== HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST) {
569
+ issue(
570
+ issues,
571
+ 'vm-launch-sandbox-launcher-drift',
572
+ 'sandbox.launcherDigest',
573
+ 'The packaged Windows sandbox launcher changed after module initialization.'
574
+ );
575
+ }
576
+ return {
577
+ schema: HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA,
578
+ ready: issues.length === 0,
579
+ kind: SANDBOX_KIND,
580
+ digest: issues.length === 0 ? digest : null,
581
+ bytes: issues.length === 0 ? content.length : null,
582
+ issues,
583
+ };
584
+ }
585
+
586
+ function logSummary(value) {
587
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
588
+ return { bytes: bytes.length, digest: hashBytes(bytes) };
589
+ }
590
+
591
+ function normalizedDiagnostics(value, command) {
592
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
593
+ if (bytes.length === 0) return bytes;
594
+ const prefix = Buffer.from(`${command}: `, 'utf8');
595
+ const chunks = [];
596
+ let offset = 0;
597
+ let scan = 0;
598
+ for (let index = bytes.indexOf(prefix, scan); index !== -1; index = bytes.indexOf(prefix, scan)) {
599
+ if (index !== 0 && bytes[index - 1] !== 0x0a) {
600
+ scan = index + 1;
601
+ continue;
602
+ }
603
+ chunks.push(bytes.subarray(offset, index));
604
+ offset = index + prefix.length;
605
+ scan = offset;
606
+ }
607
+ chunks.push(bytes.subarray(offset));
608
+ return Buffer.concat(chunks);
609
+ }
610
+
611
+ function finishReceipt(receipt) {
612
+ return { ...receipt, receiptHash: hashJson(receipt) };
613
+ }
614
+
615
+ function baseReceipt(plan, now, issues, adapter) {
616
+ return {
617
+ schema: adapter.receiptSchema,
618
+ generatedAt: now.toISOString(),
619
+ id: validId(plan?.id) ? plan.id : null,
620
+ status: 'blocked',
621
+ verified: false,
622
+ deterministic: false,
623
+ hardwareBacked: false,
624
+ acceleration: {
625
+ adapter: adapter.accelerationName,
626
+ evidence: adapter.hardwareBacked ? 'two-explicit-successful-launches' : 'software-emulation',
627
+ verified: false,
628
+ },
629
+ isolation: adapter.hostSandboxed
630
+ ? {
631
+ hostProcess: SANDBOX_KIND,
632
+ scope: 'integrity-privilege-lifetime-resource-ui',
633
+ verified: false,
634
+ launcherDigest: DIGEST_PATTERN.test(plan?.sandbox?.launcherDigest || '')
635
+ ? plan.sandbox.launcherDigest
636
+ : null,
637
+ controls: {
638
+ filteredToken: false,
639
+ disableMaxPrivilege: false,
640
+ enabledPrivilegeCount: null,
641
+ privilegesBounded: false,
642
+ lowIntegrity: false,
643
+ assignedBeforeResume: false,
644
+ handleAllowlist: false,
645
+ killOnClose: false,
646
+ activeProcessLimit: false,
647
+ processMemoryLimit: false,
648
+ uiRestrictions: false,
649
+ writableTempLowIntegrity: false,
650
+ },
651
+ }
652
+ : {
653
+ hostProcess: 'ambient-windows-process',
654
+ verified: false,
655
+ },
656
+ measurementDigest: null,
657
+ executor: {
658
+ kind: plan?.executor?.kind === 'qemu-system' ? 'qemu-system' : null,
659
+ binaryDigest: DIGEST_PATTERN.test(plan?.executor?.binaryDigest || '')
660
+ ? plan.executor.binaryDigest
661
+ : null,
662
+ runtimeDigest: DIGEST_PATTERN.test(plan?.executor?.runtimeDigest || '')
663
+ ? plan.executor.runtimeDigest
664
+ : null,
665
+ },
666
+ guest: {
667
+ kernelDigest: DIGEST_PATTERN.test(plan?.guest?.kernelDigest || '')
668
+ ? plan.guest.kernelDigest
669
+ : null,
670
+ initrdDigest: DIGEST_PATTERN.test(plan?.guest?.initrdDigest || '')
671
+ ? plan.guest.initrdDigest
672
+ : null,
673
+ expectedConsoleDigest: DIGEST_PATTERN.test(plan?.guest?.expectedConsoleDigest || '')
674
+ ? plan.guest.expectedConsoleDigest
675
+ : null,
676
+ },
677
+ target:
678
+ plan?.target?.architecture === 'amd64' &&
679
+ plan?.target?.machine === 'q35' &&
680
+ plan?.target?.accelerator === adapter.accelerator
681
+ ? { architecture: 'amd64', machine: 'q35', accelerator: adapter.accelerator }
682
+ : null,
683
+ resources:
684
+ isRecord(plan?.resources) &&
685
+ Number.isInteger(plan.resources.memoryMiB) &&
686
+ Number.isInteger(plan.resources.cpus) &&
687
+ Number.isInteger(plan.resources.timeoutSeconds)
688
+ ? {
689
+ memoryMiB: plan.resources.memoryMiB,
690
+ cpus: plan.resources.cpus,
691
+ timeoutSeconds: plan.resources.timeoutSeconds,
692
+ }
693
+ : null,
694
+ policy: { ...adapter.policy },
695
+ launches: [],
696
+ coverage: {
697
+ includedLayers: [],
698
+ missingLayers: [
699
+ 'guest-artifact-measurement',
700
+ 'hardware-hypervisor-acceleration',
701
+ 'host-process-isolation',
702
+ 'machine-vm-launch',
703
+ 'virtual-device-minimization',
704
+ ],
705
+ },
706
+ boundaries: [
707
+ ...BOUNDARIES.filter(
708
+ (layer) =>
709
+ !(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration') &&
710
+ !(adapter.hostSandboxed && layer === 'host-process-isolation')
711
+ ),
712
+ ...(adapter.hostSandboxed
713
+ ? ['host-filesystem-confidentiality', 'host-network-isolation']
714
+ : []),
715
+ ],
716
+ issues,
717
+ };
718
+ }
719
+
720
+ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
721
+ return [
722
+ '-no-user-config',
723
+ '-nodefaults',
724
+ '-machine',
725
+ `q35,accel=${adapter.accelerator},usb=off`,
726
+ ...(adapter === TCG_ADAPTER ? ['-cpu', 'max'] : []),
727
+ '-m',
728
+ `${plan.resources.memoryMiB}M`,
729
+ '-smp',
730
+ String(plan.resources.cpus),
731
+ '-display',
732
+ 'none',
733
+ '-serial',
734
+ 'stdio',
735
+ '-monitor',
736
+ 'none',
737
+ '-nic',
738
+ 'none',
739
+ '-no-reboot',
740
+ '-kernel',
741
+ kernelPath,
742
+ '-initrd',
743
+ initrdPath,
744
+ '-append',
745
+ 'console=ttyS0,115200 quiet loglevel=0 rdinit=/init panic=-1 random.trust_cpu=off',
746
+ '-device',
747
+ 'isa-debug-exit,iobase=0xf4,iosize=0x04',
748
+ ];
749
+ }
750
+
751
+ function minimalEnvironment(snapshotRoot, runtimeDirectory, temporaryDirectory = snapshotRoot) {
752
+ const env = {
753
+ HOME: snapshotRoot,
754
+ USERPROFILE: snapshotRoot,
755
+ TEMP: temporaryDirectory,
756
+ TMP: temporaryDirectory,
757
+ PATH: runtimeDirectory,
758
+ LANG: 'C',
759
+ LC_ALL: 'C',
760
+ };
761
+ if (typeof process.env.SystemRoot === 'string') env.SystemRoot = process.env.SystemRoot;
762
+ if (typeof process.env.WINDIR === 'string') env.WINDIR = process.env.WINDIR;
763
+ return env;
764
+ }
765
+
766
+ const SANDBOX_CONTROL_KEYS = Object.freeze([
767
+ 'filteredToken',
768
+ 'disableMaxPrivilege',
769
+ 'enabledPrivilegeCount',
770
+ 'privilegesBounded',
771
+ 'lowIntegrity',
772
+ 'assignedBeforeResume',
773
+ 'handleAllowlist',
774
+ 'killOnClose',
775
+ 'activeProcessLimit',
776
+ 'processMemoryLimit',
777
+ 'uiRestrictions',
778
+ 'writableTempLowIntegrity',
779
+ ]);
780
+
781
+ function canonicalBase64(value) {
782
+ if (
783
+ typeof value !== 'string' ||
784
+ value.length > Math.ceil(MAX_CONSOLE_BYTES / 3) * 4 ||
785
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)
786
+ ) {
787
+ return null;
788
+ }
789
+ const bytes = Buffer.from(value, 'base64');
790
+ return bytes.toString('base64') === value && bytes.length <= MAX_CONSOLE_BYTES ? bytes : null;
791
+ }
792
+
793
+ function parseSandboxProtocol(result) {
794
+ if (
795
+ result?.status !== 0 ||
796
+ result?.signal ||
797
+ result?.error ||
798
+ logSummary(result?.stderr).bytes !== 0
799
+ ) {
800
+ return { ready: false, issueCode: 'vm-launch-sandbox-launcher-failed' };
801
+ }
802
+ let message;
803
+ try {
804
+ const encoded = Buffer.isBuffer(result.stdout)
805
+ ? result.stdout.toString('utf8')
806
+ : String(result.stdout || '');
807
+ message = JSON.parse(encoded.trim());
808
+ } catch {
809
+ return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
810
+ }
811
+ if (
812
+ !isRecord(message) ||
813
+ message.protocol !== HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL ||
814
+ !isRecord(message.isolation) ||
815
+ Object.keys(message).some(
816
+ (key) =>
817
+ ![
818
+ 'protocol',
819
+ 'launched',
820
+ 'timedOut',
821
+ 'exitCode',
822
+ 'isolation',
823
+ 'stdoutBase64',
824
+ 'stderrBase64',
825
+ 'errorStage',
826
+ 'errorCode',
827
+ ].includes(key)
828
+ ) ||
829
+ Object.keys(message.isolation).some((key) => !SANDBOX_CONTROL_KEYS.includes(key))
830
+ ) {
831
+ return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
832
+ }
833
+ const stdout = canonicalBase64(message.stdoutBase64);
834
+ const stderr = canonicalBase64(message.stderrBase64);
835
+ const controlsReady =
836
+ SANDBOX_CONTROL_KEYS.every(
837
+ (key) => key === 'enabledPrivilegeCount' || message.isolation[key] === true
838
+ ) &&
839
+ Number.isInteger(message.isolation.enabledPrivilegeCount) &&
840
+ message.isolation.enabledPrivilegeCount >= 0 &&
841
+ message.isolation.enabledPrivilegeCount <= 1;
842
+ if (
843
+ message.launched !== true ||
844
+ message.timedOut !== false ||
845
+ !Number.isInteger(message.exitCode) ||
846
+ message.errorStage !== null ||
847
+ message.errorCode !== 0 ||
848
+ !controlsReady ||
849
+ !stdout ||
850
+ !stderr
851
+ ) {
852
+ return { ready: false, issueCode: 'vm-launch-sandbox-evidence-invalid' };
853
+ }
854
+ return {
855
+ ready: true,
856
+ status: message.exitCode,
857
+ signal: null,
858
+ stdout,
859
+ stderr,
860
+ isolation: Object.fromEntries(SANDBOX_CONTROL_KEYS.map((key) => [key, message.isolation[key]])),
861
+ };
862
+ }
863
+
864
+ function copyRuntimeSnapshot(sourceDirectory, report, destinationDirectory) {
865
+ mkdirSync(destinationDirectory, { recursive: true });
866
+ for (const file of report.files) {
867
+ const target = join(destinationDirectory, file.path);
868
+ mkdirSync(dirname(target), { recursive: true });
869
+ copyFileSync(join(sourceDirectory, file.path), target);
870
+ }
871
+ }
872
+
873
+ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
874
+ const executor = inspectVmExecutor({ executorDirectory: runtimeDirectory });
875
+ const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
876
+ const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
877
+ return (
878
+ executor.ready &&
879
+ executor.digest === plan.executor.runtimeDigest &&
880
+ executor.binaryDigest === plan.executor.binaryDigest &&
881
+ kernel.ready &&
882
+ kernel.digest === plan.guest.kernelDigest &&
883
+ initrd.ready &&
884
+ initrd.digest === plan.guest.initrdDigest
885
+ );
886
+ }
887
+
888
+ function sandboxLauncherSnapshotMatches(plan, launcherPath, adapter) {
889
+ if (!adapter.hostSandboxed) return true;
890
+ try {
891
+ const stats = lstatSync(launcherPath);
892
+ return (
893
+ stats.isFile() &&
894
+ !stats.isSymbolicLink() &&
895
+ hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest
896
+ );
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+
902
+ function runVmLaunchWithProcessRunner(
903
+ { plan, executorDirectory, kernelPath, initrdPath, now = new Date() } = {},
904
+ processRunner,
905
+ adapter
906
+ ) {
907
+ const inspection = inspectVmLaunchPlanForAdapter(plan, adapter);
908
+ const receipt = baseReceipt(plan, now, [...inspection.issues], adapter);
909
+ if (!inspection.ready) return finishReceipt(receipt);
910
+
911
+ const executor = inspectVmExecutor({ executorDirectory });
912
+ const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
913
+ const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
914
+ const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
915
+ receipt.issues.push(
916
+ ...executor.issues,
917
+ ...kernel.issues,
918
+ ...initrd.issues,
919
+ ...(sandboxLauncher?.issues || [])
920
+ );
921
+ if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
922
+ issue(
923
+ receipt.issues,
924
+ 'vm-launch-runtime-mismatch',
925
+ 'executor.runtimeDigest',
926
+ 'QEMU runtime closure does not match the pinned plan digest.'
927
+ );
928
+ }
929
+ if (executor.ready && executor.binaryDigest !== plan.executor.binaryDigest) {
930
+ issue(
931
+ receipt.issues,
932
+ 'vm-launch-binary-mismatch',
933
+ 'executor.binaryDigest',
934
+ 'QEMU binary does not match the pinned plan digest.'
935
+ );
936
+ }
937
+ if (kernel.ready && kernel.digest !== plan.guest.kernelDigest) {
938
+ issue(
939
+ receipt.issues,
940
+ 'vm-launch-kernel-mismatch',
941
+ 'guest.kernelDigest',
942
+ 'Guest kernel does not match the pinned plan digest.'
943
+ );
944
+ }
945
+ if (initrd.ready && initrd.digest !== plan.guest.initrdDigest) {
946
+ issue(
947
+ receipt.issues,
948
+ 'vm-launch-initrd-mismatch',
949
+ 'guest.initrdDigest',
950
+ 'Guest initrd does not match the pinned plan digest.'
951
+ );
952
+ }
953
+ if (
954
+ adapter.hostSandboxed &&
955
+ sandboxLauncher?.ready &&
956
+ sandboxLauncher.digest !== plan.sandbox.launcherDigest
957
+ ) {
958
+ issue(
959
+ receipt.issues,
960
+ 'vm-launch-sandbox-launcher-mismatch',
961
+ 'sandbox.launcherDigest',
962
+ 'Packaged Windows sandbox launcher does not match the pinned plan digest.'
963
+ );
964
+ }
965
+ if (receipt.issues.length > 0) return finishReceipt(receipt);
966
+
967
+ const sourceRuntime = resolve(executorDirectory);
968
+ const sourceKernel = resolve(kernelPath);
969
+ const sourceInitrd = resolve(initrdPath);
970
+ const snapshotRoot = mkdtempSync(join(tmpdir(), 'holosystem-vm-launch-'));
971
+ const snapshotRuntime = join(snapshotRoot, 'runtime');
972
+ const snapshotKernel = join(snapshotRoot, 'vmlinuz');
973
+ const snapshotInitrd = join(snapshotRoot, 'initramfs');
974
+ const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
975
+ const snapshotTemporary = join(snapshotRoot, 'sandbox-temp');
976
+
977
+ try {
978
+ try {
979
+ copyRuntimeSnapshot(sourceRuntime, executor, snapshotRuntime);
980
+ copyFileSync(sourceKernel, snapshotKernel);
981
+ copyFileSync(sourceInitrd, snapshotInitrd);
982
+ if (adapter.hostSandboxed) {
983
+ copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
984
+ mkdirSync(snapshotTemporary);
985
+ }
986
+ } catch {
987
+ issue(
988
+ receipt.issues,
989
+ 'vm-launch-snapshot-failed',
990
+ 'snapshot',
991
+ 'A private verified launch snapshot could not be materialized.'
992
+ );
993
+ return finishReceipt(receipt);
994
+ }
995
+
996
+ const pinnedExecutor = inspectVmExecutor({ executorDirectory: snapshotRuntime });
997
+ const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
998
+ const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
999
+ let pinnedLauncherDigest = null;
1000
+ if (adapter.hostSandboxed) {
1001
+ try {
1002
+ const stats = lstatSync(snapshotLauncher);
1003
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
1004
+ pinnedLauncherDigest = hashBytes(readFileSync(snapshotLauncher));
1005
+ } catch {
1006
+ pinnedLauncherDigest = null;
1007
+ }
1008
+ }
1009
+ if (
1010
+ !pinnedExecutor.ready ||
1011
+ pinnedExecutor.digest !== plan.executor.runtimeDigest ||
1012
+ pinnedExecutor.binaryDigest !== plan.executor.binaryDigest
1013
+ ) {
1014
+ issue(
1015
+ receipt.issues,
1016
+ 'vm-launch-runtime-snapshot-mismatch',
1017
+ 'executor.runtimeDigest',
1018
+ 'QEMU changed while its private launch snapshot was materialized.'
1019
+ );
1020
+ }
1021
+ if (!pinnedKernel.ready || pinnedKernel.digest !== plan.guest.kernelDigest) {
1022
+ issue(
1023
+ receipt.issues,
1024
+ 'vm-launch-kernel-snapshot-mismatch',
1025
+ 'guest.kernelDigest',
1026
+ 'Guest kernel changed while its private launch snapshot was materialized.'
1027
+ );
1028
+ }
1029
+ if (!pinnedInitrd.ready || pinnedInitrd.digest !== plan.guest.initrdDigest) {
1030
+ issue(
1031
+ receipt.issues,
1032
+ 'vm-launch-initrd-snapshot-mismatch',
1033
+ 'guest.initrdDigest',
1034
+ 'Guest initrd changed while its private launch snapshot was materialized.'
1035
+ );
1036
+ }
1037
+ if (adapter.hostSandboxed && pinnedLauncherDigest !== plan.sandbox.launcherDigest) {
1038
+ issue(
1039
+ receipt.issues,
1040
+ 'vm-launch-sandbox-launcher-snapshot-mismatch',
1041
+ 'sandbox.launcherDigest',
1042
+ 'Windows sandbox launcher changed while its private snapshot was materialized.'
1043
+ );
1044
+ }
1045
+ if (receipt.issues.length > 0) return finishReceipt(receipt);
1046
+
1047
+ receipt.measurementDigest = hashJson({
1048
+ plan: normalizedPlan(plan),
1049
+ executorDigest: pinnedExecutor.digest,
1050
+ binaryDigest: pinnedExecutor.binaryDigest,
1051
+ kernelDigest: pinnedKernel.digest,
1052
+ initrdDigest: pinnedInitrd.digest,
1053
+ ...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
1054
+ policy: adapter.policy,
1055
+ });
1056
+
1057
+ const qemuCommand = join(snapshotRuntime, EXECUTOR_BINARY);
1058
+ const qemuArgs = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
1059
+ const command = adapter.hostSandboxed ? snapshotLauncher : qemuCommand;
1060
+ const args = adapter.hostSandboxed
1061
+ ? [
1062
+ '--executable',
1063
+ qemuCommand,
1064
+ '--working-directory',
1065
+ snapshotRuntime,
1066
+ '--sandbox-root',
1067
+ snapshotRoot,
1068
+ '--writable-temp',
1069
+ snapshotTemporary,
1070
+ '--timeout-ms',
1071
+ String(plan.resources.timeoutSeconds * 1000),
1072
+ '--process-memory-bytes',
1073
+ String(SANDBOX_PROCESS_MEMORY_BYTES),
1074
+ '--',
1075
+ ...qemuArgs,
1076
+ ]
1077
+ : qemuArgs;
1078
+ const options = {
1079
+ encoding: null,
1080
+ env: minimalEnvironment(
1081
+ snapshotRoot,
1082
+ snapshotRuntime,
1083
+ adapter.hostSandboxed ? snapshotTemporary : snapshotRoot
1084
+ ),
1085
+ maxBuffer: adapter.hostSandboxed ? SANDBOX_PROTOCOL_BYTES : MAX_CONSOLE_BYTES,
1086
+ shell: false,
1087
+ timeout: plan.resources.timeoutSeconds * 1000 + (adapter.hostSandboxed ? 5000 : 0),
1088
+ windowsHide: true,
1089
+ };
1090
+
1091
+ for (let index = 0; index < plan.launches; index += 1) {
1092
+ if (
1093
+ !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1094
+ !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1095
+ ) {
1096
+ issue(
1097
+ receipt.issues,
1098
+ 'vm-launch-snapshot-drift',
1099
+ `launches[${index}].snapshot`,
1100
+ 'The private launch snapshot changed outside the measured execution boundary.'
1101
+ );
1102
+ break;
1103
+ }
1104
+ let result;
1105
+ try {
1106
+ result = processRunner(command, args, options);
1107
+ } catch {
1108
+ result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
1109
+ }
1110
+ if (adapter.hostSandboxed) {
1111
+ const protocol = parseSandboxProtocol(result);
1112
+ if (!protocol.ready) {
1113
+ issue(
1114
+ receipt.issues,
1115
+ protocol.issueCode,
1116
+ `launches[${index}].isolation`,
1117
+ 'Measured Windows sandbox launcher did not return complete closed-protocol evidence.'
1118
+ );
1119
+ result = {
1120
+ status: null,
1121
+ signal: null,
1122
+ stdout: Buffer.alloc(0),
1123
+ stderr: Buffer.alloc(0),
1124
+ isolation: null,
1125
+ };
1126
+ } else {
1127
+ result = protocol;
1128
+ }
1129
+ }
1130
+ const stdout = logSummary(result?.stdout);
1131
+ const stderr = logSummary(
1132
+ adapter.requiresDiagnosticsDigest
1133
+ ? normalizedDiagnostics(result?.stderr, qemuCommand)
1134
+ : result?.stderr
1135
+ );
1136
+ const launch = {
1137
+ index: index + 1,
1138
+ exitCode: Number.isInteger(result?.status) ? result.status : null,
1139
+ signal: typeof result?.signal === 'string' ? result.signal : null,
1140
+ stdout,
1141
+ stderr,
1142
+ ...(adapter.hostSandboxed ? { isolation: result?.isolation || null } : {}),
1143
+ };
1144
+ receipt.launches.push(launch);
1145
+ if (result?.status === null || result?.status === undefined || result?.error) {
1146
+ issue(
1147
+ receipt.issues,
1148
+ 'vm-launch-execution-failed',
1149
+ `launches[${index}]`,
1150
+ 'Measured VM execution failed or exceeded its bound.'
1151
+ );
1152
+ } else if (result.status !== EXPECTED_EXIT_CODE) {
1153
+ issue(
1154
+ receipt.issues,
1155
+ 'vm-launch-exit-mismatch',
1156
+ `launches[${index}].exitCode`,
1157
+ 'Guest did not exit through the pinned debug-exit signal.'
1158
+ );
1159
+ }
1160
+ if (stdout.digest !== plan.guest.expectedConsoleDigest) {
1161
+ issue(
1162
+ receipt.issues,
1163
+ 'vm-launch-console-mismatch',
1164
+ `launches[${index}].stdout`,
1165
+ 'Guest serial output does not match the pinned success digest.'
1166
+ );
1167
+ }
1168
+ if (
1169
+ adapter.requiresDiagnosticsDigest &&
1170
+ stderr.digest !== plan.guest.expectedDiagnosticsDigest
1171
+ ) {
1172
+ issue(
1173
+ receipt.issues,
1174
+ 'vm-launch-diagnostics-mismatch',
1175
+ `launches[${index}].stderr`,
1176
+ 'Host emulator diagnostics do not match the pinned adapter digest; raw bytes are withheld.'
1177
+ );
1178
+ } else if (!adapter.requiresDiagnosticsDigest && stderr.bytes !== 0) {
1179
+ issue(
1180
+ receipt.issues,
1181
+ 'vm-launch-diagnostics-present',
1182
+ `launches[${index}].stderr`,
1183
+ 'Host emulator diagnostics were emitted; raw bytes are withheld.'
1184
+ );
1185
+ }
1186
+ if (
1187
+ !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1188
+ !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1189
+ ) {
1190
+ issue(
1191
+ receipt.issues,
1192
+ 'vm-launch-snapshot-drift',
1193
+ `launches[${index}].snapshot`,
1194
+ 'The private launch snapshot changed during measured execution.'
1195
+ );
1196
+ break;
1197
+ }
1198
+ }
1199
+ } finally {
1200
+ rmSync(snapshotRoot, { recursive: true, force: true });
1201
+ }
1202
+
1203
+ const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr, isolation }) =>
1204
+ hashJson({
1205
+ exitCode,
1206
+ signal,
1207
+ stdout,
1208
+ stderr,
1209
+ ...(adapter.hostSandboxed ? { isolation } : {}),
1210
+ })
1211
+ );
1212
+ if (launchSignatures.length === plan.launches && new Set(launchSignatures).size !== 1) {
1213
+ issue(
1214
+ receipt.issues,
1215
+ 'vm-launch-nondeterministic',
1216
+ 'launches',
1217
+ 'Clean measured launches produced different observable results.'
1218
+ );
1219
+ }
1220
+ receipt.deterministic =
1221
+ receipt.issues.length === 0 &&
1222
+ launchSignatures.length === plan.launches &&
1223
+ new Set(launchSignatures).size === 1;
1224
+ if (receipt.deterministic) {
1225
+ receipt.status = 'verified';
1226
+ receipt.verified = true;
1227
+ receipt.hardwareBacked = adapter.hardwareBacked;
1228
+ receipt.acceleration.verified = adapter.hardwareBacked;
1229
+ if (adapter.hostSandboxed) {
1230
+ receipt.isolation = {
1231
+ ...receipt.isolation,
1232
+ verified: true,
1233
+ controls: { ...receipt.launches[0].isolation },
1234
+ };
1235
+ }
1236
+ receipt.coverage = {
1237
+ includedLayers: [
1238
+ 'guest-artifact-measurement',
1239
+ ...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
1240
+ ...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
1241
+ 'machine-vm-launch',
1242
+ 'virtual-device-minimization',
1243
+ ],
1244
+ missingLayers: [
1245
+ ...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
1246
+ ...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
1247
+ ...(adapter.hostSandboxed
1248
+ ? ['host-filesystem-confidentiality', 'host-network-isolation']
1249
+ : []),
1250
+ ],
1251
+ };
1252
+ }
1253
+ return finishReceipt(receipt);
1254
+ }
1255
+
1256
+ export function runVmLaunch(options = {}) {
1257
+ return runVmLaunchWithProcessRunner(options, spawnSync, TCG_ADAPTER);
1258
+ }
1259
+
1260
+ export function runWhpxVmLaunch(options = {}) {
1261
+ return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_ADAPTER);
1262
+ }
1263
+
1264
+ export function runWhpxSandboxedVmLaunch(options = {}) {
1265
+ return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
1266
+ }
1267
+
1268
+ // Repository tests need a deterministic process boundary without publishing an
1269
+ // injectable executor through the package root export.
1270
+ export function runVmLaunchWithProcessRunnerForTest(options = {}) {
1271
+ if (typeof options.processRunner !== 'function') {
1272
+ throw new TypeError('processRunner test adapter is required.');
1273
+ }
1274
+ const { processRunner, ...launchOptions } = options;
1275
+ return runVmLaunchWithProcessRunner(launchOptions, processRunner, TCG_ADAPTER);
1276
+ }
1277
+
1278
+ export function runWhpxVmLaunchWithProcessRunnerForTest(options = {}) {
1279
+ if (typeof options.processRunner !== 'function') {
1280
+ throw new TypeError('processRunner test adapter is required.');
1281
+ }
1282
+ const { processRunner, ...launchOptions } = options;
1283
+ return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_ADAPTER);
1284
+ }
1285
+
1286
+ export function runWhpxSandboxedVmLaunchWithProcessRunnerForTest(options = {}) {
1287
+ if (typeof options.processRunner !== 'function') {
1288
+ throw new TypeError('processRunner test adapter is required.');
1289
+ }
1290
+ const { processRunner, ...launchOptions } = options;
1291
+ return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_SANDBOXED_ADAPTER);
1292
+ }