@holoscript/holosystem 0.2.3 → 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.
package/src/vm-launch.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from 'node:fs';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { dirname, join, relative, resolve, sep } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
14
15
 
15
16
  export const HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.vm-launch-plan.v1';
16
17
  export const HOLOSYSTEM_VM_EXECUTOR_SCHEMA = 'holoscript.holosystem.vm-executor.v1';
@@ -19,6 +20,14 @@ export const HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA = 'holoscript.holosystem.vm-lau
19
20
  export const HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.whpx-vm-launch-plan.v1';
20
21
  export const HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA =
21
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';
22
31
 
23
32
  const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
24
33
  const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
@@ -29,6 +38,16 @@ const MAX_KERNEL_BYTES = 128 * 1024 * 1024;
29
38
  const MAX_INITRD_BYTES = 512 * 1024 * 1024;
30
39
  const MAX_CONSOLE_BYTES = 1024 * 1024;
31
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
+ );
32
51
  const TCG_POLICY = Object.freeze({
33
52
  accelerator: 'tcg',
34
53
  userConfiguration: 'disabled',
@@ -55,6 +74,13 @@ const WHPX_POLICY = Object.freeze({
55
74
  guestSignal: 'serial-digest-and-debug-exit',
56
75
  diagnostics: 'pinned-digest',
57
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
+ });
58
84
  const BOUNDARIES = Object.freeze([
59
85
  'guest-artifact-provenance',
60
86
  'hardware-hypervisor-acceleration',
@@ -82,6 +108,17 @@ const WHPX_ADAPTER = Object.freeze({
82
108
  receiptSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
83
109
  policy: WHPX_POLICY,
84
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,
85
122
  });
86
123
 
87
124
  function isRecord(value) {
@@ -169,6 +206,9 @@ function normalizedPlan(plan) {
169
206
  cpus: plan.resources.cpus,
170
207
  timeoutSeconds: plan.resources.timeoutSeconds,
171
208
  },
209
+ sandbox: plan.sandbox
210
+ ? { kind: plan.sandbox.kind, launcherDigest: plan.sandbox.launcherDigest }
211
+ : null,
172
212
  launches: plan.launches,
173
213
  };
174
214
  }
@@ -182,7 +222,17 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
182
222
 
183
223
  knownKeys(
184
224
  plan,
185
- new Set(['schema', 'id', 'host', 'executor', 'target', 'guest', 'resources', 'launches']),
225
+ new Set([
226
+ 'schema',
227
+ 'id',
228
+ 'host',
229
+ 'executor',
230
+ 'target',
231
+ 'guest',
232
+ 'resources',
233
+ ...(adapter.hostSandboxed ? ['sandbox'] : []),
234
+ 'launches',
235
+ ]),
186
236
  '',
187
237
  issues
188
238
  );
@@ -280,6 +330,19 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
280
330
  );
281
331
  }
282
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
+
283
346
  return { ready: issues.length === 0, issues };
284
347
  }
285
348
 
@@ -291,6 +354,10 @@ export function inspectWhpxVmLaunchPlan(plan) {
291
354
  return inspectVmLaunchPlanForAdapter(plan, WHPX_ADAPTER);
292
355
  }
293
356
 
357
+ export function inspectWhpxSandboxedVmLaunchPlan(plan) {
358
+ return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
359
+ }
360
+
294
361
  function scanRuntime(directory, issues) {
295
362
  const files = [];
296
363
  let totalBytes = 0;
@@ -482,6 +549,40 @@ export function inspectVmLaunchAsset({ assetPath, kind } = {}) {
482
549
  };
483
550
  }
484
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
+
485
586
  function logSummary(value) {
486
587
  const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
487
588
  return { bytes: bytes.length, digest: hashBytes(bytes) };
@@ -525,10 +626,33 @@ function baseReceipt(plan, now, issues, adapter) {
525
626
  evidence: adapter.hardwareBacked ? 'two-explicit-successful-launches' : 'software-emulation',
526
627
  verified: false,
527
628
  },
528
- isolation: {
529
- hostProcess: 'ambient-windows-process',
530
- verified: false,
531
- },
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
+ },
532
656
  measurementDigest: null,
533
657
  executor: {
534
658
  kind: plan?.executor?.kind === 'qemu-system' ? 'qemu-system' : null,
@@ -579,9 +703,16 @@ function baseReceipt(plan, now, issues, adapter) {
579
703
  'virtual-device-minimization',
580
704
  ],
581
705
  },
582
- boundaries: BOUNDARIES.filter(
583
- (layer) => !(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration')
584
- ),
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
+ ],
585
716
  issues,
586
717
  };
