@holoscript/holosystem 0.2.4 → 0.2.6

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
@@ -8,6 +8,7 @@ import {
8
8
  readdirSync,
9
9
  readFileSync,
10
10
  rmSync,
11
+ writeFileSync,
11
12
  } from 'node:fs';
12
13
  import { tmpdir } from 'node:os';
13
14
  import { dirname, join, relative, resolve, sep } from 'node:path';
@@ -24,10 +25,22 @@ export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA =
24
25
  'holoscript.holosystem.whpx-sandboxed-vm-launch-plan.v1';
25
26
  export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA =
26
27
  'holoscript.holosystem.whpx-sandboxed-vm-launch-receipt.v1';
28
+ export const HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA =
29
+ 'holoscript.holosystem.whpx-appcontainer-vm-launch-plan.v1';
30
+ export const HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA =
31
+ 'holoscript.holosystem.whpx-appcontainer-vm-launch-receipt.v1';
32
+ export const HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA =
33
+ 'holoscript.holosystem.appcontainer-vm-launch-plan.v1';
34
+ export const HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA =
35
+ 'holoscript.holosystem.appcontainer-vm-launch-receipt.v1';
27
36
  export const HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL =
28
37
  'holoscript.holosystem.windows-sandbox-launch.v1';
38
+ export const HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL =
39
+ 'holoscript.holosystem.windows-appcontainer-launch.v1';
29
40
  export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA =
30
41
  'holoscript.holosystem.windows-sandbox-launcher.v1';
42
+ export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA =
43
+ 'holoscript.holosystem.windows-appcontainer-canary.v1';
31
44
 
32
45
  const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
33
46
  const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
@@ -39,15 +52,23 @@ const MAX_INITRD_BYTES = 512 * 1024 * 1024;
39
52
  const MAX_CONSOLE_BYTES = 1024 * 1024;
40
53
  const EXPECTED_EXIT_CODE = 33;
41
54
  const SANDBOX_LAUNCHER_BINARY = 'holosystem-sandbox-launcher.exe';
55
+ const APPCONTAINER_CANARY_BINARY = 'holosystem-appcontainer-canary.exe';
42
56
  const SANDBOX_KIND = 'windows-low-integrity-job-v1';
57
+ const APPCONTAINER_KIND = 'windows-appcontainer-deny-v1';
43
58
  const SANDBOX_PROCESS_MEMORY_BYTES = 512 * 1024 * 1024;
44
59
  const SANDBOX_PROTOCOL_BYTES = 3 * 1024 * 1024;
45
60
  const SANDBOX_LAUNCHER_PATH = fileURLToPath(
46
61
  new URL(`../native/windows-x64/${SANDBOX_LAUNCHER_BINARY}`, import.meta.url)
47
62
  );
63
+ const APPCONTAINER_CANARY_PATH = fileURLToPath(
64
+ new URL(`../native/windows-x64/${APPCONTAINER_CANARY_BINARY}`, import.meta.url)
65
+ );
48
66
  export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST = hashBytes(
49
67
  readFileSync(SANDBOX_LAUNCHER_PATH)
50
68
  );