587
718
  }
@@ -617,12 +748,12 @@ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
617
748
  ];
618
749
  }
619
750
 
620
- function minimalEnvironment(snapshotRoot, runtimeDirectory) {
751
+ function minimalEnvironment(snapshotRoot, runtimeDirectory, temporaryDirectory = snapshotRoot) {
621
752
  const env = {
622
753
  HOME: snapshotRoot,
623
754
  USERPROFILE: snapshotRoot,
624
- TEMP: snapshotRoot,
625
- TMP: snapshotRoot,
755
+ TEMP: temporaryDirectory,
756
+ TMP: temporaryDirectory,
626
757
  PATH: runtimeDirectory,
627
758
  LANG: 'C',
628
759
  LC_ALL: 'C',
@@ -632,6 +763,104 @@ function minimalEnvironment(snapshotRoot, runtimeDirectory) {
632
763
  return env;
633
764
  }
634
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
+
635
864
  function copyRuntimeSnapshot(sourceDirectory, report, destinationDirectory) {
636
865
  mkdirSync(destinationDirectory, { recursive: true });
637
866
  for (const file of report.files) {
@@ -656,6 +885,20 @@ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
656
885
  );
657
886
  }
658
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
+
659
902
  function runVmLaunchWithProcessRunner(
660
903
  { plan, executorDirectory, kernelPath, initrdPath, now = new Date() } = {},
661
904
  processRunner,
@@ -668,7 +911,13 @@ function runVmLaunchWithProcessRunner(
668
911
  const executor = inspectVmExecutor({ executorDirectory });
669
912
  const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
670
913
  const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
671
- receipt.issues.push(...executor.issues, ...kernel.issues, ...initrd.issues);
914
+ const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
915
+ receipt.issues.push(
916
+ ...executor.issues,
917
+ ...kernel.issues,
918
+ ...initrd.issues,
919
+ ...(sandboxLauncher?.issues || [])
920
+ );
672
921
  if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
673
922
  issue(
674
923
  receipt.issues,
@@ -701,6 +950,18 @@ function runVmLaunchWithProcessRunner(
701
950
  'Guest initrd does not match the pinned plan digest.'
702
951
  );
703
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
+ }
704
965
  if (receipt.issues.length > 0) return finishReceipt(receipt);
705
966
 
706
967
  const sourceRuntime = resolve(executorDirectory);
@@ -710,12 +971,18 @@ function runVmLaunchWithProcessRunner(
710
971
  const snapshotRuntime = join(snapshotRoot, 'runtime');
711
972
  const snapshotKernel = join(snapshotRoot, 'vmlinuz');
712
973
  const snapshotInitrd = join(snapshotRoot, 'initramfs');
974
+ const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
975
+ const snapshotTemporary = join(snapshotRoot, 'sandbox-temp');
713
976
 
714
977
  try {
715
978
  try {
716
979
  copyRuntimeSnapshot(sourceRuntime, executor, snapshotRuntime);
717
980
  copyFileSync(sourceKernel, snapshotKernel);
718
981
  copyFileSync(sourceInitrd, snapshotInitrd);
982
+ if (adapter.hostSandboxed) {
983
+ copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
984
+ mkdirSync(snapshotTemporary);
985
+ }
719
986
  } catch {
720
987
  issue(
721
988
  receipt.issues,
@@ -729,6 +996,16 @@ function runVmLaunchWithProcessRunner(
729
996
  const pinnedExecutor = inspectVmExecutor({ executorDirectory: snapshotRuntime });
730
997
  const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
731
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
+ }
732
1009
  if (
733
1010
  !pinnedExecutor.ready ||
734
1011
  pinnedExecutor.digest !== plan.executor.runtimeDigest ||
@@ -757,6 +1034,14 @@ function runVmLaunchWithProcessRunner(
757
1034
  'Guest initrd changed while its private launch snapshot was materialized.'
758
1035
  );
759
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
+ }
760
1045
  if (receipt.issues.length > 0) return finishReceipt(receipt);
761
1046
 
762
1047
  receipt.measurementDigest = hashJson({
@@ -765,22 +1050,49 @@ function runVmLaunchWithProcessRunner(
765
1050
  binaryDigest: pinnedExecutor.binaryDigest,
766
1051
  kernelDigest: pinnedKernel.digest,
767
1052
  initrdDigest: pinnedInitrd.digest,
1053
+ ...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
768
1054
  policy: adapter.policy,
769
1055
  });
770
1056
 
771
- const command = join(snapshotRuntime, EXECUTOR_BINARY);
772
- const args = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
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;
773
1078
  const options = {
774
1079
  encoding: null,
775
- env: minimalEnvironment(snapshotRoot, snapshotRuntime),
776
- maxBuffer: MAX_CONSOLE_BYTES,
1080
+ env: minimalEnvironment(
1081
+ snapshotRoot,
1082
+ snapshotRuntime,
1083
+ adapter.hostSandboxed ? snapshotTemporary : snapshotRoot
1084
+ ),
1085
+ maxBuffer: adapter.hostSandboxed ? SANDBOX_PROTOCOL_BYTES : MAX_CONSOLE_BYTES,
777
1086
  shell: false,
778
- timeout: plan.resources.timeoutSeconds * 1000,
1087
+ timeout: plan.resources.timeoutSeconds * 1000 + (adapter.hostSandboxed ? 5000 : 0),
779
1088
  windowsHide: true,
780
1089
  };
781
1090
 
782
1091
  for (let index = 0; index < plan.launches; index += 1) {
783
- if (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
1092
+ if (
1093
+ !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1094
+ !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1095
+ ) {
784
1096
  issue(
785
1097
  receipt.issues,
786
1098
  'vm-launch-snapshot-drift',
@@ -795,10 +1107,30 @@ function runVmLaunchWithProcessRunner(
795
1107
  } catch {
796
1108
  result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
797
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
+ }
798
1130
  const stdout = logSummary(result?.stdout);
799
1131
  const stderr = logSummary(
800
1132
  adapter.requiresDiagnosticsDigest
801
- ? normalizedDiagnostics(result?.stderr, command)
1133
+ ? normalizedDiagnostics(result?.stderr, qemuCommand)
802
1134
  : result?.stderr
803
1135
  );
804
1136
  const launch = {
@@ -807,6 +1139,7 @@ function runVmLaunchWithProcessRunner(
807
1139
  signal: typeof result?.signal === 'string' ? result.signal : null,
808
1140
  stdout,
809
1141
  stderr,
1142
+ ...(adapter.hostSandboxed ? { isolation: result?.isolation || null } : {}),
810
1143
  };
811
1144
  receipt.launches.push(launch);
812
1145
  if (result?.status === null || result?.status === undefined || result?.error) {
@@ -850,7 +1183,10 @@ function runVmLaunchWithProcessRunner(
850
1183
  'Host emulator diagnostics were emitted; raw bytes are withheld.'
851
1184
  );
852
1185
  }
853
- if (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
1186
+ if (
1187
+ !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1188
+ !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1189
+ ) {
854
1190
  issue(
855
1191
  receipt.issues,
856
1192
  'vm-launch-snapshot-drift',
@@ -864,8 +1200,14 @@ function runVmLaunchWithProcessRunner(
864
1200
  rmSync(snapshotRoot, { recursive: true, force: true });
865
1201
  }
866
1202
 
867
- const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr }) =>
868
- hashJson({ exitCode, signal, stdout, stderr })
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
+ })
869
1211
  );
870
1212
  if (launchSignatures.length === plan.launches && new Set(launchSignatures).size !== 1) {
871
1213
  issue(
@@ -884,16 +1226,27 @@ function runVmLaunchWithProcessRunner(
884
1226
  receipt.verified = true;
885
1227
  receipt.hardwareBacked = adapter.hardwareBacked;
886
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
+ }
887
1236
  receipt.coverage = {
888
1237
  includedLayers: [
889
1238
  'guest-artifact-measurement',
890
1239
  ...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
1240
+ ...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
891
1241
  'machine-vm-launch',
892
1242
  'virtual-device-minimization',
893
1243
  ],
894
1244
  missingLayers: [
895
1245
  ...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
896
- 'host-process-isolation',
1246
+ ...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
1247
+ ...(adapter.hostSandboxed
1248
+ ? ['host-filesystem-confidentiality', 'host-network-isolation']
1249
+ : []),
897
1250
  ],
898
1251
  };
899
1252
  }
@@ -908,6 +1261,10 @@ export function runWhpxVmLaunch(options = {}) {
908
1261
  return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_ADAPTER);
909
1262
  }
910
1263
 
1264
+ export function runWhpxSandboxedVmLaunch(options = {}) {
1265
+ return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
1266
+ }
1267
+
911
1268
  // Repository tests need a deterministic process boundary without publishing an
912
1269
  // injectable executor through the package root export.
913
1270
  export function runVmLaunchWithProcessRunnerForTest(options = {}) {
@@ -925,3 +1282,11 @@ export function runWhpxVmLaunchWithProcessRunnerForTest(options = {}) {
925
1282
  const { processRunner, ...launchOptions } = options;
926
1283
  return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_ADAPTER);
927
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
+ }