69
+ export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST = hashBytes(
70
+ readFileSync(APPCONTAINER_CANARY_PATH)
71
+ );
51
72
  const TCG_POLICY = Object.freeze({
52
73
  accelerator: 'tcg',
53
74
  userConfiguration: 'disabled',
@@ -81,6 +102,27 @@ const WHPX_SANDBOXED_POLICY = Object.freeze({
81
102
  job: 'pre-resume-kill-on-close-active-process-memory-ui',
82
103
  writableTemp: 'low-integrity-private-snapshot',
83
104
  });
105
+ const WHPX_APPCONTAINER_POLICY = Object.freeze({
106
+ ...WHPX_POLICY,
107
+ hostProcess: APPCONTAINER_KIND,
108
+ token: 'disable-max-privilege-appcontainer-low-integrity',
109
+ capabilities: 'none',
110
+ filesystem: 'snapshot-read-execute-writable-temp-modify',
111
+ network: 'appcontainer-default-deny',
112
+ canaries: 'caller-readable-sentinel-and-loopback-listener',
113
+ job: 'pre-resume-kill-on-close-active-process-memory-ui',
114
+ });
115
+ const APPCONTAINER_TCG_POLICY = Object.freeze({
116
+ ...TCG_POLICY,
117
+ network: 'appcontainer-default-deny',
118
+ hostProcess: APPCONTAINER_KIND,
119
+ token: 'disable-max-privilege-appcontainer-low-integrity',
120
+ capabilities: 'none',
121
+ filesystem: 'snapshot-read-execute-writable-temp-modify',
122
+ canaries: 'caller-readable-sentinel-and-loopback-listener',
123
+ job: 'pre-resume-kill-on-close-active-process-memory-ui',
124
+ translationBufferMiB: 64,
125
+ });
84
126
  const BOUNDARIES = Object.freeze([
85
127
  'guest-artifact-provenance',
86
128
  'hardware-hypervisor-acceleration',
@@ -119,6 +161,44 @@ const WHPX_SANDBOXED_ADAPTER = Object.freeze({
119
161
  policy: WHPX_SANDBOXED_POLICY,
120
162
  requiresDiagnosticsDigest: true,
121
163
  hostSandboxed: true,
164
+ sandboxKind: SANDBOX_KIND,
165
+ sandboxProtocol: HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL,
166
+ isolationScope: 'integrity-privilege-lifetime-resource-ui',
167
+ filesystemConfidentiality: false,
168
+ networkIsolation: false,
169
+ appContainer: false,
170
+ });
171
+ const WHPX_APPCONTAINER_ADAPTER = Object.freeze({
172
+ accelerator: 'whpx',
173
+ accelerationName: 'qemu-whpx',
174
+ hardwareBacked: true,
175
+ planSchema: HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
176
+ receiptSchema: HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
177
+ policy: WHPX_APPCONTAINER_POLICY,
178
+ requiresDiagnosticsDigest: true,
179
+ hostSandboxed: true,
180
+ sandboxKind: APPCONTAINER_KIND,
181
+ sandboxProtocol: HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL,
182
+ isolationScope: 'appcontainer-zero-capability-snapshot-grant',
183
+ filesystemConfidentiality: true,
184
+ networkIsolation: true,
185
+ appContainer: true,
186
+ });
187
+ const APPCONTAINER_TCG_ADAPTER = Object.freeze({
188
+ accelerator: 'tcg',
189
+ accelerationName: 'qemu-tcg',
190
+ hardwareBacked: false,
191
+ planSchema: HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
192
+ receiptSchema: HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
193
+ policy: APPCONTAINER_TCG_POLICY,
194
+ requiresDiagnosticsDigest: false,
195
+ hostSandboxed: true,
196
+ sandboxKind: APPCONTAINER_KIND,
197
+ sandboxProtocol: HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL,
198
+ isolationScope: 'appcontainer-zero-capability-snapshot-grant',
199
+ filesystemConfidentiality: true,
200
+ networkIsolation: true,
201
+ appContainer: true,
122
202
  });
123
203
 
124
204
  function isRecord(value) {
@@ -331,16 +411,24 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
331
411
  }
332
412
 
333
413
  if (adapter.hostSandboxed) {
334
- knownKeys(plan.sandbox, new Set(['kind', 'launcherDigest']), 'sandbox', issues);
335
- if (plan.sandbox?.kind !== SANDBOX_KIND) {
414
+ knownKeys(
415
+ plan.sandbox,
416
+ new Set(['kind', 'launcherDigest', ...(adapter.appContainer ? ['canaryDigest'] : [])]),
417
+ 'sandbox',
418
+ issues
419
+ );
420
+ if (plan.sandbox?.kind !== adapter.sandboxKind) {
336
421
  issue(
337
422
  issues,
338
423
  'vm-launch-sandbox-unsupported',
339
424
  'sandbox.kind',
340
- `This adapter requires ${SANDBOX_KIND}.`
425
+ `This adapter requires ${adapter.sandboxKind}.`
341
426
  );
342
427
  }
343
428
  inspectDigest(plan.sandbox?.launcherDigest, 'sandbox.launcherDigest', issues);
429
+ if (adapter.appContainer) {
430
+ inspectDigest(plan.sandbox?.canaryDigest, 'sandbox.canaryDigest', issues);
431
+ }
344
432
  }
345
433
 
346
434
  return { ready: issues.length === 0, issues };
@@ -358,6 +446,14 @@ export function inspectWhpxSandboxedVmLaunchPlan(plan) {
358
446
  return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
359
447
  }
360
448
 
449
+ export function inspectWhpxAppContainerVmLaunchPlan(plan) {
450
+ return inspectVmLaunchPlanForAdapter(plan, WHPX_APPCONTAINER_ADAPTER);
451
+ }
452
+
453
+ export function inspectAppContainerVmLaunchPlan(plan) {
454
+ return inspectVmLaunchPlanForAdapter(plan, APPCONTAINER_TCG_ADAPTER);
455
+ }
456
+
361
457
  function scanRuntime(directory, issues) {
362
458
  const files = [];
363
459
  let totalBytes = 0;
@@ -583,6 +679,40 @@ export function inspectWindowsVmSandboxLauncher() {
583
679
  };
584
680
  }
585
681
 
682
+ export function inspectWindowsVmAppContainerCanary() {
683
+ const issues = [];
684
+ let content = null;
685
+ try {
686
+ const stats = lstatSync(APPCONTAINER_CANARY_PATH);
687
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
688
+ content = readFileSync(APPCONTAINER_CANARY_PATH);
689
+ } catch {
690
+ issue(
691
+ issues,
692
+ 'vm-launch-appcontainer-canary-invalid',
693
+ 'sandbox.canary',
694
+ 'The packaged Windows AppContainer canary must be a readable regular file.'
695
+ );
696
+ }
697
+ const digest = content ? hashBytes(content) : null;
698
+ if (digest && digest !== HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST) {
699
+ issue(
700
+ issues,
701
+ 'vm-launch-appcontainer-canary-drift',
702
+ 'sandbox.canaryDigest',
703
+ 'The packaged Windows AppContainer canary changed after module initialization.'
704
+ );
705
+ }
706
+ return {
707
+ schema: HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA,
708
+ ready: issues.length === 0,
709
+ kind: APPCONTAINER_KIND,
710
+ digest: issues.length === 0 ? digest : null,
711
+ bytes: issues.length === 0 ? content.length : null,
712
+ issues,
713
+ };
714
+ }
715
+
586
716
  function logSummary(value) {
587
717
  const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
588
718
  return { bytes: bytes.length, digest: hashBytes(bytes) };
@@ -628,26 +758,20 @@ function baseReceipt(plan, now, issues, adapter) {
628
758
  },
629
759
  isolation: adapter.hostSandboxed
630
760
  ? {
631
- hostProcess: SANDBOX_KIND,
632
- scope: 'integrity-privilege-lifetime-resource-ui',
761
+ hostProcess: adapter.sandboxKind,
762
+ scope: adapter.isolationScope,
633
763
  verified: false,
634
764
  launcherDigest: DIGEST_PATTERN.test(plan?.sandbox?.launcherDigest || '')
635
765
  ? plan.sandbox.launcherDigest
636
766
  : 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
- },
767
+ ...(adapter.appContainer
768
+ ? {
769
+ canaryDigest: DIGEST_PATTERN.test(plan?.sandbox?.canaryDigest || '')
770
+ ? plan.sandbox.canaryDigest
771
+ : null,
772
+ }
773
+ : {}),
774
+ controls: emptyIsolationControls(adapter),
651
775
  }
652
776
  : {
653
777
  hostProcess: 'ambient-windows-process',
@@ -709,21 +833,24 @@ function baseReceipt(plan, now, issues, adapter) {
709
833
  !(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration') &&
710
834
  !(adapter.hostSandboxed && layer === 'host-process-isolation')
711
835
  ),
712
- ...(adapter.hostSandboxed
713
- ? ['host-filesystem-confidentiality', 'host-network-isolation']
836
+ ...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
837
+ ? ['host-filesystem-confidentiality']
714
838
  : []),
839
+ ...(adapter.hostSandboxed && !adapter.networkIsolation ? ['host-network-isolation'] : []),
715
840
  ],
716
841
  issues,
717
842
  };
718
843
  }
719
844
 
720
845
  function qemuArguments(plan, kernelPath, initrdPath, adapter) {
846
+ const appContainerTcg = adapter === APPCONTAINER_TCG_ADAPTER;
721
847
  return [
722
848
  '-no-user-config',
723
849
  '-nodefaults',
724
850
  '-machine',
725
- `q35,accel=${adapter.accelerator},usb=off`,
726
- ...(adapter === TCG_ADAPTER ? ['-cpu', 'max'] : []),
851
+ appContainerTcg ? 'q35,usb=off' : `q35,accel=${adapter.accelerator},usb=off`,
852
+ ...(appContainerTcg ? ['-accel', 'tcg,tb-size=64'] : []),
853
+ ...([TCG_ADAPTER, APPCONTAINER_TCG_ADAPTER].includes(adapter) ? ['-cpu', 'max'] : []),
727
854
  '-m',
728
855
  `${plan.resources.memoryMiB}M`,
729
856
  '-smp',
@@ -749,17 +876,29 @@ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
749
876
  }
750
877
 
751
878
  function minimalEnvironment(snapshotRoot, runtimeDirectory, temporaryDirectory = snapshotRoot) {
879
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR;
880
+ const systemDrive = process.env.SystemDrive || systemRoot?.slice(0, 2);
752
881
  const env = {
882
+ APPDATA: temporaryDirectory,
753
883
  HOME: snapshotRoot,
884
+ HOMEDRIVE: systemDrive,
885
+ HOMEPATH: '\\',
886
+ LOCALAPPDATA: temporaryDirectory,
887
+ OS: 'Windows_NT',
888
+ Path: runtimeDirectory,
889
+ PATHEXT: '.COM;.EXE;.BAT;.CMD',
754
890
  USERPROFILE: snapshotRoot,
755
891
  TEMP: temporaryDirectory,
756
892
  TMP: temporaryDirectory,
757
- PATH: runtimeDirectory,
758
893
  LANG: 'C',
759
894
  LC_ALL: 'C',
760
895
  };
761
- if (typeof process.env.SystemRoot === 'string') env.SystemRoot = process.env.SystemRoot;
762
- if (typeof process.env.WINDIR === 'string') env.WINDIR = process.env.WINDIR;
896
+ if (systemDrive) env.SystemDrive = systemDrive;
897
+ if (systemRoot) {
898
+ env.SystemRoot = systemRoot;
899
+ env.windir = systemRoot;
900
+ env.ComSpec = join(systemRoot, 'System32', 'cmd.exe');
901
+ }
763
902
  return env;
764
903
  }
765
904
 
@@ -777,6 +916,74 @@ const SANDBOX_CONTROL_KEYS = Object.freeze([
777
916
  'uiRestrictions',
778
917
  'writableTempLowIntegrity',
779
918
  ]);
919
+ const APPCONTAINER_CONTROL_KEYS = Object.freeze([
920
+ 'filteredToken',
921
+ 'disableMaxPrivilege',
922
+ 'enabledPrivilegeCount',
923
+ 'privilegesBounded',
924
+ 'lowIntegrity',
925
+ 'assignedBeforeResume',
926
+ 'handleAllowlist',
927
+ 'killOnClose',
928
+ 'activeProcessLimit',
929
+ 'processMemoryLimit',
930
+ 'uiRestrictions',
931
+ 'appContainer',
932
+ 'appContainerSidMatched',
933
+ 'capabilityCount',
934
+ 'snapshotReadExecuteGrant',
935
+ 'writableTempModifyGrant',
936
+ 'filesystemCanaryDenied',
937
+ 'filesystemCanaryError',
938
+ 'networkCanaryDenied',
939
+ 'networkCanaryError',
940
+ 'loopbackAccepted',
941
+ 'profileDeleted',
942
+ ]);
943
+
944
+ function isolationControlKeys(adapter) {
945
+ return adapter.appContainer ? APPCONTAINER_CONTROL_KEYS : SANDBOX_CONTROL_KEYS;
946
+ }
947
+
948
+ function emptyIsolationControls(adapter) {
949
+ return Object.fromEntries(
950
+ isolationControlKeys(adapter).map((key) => [
951
+ key,
952
+ ['enabledPrivilegeCount', 'capabilityCount', 'filesystemCanaryError', 'networkCanaryError'].includes(
953
+ key
954
+ )
955
+ ? null
956
+ : false,
957
+ ])
958
+ );
959
+ }
960
+
961
+ function isolationControlsReady(controls, adapter) {
962
+ if (
963
+ !Number.isInteger(controls.enabledPrivilegeCount) ||
964
+ controls.enabledPrivilegeCount < 0 ||
965
+ controls.enabledPrivilegeCount > 1
966
+ ) {
967
+ return false;
968
+ }
969
+ if (!adapter.appContainer) {
970
+ return SANDBOX_CONTROL_KEYS.every(
971
+ (key) => key === 'enabledPrivilegeCount' || controls[key] === true
972
+ );
973
+ }
974
+ return (
975
+ APPCONTAINER_CONTROL_KEYS.every(
976
+ (key) =>
977
+ ['enabledPrivilegeCount', 'capabilityCount', 'filesystemCanaryError', 'networkCanaryError'].includes(
978
+ key
979
+ ) ||
980
+ (key === 'loopbackAccepted' ? controls[key] === false : controls[key] === true)
981
+ ) &&
982
+ controls.capabilityCount === 0 &&
983
+ controls.filesystemCanaryError === 5 &&
984
+ [10013, 10060].includes(controls.networkCanaryError)
985
+ );
986
+ }
780
987
 
781
988
  function canonicalBase64(value) {
782
989
  if (
@@ -790,7 +997,7 @@ function canonicalBase64(value) {
790
997
  return bytes.toString('base64') === value && bytes.length <= MAX_CONSOLE_BYTES ? bytes : null;
791
998
  }
792
999
 
793
- function parseSandboxProtocol(result) {
1000
+ function parseSandboxProtocol(result, adapter) {
794
1001
  if (
795
1002
  result?.status !== 0 ||
796
1003
  result?.signal ||
@@ -810,7 +1017,7 @@ function parseSandboxProtocol(result) {
810
1017
  }
811
1018
  if (
812
1019
  !isRecord(message) ||
813
- message.protocol !== HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL ||
1020
+ message.protocol !== adapter.sandboxProtocol ||
814
1021
  !isRecord(message.isolation) ||
815
1022
  Object.keys(message).some(
816
1023
  (key) =>
@@ -826,19 +1033,14 @@ function parseSandboxProtocol(result) {
826
1033
  'errorCode',
827
1034
  ].includes(key)
828
1035
  ) ||
829
- Object.keys(message.isolation).some((key) => !SANDBOX_CONTROL_KEYS.includes(key))
1036
+ Object.keys(message.isolation).some((key) => !isolationControlKeys(adapter).includes(key)) ||
1037
+ Object.keys(message.isolation).length !== isolationControlKeys(adapter).length
830
1038
  ) {
831
1039
  return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
832
1040
  }
833
1041
  const stdout = canonicalBase64(message.stdoutBase64);
834
1042
  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;
1043
+ const controlsReady = isolationControlsReady(message.isolation, adapter);
842
1044
  if (
843
1045
  message.launched !== true ||
844
1046
  message.timedOut !== false ||
@@ -857,7 +1059,9 @@ function parseSandboxProtocol(result) {
857
1059
  signal: null,
858
1060
  stdout,
859
1061
  stderr,
860
- isolation: Object.fromEntries(SANDBOX_CONTROL_KEYS.map((key) => [key, message.isolation[key]])),
1062
+ isolation: Object.fromEntries(
1063
+ isolationControlKeys(adapter).map((key) => [key, message.isolation[key]])
1064
+ ),
861
1065
  };
862
1066
  }
863
1067
 
@@ -885,14 +1089,19 @@ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
885
1089
  );
886
1090
  }
887
1091
 
888
- function sandboxLauncherSnapshotMatches(plan, launcherPath, adapter) {
1092
+ function sandboxArtifactsSnapshotMatch(plan, launcherPath, canaryPath, adapter) {
889
1093
  if (!adapter.hostSandboxed) return true;
890
1094
  try {
891
1095
  const stats = lstatSync(launcherPath);
1096
+ const canaryStats = adapter.appContainer ? lstatSync(canaryPath) : null;
892
1097
  return (
893
1098
  stats.isFile() &&
894
1099
  !stats.isSymbolicLink() &&
895
- hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest
1100
+ hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest &&
1101
+ (!adapter.appContainer ||
1102
+ (canaryStats.isFile() &&
1103
+ !canaryStats.isSymbolicLink() &&
1104
+ hashBytes(readFileSync(canaryPath)) === plan.sandbox.canaryDigest))
896
1105
  );
897
1106
  } catch {
898
1107
  return false;
@@ -912,11 +1121,13 @@ function runVmLaunchWithProcessRunner(
912
1121
  const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
913
1122
  const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
914
1123
  const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
1124
+ const appContainerCanary = adapter.appContainer ? inspectWindowsVmAppContainerCanary() : null;
915
1125
  receipt.issues.push(
916
1126
  ...executor.issues,
917
1127
  ...kernel.issues,
918
1128
  ...initrd.issues,
919
- ...(sandboxLauncher?.issues || [])
1129
+ ...(sandboxLauncher?.issues || []),
1130
+ ...(appContainerCanary?.issues || [])
920
1131
  );
921
1132
  if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
922
1133
  issue(
@@ -962,6 +1173,18 @@ function runVmLaunchWithProcessRunner(
962
1173
  'Packaged Windows sandbox launcher does not match the pinned plan digest.'
963
1174
  );
964
1175
  }
1176
+ if (
1177
+ adapter.appContainer &&
1178
+ appContainerCanary?.ready &&
1179
+ appContainerCanary.digest !== plan.sandbox.canaryDigest
1180
+ ) {
1181
+ issue(
1182
+ receipt.issues,
1183
+ 'vm-launch-appcontainer-canary-mismatch',
1184
+ 'sandbox.canaryDigest',
1185
+ 'Packaged Windows AppContainer canary does not match the pinned plan digest.'
1186
+ );
1187
+ }
965
1188
  if (receipt.issues.length > 0) return finishReceipt(receipt);
966
1189
 
967
1190
  const sourceRuntime = resolve(executorDirectory);
@@ -972,7 +1195,12 @@ function runVmLaunchWithProcessRunner(
972
1195
  const snapshotKernel = join(snapshotRoot, 'vmlinuz');
973
1196
  const snapshotInitrd = join(snapshotRoot, 'initramfs');
974
1197
  const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
1198
+ const snapshotCanary = join(snapshotRoot, APPCONTAINER_CANARY_BINARY);
975
1199
  const snapshotTemporary = join(snapshotRoot, 'sandbox-temp');
1200
+ const protectedRoot = adapter.appContainer
1201
+ ? mkdtempSync(join(tmpdir(), 'holosystem-vm-protected-'))
1202
+ : null;
1203
+ const protectedSentinel = protectedRoot ? join(protectedRoot, 'caller-readable.bin') : null;
976
1204
 
977
1205
  try {
978
1206
  try {
@@ -983,6 +1211,10 @@ function runVmLaunchWithProcessRunner(
983
1211
  copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
984
1212
  mkdirSync(snapshotTemporary);
985
1213
  }
1214
+ if (adapter.appContainer) copyFileSync(APPCONTAINER_CANARY_PATH, snapshotCanary);
1215
+ if (adapter.appContainer) {
1216
+ writeFileSync(protectedSentinel, Buffer.from('holosystem-appcontainer-read-canary-v1'));
1217
+ }
986
1218
  } catch {
987
1219
  issue(
988
1220
  receipt.issues,
@@ -997,6 +1229,7 @@ function runVmLaunchWithProcessRunner(
997
1229
  const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
998
1230
  const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
999
1231
  let pinnedLauncherDigest = null;
1232
+ let pinnedCanaryDigest = null;
1000
1233
  if (adapter.hostSandboxed) {
1001
1234
  try {
1002
1235
  const stats = lstatSync(snapshotLauncher);
@@ -1006,6 +1239,15 @@ function runVmLaunchWithProcessRunner(
1006
1239
  pinnedLauncherDigest = null;
1007
1240
  }
1008
1241
  }
1242
+ if (adapter.appContainer) {
1243
+ try {
1244
+ const stats = lstatSync(snapshotCanary);
1245
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
1246
+ pinnedCanaryDigest = hashBytes(readFileSync(snapshotCanary));
1247
+ } catch {
1248
+ pinnedCanaryDigest = null;
1249
+ }
1250
+ }
1009
1251
  if (
1010
1252
  !pinnedExecutor.ready ||
1011
1253
  pinnedExecutor.digest !== plan.executor.runtimeDigest ||
@@ -1042,6 +1284,14 @@ function runVmLaunchWithProcessRunner(
1042
1284
  'Windows sandbox launcher changed while its private snapshot was materialized.'
1043
1285
  );
1044
1286
  }
1287
+ if (adapter.appContainer && pinnedCanaryDigest !== plan.sandbox.canaryDigest) {
1288
+ issue(
1289
+ receipt.issues,
1290
+ 'vm-launch-appcontainer-canary-snapshot-mismatch',
1291
+ 'sandbox.canaryDigest',
1292
+ 'Windows AppContainer canary changed while its private snapshot was materialized.'
1293
+ );
1294
+ }
1045
1295
  if (receipt.issues.length > 0) return finishReceipt(receipt);
1046
1296
 
1047
1297
  receipt.measurementDigest = hashJson({
@@ -1051,6 +1301,7 @@ function runVmLaunchWithProcessRunner(
1051
1301
  kernelDigest: pinnedKernel.digest,
1052
1302
  initrdDigest: pinnedInitrd.digest,
1053
1303
  ...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
1304
+ ...(adapter.appContainer ? { appContainerCanaryDigest: pinnedCanaryDigest } : {}),
1054
1305
  policy: adapter.policy,
1055
1306
  });
1056
1307
 
@@ -1071,6 +1322,16 @@ function runVmLaunchWithProcessRunner(
1071
1322
  String(plan.resources.timeoutSeconds * 1000),
1072
1323
  '--process-memory-bytes',
1073
1324
  String(SANDBOX_PROCESS_MEMORY_BYTES),
1325
+ ...(adapter.appContainer
1326
+ ? [
1327
+ '--appcontainer-deny',
1328
+ 'v1',
1329
+ '--protected-sentinel',
1330
+ protectedSentinel,
1331
+ '--canary-executable',
1332
+ snapshotCanary,
1333
+ ]
1334
+ : []),
1074
1335
  '--',
1075
1336
  ...qemuArgs,
1076
1337
  ]
@@ -1091,7 +1352,7 @@ function runVmLaunchWithProcessRunner(
1091
1352
  for (let index = 0; index < plan.launches; index += 1) {
1092
1353
  if (
1093
1354
  !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1094
- !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1355
+ !sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
1095
1356
  ) {
1096
1357
  issue(
1097
1358
  receipt.issues,
@@ -1108,7 +1369,7 @@ function runVmLaunchWithProcessRunner(
1108
1369
  result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
1109
1370
  }
1110
1371
  if (adapter.hostSandboxed) {
1111
- const protocol = parseSandboxProtocol(result);
1372
+ const protocol = parseSandboxProtocol(result, adapter);
1112
1373
  if (!protocol.ready) {
1113
1374
  issue(
1114
1375
  receipt.issues,
@@ -1185,7 +1446,7 @@ function runVmLaunchWithProcessRunner(
1185
1446
  }
1186
1447
  if (
1187
1448
  !launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
1188
- !sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
1449
+ !sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
1189
1450
  ) {
1190
1451
  issue(
1191
1452
  receipt.issues,
@@ -1198,6 +1459,7 @@ function runVmLaunchWithProcessRunner(
1198
1459
  }
1199
1460
  } finally {
1200
1461
  rmSync(snapshotRoot, { recursive: true, force: true });
1462
+ if (protectedRoot) rmSync(protectedRoot, { recursive: true, force: true });
1201
1463
  }
1202
1464
 
1203
1465
  const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr, isolation }) =>
@@ -1238,14 +1500,19 @@ function runVmLaunchWithProcessRunner(
1238
1500
  'guest-artifact-measurement',
1239
1501
  ...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
1240
1502
  ...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
1503
+ ...(adapter.filesystemConfidentiality ? ['host-filesystem-confidentiality'] : []),
1504
+ ...(adapter.networkIsolation ? ['host-network-isolation'] : []),
1241
1505
  'machine-vm-launch',
1242
1506
  'virtual-device-minimization',
1243
1507
  ],
1244
1508
  missingLayers: [
1245
1509
  ...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
1246
1510
  ...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
1247
- ...(adapter.hostSandboxed
1248
- ? ['host-filesystem-confidentiality', 'host-network-isolation']
1511
+ ...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
1512
+ ? ['host-filesystem-confidentiality']
1513
+ : []),
1514
+ ...(adapter.hostSandboxed && !adapter.networkIsolation
1515
+ ? ['host-network-isolation']
1249
1516
  : []),
1250
1517
  ],
1251
1518
  };
@@ -1265,6 +1532,14 @@ export function runWhpxSandboxedVmLaunch(options = {}) {
1265
1532
  return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
1266
1533
  }
1267
1534
 
1535
+ export function runWhpxAppContainerVmLaunch(options = {}) {
1536
+ return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_APPCONTAINER_ADAPTER);
1537
+ }
1538
+
1539
+ export function runAppContainerVmLaunch(options = {}) {
1540
+ return runVmLaunchWithProcessRunner(options, spawnSync, APPCONTAINER_TCG_ADAPTER);
1541
+ }
1542
+
1268
1543
  // Repository tests need a deterministic process boundary without publishing an
1269
1544
  // injectable executor through the package root export.
1270
1545
  export function runVmLaunchWithProcessRunnerForTest(options = {}) {
@@ -1290,3 +1565,19 @@ export function runWhpxSandboxedVmLaunchWithProcessRunnerForTest(options = {}) {
1290
1565
  const { processRunner, ...launchOptions } = options;
1291
1566
  return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_SANDBOXED_ADAPTER);
1292
1567
  }
1568
+
1569
+ export function runWhpxAppContainerVmLaunchWithProcessRunnerForTest(options = {}) {
1570
+ if (typeof options.processRunner !== 'function') {
1571
+ throw new TypeError('processRunner test adapter is required.');
1572
+ }
1573
+ const { processRunner, ...launchOptions } = options;
1574
+ return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_APPCONTAINER_ADAPTER);
1575
+ }
1576
+
1577
+ export function runAppContainerVmLaunchWithProcessRunnerForTest(options = {}) {
1578
+ if (typeof options.processRunner !== 'function') {
1579
+ throw new TypeError('processRunner test adapter is required.');
1580
+ }
1581
+ const { processRunner, ...launchOptions } = options;
1582
+ return runVmLaunchWithProcessRunner(launchOptions, processRunner, APPCONTAINER_TCG_ADAPTER);
1583
+ }