chainlesschain 0.162.181 → 0.162.182

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/package.json +2 -1
  2. package/scripts/build-windows-sandbox-helper.ps1 +163 -0
  3. package/src/assets/web-panel/assets/{AIOps-cq0dwbnf.js → AIOps-DzZ5_xoj.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CvcgGl3w.js → ActionButton-DeLXddIg.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-hM2-jFqg.js → Analytics-rvDCmet1.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-B9_Gsd_g.js → AppLayout-Yt6QVDnp.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-B7bu52Og.js → Artifacts-BS112K5L.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DThfDgqB.js → Audit-0wJuQrNu.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DjlFiD4v.js → BackgroundAgents-B90PMgbn.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-CK9xJKnG.js → Backup-TkgMglQQ.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DEiSU8K8.js → BaseInput-CQVS9p1l.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CpT96czj.js → Chat-YTjQHoC7.js} +5 -5
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BKPPNmZI.js → ChatBubbleRenderer-PR074hUN.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-C1GfDnO0.js → Checkbox-5bwaOabq.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-3DAKgiLx.js → Codegen-BFmNQ_Xe.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-74cOwZU1.js → Col-BT_u4Dzp.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-tn9Kkz9j.js → Community-CJP52u5b.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-BcoXH7m4.js → Compact-BpzhJE9i.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-DIPGUBgi.js → Compliance-Cg7rfslp.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-doD5_91c.js → Cowork-JisxL6Yy.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-C-IDvubz.js → Cron-Bu7Ug_MC.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CdcCoBdj.js → Crosschain-CvuqoLdj.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-DdH5aMMA.js → DID-C2u9G8Pu.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-DHLA4mm0.js → Dashboard-BFt5Yv3d.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-dEeCt7Md.js → Dropdown-DIaT9Wxq.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-BMaecFv9.js → EmailListRenderer-D_A601Y4.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DUaUukTN.js → FamilyGuardDashboard--IyQ8I8w.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-CMBE0_rp.js → Federation-BihWHxuY.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-PG-JDbXq.js → FormItemContext-BB8dOXc_.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-Bcb0p88m.js → GenericCardRenderer-DQYhj1HH.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-Bgv15myB.js → Git-B-2QZxGf.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-DB8YTArI.js → Governance-CusSb-nG.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-BQMwCWaS.js → Inference-BAjmvPPL.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-B1_ZziiB.js → KnowledgeGraph-DoNYWKYq.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DovEEnt_.js → Logs-B8fKWD-n.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-D201OL4T.js → MarkdownRenderer-BlJm1J7E.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-BlCXSpyB.js → Marketplace-CyOscbSL.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-CsCR09Wj.js → McpTools-Ex6Om5da.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-CBVRLKno.js → Memory-CDcD8l8r.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-C9OIafgA.js → MobileBridge-xfBqoFDg.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-D_zoJ8Xs.js → MobileProjects-BwObMMA2.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-vE-HdvLj.js → Mtc-CIWd36OV.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-CPLaekCd.js → MtcAudit-CPX0ouuA.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-C_UP9C2o.js → Multisig-BDJsIUuW.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-2nODJcU1.js → NLProgramming-DEkbuD3N.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-BhE-SJGu.js → Notes-DJpHRhH1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-D21q7BNU.js → NotificationSettings-C1UZ-Qd5.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-0b62GYOD.js → OrderTableRenderer-GlJtqLjT.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-CEqmw_sb.js → Organization-Bbhj0fac.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-MW-gMDO_.js → Overflow-1H04x3kx.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CY0hjI_G.js → P2P-CDpPe3et.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-BbWh7Man.js → PdhVaultBrowser-MUX0Bpvw.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-ChqEA_4Q.js → Permissions-A07-0Ci1.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-B3YrbYfD.js → PersonalDataHub-DhhDWxAZ.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-B8WBhhlW.js → Pipeline-CyfbJfe-.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-Cmb0PXNx.js → Privacy-CNFps3ED.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-hF6zW3Nz.js → ProjectInit--T-u3Dr5.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-BFSVfpW0.js → ProjectSettings-DJmrK2fC.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-DAWkVFkc.js → Projects-Qx_MdX9k.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-B8yux8Dx.js → Providers-DLzzbOZd.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-N76cQluO.js → QrScannerModal-CexOomKS.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-ClvZ0KYR.js → QuickAsk-QklGi19_.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-B-GfgBpT.js → Recommend-Bb9kwSw8.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-mlglCdQi.js → RemoteSession-DZuVNNN5.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-DJIZzQeG.js → Reputation-Dr5cLP0N.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-COzxu_bb.js → Row-oCQHYv5t.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-CoYsJQuE.js → RssFeed-IFDEykIR.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-DC77Mq9S.js → Search-CspVIxDc.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-Loy2EdMO.js → Security-Bm66o_DH.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-GUD07Eu0.js → Services-DvanNqXl.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-DWzkptYU.js → Skeleton-CHjEe9_O.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-CEDn1r-Z.js → Skills-Cb8jWjMX.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-ub_k4pXh.js → Sla-DxZm1491.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-D7HLX9CA.js → SpeechSettings-DWxEEclC.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-DbwOvl6m.js → SyncSettings-FIvOFw5p.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-CBQzoAu5.js → Tasks-CyuLnDFR.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-BNVeIhud.js → Templates-CaNjzV_t.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-PS9AzhXU.js → Tenant-DjlP6TmH.js} +1 -1
  79. package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +3 -0
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CrmXMOkg.js → TimelineRenderer-DoZ8y7y8.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-DD-VDHw1.js → Tokens-390cg2TC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-BsoN_n-n.js → Trigger-CLrtCj5_.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-n8LmN4ZV.js → Trust-DpESfSPn.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-C9_7MImA.js → UkeySign-CN6279O0.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DyBteNgn.js → VideoEditing-DPtAUlSe.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-BS5ixeXI.js → Wallet-nKOMV_1n.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-spc4UImU.js → WebAuthn-HuXlSOc4.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-D7iVvaBO.js → WorkflowEditor-CUfFjARi.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-6CmBkHrL.js → chat-6Eq0PcZJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BGoCLkxI.js → colors-BP_Sf6Iy.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-fYZBetMO.js → compact-item-BufMKIh5.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-BeM-1UVj.js → createContext-BBMXxWSW.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-CE7ZuEnV.js → hasIn-fUK_tRO2.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-QIpnpZgz.js → index-2A5ph8b2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BU_2qiDk.js → index-3FuwBewR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index--lap6cf7.js → index-6GmBa4Kg.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-De0quFDw.js → index-B2vS66eI.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D1r9tT3J.js → index-B7Etk0Te.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-YjcumOZ4.js → index-B9WYSn4m.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-MtOuq2i6.js → index-BHy6yNiF.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-RMZ8a7Yt.js → index-BMLHv389.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-C0v6YKKO.js → index-BWKoBU8R.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CRnUE8Cb.js → index-Bkg4ZePE.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BsMfoaeo.js → index-C44atv4k.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DoXk5KJ8.js → index-CE2DdTP2.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B4wiTEOM.js → index-CGjmILDm.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-zb7rVOhV.js → index-CW_G-MXw.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-ATcSr_2O.js → index-CZ6Yt17L.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C9Y3k0HI.js → index-CeASi4fr.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-B2U-ObMq.js → index-CgiECHu2.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Puq6Luf1.js → index-Cp8vhyZy.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BV551878.js → index-CwSf5pnf.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BowH1eLQ.js → index-D4_MuGkd.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B3CDgvNW.js → index-D9vU25ij.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-WUrJ6S1j.js → index-DASluSqY.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CRLjEP1H.js → index-DLpIs_P_.js} +1 -1
  118. package/src/assets/web-panel/assets/index-DOZTSoS7.js +1 -0
  119. package/src/assets/web-panel/assets/{index-DT3zPSVX.js → index-DPdUNk1i.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BunaQCa0.js → index-DVsqvI4M.js} +1 -1
  121. package/src/assets/web-panel/assets/index-DhpvjpqI.js +1 -0
  122. package/src/assets/web-panel/assets/{index-CIpVrf5P.js → index-DmkMXwnR.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-lJBjB1QX.js → index-DocdRcbd.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BEVm0lCS.js → index-Dyngbd29.js} +19 -19
  125. package/src/assets/web-panel/assets/{index-C4SJVjs4.js → index-GwdRUZ-x.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-ukg99D1V.js → index-LvCBfTLo.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BfPL-m46.js → index-cpEli44X.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CpIGIB85.js → index-fzaMtSJ8.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BNZDILcO.js → index-pum7HYBA.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dw1PyYPm.js → index-t8N1tSeX.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cip8emOQ.js → index-uCbEN7cY.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DsvfrESp.js → index-wKTLbKyc.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-B8BsdNV_.js → index-wV6iGdpY.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CYHMs1ag.js → initDefaultProps-V5Fo95FM.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DDB2Ffk5.js → motion-BYWU0hUT.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-Bg3cD-dX.js → move-Cr1M-J2h.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-B5nedjTS.js → mtc-parser-BysQKi2N.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BHyuUILu.js → omit-D1zHU8GC.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-C9YQnEN9.js → pickAttrs-CUArxtfi.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DBUFYUe7.js → placementArrow-ChoYEys0.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-nBs2Obnk.js → responsiveObserve-BQRs3Mo7.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-DkCEaXNU.js → slide-B8zbsCkg.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-aqE1Wi6U.js → statusUtils-C2U8Gs6h.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-CN5bhfHB.js → styleChecker-Dd8MgL_V.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-B_oX_kvJ.js → useFlexGapSupport-hknqY5NH.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-rCuXHeme.js → useFs-CLTAmcfw.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DZR9Abyd.js → usePersonalDataHub-ByegHbiB.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-ConCYQlX.js → vnode-DqlAy5DP.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-CUdDxG5M.js → zoom-DiXiztQn.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agenda.js +115 -3
  152. package/src/commands/agent.js +6 -0
  153. package/src/data/changelog.json +13 -0
  154. package/src/gateways/terminal/PtyManager.js +45 -4
  155. package/src/gateways/ws/ws-server.js +10 -1
  156. package/src/harness/background-task-manager.js +70 -2
  157. package/src/harness/worktree-isolator.js +14 -3
  158. package/src/lib/agent-core.js +1 -0
  159. package/src/lib/agent-schedule-store.js +45 -22
  160. package/src/lib/hook-runner.js +6 -2
  161. package/src/lib/packer/pkg-config-generator.js +3 -1
  162. package/src/lib/plugin-runtime/bin.js +1148 -70
  163. package/src/lib/plugin-runtime/manifest.js +68 -12
  164. package/src/lib/plugin-runtime/scopes.js +20 -10
  165. package/src/lib/process-execution-broker/index.js +375 -48
  166. package/src/lib/process-execution-broker/platform-sandbox.js +3831 -276
  167. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  168. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  169. package/src/lib/process-execution-broker/windows-sandbox.cs +3856 -0
  170. package/src/lib/repl-bang-memorize.js +67 -10
  171. package/src/lib/slash-commands.js +27 -4
  172. package/src/repl/agent-repl.js +4 -3
  173. package/src/runtime/agent-core.js +376 -52
  174. package/src/runtime/agent-runtime.js +13 -1
  175. package/src/runtime/headless-runner.js +4 -3
  176. package/src/assets/web-panel/assets/Terminal-Cgb4eLB_.js +0 -3
  177. package/src/assets/web-panel/assets/devWarning-BzPwHnw8.js +0 -1
  178. package/src/assets/web-panel/assets/index-DacrAdc-.js +0 -1
  179. package/src/assets/web-panel/assets/index-EHzNJozo.js +0 -1
  180. package/src/lib/process-execution-broker/windows-sandbox.ps1 +0 -999
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * Platform enforcement currently available:
5
5
  * - macOS: Seatbelt sandbox-exec profiles
6
- * - Linux: prlimit resource-limit wrapper, plus a non-promoting bubblewrap
7
- * runtime probe for explicitly requested filesystem/network boundaries
8
- * - Windows: Win32 Job Object + restricted-token wrapper
6
+ * - Linux: prlimit resource-limit wrapper, plus a narrow bubblewrap backend
7
+ * for an attested direct strict Plugin Node bin or static ELF native bin.
8
+ * Shells, dynamically linked binaries, broader working directories,
9
+ * host-writable paths, inherited descriptors, and network egress remain
10
+ * fail-closed.
11
+ * - Windows: Win32 Job Object + restricted-token wrapper, with an attested
12
+ * zero-capability AppContainer for explicit filesystem/network boundaries
9
13
  *
10
14
  * Security model:
11
15
  * Adapters return a truthful spawn plan. An unavailable primitive is never
@@ -55,40 +59,187 @@ export const SANDBOX_BOUNDARIES = Object.freeze({
55
59
  */
56
60
 
57
61
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
62
+ const WINDOWS_SANDBOX_HELPER_SOURCE = fs.readFileSync(
63
+ path.join(MODULE_DIR, "windows-sandbox.cs"),
64
+ );
65
+ const WINDOWS_SANDBOX_HELPER_SOURCE_DIGEST = crypto
66
+ .createHash("sha256")
67
+ .update(WINDOWS_SANDBOX_HELPER_SOURCE)
68
+ .digest("hex");
69
+ const WINDOWS_SANDBOX_HELPER_ASSEMBLY = fs.readFileSync(
70
+ path.join(MODULE_DIR, "windows-sandbox-helper.dll"),
71
+ );
72
+ const WINDOWS_SANDBOX_HELPER_ASSEMBLY_DIGEST = crypto
73
+ .createHash("sha256")
74
+ .update(WINDOWS_SANDBOX_HELPER_ASSEMBLY)
75
+ .digest("hex");
76
+ const WINDOWS_SANDBOX_HELPER_EXECUTABLE = fs.readFileSync(
77
+ path.join(MODULE_DIR, "windows-sandbox-helper.exe"),
78
+ );
79
+ const WINDOWS_SANDBOX_HELPER_EXECUTABLE_DIGEST = crypto
80
+ .createHash("sha256")
81
+ .update(WINDOWS_SANDBOX_HELPER_EXECUTABLE)
82
+ .digest("hex");
83
+ const WINDOWS_TRUSTED_SYSTEM_ROOT =
84
+ os.platform() === "win32"
85
+ ? (() => {
86
+ try {
87
+ const resolved = fs.realpathSync.native(
88
+ String.raw`\\?\GLOBALROOT\SystemRoot`,
89
+ );
90
+ const normalized = path.win32.resolve(resolved);
91
+ return /^[A-Za-z]:\\/.test(normalized) && !normalized.includes("\0")
92
+ ? normalized
93
+ : null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ })()
98
+ : "C:\\Windows";
58
99
  const WINDOWS_IDENTITY_WAIT = new Int32Array(new SharedArrayBuffer(4));
100
+ const WINDOWS_RESTRICTED_TOKEN_BACKEND = "windows-job-restricted-token";
101
+ const WINDOWS_APPCONTAINER_BACKEND =
102
+ "windows-appcontainer-job-restricted-token";
103
+ const WINDOWS_ADAPTER_IDLE_TTL_MS = 60_000;
104
+ // Every native helper operation starts a trusted System32 PowerShell host and
105
+ // byte-loads the checked-in assembly. Hosted Windows runners can spend well
106
+ // over ten seconds in that bootstrap before the managed operation begins.
107
+ const WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS = 120_000;
108
+ const WINDOWS_SANDBOX_HOST_ENV_DENYLIST = new Set([
109
+ "APPDOMAIN_MANAGER_ASM",
110
+ "APPDOMAIN_MANAGER_TYPE",
111
+ "COMPLUS_APPLICATIONMIGRATIONRUNTIMEACTIVATIONCONFIGPATH",
112
+ "COMPLUS_INSTALLROOT",
113
+ "COMPLUS_STARTUPHOOK",
114
+ "COMPLUS_VERSION",
115
+ "CORECLR_ENABLE_PROFILING",
116
+ "CORECLR_PROFILER",
117
+ "CORECLR_PROFILER_PATH",
118
+ "CORECLR_PROFILER_PATH_32",
119
+ "CORECLR_PROFILER_PATH_64",
120
+ "CORECLR_PROFILER_PATH_ARM32",
121
+ "CORECLR_PROFILER_PATH_ARM64",
122
+ "COR_ENABLE_PROFILING",
123
+ "COR_PROFILER",
124
+ "COR_PROFILER_PATH",
125
+ "COR_PROFILER_PATH_32",
126
+ "COR_PROFILER_PATH_64",
127
+ "DOTNET_ENABLE_PROFILING",
128
+ "DOTNET_PROFILER",
129
+ "DOTNET_PROFILER_PATH",
130
+ "DOTNET_PROFILER_PATH_32",
131
+ "DOTNET_PROFILER_PATH_64",
132
+ "DOTNET_PROFILER_PATH_ARM32",
133
+ "DOTNET_PROFILER_PATH_ARM64",
134
+ "DOTNET_STARTUP_HOOKS",
135
+ ]);
136
+ const WINDOWS_PLUGIN_NODE_ENV_DENYLIST = new Set([
137
+ "NODE_CHANNEL_FD",
138
+ "NODE_OPTIONS",
139
+ "OPENSSL_CONF",
140
+ "OPENSSL_CONF_INCLUDE",
141
+ "OPENSSL_ENGINES",
142
+ "OPENSSL_MODULES",
143
+ ]);
59
144
  const LINUX_BWRAP_PATH = "/usr/bin/bwrap";
60
- const LINUX_BWRAP_RUNTIME_PROBE_ARGS = Object.freeze([
61
- "--die-with-parent",
62
- "--new-session",
63
- "--unshare-all",
64
- "--ro-bind",
65
- "/",
66
- "/",
67
- "--proc",
68
- "/proc",
69
- "--dev",
70
- "/dev",
71
- "--tmpfs",
72
- "/tmp",
73
- "--",
74
- "/bin/true",
145
+ const LINUX_LDD_PATH = "/usr/bin/ldd";
146
+ const LINUX_BWRAP_BACKEND = "linux-bwrap";
147
+ const LINUX_BWRAP_NODE_PROBE_SENTINEL = "chainless-linux-bwrap-plugin-node-v1";
148
+ const LINUX_BWRAP_MAX_PLUGIN_ENTRIES = 512;
149
+ const LINUX_BWRAP_FIRST_MOUNT_FD = 3;
150
+ const LINUX_ATTESTED_FILE_MAX_BYTES = 256 * 1024 * 1024;
151
+ const LINUX_ATTESTATION_HASH_CHUNK_BYTES = 1024 * 1024;
152
+ const LINUX_ELF64_HEADER_BYTES = 64;
153
+ const LINUX_ELF64_PROGRAM_HEADER_BYTES = 56;
154
+ const LINUX_ELF64_DYNAMIC_ENTRY_BYTES = 16;
155
+ const LINUX_ELF_MAX_PROGRAM_HEADERS = 128;
156
+ const LINUX_ELF_MAX_DYNAMIC_ENTRIES = 4096;
157
+ const LINUX_ELF_TYPE_EXEC = 2;
158
+ const LINUX_ELF_TYPE_DYN = 3;
159
+ const LINUX_ELF_PROGRAM_LOAD = 1;
160
+ const LINUX_ELF_PROGRAM_DYNAMIC = 2;
161
+ const LINUX_ELF_PROGRAM_INTERP = 3;
162
+ const LINUX_ELF_PROGRAM_GNU_STACK = 0x6474e551;
163
+ const LINUX_ELF_DYNAMIC_NULL = 0n;
164
+ const LINUX_ELF_DYNAMIC_NEEDED = 1n;
165
+ const LINUX_ELF_DYNAMIC_FLAGS_1 = 0x6ffffffbn;
166
+ const LINUX_ELF_DYNAMIC_FLAG_PIE = 0x08000000n;
167
+ const LINUX_ELF_MACHINES = Object.freeze({
168
+ x64: 62,
169
+ arm64: 183,
170
+ riscv64: 243,
171
+ });
172
+ // Linux asm-generic: __O_TMPFILE (0x400000) | O_DIRECTORY (0x010000).
173
+ // x64, arm64, and riscv64 use this value; unsupported seccomp architectures
174
+ // already fail closed before the anonymous filter is created.
175
+ const LINUX_O_TMPFILE = 0x410000;
176
+ const LINUX_SECCOMP_FILTERS = Object.freeze({
177
+ x64: Object.freeze({
178
+ auditArch: 0xc000003e,
179
+ socketSyscall: 41,
180
+ socketpairSyscall: 53,
181
+ ioUringSetupSyscall: 425,
182
+ x32SyscallBit: 0x40000000,
183
+ }),
184
+ arm64: Object.freeze({
185
+ auditArch: 0xc00000b7,
186
+ socketSyscall: 198,
187
+ socketpairSyscall: 199,
188
+ ioUringSetupSyscall: 425,
189
+ }),
190
+ riscv64: Object.freeze({
191
+ auditArch: 0xc00000f3,
192
+ socketSyscall: 198,
193
+ socketpairSyscall: 199,
194
+ ioUringSetupSyscall: 425,
195
+ }),
196
+ });
197
+ const LINUX_LOCAL_FILESYSTEM_TYPES = new Set([
198
+ "btrfs",
199
+ "erofs",
200
+ "ext2",
201
+ "ext3",
202
+ "ext4",
203
+ "overlay",
204
+ "squashfs",
205
+ "tmpfs",
206
+ "xfs",
207
+ "zfs",
75
208
  ]);
76
209
  const DEFAULT_RUNTIME = Object.freeze({
77
210
  platform: os.platform(),
211
+ arch: process.arch,
78
212
  fs,
79
213
  tmpdir: () => os.tmpdir(),
80
214
  homedir: () => os.homedir(),
81
215
  randomBytes: (size) => crypto.randomBytes(size),
82
216
  resolvePath: (value) => path.resolve(value),
83
217
  joinPath: (...parts) => path.join(...parts),
84
- windowsDir: () => process.env.WINDIR || "C:\\Windows",
218
+ // GLOBALROOT resolves the kernel-owned SystemRoot object rather than the
219
+ // caller-controlled WINDIR/SystemRoot environment variables.
220
+ windowsDir: () => WINDOWS_TRUSTED_SYSTEM_ROOT,
85
221
  moduleDir: MODULE_DIR,
86
222
  now: () => Date.now(),
87
223
  sleepSync: (milliseconds) =>
88
224
  Atomics.wait(WINDOWS_IDENTITY_WAIT, 0, 0, milliseconds),
89
225
  spawnSync: nativeSpawnSync,
226
+ warn: (message) => process.emitWarning(message),
90
227
  });
91
228
 
229
+ let windowsAdapterCache = null;
230
+ const windowsAdapterCacheEntries = new Set();
231
+ const windowsTemporaryCleanupBacklog = new Set();
232
+ let windowsTemporaryCleanupRetryTimer = null;
233
+ const windowsAppContainerCleanupBacklog = new Set();
234
+ // Each AppContainer retry starts a synchronous, digest-attested PowerShell
235
+ // helper. Bound automatic retries so a permanently unsupported Windows host
236
+ // cannot repeatedly block a long-lived CLI; explicit cleanup/reset and process
237
+ // teardown remain available after this backoff is exhausted.
238
+ const WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS = Object.freeze([
239
+ 250, 1_000, 5_000,
240
+ ]);
241
+ let windowsAdapterExitCleanupRegistered = false;
242
+
92
243
  function resolveRuntime(overrides = {}) {
93
244
  return { ...DEFAULT_RUNTIME, ...overrides };
94
245
  }
@@ -121,7 +272,17 @@ function normalizeSandboxRequest(profileOrRequest, explicitRequest) {
121
272
  ),
122
273
  ]
123
274
  : [];
124
- return { profile, requiredBoundaries };
275
+ return {
276
+ profile,
277
+ requiredBoundaries,
278
+ sync: request.sync === true,
279
+ executionContract:
280
+ request.executionContract &&
281
+ typeof request.executionContract === "object" &&
282
+ !Array.isArray(request.executionContract)
283
+ ? request.executionContract
284
+ : null,
285
+ };
125
286
  }
126
287
 
127
288
  /**
@@ -204,7 +365,6 @@ function createSandboxPlan(input) {
204
365
  applied: false,
205
366
  enforcement: null,
206
367
  backend: null,
207
- guarantees: Object.freeze([]),
208
368
  reason: null,
209
369
  ...input,
210
370
  args: Object.freeze([...(input.args || [])]),
@@ -401,60 +561,771 @@ export function applyMacSandbox(
401
561
  // Windows Job Object + Restricted Token
402
562
  // ---------------------------------------------------------------------------
403
563
 
564
+ function cleanupWindowsTemporaryPath(
565
+ runtime,
566
+ targetPath,
567
+ attempts = 100,
568
+ delayMs = 10,
569
+ ) {
570
+ if (!targetPath) return true;
571
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
572
+ try {
573
+ runtime.fs.unlinkSync(targetPath);
574
+ return true;
575
+ } catch (error) {
576
+ if (error?.code === "ENOENT") return true;
577
+ }
578
+ if (attempt + 1 < attempts) runtime.sleepSync(delayMs);
579
+ }
580
+ return false;
581
+ }
582
+
583
+ function cleanupOrTrackWindowsTemporaryPath(
584
+ runtime,
585
+ targetPath,
586
+ attempts = 100,
587
+ ) {
588
+ if (cleanupWindowsTemporaryPath(runtime, targetPath, attempts)) {
589
+ for (const residual of [...windowsTemporaryCleanupBacklog]) {
590
+ if (residual.runtime === runtime && residual.targetPath === targetPath) {
591
+ windowsTemporaryCleanupBacklog.delete(residual);
592
+ }
593
+ }
594
+ if (
595
+ windowsTemporaryCleanupBacklog.size === 0 &&
596
+ windowsTemporaryCleanupRetryTimer
597
+ ) {
598
+ clearTimeout(windowsTemporaryCleanupRetryTimer);
599
+ windowsTemporaryCleanupRetryTimer = null;
600
+ }
601
+ return true;
602
+ }
603
+ if (
604
+ ![...windowsTemporaryCleanupBacklog].some(
605
+ (residual) =>
606
+ residual.runtime === runtime && residual.targetPath === targetPath,
607
+ )
608
+ ) {
609
+ windowsTemporaryCleanupBacklog.add({ runtime, targetPath });
610
+ }
611
+ registerWindowsAdapterExitCleanup();
612
+ scheduleWindowsTemporaryCleanupRetry();
613
+ return false;
614
+ }
615
+
616
+ function retryWindowsTemporaryCleanupBacklog(attempts = 100) {
617
+ let cleaned = true;
618
+ for (const residual of [...windowsTemporaryCleanupBacklog]) {
619
+ if (
620
+ cleanupWindowsTemporaryPath(
621
+ residual.runtime,
622
+ residual.targetPath,
623
+ attempts,
624
+ )
625
+ ) {
626
+ windowsTemporaryCleanupBacklog.delete(residual);
627
+ } else {
628
+ cleaned = false;
629
+ }
630
+ }
631
+ if (
632
+ windowsTemporaryCleanupBacklog.size === 0 &&
633
+ windowsTemporaryCleanupRetryTimer
634
+ ) {
635
+ clearTimeout(windowsTemporaryCleanupRetryTimer);
636
+ windowsTemporaryCleanupRetryTimer = null;
637
+ }
638
+ return cleaned;
639
+ }
640
+
641
+ function scheduleWindowsTemporaryCleanupRetry() {
642
+ if (
643
+ windowsTemporaryCleanupRetryTimer ||
644
+ windowsTemporaryCleanupBacklog.size === 0
645
+ ) {
646
+ return;
647
+ }
648
+ windowsTemporaryCleanupRetryTimer = setTimeout(() => {
649
+ windowsTemporaryCleanupRetryTimer = null;
650
+ retryWindowsTemporaryCleanupBacklog(1);
651
+ if (windowsTemporaryCleanupBacklog.size > 0) {
652
+ scheduleWindowsTemporaryCleanupRetry();
653
+ }
654
+ }, 250);
655
+ windowsTemporaryCleanupRetryTimer.unref?.();
656
+ }
657
+
658
+ function cleanupWindowsTemporaryDirectory(
659
+ runtime,
660
+ targetPath,
661
+ attempts = 100,
662
+ delayMs = 10,
663
+ ) {
664
+ if (!targetPath) return true;
665
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
666
+ try {
667
+ runtime.fs.rmdirSync(targetPath);
668
+ return true;
669
+ } catch (error) {
670
+ if (error?.code === "ENOENT") return true;
671
+ }
672
+ if (attempt + 1 < attempts) runtime.sleepSync(delayMs);
673
+ }
674
+ return false;
675
+ }
676
+
677
+ function sha256(value) {
678
+ return crypto.createHash("sha256").update(value).digest("hex");
679
+ }
680
+
681
+ function windowsFileIdentity(stat) {
682
+ const timestamp = (nanosecondsKey, millisecondsKey) => {
683
+ if (stat[nanosecondsKey] !== undefined) {
684
+ return String(stat[nanosecondsKey]);
685
+ }
686
+ return String(Math.trunc(Number(stat[millisecondsKey] || 0) * 1_000_000));
687
+ };
688
+ return Object.freeze({
689
+ dev: String(stat.dev),
690
+ ino: String(stat.ino),
691
+ size: String(stat.size),
692
+ mode: String(stat.mode),
693
+ birthtimeNs: timestamp("birthtimeNs", "birthtimeMs"),
694
+ ctimeNs: timestamp("ctimeNs", "ctimeMs"),
695
+ mtimeNs: timestamp("mtimeNs", "mtimeMs"),
696
+ });
697
+ }
698
+
699
+ function sameWindowsFileIdentity(left, right) {
700
+ return (
701
+ left.dev === right.dev &&
702
+ left.ino === right.ino &&
703
+ left.size === right.size &&
704
+ left.mode === right.mode &&
705
+ left.birthtimeNs === right.birthtimeNs &&
706
+ left.ctimeNs === right.ctimeNs &&
707
+ left.mtimeNs === right.mtimeNs
708
+ );
709
+ }
710
+
711
+ function inspectWindowsAdapterAssembly(runtime, assemblyPath) {
712
+ if (typeof runtime.fs.lstatSync === "function") {
713
+ const linkStat = runtime.fs.lstatSync(assemblyPath, { bigint: true });
714
+ if (
715
+ linkStat.isSymbolicLink?.() ||
716
+ (typeof linkStat.isFile === "function" && !linkStat.isFile())
717
+ ) {
718
+ throw new Error("Windows in-memory adapter is not a regular file");
719
+ }
720
+ }
721
+ const before = runtime.fs.statSync(assemblyPath, { bigint: true });
722
+ if (typeof before.isFile === "function" && !before.isFile()) {
723
+ throw new Error("Windows in-memory adapter is not a regular file");
724
+ }
725
+ const beforeIdentity = windowsFileIdentity(before);
726
+ const assemblyDigest = sha256(runtime.fs.readFileSync(assemblyPath));
727
+ const afterIdentity = windowsFileIdentity(
728
+ runtime.fs.statSync(assemblyPath, { bigint: true }),
729
+ );
730
+ if (!sameWindowsFileIdentity(beforeIdentity, afterIdentity)) {
731
+ throw new Error("Windows in-memory adapter changed during attestation");
732
+ }
733
+ return { assemblyDigest, fileIdentity: afterIdentity };
734
+ }
735
+
736
+ function verifyWindowsAdapterEntry(entry, runtime, source) {
737
+ if (
738
+ entry.retired ||
739
+ entry.cleaned ||
740
+ entry.runtimeFs !== runtime.fs ||
741
+ entry.tempDirectory !== source.tempDirectory ||
742
+ entry.sourceDigest !== source.sourceDigest ||
743
+ entry.sourceContractDigest !== source.sourceContractDigest ||
744
+ entry.loaderMode !== source.loaderMode
745
+ ) {
746
+ return false;
747
+ }
748
+ try {
749
+ const attestation = inspectWindowsAdapterAssembly(
750
+ runtime,
751
+ entry.assemblyPath,
752
+ );
753
+ return (
754
+ attestation.assemblyDigest === entry.assemblyDigest &&
755
+ sameWindowsFileIdentity(attestation.fileIdentity, entry.fileIdentity)
756
+ );
757
+ } catch {
758
+ return false;
759
+ }
760
+ }
761
+
762
+ function cleanupWindowsAdapterEntry(entry, attempts = 100) {
763
+ if (entry.cleaned) return true;
764
+ if (entry.idleTimer) {
765
+ clearTimeout(entry.idleTimer);
766
+ entry.idleTimer = null;
767
+ }
768
+ const deleted = cleanupOrTrackWindowsTemporaryPath(
769
+ entry.runtime,
770
+ entry.assemblyPath,
771
+ attempts,
772
+ );
773
+ const directoryDeleted =
774
+ deleted &&
775
+ cleanupWindowsTemporaryDirectory(
776
+ entry.runtime,
777
+ entry.adapterDirectory,
778
+ attempts,
779
+ );
780
+ if (deleted && directoryDeleted) {
781
+ entry.cleaned = true;
782
+ windowsAdapterCacheEntries.delete(entry);
783
+ }
784
+ return deleted && directoryDeleted;
785
+ }
786
+
787
+ function retireWindowsAdapterEntry(entry) {
788
+ if (!entry) return;
789
+ entry.retired = true;
790
+ if (windowsAdapterCache === entry) windowsAdapterCache = null;
791
+ if (entry.refCount === 0) cleanupWindowsAdapterEntry(entry);
792
+ }
793
+
794
+ function cleanupAllWindowsAdapterEntries(attempts = 100) {
795
+ retryWindowsAppContainerCleanupBacklog();
796
+ windowsAdapterCache = null;
797
+ for (const entry of [...windowsAdapterCacheEntries]) {
798
+ entry.retired = true;
799
+ entry.refCount = 0;
800
+ cleanupWindowsAdapterEntry(entry, attempts);
801
+ }
802
+ retryWindowsTemporaryCleanupBacklog(attempts);
803
+ }
804
+
805
+ function registerWindowsAdapterExitCleanup() {
806
+ if (windowsAdapterExitCleanupRegistered) return;
807
+ windowsAdapterExitCleanupRegistered = true;
808
+ process.on("beforeExit", () => {
809
+ cleanupAllWindowsAdapterEntries();
810
+ });
811
+ process.once("exit", () => {
812
+ cleanupAllWindowsAdapterEntries();
813
+ });
814
+ }
815
+
404
816
  /**
405
- * Materialize the shipped PowerShell/Win32 adapter into a physical temporary
406
- * file. This is required for pkg builds where source assets live in a virtual
407
- * snapshot that an external powershell.exe process cannot open directly.
817
+ * Explicitly discard the process-local Windows helper cache. Production keeps
818
+ * an attested helper until process teardown; tests and embedding hosts may use
819
+ * this hook at a known quiescent point to prove cleanup.
408
820
  */
409
- function materializeWindowsAdapter(runtime) {
410
- const sourcePath =
411
- runtime.windowsAdapterScriptPath ||
412
- runtime.joinPath(runtime.moduleDir, "windows-sandbox.ps1");
413
- let content = runtime.windowsAdapterContent;
414
- if (content === undefined) {
415
- if (!runtime.fs.existsSync(sourcePath)) {
416
- return { reason: "windows_native_adapter_missing" };
821
+ export function resetWindowsSandboxAdapterCache() {
822
+ let cleaned = retryWindowsAppContainerCleanupBacklog();
823
+ windowsAdapterCache = null;
824
+ for (const entry of [...windowsAdapterCacheEntries]) {
825
+ entry.retired = true;
826
+ entry.refCount = 0;
827
+ if (!cleanupWindowsAdapterEntry(entry)) cleaned = false;
828
+ }
829
+ if (!retryWindowsTemporaryCleanupBacklog()) cleaned = false;
830
+ return cleaned;
831
+ }
832
+
833
+ function scheduleWindowsAdapterIdleCleanup(entry) {
834
+ if (
835
+ entry.retired ||
836
+ entry.cleaned ||
837
+ entry.refCount !== 0 ||
838
+ entry.idleTimer
839
+ ) {
840
+ return;
841
+ }
842
+ const configuredTtl = Number(entry.runtime.windowsAdapterIdleTtlMs);
843
+ const idleTtlMs =
844
+ Number.isFinite(configuredTtl) && configuredTtl >= 0
845
+ ? configuredTtl
846
+ : WINDOWS_ADAPTER_IDLE_TTL_MS;
847
+ entry.idleTimer = setTimeout(() => {
848
+ entry.idleTimer = null;
849
+ if (
850
+ entry.refCount === 0 &&
851
+ !entry.retired &&
852
+ !entry.cleaned &&
853
+ windowsAdapterCache === entry
854
+ ) {
855
+ retireWindowsAdapterEntry(entry);
417
856
  }
418
- try {
419
- content = runtime.fs.readFileSync(sourcePath, "utf8");
420
- } catch {
421
- return { reason: "windows_native_adapter_unreadable" };
857
+ }, idleTtlMs);
858
+ entry.idleTimer.unref?.();
859
+ }
860
+
861
+ function loadWindowsAdapterSource(runtime, loaderMode) {
862
+ const bundled = runtime.windowsAdapterContent === undefined;
863
+ const content = bundled
864
+ ? loaderMode === "managed-executable"
865
+ ? WINDOWS_SANDBOX_HELPER_EXECUTABLE
866
+ : WINDOWS_SANDBOX_HELPER_ASSEMBLY
867
+ : Buffer.from(runtime.windowsAdapterContent);
868
+ return {
869
+ content,
870
+ sourceDigest: bundled
871
+ ? loaderMode === "managed-executable"
872
+ ? WINDOWS_SANDBOX_HELPER_EXECUTABLE_DIGEST
873
+ : WINDOWS_SANDBOX_HELPER_ASSEMBLY_DIGEST
874
+ : sha256(content),
875
+ sourceContractDigest: bundled ? WINDOWS_SANDBOX_HELPER_SOURCE_DIGEST : null,
876
+ tempDirectory: runtime.tmpdir(),
877
+ loaderMode,
878
+ };
879
+ }
880
+
881
+ /**
882
+ * Materialize the trusted in-process assembly bytes at a fresh path. The
883
+ * managed image is never executed from this user-writable location. A
884
+ * protected PowerShell host reads the bytes once, verifies the expected
885
+ * digest, and loads those exact bytes with Assembly.Load(byte[]).
886
+ */
887
+ function materializeWindowsAdapter(runtime, source) {
888
+ const adapterBaseName = `chainless-win-sandbox-${runtime
889
+ .randomBytes(24)
890
+ .toString("hex")}`;
891
+ const adapterDirectory =
892
+ source.loaderMode === "managed-executable"
893
+ ? runtime.joinPath(source.tempDirectory, adapterBaseName)
894
+ : null;
895
+ const assemblyPath = runtime.joinPath(
896
+ adapterDirectory || source.tempDirectory,
897
+ source.loaderMode === "managed-executable"
898
+ ? "windows-sandbox-helper.exe"
899
+ : `${adapterBaseName}.dll`,
900
+ );
901
+ if (
902
+ (adapterDirectory && runtime.fs.existsSync(adapterDirectory)) ||
903
+ runtime.fs.existsSync(assemblyPath)
904
+ ) {
905
+ return { reason: "windows_native_adapter_random_path_collision" };
906
+ }
907
+ try {
908
+ if (adapterDirectory) {
909
+ runtime.fs.mkdirSync(adapterDirectory, {
910
+ mode: 0o700,
911
+ recursive: false,
912
+ });
913
+ }
914
+ runtime.fs.writeFileSync(assemblyPath, source.content, {
915
+ mode: adapterDirectory ? 0o500 : 0o600,
916
+ flag: "wx",
917
+ });
918
+ } catch (error) {
919
+ const assemblyDeleted = cleanupOrTrackWindowsTemporaryPath(
920
+ runtime,
921
+ assemblyPath,
922
+ );
923
+ const directoryDeleted =
924
+ assemblyDeleted &&
925
+ cleanupWindowsTemporaryDirectory(runtime, adapterDirectory);
926
+ if (!assemblyDeleted || !directoryDeleted) {
927
+ return { reason: "windows_native_adapter_compile_cleanup_unverified" };
422
928
  }
929
+ if (error?.code === "EEXIST") {
930
+ return { reason: "windows_native_adapter_random_path_collision" };
931
+ }
932
+ return { reason: "windows_native_adapter_materialize_failed" };
423
933
  }
424
- const adapterHash = crypto
425
- .createHash("sha256")
426
- .update(String(content))
427
- .digest("hex")
428
- .slice(0, 24);
429
- const cacheExecutable = runtime.joinPath(
934
+ return {
935
+ adapterDirectory,
936
+ assemblyPath,
937
+ cleanupAssembly: () => {
938
+ const assemblyDeleted = cleanupOrTrackWindowsTemporaryPath(
939
+ runtime,
940
+ assemblyPath,
941
+ );
942
+ return (
943
+ assemblyDeleted &&
944
+ cleanupWindowsTemporaryDirectory(runtime, adapterDirectory)
945
+ );
946
+ },
947
+ };
948
+ }
949
+
950
+ function materializeWindowsAdapterInvocation(runtime, helperArgs) {
951
+ const payloadPath = runtime.joinPath(
430
952
  runtime.tmpdir(),
431
- `chainless-win-sandbox-${adapterHash}.exe`,
953
+ `chainless-win-sandbox-invocation-${runtime
954
+ .randomBytes(24)
955
+ .toString("hex")}.json`,
432
956
  );
433
- if (runtime.fs.existsSync(cacheExecutable)) {
957
+ if (runtime.fs.existsSync(payloadPath)) {
958
+ return { reason: "windows_adapter_invocation_random_path_collision" };
959
+ }
960
+ let content;
961
+ try {
962
+ content = Buffer.from(
963
+ [
964
+ "CC_WINDOWS_SANDBOX_INVOCATION_V1",
965
+ ...Array.from(helperArgs || [], (value) =>
966
+ Buffer.from(String(value), "utf8").toString("base64"),
967
+ ),
968
+ "",
969
+ ].join("\n"),
970
+ "utf8",
971
+ );
972
+ } catch {
973
+ return { reason: "windows_adapter_invocation_encode_failed" };
974
+ }
975
+ if (content.length > 8 * 1024 * 1024) {
976
+ return { reason: "windows_adapter_invocation_too_large" };
977
+ }
978
+ try {
979
+ runtime.fs.writeFileSync(payloadPath, content, {
980
+ mode: 0o600,
981
+ flag: "wx",
982
+ });
983
+ } catch (error) {
984
+ if (error?.code === "EEXIST") {
985
+ return { reason: "windows_adapter_invocation_random_path_collision" };
986
+ }
434
987
  return {
435
- cacheExecutable,
436
- bootstrapRequired: false,
988
+ reason: cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath)
989
+ ? "windows_adapter_invocation_materialize_failed"
990
+ : "windows_adapter_invocation_cleanup_unverified",
437
991
  };
438
992
  }
439
- const scriptPath = runtime.joinPath(
440
- runtime.tmpdir(),
441
- `chainless-win-sandbox-${runtime.randomBytes(12).toString("hex")}.ps1`,
993
+ return {
994
+ payloadPath,
995
+ payloadDigest: sha256(content),
996
+ cleanup: () => cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath),
997
+ };
998
+ }
999
+
1000
+ function windowsPowerShellBootstrap(entry, invocation) {
1001
+ const encodeUtf8 = (value) =>
1002
+ Buffer.from(String(value), "utf8").toString("base64");
1003
+ const assemblyPath = encodeUtf8(entry.assemblyPath);
1004
+ const payloadPath = encodeUtf8(invocation.payloadPath);
1005
+ const bootstrap = [
1006
+ "$ErrorActionPreference='Stop'",
1007
+ "$ProgressPreference='SilentlyContinue'",
1008
+ "try {",
1009
+ `$ccAssemblyPath=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${assemblyPath}'))`,
1010
+ `$ccPayloadPath=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payloadPath}'))`,
1011
+ `$ccExpectedAssembly='${entry.assemblyDigest}'`,
1012
+ `$ccExpectedPayload='${invocation.payloadDigest}'`,
1013
+ "$ccPayloadBytes=[IO.File]::ReadAllBytes($ccPayloadPath)",
1014
+ "[IO.File]::Delete($ccPayloadPath)",
1015
+ "if([IO.File]::Exists($ccPayloadPath)){throw 'Windows sandbox adapter invocation cleanup failed'}",
1016
+ "$ccAssemblyBytes=[IO.File]::ReadAllBytes($ccAssemblyPath)",
1017
+ "if($ccAssemblyBytes.Length -gt 1048576 -or $ccPayloadBytes.Length -gt 8388608){throw 'Windows sandbox adapter input exceeds its limit'}",
1018
+ "$ccSha=[Security.Cryptography.SHA256]::Create()",
1019
+ "try{$ccAssemblyHash=([BitConverter]::ToString($ccSha.ComputeHash($ccAssemblyBytes))).Replace('-','').ToLowerInvariant();$ccSha.Initialize();$ccPayloadHash=([BitConverter]::ToString($ccSha.ComputeHash($ccPayloadBytes))).Replace('-','').ToLowerInvariant()}finally{$ccSha.Dispose()}",
1020
+ "if(![String]::Equals($ccAssemblyHash,$ccExpectedAssembly,[StringComparison]::Ordinal)-or ![String]::Equals($ccPayloadHash,$ccExpectedPayload,[StringComparison]::Ordinal)){throw 'Windows sandbox adapter input digest mismatch'}",
1021
+ "$ccUtf8=New-Object Text.UTF8Encoding($false,$true)",
1022
+ "$ccLines=$ccUtf8.GetString($ccPayloadBytes).Split([char]10)",
1023
+ "if($ccLines.Length -lt 2 -or $ccLines[0] -cne 'CC_WINDOWS_SANDBOX_INVOCATION_V1' -or $ccLines[$ccLines.Length-1] -cne ''){throw 'Windows sandbox adapter invocation is invalid'}",
1024
+ "$ccArgCount=$ccLines.Length-2",
1025
+ "$ccArgs=[Array]::CreateInstance([string],$ccArgCount)",
1026
+ "for($ccIndex=0;$ccIndex -lt $ccArgCount;$ccIndex++){$ccArgs[$ccIndex]=$ccUtf8.GetString([Convert]::FromBase64String($ccLines[$ccIndex+1]))}",
1027
+ "$ccAssembly=[Reflection.Assembly]::Load($ccAssemblyBytes)",
1028
+ "$ccProgram=$ccAssembly.GetType('ChainlessChain.WindowsSandbox.Program',$true)",
1029
+ "$ccMain=$ccProgram.GetMethod('Main',[Reflection.BindingFlags]'Public,Static')",
1030
+ "if($null -eq $ccMain){throw 'Windows sandbox adapter entry point is missing'}",
1031
+ "$ccExit=$ccMain.Invoke($null,[object[]]@(,[string[]]$ccArgs))",
1032
+ "exit [int]$ccExit",
1033
+ "} catch {",
1034
+ "[Console]::Error.WriteLine('CC_WINDOWS_SANDBOX_ERROR: '+$_.Exception.Message)",
1035
+ "exit 125",
1036
+ "}",
1037
+ ].join("\n");
1038
+ return Buffer.from(bootstrap, "utf16le").toString("base64");
1039
+ }
1040
+
1041
+ function buildWindowsAdapterInvocation(entry, invocation) {
1042
+ if (entry.loaderMode === "managed-executable") {
1043
+ return {
1044
+ command: entry.assemblyPath,
1045
+ args: [
1046
+ "--adapter-path",
1047
+ entry.assemblyPath,
1048
+ "--invocation-file",
1049
+ invocation.payloadPath,
1050
+ invocation.payloadDigest,
1051
+ ],
1052
+ cleanup: invocation.cleanup,
1053
+ };
1054
+ }
1055
+ return {
1056
+ command: entry.powershellPath,
1057
+ args: [
1058
+ "-NoLogo",
1059
+ "-NoProfile",
1060
+ "-NonInteractive",
1061
+ "-ExecutionPolicy",
1062
+ "Bypass",
1063
+ "-EncodedCommand",
1064
+ windowsPowerShellBootstrap(entry, invocation),
1065
+ ],
1066
+ cleanup: invocation.cleanup,
1067
+ };
1068
+ }
1069
+
1070
+ function resolveWindowsPowerShellHost(runtime, loaderMode) {
1071
+ if (loaderMode !== "powershell-byte-assembly") return null;
1072
+ const windowsDirectory = path.win32.resolve(runtime.windowsDir());
1073
+ const driveRoot = path.win32.parse(windowsDirectory).root;
1074
+ const modernHost = runtime.joinPath(
1075
+ driveRoot,
1076
+ "Program Files",
1077
+ "PowerShell",
1078
+ "7",
1079
+ "pwsh.exe",
1080
+ );
1081
+ const realpath =
1082
+ runtime.fs.realpathSync?.native || runtime.fs.realpathSync || null;
1083
+ if (runtime.fs.existsSync(modernHost) && typeof realpath === "function") {
1084
+ try {
1085
+ if (
1086
+ windowsCanonicalPathKey(realpath.call(runtime.fs, modernHost)) ===
1087
+ windowsCanonicalPathKey(modernHost)
1088
+ ) {
1089
+ return modernHost;
1090
+ }
1091
+ } catch {
1092
+ // Fall through to the protected in-box host.
1093
+ }
1094
+ }
1095
+
1096
+ const inboxHost = runtime.joinPath(
1097
+ windowsDirectory,
1098
+ "System32",
1099
+ "WindowsPowerShell",
1100
+ "v1.0",
1101
+ "powershell.exe",
442
1102
  );
1103
+ return runtime.fs.existsSync(inboxHost) ? inboxHost : null;
1104
+ }
1105
+
1106
+ function createFreshWindowsAdapter(runtime, source, hostEnvironment) {
1107
+ const materialized = materializeWindowsAdapter(runtime, source);
1108
+ if (!materialized.assemblyPath) return materialized;
1109
+
1110
+ const powershell = resolveWindowsPowerShellHost(runtime, source.loaderMode);
1111
+ if (source.loaderMode === "powershell-byte-assembly" && !powershell) {
1112
+ const assemblyDeleted = materialized.cleanupAssembly();
1113
+ return {
1114
+ reason: assemblyDeleted
1115
+ ? "windows_powershell_host_unavailable"
1116
+ : "windows_native_adapter_compile_cleanup_unverified",
1117
+ };
1118
+ }
1119
+
1120
+ let attestation;
443
1121
  try {
444
- runtime.fs.writeFileSync(scriptPath, content, { mode: 0o600 });
1122
+ attestation = inspectWindowsAdapterAssembly(
1123
+ runtime,
1124
+ materialized.assemblyPath,
1125
+ );
1126
+ if (attestation.assemblyDigest !== source.sourceDigest) {
1127
+ throw new Error("Materialized Windows adapter digest mismatch");
1128
+ }
445
1129
  } catch {
446
- return { reason: "windows_native_adapter_materialize_failed" };
1130
+ const assemblyDeleted = materialized.cleanupAssembly();
1131
+ return {
1132
+ reason: assemblyDeleted
1133
+ ? "windows_native_adapter_materialize_attestation_failed"
1134
+ : "windows_native_adapter_compile_cleanup_unverified",
1135
+ };
1136
+ }
1137
+
1138
+ const entry = {
1139
+ runtime,
1140
+ runtimeFs: runtime.fs,
1141
+ tempDirectory: source.tempDirectory,
1142
+ sourceDigest: source.sourceDigest,
1143
+ sourceContractDigest: source.sourceContractDigest,
1144
+ loaderMode: source.loaderMode,
1145
+ powershellPath: powershell,
1146
+ adapterDirectory: materialized.adapterDirectory,
1147
+ assemblyPath: materialized.assemblyPath,
1148
+ assemblyDigest: attestation.assemblyDigest,
1149
+ fileIdentity: attestation.fileIdentity,
1150
+ refCount: 0,
1151
+ idleTimer: null,
1152
+ retired: false,
1153
+ cleaned: false,
1154
+ };
1155
+ const probePayload = materializeWindowsAdapterInvocation(runtime, [
1156
+ "--probe-helper",
1157
+ ]);
1158
+ if (!probePayload.payloadPath) {
1159
+ const assemblyDeleted = materialized.cleanupAssembly();
1160
+ return assemblyDeleted
1161
+ ? probePayload
1162
+ : { reason: "windows_native_adapter_compile_cleanup_unverified" };
1163
+ }
1164
+ const probeInvocation = buildWindowsAdapterInvocation(entry, probePayload);
1165
+ let probeResult;
1166
+ let probePayloadDeleted = false;
1167
+ try {
1168
+ probeResult = runtime.spawnSync(
1169
+ probeInvocation.command,
1170
+ probeInvocation.args,
1171
+ {
1172
+ shell: false,
1173
+ windowsHide: true,
1174
+ encoding: "utf8",
1175
+ timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
1176
+ cwd: runtime.joinPath(runtime.windowsDir(), "System32"),
1177
+ env: hostEnvironment,
1178
+ },
1179
+ );
1180
+ } catch (error) {
1181
+ probeResult = { error, status: null };
1182
+ } finally {
1183
+ probePayloadDeleted = probeInvocation.cleanup();
1184
+ }
1185
+ let probeReady = false;
1186
+ try {
1187
+ const probe = JSON.parse(String(probeResult?.stdout || "").trim());
1188
+ probeReady =
1189
+ probeResult?.status === 0 &&
1190
+ probe?.ready === true &&
1191
+ probe?.hostRuntime ===
1192
+ (source.loaderMode === "managed-executable"
1193
+ ? "managed-executable-v1"
1194
+ : "powershell-byte-assembly-v1") &&
1195
+ (source.sourceContractDigest === null ||
1196
+ probe?.sourceSha256 === source.sourceContractDigest);
1197
+ } catch {
1198
+ probeReady = false;
1199
+ }
1200
+ if (
1201
+ !probePayloadDeleted ||
1202
+ !probeReady ||
1203
+ !verifyWindowsAdapterEntry(entry, runtime, source)
1204
+ ) {
1205
+ const assemblyDeleted = materialized.cleanupAssembly();
1206
+ return {
1207
+ reason:
1208
+ !assemblyDeleted || !probePayloadDeleted
1209
+ ? "windows_native_adapter_compile_cleanup_unverified"
1210
+ : "windows_in_memory_adapter_probe_failed",
1211
+ };
1212
+ }
1213
+
1214
+ windowsAdapterCacheEntries.add(entry);
1215
+ windowsAdapterCache = entry;
1216
+ registerWindowsAdapterExitCleanup();
1217
+ return { entry };
1218
+ }
1219
+
1220
+ function acquireWindowsAdapterLease(runtime, source, hostEnvironment) {
1221
+ if (windowsAdapterCache) {
1222
+ if (verifyWindowsAdapterEntry(windowsAdapterCache, runtime, source)) {
1223
+ if (windowsAdapterCache.idleTimer) {
1224
+ clearTimeout(windowsAdapterCache.idleTimer);
1225
+ windowsAdapterCache.idleTimer = null;
1226
+ }
1227
+ windowsAdapterCache.refCount += 1;
1228
+ return {
1229
+ entry: windowsAdapterCache,
1230
+ released: false,
1231
+ };
1232
+ }
1233
+ retireWindowsAdapterEntry(windowsAdapterCache);
1234
+ }
1235
+
1236
+ const compiled = createFreshWindowsAdapter(runtime, source, hostEnvironment);
1237
+ if (!compiled.entry) return compiled;
1238
+ compiled.entry.refCount += 1;
1239
+ return { entry: compiled.entry, released: false };
1240
+ }
1241
+
1242
+ function releaseWindowsAdapterLease(lease) {
1243
+ if (!lease || lease.released) return;
1244
+ lease.released = true;
1245
+ lease.entry.refCount = Math.max(0, lease.entry.refCount - 1);
1246
+ if (lease.entry.retired && lease.entry.refCount === 0) {
1247
+ cleanupWindowsAdapterEntry(lease.entry);
1248
+ } else if (lease.entry.refCount === 0) {
1249
+ scheduleWindowsAdapterIdleCleanup(lease.entry);
447
1250
  }
1251
+ }
1252
+
1253
+ function createWindowsAdapterController(runtime, source, hostEnvironment) {
1254
+ const acquired = acquireWindowsAdapterLease(runtime, source, hostEnvironment);
1255
+ if (!acquired.entry) return acquired;
1256
+ let lease = acquired;
1257
+ let released = false;
1258
+
1259
+ const ensureEntry = () => {
1260
+ if (released) {
1261
+ const error = new Error("Windows native adapter lease was released");
1262
+ error.adapterReason = "windows_native_adapter_lease_released";
1263
+ throw error;
1264
+ }
1265
+ if (!verifyWindowsAdapterEntry(lease.entry, runtime, source)) {
1266
+ retireWindowsAdapterEntry(lease.entry);
1267
+ releaseWindowsAdapterLease(lease);
1268
+ const refreshed = acquireWindowsAdapterLease(
1269
+ runtime,
1270
+ source,
1271
+ hostEnvironment,
1272
+ );
1273
+ if (!refreshed.entry) {
1274
+ const error = new Error(
1275
+ `Windows native adapter refresh failed: ${refreshed.reason}`,
1276
+ );
1277
+ error.adapterReason = refreshed.reason;
1278
+ throw error;
1279
+ }
1280
+ lease = refreshed;
1281
+ }
1282
+ return lease.entry;
1283
+ };
1284
+
1285
+ const createInvocation = (helperArgs) => {
1286
+ const entry = ensureEntry();
1287
+ const payload = materializeWindowsAdapterInvocation(runtime, helperArgs);
1288
+ if (!payload.payloadPath) {
1289
+ const error = new Error(
1290
+ `Windows in-memory adapter invocation failed: ${payload.reason}`,
1291
+ );
1292
+ error.adapterReason = payload.reason;
1293
+ throw error;
1294
+ }
1295
+ return buildWindowsAdapterInvocation(entry, payload);
1296
+ };
1297
+
448
1298
  return {
449
- scriptPath,
450
- cacheExecutable,
451
- bootstrapRequired: true,
452
- cleanup: () => {
1299
+ ensureExecutable: ensureEntry,
1300
+ createInvocation,
1301
+ spawnSync: (args, options) => {
1302
+ const invocation = createInvocation(args);
1303
+ let result;
1304
+ let failure;
453
1305
  try {
454
- runtime.fs.unlinkSync(scriptPath);
455
- } catch {
456
- // The temporary adapter may already have been removed.
1306
+ result = runtime.spawnSync(invocation.command, invocation.args, {
1307
+ ...options,
1308
+ cwd: runtime.joinPath(runtime.windowsDir(), "System32"),
1309
+ env: hostEnvironment,
1310
+ });
1311
+ } catch (error) {
1312
+ failure = error;
1313
+ }
1314
+ if (!invocation.cleanup()) {
1315
+ const cleanupError = new Error(
1316
+ "Windows sandbox adapter invocation cleanup could not be verified",
1317
+ failure ? { cause: failure } : undefined,
1318
+ );
1319
+ cleanupError.code = "ERR_WINDOWS_SANDBOX_INVOCATION_CLEANUP";
1320
+ throw cleanupError;
457
1321
  }
1322
+ if (failure) throw failure;
1323
+ return result;
1324
+ },
1325
+ release: () => {
1326
+ if (released) return;
1327
+ released = true;
1328
+ releaseWindowsAdapterLease(lease);
458
1329
  },
459
1330
  };
460
1331
  }
@@ -463,7 +1334,8 @@ function waitForWindowsTargetIdentity(
463
1334
  proc,
464
1335
  identityPath,
465
1336
  runtime,
466
- timeoutMs = 10_000,
1337
+ expectedAppContainer = null,
1338
+ timeoutMs = 30_000,
467
1339
  ) {
468
1340
  const deadline = runtime.now() + timeoutMs;
469
1341
  let lastError;
@@ -496,15 +1368,50 @@ function waitForWindowsTargetIdentity(
496
1368
  "Windows sandbox target PID unexpectedly equals its helper PID",
497
1369
  );
498
1370
  }
1371
+ if (expectedAppContainer) {
1372
+ if (identity?.appContainer !== true) {
1373
+ throw new Error(
1374
+ "Windows sandbox identity did not attest an AppContainer target",
1375
+ );
1376
+ }
1377
+ if (
1378
+ identity?.appContainerSid !== expectedAppContainer.appContainerSid
1379
+ ) {
1380
+ throw new Error(
1381
+ "Windows sandbox identity named an unexpected AppContainer SID",
1382
+ );
1383
+ }
1384
+ if (identity?.capabilityCount !== 0) {
1385
+ throw new Error(
1386
+ "Windows sandbox identity did not attest zero capabilities",
1387
+ );
1388
+ }
1389
+ }
499
1390
 
500
1391
  const wrapperPid = proc.pid;
501
1392
  proc.sandboxWrapperPid = wrapperPid;
502
1393
  proc.sandboxTargetPid = targetPid;
1394
+ if (expectedAppContainer) {
1395
+ proc.sandboxAppContainerProfile =
1396
+ expectedAppContainer.appContainerProfileName;
1397
+ proc.sandboxAppContainerSid = expectedAppContainer.appContainerSid;
1398
+ }
503
1399
  // ChildProcess.pid is a writable data property. The native ChildProcess
504
1400
  // handle still points at the helper, so kill()/ref()/unref() retain Job
505
1401
  // lifetime semantics while callers observe and persist the real target.
506
1402
  proc.pid = targetPid;
507
- return { targetPid, wrapperPid };
1403
+ return {
1404
+ targetPid,
1405
+ wrapperPid,
1406
+ ...(expectedAppContainer
1407
+ ? {
1408
+ appContainerProfileName:
1409
+ expectedAppContainer.appContainerProfileName,
1410
+ appContainerSid: expectedAppContainer.appContainerSid,
1411
+ capabilityCount: 0,
1412
+ }
1413
+ : {}),
1414
+ };
508
1415
  } catch (error) {
509
1416
  lastError = error;
510
1417
  if (
@@ -524,38 +1431,637 @@ function waitForWindowsTargetIdentity(
524
1431
  );
525
1432
  }
526
1433
 
527
- /**
528
- * Wrap a target with the built-in Windows PowerShell host. The shipped script
529
- * P/Invokes Win32 to create a restricted primary token, starts the target
530
- * suspended, assigns it to a kill-on-close Job Object, then resumes it. The
531
- * target therefore cannot run before both native boundaries are active.
532
- *
533
- * @param {string} command
534
- * @param {string[]} args
535
- * @param {Object} spawnOpts
536
- * @param {Object} sandboxOpts
537
- */
538
- export function applyWindowsSandbox(
539
- command,
540
- args,
541
- spawnOpts,
542
- sandboxOpts = {},
543
- runtimeOverrides = {},
544
- ) {
545
- const runtime = resolveRuntime(runtimeOverrides);
546
- const base = {
547
- platform: runtime.platform,
548
- profile: sandboxOpts.profileName || "default",
549
- backend: "windows-job-restricted-token",
1434
+ function windowsNodeSnapshotProbeResult(result) {
1435
+ const runtimeProbe = {
1436
+ kind: "windows-plugin-node-entry-snapshot-v1",
1437
+ attempted: true,
1438
+ runnable: false,
1439
+ reason: "probe_failed",
1440
+ probeRuntime: "node",
1441
+ targetRuntime: "node",
1442
+ };
1443
+ if (result?.error || result?.status !== 0) {
1444
+ return {
1445
+ ...runtimeProbe,
1446
+ reason: windowsSandboxHelperFailureReason(result, "probe_failed"),
1447
+ };
1448
+ }
1449
+
1450
+ try {
1451
+ const attestation = JSON.parse(String(result.stdout || "").trim());
1452
+ if (
1453
+ attestation?.ready !== true ||
1454
+ attestation?.targetRuntime !== "node" ||
1455
+ attestation?.contentSnapshot !== true
1456
+ ) {
1457
+ return { ...runtimeProbe, reason: "invalid_attestation" };
1458
+ }
1459
+ } catch {
1460
+ return { ...runtimeProbe, reason: "invalid_attestation" };
1461
+ }
1462
+ return { ...runtimeProbe, runnable: true, reason: null };
1463
+ }
1464
+
1465
+ function windowsSandboxHelperFailureReason(result, fallback) {
1466
+ if (result?.error) {
1467
+ const errorCode = String(result.error.code || "").toLowerCase();
1468
+ if (
1469
+ errorCode === "etimedout" ||
1470
+ result.error.killed === true ||
1471
+ result.error.signal
1472
+ ) {
1473
+ return `${fallback}_timeout`;
1474
+ }
1475
+ if (/^[a-z0-9_]+$/.test(errorCode)) {
1476
+ return `${fallback}_spawn_${errorCode}`;
1477
+ }
1478
+ return `${fallback}_spawn_error`;
1479
+ }
1480
+
1481
+ if (result?.status === 0) return fallback;
1482
+ const status = Number.isSafeInteger(result?.status)
1483
+ ? result.status
1484
+ : "unknown";
1485
+ const stderr = String(result?.stderr || "");
1486
+ const knownFailures = [
1487
+ [
1488
+ /CreateAppContainerProfile\(cleanup verification\)/i,
1489
+ "appcontainer_cleanup_verification_create",
1490
+ ],
1491
+ [/CreateAppContainerProfile/i, "appcontainer_profile_create"],
1492
+ [/DeleteAppContainerProfile/i, "appcontainer_profile_delete"],
1493
+ [
1494
+ /CreateProcess(?:AsUser)?\(AppContainer\)/i,
1495
+ "appcontainer_process_create",
1496
+ ],
1497
+ [/Target application contains a reparse component/i, "target_reparse"],
1498
+ [
1499
+ /Target working directory contains a reparse component/i,
1500
+ "working_directory_reparse",
1501
+ ],
1502
+ [
1503
+ /Target PATH directory contains a reparse component/i,
1504
+ "path_directory_reparse",
1505
+ ],
1506
+ [/Target application must use a local DOS drive/i, "target_nonlocal"],
1507
+ [
1508
+ /Target working directory must use a local DOS drive/i,
1509
+ "working_directory_nonlocal",
1510
+ ],
1511
+ [
1512
+ /Node entry snapshot probe returned a non-zero exit code/i,
1513
+ "node_snapshot_nonzero",
1514
+ ],
1515
+ [
1516
+ /Node entry snapshot probe did not execute its verified source/i,
1517
+ "node_snapshot_unverified",
1518
+ ],
1519
+ [
1520
+ /AppContainer readiness target returned a non-zero exit code/i,
1521
+ "appcontainer_readiness_nonzero",
1522
+ ],
1523
+ [
1524
+ /AppContainer SID changed during readiness attestation/i,
1525
+ "appcontainer_sid_changed",
1526
+ ],
1527
+ ];
1528
+ const classified = knownFailures.find(([pattern]) => pattern.test(stderr));
1529
+ const nativeCode = stderr.match(
1530
+ /\b(?:hresult|win32)=(0x[0-9a-f]+|\d+)\b/i,
1531
+ )?.[1];
1532
+ return [
1533
+ fallback,
1534
+ `helper_exit_${status}`,
1535
+ classified?.[1],
1536
+ nativeCode?.toLowerCase(),
1537
+ ]
1538
+ .filter(Boolean)
1539
+ .join("_");
1540
+ }
1541
+
1542
+ function appContainerReadinessResult(
1543
+ result,
1544
+ profileName,
1545
+ targetRuntime = null,
1546
+ ) {
1547
+ const runtimeProbe = {
1548
+ kind: "windows-appcontainer-launch-attestation-v1",
1549
+ attempted: true,
1550
+ runnable: false,
1551
+ reason: "probe_failed",
1552
+ };
1553
+ if (result?.error || result?.status !== 0) {
1554
+ return {
1555
+ runtimeProbe: {
1556
+ ...runtimeProbe,
1557
+ reason: windowsSandboxHelperFailureReason(result, "probe_failed"),
1558
+ },
1559
+ readiness: null,
1560
+ };
1561
+ }
1562
+
1563
+ let readiness;
1564
+ try {
1565
+ readiness = JSON.parse(String(result.stdout || "").trim());
1566
+ } catch {
1567
+ return {
1568
+ runtimeProbe: {
1569
+ ...runtimeProbe,
1570
+ reason: "invalid_attestation",
1571
+ },
1572
+ readiness: null,
1573
+ };
1574
+ }
1575
+ const sidPattern = /^S-1-15-2(?:-\d+){7}$/;
1576
+ if (
1577
+ readiness?.ready !== true ||
1578
+ readiness?.profileName !== profileName ||
1579
+ typeof readiness?.appContainerSid !== "string" ||
1580
+ !sidPattern.test(readiness.appContainerSid) ||
1581
+ readiness?.capabilityCount !== 0 ||
1582
+ readiness?.tokenAttested !== true ||
1583
+ readiness?.restrictedTokenAttested !== true ||
1584
+ (targetRuntime !== null &&
1585
+ (readiness?.probeRuntime !== targetRuntime ||
1586
+ readiness?.targetRuntime !== targetRuntime))
1587
+ ) {
1588
+ return {
1589
+ runtimeProbe: {
1590
+ ...runtimeProbe,
1591
+ reason: "invalid_attestation",
1592
+ },
1593
+ readiness: null,
1594
+ };
1595
+ }
1596
+ return {
1597
+ runtimeProbe: {
1598
+ ...runtimeProbe,
1599
+ runnable: true,
1600
+ reason: null,
1601
+ ...(targetRuntime
1602
+ ? {
1603
+ probeRuntime: targetRuntime,
1604
+ targetRuntime,
1605
+ }
1606
+ : {}),
1607
+ },
1608
+ readiness,
1609
+ };
1610
+ }
1611
+
1612
+ function deleteWindowsAppContainerProfile(
1613
+ adapter,
1614
+ profileName,
1615
+ expectedSid = null,
1616
+ failure = null,
1617
+ ) {
1618
+ const helperArgs = ["--delete-appcontainer", profileName];
1619
+ if (expectedSid) helperArgs.push(expectedSid);
1620
+ try {
1621
+ const result = adapter.spawnSync(helperArgs, {
1622
+ shell: false,
1623
+ windowsHide: true,
1624
+ encoding: "utf8",
1625
+ timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
1626
+ });
1627
+ if (result?.error || result?.status !== 0) {
1628
+ if (failure) {
1629
+ failure.reason = windowsSandboxHelperFailureReason(
1630
+ result,
1631
+ "cleanup_failed",
1632
+ );
1633
+ }
1634
+ return false;
1635
+ }
1636
+ let deletion;
1637
+ try {
1638
+ deletion = JSON.parse(String(result.stdout || "").trim());
1639
+ } catch {
1640
+ if (failure) {
1641
+ failure.reason = "cleanup_failed_invalid_attestation";
1642
+ }
1643
+ return false;
1644
+ }
1645
+ const deleted =
1646
+ deletion?.deleted === true &&
1647
+ deletion?.absent === true &&
1648
+ deletion?.profileName === profileName;
1649
+ if (!deleted && failure) {
1650
+ failure.reason = "cleanup_failed_invalid_attestation";
1651
+ }
1652
+ return deleted;
1653
+ } catch (error) {
1654
+ if (failure) {
1655
+ failure.reason = windowsSandboxHelperFailureReason(
1656
+ { error, status: null },
1657
+ "cleanup_failed",
1658
+ );
1659
+ }
1660
+ return false;
1661
+ }
1662
+ }
1663
+
1664
+ function appContainerCleanupFailureReason(readinessReason, cleanupReason) {
1665
+ const readiness =
1666
+ typeof readinessReason === "string" && readinessReason
1667
+ ? readinessReason
1668
+ : "unknown_readiness";
1669
+ const cleanup =
1670
+ typeof cleanupReason === "string" && cleanupReason
1671
+ ? cleanupReason
1672
+ : "unknown_cleanup";
1673
+ return `cleanup_unverified_after_${readiness}_because_${cleanup}`;
1674
+ }
1675
+
1676
+ function retryWindowsAppContainerCleanup(record) {
1677
+ if (record.cleaned) return true;
1678
+ let adapter;
1679
+ try {
1680
+ adapter = createWindowsAdapterController(
1681
+ record.runtime,
1682
+ record.source,
1683
+ record.hostEnvironment,
1684
+ );
1685
+ if (!adapter.ensureExecutable) return false;
1686
+ if (
1687
+ !deleteWindowsAppContainerProfile(
1688
+ adapter,
1689
+ record.profileName,
1690
+ record.expectedSid,
1691
+ )
1692
+ ) {
1693
+ return false;
1694
+ }
1695
+ record.cleaned = true;
1696
+ if (record.automaticTimer) {
1697
+ clearTimeout(record.automaticTimer);
1698
+ record.automaticTimer = null;
1699
+ }
1700
+ windowsAppContainerCleanupBacklog.delete(record);
1701
+ return true;
1702
+ } catch {
1703
+ return false;
1704
+ } finally {
1705
+ adapter?.release?.();
1706
+ }
1707
+ }
1708
+
1709
+ function retryWindowsAppContainerCleanupBacklog() {
1710
+ let cleaned = true;
1711
+ for (const record of [...windowsAppContainerCleanupBacklog]) {
1712
+ if (!retryWindowsAppContainerCleanup(record)) cleaned = false;
1713
+ }
1714
+ return cleaned;
1715
+ }
1716
+
1717
+ function scheduleWindowsAppContainerCleanupRetry(record) {
1718
+ if (
1719
+ record.cleaned ||
1720
+ record.automaticTimer ||
1721
+ record.automaticAttempts >=
1722
+ WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS.length
1723
+ ) {
1724
+ return;
1725
+ }
1726
+ record.automaticTimer = setTimeout(() => {
1727
+ record.automaticTimer = null;
1728
+ if (record.cleaned || !windowsAppContainerCleanupBacklog.has(record)) {
1729
+ return;
1730
+ }
1731
+ record.automaticAttempts += 1;
1732
+ if (!retryWindowsAppContainerCleanup(record)) {
1733
+ scheduleWindowsAppContainerCleanupRetry(record);
1734
+ }
1735
+ }, WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS[record.automaticAttempts]);
1736
+ record.automaticTimer.unref?.();
1737
+ }
1738
+
1739
+ function trackWindowsAppContainerCleanup(
1740
+ runtime,
1741
+ source,
1742
+ hostEnvironment,
1743
+ profileName,
1744
+ expectedSid,
1745
+ ) {
1746
+ let record = [...windowsAppContainerCleanupBacklog].find(
1747
+ (candidate) =>
1748
+ candidate.runtime === runtime && candidate.profileName === profileName,
1749
+ );
1750
+ if (!record) {
1751
+ record = {
1752
+ runtime,
1753
+ source,
1754
+ hostEnvironment,
1755
+ profileName,
1756
+ expectedSid,
1757
+ cleaned: false,
1758
+ automaticAttempts: 0,
1759
+ automaticTimer: null,
1760
+ };
1761
+ windowsAppContainerCleanupBacklog.add(record);
1762
+ } else if (!record.expectedSid && expectedSid) {
1763
+ record.expectedSid = expectedSid;
1764
+ }
1765
+ registerWindowsAdapterExitCleanup();
1766
+ scheduleWindowsAppContainerCleanupRetry(record);
1767
+ return record;
1768
+ }
1769
+
1770
+ function windowsCanonicalPathKey(value) {
1771
+ if (typeof value !== "string" || !value || value.includes("\0")) return null;
1772
+ try {
1773
+ return path.win32.resolve(value).toLowerCase();
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ }
1778
+
1779
+ function sanitizeWindowsSandboxEnvironment(environment, pluginNodeSnapshot) {
1780
+ const sanitized = {};
1781
+ const normalizedKeys = new Map();
1782
+ for (const [key, value] of Object.entries(environment || process.env)) {
1783
+ const normalized = key.toUpperCase();
1784
+ if (
1785
+ WINDOWS_SANDBOX_HOST_ENV_DENYLIST.has(normalized) ||
1786
+ (pluginNodeSnapshot && WINDOWS_PLUGIN_NODE_ENV_DENYLIST.has(normalized))
1787
+ ) {
1788
+ continue;
1789
+ }
1790
+ const previousKey = normalizedKeys.get(normalized);
1791
+ if (previousKey !== undefined) delete sanitized[previousKey];
1792
+ normalizedKeys.set(normalized, key);
1793
+ sanitized[key] = String(value);
1794
+ }
1795
+ return sanitized;
1796
+ }
1797
+
1798
+ function setWindowsEnvironmentValue(environment, name, value) {
1799
+ const normalized = name.toUpperCase();
1800
+ for (const key of Object.keys(environment)) {
1801
+ if (key.toUpperCase() === normalized) delete environment[key];
1802
+ }
1803
+ environment[name] = String(value);
1804
+ }
1805
+
1806
+ function windowsSandboxHostEnvironment(runtime) {
1807
+ const windowsDirectory = path.win32.resolve(runtime.windowsDir());
1808
+ const systemDirectory = runtime.joinPath(windowsDirectory, "System32");
1809
+ const temporaryDirectory = runtime.joinPath(windowsDirectory, "Temp");
1810
+ return {
1811
+ SystemRoot: windowsDirectory,
1812
+ WINDIR: windowsDirectory,
1813
+ ComSpec: runtime.joinPath(systemDirectory, "cmd.exe"),
1814
+ PATH: `${systemDirectory};${windowsDirectory}`,
1815
+ PATHEXT: ".COM;.EXE;.BAT;.CMD",
1816
+ TEMP: temporaryDirectory,
1817
+ TMP: temporaryDirectory,
1818
+ };
1819
+ }
1820
+
1821
+ function windowsPluginNodeEntrySnapshot(invocation, sandboxOpts, spawnOpts) {
1822
+ const contract = sandboxOpts.executionContract;
1823
+ if (!contract) return { locks: null, reason: null };
1824
+ if (contract.kind !== "strict-plugin-node-bin") {
1825
+ return {
1826
+ locks: null,
1827
+ reason: "windows_plugin_execution_contract_unsupported",
1828
+ };
1829
+ }
1830
+ if (sandboxOpts.sync !== true || spawnOpts?.detached === true) {
1831
+ return {
1832
+ locks: null,
1833
+ reason: "windows_plugin_launch_path_lock_requires_sync_foreground",
1834
+ };
1835
+ }
1836
+
1837
+ const runtimeIdentity = contract.runtimeIdentity;
1838
+ const entryIdentity = contract.entryIdentity;
1839
+ const validIdentity = (identity, maxBytes) =>
1840
+ identity &&
1841
+ typeof identity === "object" &&
1842
+ typeof identity.realPath === "string" &&
1843
+ path.win32.isAbsolute(identity.realPath) &&
1844
+ typeof identity.sha256 === "string" &&
1845
+ /^[a-f0-9]{64}$/.test(identity.sha256) &&
1846
+ identity.fileId &&
1847
+ typeof identity.fileId === "object" &&
1848
+ typeof identity.fileId.dev === "string" &&
1849
+ typeof identity.fileId.ino === "string" &&
1850
+ /^\d+$/.test(identity.fileId.dev) &&
1851
+ /^\d+$/.test(identity.fileId.ino) &&
1852
+ Number.isSafeInteger(identity.bytes) &&
1853
+ identity.bytes >= 0 &&
1854
+ identity.bytes <= maxBytes;
1855
+ if (
1856
+ !validIdentity(runtimeIdentity, 256 * 1024 * 1024) ||
1857
+ !validIdentity(entryIdentity, 64 * 1024 * 1024)
1858
+ ) {
1859
+ return {
1860
+ locks: null,
1861
+ reason: "windows_plugin_launch_path_identity_invalid",
1862
+ };
1863
+ }
1864
+
1865
+ const runtimePath = windowsCanonicalPathKey(runtimeIdentity.realPath);
1866
+ const entryPath = windowsCanonicalPathKey(entryIdentity.realPath);
1867
+ if (
1868
+ !runtimePath ||
1869
+ !entryPath ||
1870
+ runtimePath !== windowsCanonicalPathKey(contract.runtimePath) ||
1871
+ runtimePath !== windowsCanonicalPathKey(invocation.command) ||
1872
+ entryPath !== windowsCanonicalPathKey(invocation.args?.[0])
1873
+ ) {
1874
+ return {
1875
+ locks: null,
1876
+ reason: "windows_plugin_launch_path_identity_mismatch",
1877
+ };
1878
+ }
1879
+ if (path.win32.extname(entryIdentity.realPath).toLowerCase() !== ".cjs") {
1880
+ return {
1881
+ locks: null,
1882
+ reason: "windows_plugin_entry_snapshot_format_unsupported",
1883
+ };
1884
+ }
1885
+
1886
+ return {
1887
+ locks: [
1888
+ {
1889
+ role: "runtime",
1890
+ path: runtimeIdentity.realPath,
1891
+ sha256: runtimeIdentity.sha256,
1892
+ bytes: runtimeIdentity.bytes,
1893
+ dev: runtimeIdentity.fileId.dev,
1894
+ ino: runtimeIdentity.fileId.ino,
1895
+ },
1896
+ {
1897
+ role: "entry",
1898
+ path: entryIdentity.realPath,
1899
+ sha256: entryIdentity.sha256,
1900
+ bytes: entryIdentity.bytes,
1901
+ dev: entryIdentity.fileId.dev,
1902
+ ino: entryIdentity.fileId.ino,
1903
+ },
1904
+ ],
1905
+ reason: null,
1906
+ };
1907
+ }
1908
+
1909
+ function windowsAppContainerPolicyDigest({
1910
+ profile,
1911
+ requiredBoundaries,
1912
+ guarantees,
1913
+ adapterSource,
1914
+ launchSpec,
1915
+ entrySnapshot,
1916
+ executionContract,
1917
+ }) {
1918
+ // Bind the effective enforcement policy and attested executable snapshot.
1919
+ // Transient profile names/SIDs and caller payload values are intentionally
1920
+ // excluded so identical policy identities remain stable across launches.
1921
+ const snapshotLocks = entrySnapshot.locks
1922
+ ? entrySnapshot.locks.map(
1923
+ ({ role, path: lockPath, sha256, bytes, dev, ino }) => ({
1924
+ role,
1925
+ path: lockPath,
1926
+ sha256,
1927
+ bytes,
1928
+ dev,
1929
+ ino,
1930
+ }),
1931
+ )
1932
+ : [];
1933
+ return sha256(
1934
+ JSON.stringify({
1935
+ version: 1,
1936
+ backend: WINDOWS_APPCONTAINER_BACKEND,
1937
+ profile,
1938
+ requiredBoundaries: [...new Set(requiredBoundaries)].sort(),
1939
+ guarantees: [...new Set(guarantees)].sort(),
1940
+ adapter: {
1941
+ loaderMode: adapterSource.loaderMode,
1942
+ sourceDigest: adapterSource.sourceDigest,
1943
+ sourceContractDigest: adapterSource.sourceContractDigest,
1944
+ },
1945
+ appContainer: {
1946
+ attestationKind: "windows-appcontainer-launch-attestation-v1",
1947
+ capabilities: [],
1948
+ token: "restricted-primary-lowbox",
1949
+ disableAdministratorSids: launchSpec.disableAdministratorSids,
1950
+ allowReparsePaths: launchSpec.allowReparsePaths,
1951
+ lifecycle: "ephemeral-delete-and-assert-absent",
1952
+ },
1953
+ job: {
1954
+ killOnClose: true,
1955
+ activeProcessLimit: launchSpec.activeProcessLimit,
1956
+ cpuSeconds: launchSpec.cpuSeconds,
1957
+ processMemoryBytes: launchSpec.processMemoryBytes,
1958
+ },
1959
+ execution: {
1960
+ contractKind: executionContract?.kind || null,
1961
+ contentSnapshot: snapshotLocks.length > 0,
1962
+ contentSnapshotScope:
1963
+ snapshotLocks.length > 0 ? "plugin-entry-source" : null,
1964
+ contentSnapshotMechanism:
1965
+ snapshotLocks.length > 0
1966
+ ? "verified-handle-inherited-pipe-module-compile-v1"
1967
+ : null,
1968
+ handleAtomic: false,
1969
+ launchPathLocks: snapshotLocks,
1970
+ },
1971
+ }),
1972
+ );
1973
+ }
1974
+
1975
+ /**
1976
+ * Wrap a target with a protected Windows PowerShell host. PowerShell 7 is
1977
+ * preferred only when its canonical Program Files path is intact; otherwise
1978
+ * the in-box System32 host remains the fallback. The shipped byte-loaded helper
1979
+ * P/Invokes Win32 to create a restricted primary token, starts the target
1980
+ * suspended, assigns it to a kill-on-close Job Object, then resumes it.
1981
+ * Filesystem/network requests additionally require a synchronous real-launch
1982
+ * AppContainer probe and suspended-target token/SID attestation.
1983
+ *
1984
+ * @param {string} command
1985
+ * @param {string[]} args
1986
+ * @param {Object} spawnOpts
1987
+ * @param {Object} sandboxOpts
1988
+ */
1989
+ export function applyWindowsSandbox(
1990
+ command,
1991
+ args,
1992
+ spawnOpts,
1993
+ sandboxOpts = {},
1994
+ runtimeOverrides = {},
1995
+ ) {
1996
+ let runtime = resolveRuntime(runtimeOverrides);
1997
+ const requiredBoundaries = Array.isArray(sandboxOpts.requiredBoundaries)
1998
+ ? sandboxOpts.requiredBoundaries
1999
+ : [];
2000
+ const requiresAppContainer = requiredBoundaries.some(
2001
+ (boundary) =>
2002
+ boundary === SANDBOX_BOUNDARIES.FILESYSTEM ||
2003
+ boundary === SANDBOX_BOUNDARIES.NETWORK,
2004
+ );
2005
+ const backend = requiresAppContainer
2006
+ ? WINDOWS_APPCONTAINER_BACKEND
2007
+ : WINDOWS_RESTRICTED_TOKEN_BACKEND;
2008
+ const appContainerGuarantees = [
2009
+ SANDBOX_BOUNDARIES.FILESYSTEM,
2010
+ SANDBOX_BOUNDARIES.NETWORK,
2011
+ SANDBOX_BOUNDARIES.PROCESS_TREE,
2012
+ SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
2013
+ SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
2014
+ ];
2015
+ const base = {
2016
+ platform: runtime.platform,
2017
+ profile: sandboxOpts.profileName || "default",
2018
+ backend,
550
2019
  command,
551
2020
  args,
552
2021
  options: spawnOpts,
553
2022
  };
554
- if (runtime.platform !== "win32") {
555
- return createSandboxPlan({
2023
+ let appContainerPolicyDigest = null;
2024
+ const unavailablePlan = (reason, extra = {}) =>
2025
+ createSandboxPlan({
556
2026
  ...base,
557
- reason: "platform_mismatch",
2027
+ ...(requiresAppContainer
2028
+ ? {
2029
+ backend: null,
2030
+ candidateBackend: WINDOWS_APPCONTAINER_BACKEND,
2031
+ policyAttested: false,
2032
+ policyDigest: appContainerPolicyDigest,
2033
+ }
2034
+ : {}),
2035
+ reason,
2036
+ ...extra,
558
2037
  });
2038
+ if (runtime.platform !== "win32") {
2039
+ return unavailablePlan("platform_mismatch");
2040
+ }
2041
+ let windowsDirectory;
2042
+ try {
2043
+ windowsDirectory = path.win32.resolve(runtime.windowsDir());
2044
+ } catch {
2045
+ windowsDirectory = null;
2046
+ }
2047
+ if (
2048
+ typeof windowsDirectory !== "string" ||
2049
+ !/^[A-Za-z]:\\/.test(windowsDirectory) ||
2050
+ windowsDirectory.includes("\0")
2051
+ ) {
2052
+ return unavailablePlan("windows_trusted_system_root_unavailable");
2053
+ }
2054
+ // Resolve once per plan so even trusted embedding hooks cannot redirect the
2055
+ // host executable, cwd, and environment between individual launch steps.
2056
+ runtime = { ...runtime, windowsDir: () => windowsDirectory };
2057
+
2058
+ if (
2059
+ requiresAppContainer &&
2060
+ requiredBoundaries.some(
2061
+ (boundary) => !appContainerGuarantees.includes(boundary),
2062
+ )
2063
+ ) {
2064
+ return unavailablePlan("windows_appcontainer_boundary_unsupported");
559
2065
  }
560
2066
 
561
2067
  // The hosted Windows restricted-token path cannot reliably preserve file
@@ -568,10 +2074,7 @@ export function applyWindowsSandbox(
568
2074
  Array.isArray(spawnOpts?.stdio) &&
569
2075
  spawnOpts.stdio.slice(0, 3).some((entry) => Number.isInteger(entry))
570
2076
  ) {
571
- return createSandboxPlan({
572
- ...base,
573
- reason: "windows_detached_file_stdio_unsupported",
574
- });
2077
+ return unavailablePlan("windows_detached_file_stdio_unsupported");
575
2078
  }
576
2079
 
577
2080
  const nodeIpcFd = Array.isArray(spawnOpts?.stdio)
@@ -589,18 +2092,7 @@ export function applyWindowsSandbox(
589
2092
  entry !== undefined,
590
2093
  )
591
2094
  ) {
592
- return createSandboxPlan({
593
- ...base,
594
- reason: "windows_extra_descriptor_unsupported",
595
- });
596
- }
597
-
598
- const adapter = materializeWindowsAdapter(runtime);
599
- if (!adapter.cacheExecutable) {
600
- return createSandboxPlan({
601
- ...base,
602
- reason: adapter.reason,
603
- });
2095
+ return unavailablePlan("windows_extra_descriptor_unsupported");
604
2096
  }
605
2097
 
606
2098
  const invocation = normalizeWrappedInvocation(
@@ -609,10 +2101,86 @@ export function applyWindowsSandbox(
609
2101
  spawnOpts,
610
2102
  runtime.platform,
611
2103
  );
2104
+ const entrySnapshot = windowsPluginNodeEntrySnapshot(
2105
+ invocation,
2106
+ sandboxOpts,
2107
+ spawnOpts,
2108
+ );
2109
+ if (entrySnapshot.reason) {
2110
+ return unavailablePlan(entrySnapshot.reason);
2111
+ }
2112
+ if (entrySnapshot.locks && nodeIpcFd >= 0) {
2113
+ return unavailablePlan("windows_plugin_entry_snapshot_ipc_unsupported");
2114
+ }
2115
+ const snapshotTestGate = entrySnapshot.locks
2116
+ ? runtime.windowsSnapshotTestGate
2117
+ : null;
2118
+ if (
2119
+ snapshotTestGate !== null &&
2120
+ snapshotTestGate !== undefined &&
2121
+ (typeof snapshotTestGate !== "object" ||
2122
+ typeof snapshotTestGate.token !== "string" ||
2123
+ !/^[a-f0-9]{64}$/.test(snapshotTestGate.token) ||
2124
+ typeof snapshotTestGate.releasePath !== "string" ||
2125
+ !path.win32.isAbsolute(snapshotTestGate.releasePath) ||
2126
+ snapshotTestGate.releasePath.includes("\0"))
2127
+ ) {
2128
+ return unavailablePlan("windows_plugin_entry_snapshot_test_gate_invalid");
2129
+ }
612
2130
  const profile = sandboxOpts.profileName || "default";
2131
+ const executableName = path.win32
2132
+ .basename(String(invocation.command))
2133
+ .toLowerCase();
2134
+ if (
2135
+ profile === "default" &&
2136
+ requiredBoundaries.length === 0 &&
2137
+ !entrySnapshot.locks &&
2138
+ (executableName === "git" || executableName === "git.exe")
2139
+ ) {
2140
+ return unavailablePlan("windows_git_nested_process_compatibility");
2141
+ }
2142
+ const loaderMode =
2143
+ profile !== "strict" && !requiresAppContainer && !entrySnapshot.locks
2144
+ ? "managed-executable"
2145
+ : "powershell-byte-assembly";
2146
+ const adapterSource = loadWindowsAdapterSource(runtime, loaderMode);
2147
+ if (!adapterSource.content) {
2148
+ return unavailablePlan(adapterSource.reason);
2149
+ }
2150
+ const targetEnvironment = sanitizeWindowsSandboxEnvironment(
2151
+ invocation.options.env,
2152
+ Boolean(entrySnapshot.locks),
2153
+ );
2154
+ setWindowsEnvironmentValue(targetEnvironment, "CC_WINDOWS_SANDBOXED", "1");
2155
+ setWindowsEnvironmentValue(
2156
+ targetEnvironment,
2157
+ "CC_WINDOWS_SANDBOX_PROFILE",
2158
+ profile,
2159
+ );
2160
+ if (nodeIpcFd >= 0) {
2161
+ setWindowsEnvironmentValue(
2162
+ targetEnvironment,
2163
+ "NODE_CHANNEL_FD",
2164
+ String(nodeIpcFd),
2165
+ );
2166
+ setWindowsEnvironmentValue(
2167
+ targetEnvironment,
2168
+ "NODE_CHANNEL_SERIALIZATION_MODE",
2169
+ invocation.options.serialization === "advanced" ? "advanced" : "json",
2170
+ );
2171
+ }
2172
+ const hostEnvironment = windowsSandboxHostEnvironment(runtime);
2173
+ const helperWorkingDirectory = runtime.joinPath(
2174
+ runtime.windowsDir(),
2175
+ "System32",
2176
+ );
613
2177
  const limits = sandboxOpts.limits || {};
2178
+ const targetWorkingDirectory = runtime.resolvePath(
2179
+ invocation.options.cwd || process.cwd(),
2180
+ );
614
2181
  const identityPath =
615
- spawnOpts?.detached === true
2182
+ spawnOpts?.detached === true ||
2183
+ (requiresAppContainer && sandboxOpts.sync !== true)
616
2184
  ? runtime.joinPath(
617
2185
  runtime.tmpdir(),
618
2186
  `chainless-win-sandbox-identity-${runtime
@@ -626,186 +2194,1811 @@ export function applyWindowsSandbox(
626
2194
  limits.as || (profile === "strict" ? 256 * 1024 * 1024 : 0),
627
2195
  ),
628
2196
  activeProcessLimit: profile === "strict" ? 16 : 64,
2197
+ allowReparsePaths: loaderMode === "managed-executable",
2198
+ disableAdministratorSids:
2199
+ profile === "strict" ||
2200
+ requiresAppContainer ||
2201
+ Boolean(entrySnapshot.locks),
629
2202
  command: invocation.command,
630
2203
  args: invocation.args,
631
2204
  nodeIpcFd,
632
2205
  detached: invocation.options.detached === true,
633
2206
  windowsHide: invocation.options.windowsHide === true,
2207
+ workingDirectory: targetWorkingDirectory,
2208
+ environment: targetEnvironment,
634
2209
  };
2210
+ if (entrySnapshot.locks) {
2211
+ launchSpec.launchPathLocks = entrySnapshot.locks;
2212
+ if (snapshotTestGate) {
2213
+ launchSpec.snapshotTestGateToken = snapshotTestGate.token;
2214
+ launchSpec.snapshotTestGateReleasePath = snapshotTestGate.releasePath;
2215
+ }
2216
+ }
2217
+ if (requiresAppContainer) {
2218
+ appContainerPolicyDigest = windowsAppContainerPolicyDigest({
2219
+ profile,
2220
+ requiredBoundaries,
2221
+ guarantees: appContainerGuarantees,
2222
+ adapterSource,
2223
+ launchSpec,
2224
+ entrySnapshot,
2225
+ executionContract: sandboxOpts.executionContract,
2226
+ });
2227
+ }
635
2228
  if (identityPath) launchSpec.identityPath = identityPath;
636
- const payload = Buffer.from(JSON.stringify(launchSpec), "utf8").toString(
637
- "base64",
2229
+ const cleanupIdentity = () => {
2230
+ if (!identityPath) return true;
2231
+ return cleanupOrTrackWindowsTemporaryPath(runtime, identityPath);
2232
+ };
2233
+ const appContainerProfileName = requiresAppContainer
2234
+ ? `ChainlessChain.CliSandbox.${runtime.randomBytes(12).toString("hex")}`
2235
+ : null;
2236
+ const adapter = createWindowsAdapterController(
2237
+ runtime,
2238
+ adapterSource,
2239
+ hostEnvironment,
638
2240
  );
639
- const helperArgs = [payload];
640
- const options = {
641
- ...invocation.options,
642
- shell: false,
643
- env: {
644
- ...(invocation.options.env || process.env),
645
- CC_WINDOWS_SANDBOXED: "1",
646
- CC_WINDOWS_SANDBOX_PROFILE: profile,
647
- },
2241
+ if (!adapter.ensureExecutable) {
2242
+ cleanupIdentity();
2243
+ return unavailablePlan(adapter.reason);
2244
+ }
2245
+
2246
+ let appContainer = null;
2247
+ let appContainerRuntimeProbe = null;
2248
+ let nodeSnapshotRuntimeProbe = null;
2249
+ if (entrySnapshot.locks && !requiresAppContainer) {
2250
+ let probeResult;
2251
+ try {
2252
+ probeResult = adapter.spawnSync(
2253
+ ["--probe-node-snapshot", entrySnapshot.locks[0].path],
2254
+ {
2255
+ shell: false,
2256
+ windowsHide: true,
2257
+ encoding: "utf8",
2258
+ timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
2259
+ },
2260
+ );
2261
+ } catch (error) {
2262
+ probeResult = { error, status: null };
2263
+ }
2264
+ nodeSnapshotRuntimeProbe = windowsNodeSnapshotProbeResult(probeResult);
2265
+ if (!nodeSnapshotRuntimeProbe.runnable) {
2266
+ adapter.release();
2267
+ cleanupIdentity();
2268
+ return unavailablePlan("windows_plugin_entry_snapshot_probe_failed", {
2269
+ runtimeProbe: nodeSnapshotRuntimeProbe,
2270
+ });
2271
+ }
2272
+ }
2273
+ if (requiresAppContainer) {
2274
+ let readinessResult;
2275
+ try {
2276
+ const readinessArgs = ["--prepare-appcontainer", appContainerProfileName];
2277
+ if (entrySnapshot.locks) {
2278
+ readinessArgs.push(entrySnapshot.locks[0].path);
2279
+ }
2280
+ readinessResult = adapter.spawnSync(readinessArgs, {
2281
+ shell: false,
2282
+ windowsHide: true,
2283
+ encoding: "utf8",
2284
+ timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
2285
+ });
2286
+ } catch (error) {
2287
+ readinessResult = { error, status: null };
2288
+ }
2289
+ const readiness = appContainerReadinessResult(
2290
+ readinessResult,
2291
+ appContainerProfileName,
2292
+ entrySnapshot.locks ? "node" : null,
2293
+ );
2294
+ appContainerRuntimeProbe = readiness.runtimeProbe;
2295
+ if (!readiness.readiness) {
2296
+ const cleanupFailure = {};
2297
+ const deleted = deleteWindowsAppContainerProfile(
2298
+ adapter,
2299
+ appContainerProfileName,
2300
+ null,
2301
+ cleanupFailure,
2302
+ );
2303
+ if (!deleted) {
2304
+ trackWindowsAppContainerCleanup(
2305
+ runtime,
2306
+ adapterSource,
2307
+ hostEnvironment,
2308
+ appContainerProfileName,
2309
+ null,
2310
+ );
2311
+ }
2312
+ adapter.release();
2313
+ if (!deleted) {
2314
+ appContainerRuntimeProbe = {
2315
+ ...appContainerRuntimeProbe,
2316
+ reason: appContainerCleanupFailureReason(
2317
+ appContainerRuntimeProbe.reason,
2318
+ cleanupFailure.reason,
2319
+ ),
2320
+ };
2321
+ cleanupIdentity();
2322
+ return unavailablePlan(
2323
+ "windows_appcontainer_readiness_cleanup_unverified",
2324
+ {
2325
+ runtimeProbe: appContainerRuntimeProbe,
2326
+ },
2327
+ );
2328
+ }
2329
+ cleanupIdentity();
2330
+ return unavailablePlan("windows_appcontainer_readiness_failed", {
2331
+ runtimeProbe: appContainerRuntimeProbe,
2332
+ });
2333
+ }
2334
+ appContainer = {
2335
+ appContainerProfileName,
2336
+ appContainerSid: readiness.readiness.appContainerSid,
2337
+ };
2338
+ launchSpec.appContainerProfileName = appContainer.appContainerProfileName;
2339
+ launchSpec.appContainerSid = appContainer.appContainerSid;
2340
+ setWindowsEnvironmentValue(
2341
+ targetEnvironment,
2342
+ "CC_WINDOWS_APPCONTAINER",
2343
+ "1",
2344
+ );
2345
+ setWindowsEnvironmentValue(
2346
+ targetEnvironment,
2347
+ "CC_WINDOWS_APPCONTAINER_PROFILE",
2348
+ appContainer.appContainerProfileName,
2349
+ );
2350
+ setWindowsEnvironmentValue(
2351
+ targetEnvironment,
2352
+ "CC_WINDOWS_APPCONTAINER_SID",
2353
+ appContainer.appContainerSid,
2354
+ );
2355
+ }
2356
+
2357
+ let helperInvocation;
2358
+ try {
2359
+ const payload = Buffer.from(JSON.stringify(launchSpec), "utf8").toString(
2360
+ "base64",
2361
+ );
2362
+ // This is the final synchronous operation before the broker consumes the
2363
+ // plan and spawns the helper. Internal readiness/deletion invocations also
2364
+ // pass through the same digest + file-identity attestation.
2365
+ helperInvocation = adapter.createInvocation([payload]);
2366
+ } catch (error) {
2367
+ const cleanupFailure = {};
2368
+ const deleted = appContainer
2369
+ ? deleteWindowsAppContainerProfile(
2370
+ adapter,
2371
+ appContainer.appContainerProfileName,
2372
+ appContainer.appContainerSid,
2373
+ cleanupFailure,
2374
+ )
2375
+ : true;
2376
+ if (!deleted) {
2377
+ trackWindowsAppContainerCleanup(
2378
+ runtime,
2379
+ adapterSource,
2380
+ hostEnvironment,
2381
+ appContainer.appContainerProfileName,
2382
+ appContainer.appContainerSid,
2383
+ );
2384
+ }
2385
+ adapter.release();
2386
+ cleanupIdentity();
2387
+ if (!deleted) {
2388
+ return unavailablePlan(
2389
+ "windows_appcontainer_readiness_cleanup_unverified",
2390
+ {
2391
+ runtimeProbe: appContainerRuntimeProbe
2392
+ ? {
2393
+ ...appContainerRuntimeProbe,
2394
+ runnable: false,
2395
+ reason: appContainerCleanupFailureReason(
2396
+ appContainerRuntimeProbe.reason || "ready",
2397
+ cleanupFailure.reason,
2398
+ ),
2399
+ }
2400
+ : null,
2401
+ },
2402
+ );
2403
+ }
2404
+ return unavailablePlan(
2405
+ error?.adapterReason || "windows_native_adapter_attestation_failed",
2406
+ { runtimeProbe: appContainerRuntimeProbe },
2407
+ );
2408
+ }
2409
+
2410
+ const options = {
2411
+ ...invocation.options,
2412
+ // The managed helper is the lifetime supervisor and must stay attached to
2413
+ // the broker so its bootstrap can reliably publish the target identity.
2414
+ // The payload still carries `detached: true`, so the helper creates the
2415
+ // actual target with DETACHED_PROCESS + CREATE_NEW_PROCESS_GROUP. Detaching
2416
+ // the outer PowerShell host as well caused it to stall before writing the
2417
+ // identity file on Windows.
2418
+ detached: false,
2419
+ shell: false,
2420
+ cwd: helperWorkingDirectory,
2421
+ env: hostEnvironment,
2422
+ };
2423
+ let appContainerCleanupVerified = false;
2424
+ let appContainerCleanupRecord = null;
2425
+ const cleanupAppContainer = () => {
2426
+ if (!appContainer || appContainerCleanupVerified) return true;
2427
+ if (appContainerCleanupRecord) {
2428
+ if (!retryWindowsAppContainerCleanup(appContainerCleanupRecord)) {
2429
+ return false;
2430
+ }
2431
+ appContainerCleanupRecord = null;
2432
+ appContainerCleanupVerified = true;
2433
+ return true;
2434
+ }
2435
+ // Native.Run deletes the profile on every ordinary target exit. This
2436
+ // second, intentionally idempotent path covers a failed spawn or a helper
2437
+ // that was forcibly terminated before its finally block ran. The helper
2438
+ // re-derives and compares the expected SID, then proves absence by
2439
+ // creating and deleting a fresh verification profile with the same name.
2440
+ for (let attempt = 0; attempt < 4; attempt += 1) {
2441
+ const deleted = deleteWindowsAppContainerProfile(
2442
+ adapter,
2443
+ appContainer.appContainerProfileName,
2444
+ appContainer.appContainerSid,
2445
+ );
2446
+ if (deleted) {
2447
+ appContainerCleanupVerified = true;
2448
+ return true;
2449
+ }
2450
+ if (attempt < 3) runtime.sleepSync(25);
2451
+ }
2452
+ appContainerCleanupRecord = trackWindowsAppContainerCleanup(
2453
+ runtime,
2454
+ adapterSource,
2455
+ hostEnvironment,
2456
+ appContainer.appContainerProfileName,
2457
+ appContainer.appContainerSid,
2458
+ );
2459
+ return false;
2460
+ };
2461
+ const cleanup = () => {
2462
+ const failures = [];
2463
+ const attempt = (label, action) => {
2464
+ try {
2465
+ if (action() === false) failures.push(label);
2466
+ } catch (error) {
2467
+ failures.push(`${label}: ${error?.message || error}`);
2468
+ }
2469
+ };
2470
+ try {
2471
+ attempt("target identity", cleanupIdentity);
2472
+ attempt("invocation payload", helperInvocation.cleanup);
2473
+ attempt("AppContainer profile", cleanupAppContainer);
2474
+ } finally {
2475
+ adapter.release();
2476
+ }
2477
+ if (failures.length > 0) {
2478
+ const message =
2479
+ "Windows sandbox cleanup could not be verified for " +
2480
+ failures.join(", ");
2481
+ if (sandboxOpts.sync === true) throw new Error(message);
2482
+ runtime.warn(message);
2483
+ return false;
2484
+ }
2485
+ return true;
2486
+ };
2487
+ const identityContract = identityPath
2488
+ ? {
2489
+ postSpawn: { required: true, mode: "sync" },
2490
+ postSpawnWindows: (proc) => {
2491
+ try {
2492
+ const identity = waitForWindowsTargetIdentity(
2493
+ proc,
2494
+ identityPath,
2495
+ runtime,
2496
+ appContainer,
2497
+ );
2498
+ if (!cleanupIdentity()) {
2499
+ throw new Error(
2500
+ "Windows sandbox target identity cleanup could not be verified",
2501
+ );
2502
+ }
2503
+ return identity;
2504
+ } catch (error) {
2505
+ try {
2506
+ proc.kill?.();
2507
+ } catch {
2508
+ // Closing the helper is best-effort; the thrown failure remains
2509
+ // authoritative and strict mode will fail closed.
2510
+ }
2511
+ if (!cleanupIdentity()) {
2512
+ throw new Error(
2513
+ "Windows sandbox target identity cleanup could not be verified",
2514
+ { cause: error },
2515
+ );
2516
+ }
2517
+ throw error;
2518
+ }
2519
+ },
2520
+ }
2521
+ : {};
2522
+ const entrySnapshotRuntimeProbe = entrySnapshot.locks
2523
+ ? {
2524
+ ...(appContainerRuntimeProbe || nodeSnapshotRuntimeProbe),
2525
+ contentSnapshot: true,
2526
+ contentSnapshotScope: "plugin-entry-source",
2527
+ contentSnapshotMechanism:
2528
+ "verified-handle-inherited-pipe-module-compile-v1",
2529
+ handleAtomic: false,
2530
+ }
2531
+ : appContainerRuntimeProbe;
2532
+ return createSandboxPlan({
2533
+ ...base,
2534
+ applied: true,
2535
+ enforcement: backend,
2536
+ policyAttested: requiresAppContainer ? true : null,
2537
+ policyDigest: appContainerPolicyDigest,
2538
+ runtimeProbe: entrySnapshotRuntimeProbe,
2539
+ guarantees: requiresAppContainer
2540
+ ? appContainerGuarantees
2541
+ : [
2542
+ SANDBOX_BOUNDARIES.PROCESS_TREE,
2543
+ SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
2544
+ SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
2545
+ ],
2546
+ command: helperInvocation.command,
2547
+ args: helperInvocation.args,
2548
+ options,
2549
+ ...identityContract,
2550
+ cleanup,
2551
+ });
2552
+ }
2553
+
2554
+ /**
2555
+ * Retained for injected adapters that elect to perform synchronous post-spawn
2556
+ * enforcement. The built-in wrapper completes enforcement before target resume
2557
+ * and therefore does not require this path.
2558
+ *
2559
+ * @param {ChildProcess} proc - The spawned child process
2560
+ * @param {Object} sandboxResult - Result from applyWindowsSandbox
2561
+ */
2562
+ export function postSpawnWindowsSandbox(
2563
+ proc,
2564
+ sandboxResult,
2565
+ runtimeOverrides = {},
2566
+ ) {
2567
+ const runtime = resolveRuntime(runtimeOverrides);
2568
+ if (runtime.platform !== "win32" || !sandboxResult?.postSpawn?.required) {
2569
+ return;
2570
+ }
2571
+ if (typeof sandboxResult.postSpawnWindows === "function") {
2572
+ return sandboxResult.postSpawnWindows(proc);
2573
+ }
2574
+ }
2575
+
2576
+ // ---------------------------------------------------------------------------
2577
+ // Linux resource-limit enforcement
2578
+ // ---------------------------------------------------------------------------
2579
+
2580
+ function linuxBubblewrapProbe(
2581
+ attempted,
2582
+ runnable,
2583
+ reason,
2584
+ targetRuntime = "node",
2585
+ ) {
2586
+ const native = targetRuntime === "native-static-elf";
2587
+ return {
2588
+ kind: native
2589
+ ? "linux-bwrap-plugin-native-static-elf-policy-v1"
2590
+ : "linux-bwrap-plugin-node-policy-v1",
2591
+ attempted,
2592
+ runnable,
2593
+ reason,
2594
+ probeRuntime: "node",
2595
+ targetRuntime,
2596
+ contentSnapshot: false,
2597
+ handleAtomic: false,
2598
+ };
2599
+ }
2600
+
2601
+ function linuxRealpath(runtime, value) {
2602
+ const realpathSync = runtime.fs?.realpathSync;
2603
+ if (typeof realpathSync !== "function") {
2604
+ throw new Error("realpath_unavailable");
2605
+ }
2606
+ return typeof realpathSync.native === "function"
2607
+ ? realpathSync.native(value)
2608
+ : realpathSync(value);
2609
+ }
2610
+
2611
+ function linuxPathWithin(root, target) {
2612
+ const relative = path.posix.relative(root, target);
2613
+ return (
2614
+ relative !== "" &&
2615
+ relative !== ".." &&
2616
+ !relative.startsWith("../") &&
2617
+ !path.posix.isAbsolute(relative)
2618
+ );
2619
+ }
2620
+
2621
+ function linuxIdentityMatches(runtime, identity, { executable = false } = {}) {
2622
+ if (
2623
+ !identity ||
2624
+ typeof identity.realPath !== "string" ||
2625
+ !path.posix.isAbsolute(identity.realPath) ||
2626
+ typeof identity.sha256 !== "string" ||
2627
+ !/^[a-f0-9]{64}$/.test(identity.sha256) ||
2628
+ !identity.fileId ||
2629
+ !Number.isSafeInteger(identity.bytes) ||
2630
+ identity.bytes < 0 ||
2631
+ identity.bytes > LINUX_ATTESTED_FILE_MAX_BYTES ||
2632
+ identity.mtimeMs === null
2633
+ ) {
2634
+ return false;
2635
+ }
2636
+ let fd;
2637
+ try {
2638
+ if (linuxRealpath(runtime, identity.realPath) !== identity.realPath) {
2639
+ return false;
2640
+ }
2641
+ const lst = runtime.fs.lstatSync(identity.realPath);
2642
+ if (lst.isSymbolicLink() || !lst.isFile()) return false;
2643
+ if (executable && (Number(lst.mode) & 0o111) === 0) return false;
2644
+ const constants = runtime.fs.constants || fs.constants;
2645
+ const flags =
2646
+ Number(constants.O_RDONLY) |
2647
+ Number(constants.O_NOFOLLOW || 0) |
2648
+ Number(constants.O_NONBLOCK || 0);
2649
+ fd = runtime.fs.openSync(identity.realPath, flags);
2650
+ const before = runtime.fs.fstatSync(fd);
2651
+ if (
2652
+ !before.isFile() ||
2653
+ String(before.dev) !== String(identity.fileId.dev) ||
2654
+ String(before.ino) !== String(identity.fileId.ino) ||
2655
+ Number(before.size) !== Number(identity.bytes) ||
2656
+ Number(before.mtimeMs) !== Number(identity.mtimeMs)
2657
+ ) {
2658
+ return false;
2659
+ }
2660
+ const digest = crypto.createHash("sha256");
2661
+ const chunk = Buffer.allocUnsafe(
2662
+ Math.max(1, Math.min(LINUX_ATTESTATION_HASH_CHUNK_BYTES, identity.bytes)),
2663
+ );
2664
+ let offset = 0;
2665
+ while (offset < identity.bytes) {
2666
+ const read = runtime.fs.readSync(
2667
+ fd,
2668
+ chunk,
2669
+ 0,
2670
+ Math.min(chunk.length, identity.bytes - offset),
2671
+ offset,
2672
+ );
2673
+ if (read <= 0) return false;
2674
+ digest.update(chunk.subarray(0, read));
2675
+ offset += read;
2676
+ }
2677
+ const after = runtime.fs.fstatSync(fd);
2678
+ const stable =
2679
+ String(before.dev) === String(after.dev) &&
2680
+ String(before.ino) === String(after.ino) &&
2681
+ Number(before.size) === Number(after.size) &&
2682
+ Number(before.mtimeMs) === Number(after.mtimeMs);
2683
+ if (!stable) return false;
2684
+ return digest.digest("hex") === identity.sha256;
2685
+ } catch {
2686
+ return false;
2687
+ } finally {
2688
+ if (fd !== undefined) {
2689
+ try {
2690
+ runtime.fs.closeSync(fd);
2691
+ } catch {
2692
+ // best effort
2693
+ }
2694
+ }
2695
+ }
2696
+ }
2697
+
2698
+ function linuxDirectoryIdentityMatches(runtime, identity) {
2699
+ if (
2700
+ !identity ||
2701
+ typeof identity.realPath !== "string" ||
2702
+ !path.posix.isAbsolute(identity.realPath) ||
2703
+ !identity.fileId
2704
+ ) {
2705
+ return false;
2706
+ }
2707
+ try {
2708
+ const stat = runtime.fs.lstatSync(identity.realPath);
2709
+ return (
2710
+ !stat.isSymbolicLink() &&
2711
+ stat.isDirectory() &&
2712
+ String(stat.dev) === String(identity.fileId.dev) &&
2713
+ String(stat.ino) === String(identity.fileId.ino)
2714
+ );
2715
+ } catch {
2716
+ return false;
2717
+ }
2718
+ }
2719
+
2720
+ function validateLinuxPluginTree(runtime, pluginRoot) {
2721
+ try {
2722
+ const rootStat = runtime.fs.lstatSync(pluginRoot);
2723
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return false;
2724
+ const rootDevice = String(rootStat.dev);
2725
+ const pending = [pluginRoot];
2726
+ let entries = 0;
2727
+ while (pending.length > 0) {
2728
+ const current = pending.pop();
2729
+ const children = runtime.fs.readdirSync(current, {
2730
+ withFileTypes: true,
2731
+ });
2732
+ for (const child of children) {
2733
+ entries += 1;
2734
+ if (entries > LINUX_BWRAP_MAX_PLUGIN_ENTRIES) return false;
2735
+ const childPath = path.posix.join(current, child.name);
2736
+ const stat = runtime.fs.lstatSync(childPath);
2737
+ if (
2738
+ stat.isSymbolicLink() ||
2739
+ String(stat.dev) !== rootDevice ||
2740
+ stat.isSocket?.() ||
2741
+ stat.isFIFO?.() ||
2742
+ stat.isBlockDevice?.() ||
2743
+ stat.isCharacterDevice?.()
2744
+ ) {
2745
+ return false;
2746
+ }
2747
+ if (stat.isDirectory()) {
2748
+ pending.push(childPath);
2749
+ } else if (!stat.isFile() || Number(stat.nlink) !== 1) {
2750
+ return false;
2751
+ }
2752
+ }
2753
+ }
2754
+ return true;
2755
+ } catch {
2756
+ return false;
2757
+ }
2758
+ }
2759
+
2760
+ function decodeLinuxMountInfoPath(value) {
2761
+ return value.replace(/\\(040|011|012|134)/g, (_match, escaped) => {
2762
+ const decoded = {
2763
+ "040": " ",
2764
+ "011": "\t",
2765
+ "012": "\n",
2766
+ 134: "\\",
2767
+ };
2768
+ return decoded[escaped];
2769
+ });
2770
+ }
2771
+
2772
+ function linuxPluginMountTreeIsNarrow(runtime, pluginRoot) {
2773
+ try {
2774
+ const contents = String(
2775
+ runtime.fs.readFileSync("/proc/self/mountinfo", "utf8"),
2776
+ );
2777
+ let parsed = 0;
2778
+ let coveringMount = null;
2779
+ for (const line of contents.split(/\r?\n/)) {
2780
+ if (!line) continue;
2781
+ const fields = line.split(" ");
2782
+ const separator = fields.indexOf("-");
2783
+ if (
2784
+ fields.length < 10 ||
2785
+ separator < 6 ||
2786
+ separator + 2 >= fields.length
2787
+ ) {
2788
+ return false;
2789
+ }
2790
+ parsed += 1;
2791
+ const mountPoint = decodeLinuxMountInfoPath(fields[4]);
2792
+ const filesystemType = fields[separator + 1];
2793
+ if (mountPoint === pluginRoot) return false;
2794
+ if (linuxPathWithin(pluginRoot, mountPoint)) {
2795
+ return false;
2796
+ }
2797
+ if (linuxPathWithin(mountPoint, pluginRoot)) {
2798
+ if (
2799
+ !coveringMount ||
2800
+ mountPoint.length > coveringMount.mountPoint.length
2801
+ ) {
2802
+ coveringMount = { mountPoint, filesystemType };
2803
+ }
2804
+ }
2805
+ }
2806
+ return (
2807
+ parsed > 0 &&
2808
+ coveringMount !== null &&
2809
+ LINUX_LOCAL_FILESYSTEM_TYPES.has(coveringMount.filesystemType)
2810
+ );
2811
+ } catch {
2812
+ return false;
2813
+ }
2814
+ }
2815
+
2816
+ function linuxStdioIsNarrow(stdio) {
2817
+ if (stdio === undefined || stdio === null) return true;
2818
+ if (stdio === "pipe" || stdio === "ignore") return true;
2819
+ if (!Array.isArray(stdio) || stdio.length > 3) return false;
2820
+ return stdio.every(
2821
+ (entry) =>
2822
+ entry === undefined ||
2823
+ entry === null ||
2824
+ entry === "pipe" ||
2825
+ entry === "ignore",
2826
+ );
2827
+ }
2828
+
2829
+ function assignLinuxMountChildFds(mounts) {
2830
+ return mounts.map((mount, index) => ({
2831
+ ...mount,
2832
+ childFd: LINUX_BWRAP_FIRST_MOUNT_FD + index,
2833
+ }));
2834
+ }
2835
+
2836
+ function linuxStdioWithPinnedMounts(
2837
+ stdio,
2838
+ descriptors,
2839
+ { probe = false } = {},
2840
+ ) {
2841
+ let standard;
2842
+ if (probe) {
2843
+ standard = ["ignore", "pipe", "pipe"];
2844
+ } else if (Array.isArray(stdio)) {
2845
+ standard = [...stdio];
2846
+ } else if (stdio === "ignore") {
2847
+ standard = ["ignore", "ignore", "ignore"];
2848
+ } else {
2849
+ standard = ["pipe", "pipe", "pipe"];
2850
+ }
2851
+ while (standard.length < 3) standard.push(undefined);
2852
+ return [
2853
+ ...standard.slice(0, 3),
2854
+ ...descriptors.map((descriptor) => descriptor.fd),
2855
+ ];
2856
+ }
2857
+
2858
+ function validateLinuxPluginContract(
2859
+ command,
2860
+ args,
2861
+ spawnOpts,
2862
+ contract,
2863
+ runtime,
2864
+ sync,
2865
+ ) {
2866
+ const nodeContract = contract?.kind === "strict-plugin-node-bin";
2867
+ const nativeContract =
2868
+ contract?.kind === "strict-plugin-native-static-elf-bin";
2869
+ if (
2870
+ !contract ||
2871
+ contract.contractVersion !== 1 ||
2872
+ (!nodeContract && !nativeContract)
2873
+ ) {
2874
+ return { ok: false, reason: "execution_contract_missing" };
2875
+ }
2876
+ if (
2877
+ sync !== true ||
2878
+ spawnOpts?.shell !== false ||
2879
+ spawnOpts?.detached === true ||
2880
+ !linuxStdioIsNarrow(spawnOpts?.stdio) ||
2881
+ spawnOpts?.serialization !== undefined ||
2882
+ spawnOpts?.argv0 !== undefined ||
2883
+ spawnOpts?.uid !== undefined ||
2884
+ spawnOpts?.gid !== undefined
2885
+ ) {
2886
+ return { ok: false, reason: "unsupported_launch_options" };
2887
+ }
2888
+ const invalidArgs =
2889
+ !Array.isArray(args) ||
2890
+ args.some((arg) => typeof arg !== "string" || arg.includes("\0"));
2891
+ const invalidNodeLaunch =
2892
+ nodeContract &&
2893
+ (!Array.isArray(args) ||
2894
+ command !== contract.runtimePath ||
2895
+ command !== contract.runtimeIdentity?.realPath ||
2896
+ args.length < 1 ||
2897
+ args[0] !== contract.entryIdentity?.realPath);
2898
+ const invalidNativeLaunch =
2899
+ nativeContract && command !== contract.entryIdentity?.realPath;
2900
+ if (
2901
+ typeof command !== "string" ||
2902
+ !path.posix.isAbsolute(command) ||
2903
+ invalidArgs ||
2904
+ invalidNodeLaunch ||
2905
+ invalidNativeLaunch
2906
+ ) {
2907
+ return { ok: false, reason: "launch_identity_mismatch" };
2908
+ }
2909
+ if (
2910
+ nodeContract &&
2911
+ ![".js", ".cjs", ".mjs"].includes(
2912
+ path.posix.extname(contract.entryIdentity.realPath).toLowerCase(),
2913
+ )
2914
+ ) {
2915
+ return { ok: false, reason: "unsupported_plugin_entry" };
2916
+ }
2917
+ for (const value of [
2918
+ contract.pluginRoot,
2919
+ contract.workingDirectory,
2920
+ contract.rootIdentity?.realPath,
2921
+ contract.entryIdentity.realPath,
2922
+ contract.runtimeIdentity.realPath,
2923
+ ]) {
2924
+ if (
2925
+ typeof value !== "string" ||
2926
+ !path.posix.isAbsolute(value) ||
2927
+ value.includes("\0")
2928
+ ) {
2929
+ return { ok: false, reason: "noncanonical_contract_path" };
2930
+ }
2931
+ }
2932
+ try {
2933
+ if (
2934
+ linuxRealpath(runtime, contract.pluginRoot) !== contract.pluginRoot ||
2935
+ linuxRealpath(runtime, contract.workingDirectory) !==
2936
+ contract.workingDirectory ||
2937
+ contract.workingDirectory !== contract.pluginRoot ||
2938
+ linuxRealpath(runtime, spawnOpts.cwd) !== contract.pluginRoot ||
2939
+ contract.rootIdentity?.realPath !== contract.pluginRoot ||
2940
+ !linuxPathWithin(contract.pluginRoot, contract.entryIdentity.realPath)
2941
+ ) {
2942
+ return { ok: false, reason: "noncanonical_contract_path" };
2943
+ }
2944
+ } catch {
2945
+ return { ok: false, reason: "contract_path_unavailable" };
2946
+ }
2947
+ const forbiddenRoots = new Set([
2948
+ "/",
2949
+ "/bin",
2950
+ "/dev",
2951
+ "/etc",
2952
+ "/home",
2953
+ "/lib",
2954
+ "/lib64",
2955
+ "/proc",
2956
+ "/root",
2957
+ "/run",
2958
+ "/sys",
2959
+ "/tmp",
2960
+ "/usr",
2961
+ "/var",
2962
+ path.posix.resolve(runtime.homedir()),
2963
+ ]);
2964
+ if (
2965
+ forbiddenRoots.has(contract.pluginRoot) ||
2966
+ ["/dev", "/proc", "/run", "/sys"].some(
2967
+ (root) =>
2968
+ contract.pluginRoot === root ||
2969
+ contract.pluginRoot.startsWith(`${root}/`),
2970
+ )
2971
+ ) {
2972
+ return { ok: false, reason: "broad_plugin_root_disallowed" };
2973
+ }
2974
+ if (
2975
+ !linuxDirectoryIdentityMatches(runtime, contract.rootIdentity) ||
2976
+ !linuxIdentityMatches(runtime, contract.entryIdentity, {
2977
+ executable: nativeContract,
2978
+ }) ||
2979
+ !linuxIdentityMatches(runtime, contract.runtimeIdentity, {
2980
+ executable: true,
2981
+ })
2982
+ ) {
2983
+ return { ok: false, reason: "execution_identity_changed" };
2984
+ }
2985
+ let entryFormat = null;
2986
+ if (nativeContract) {
2987
+ try {
2988
+ entryFormat = inspectLinuxStaticNativePath(
2989
+ runtime,
2990
+ contract.entryIdentity,
2991
+ );
2992
+ } catch (error) {
2993
+ return {
2994
+ ok: false,
2995
+ reason: error.message || "native_entry_unattested",
2996
+ };
2997
+ }
2998
+ }
2999
+ if (
3000
+ !validateLinuxPluginTree(runtime, contract.pluginRoot) ||
3001
+ !linuxPluginMountTreeIsNarrow(runtime, contract.pluginRoot)
3002
+ ) {
3003
+ return { ok: false, reason: "plugin_tree_unattested" };
3004
+ }
3005
+ return {
3006
+ ok: true,
3007
+ contract,
3008
+ entryRuntime: nativeContract ? "native-static-elf" : "node",
3009
+ entryFormat,
3010
+ };
3011
+ }
3012
+
3013
+ function linuxStatMatches(left, right) {
3014
+ return (
3015
+ String(left.dev) === String(right.dev) &&
3016
+ String(left.ino) === String(right.ino) &&
3017
+ Number(left.size) === Number(right.size) &&
3018
+ Number(left.mtimeMs) === Number(right.mtimeMs)
3019
+ );
3020
+ }
3021
+
3022
+ function linuxOpenStatMatches(left, right) {
3023
+ const timestamp = (stat, nanosecondsKey, millisecondsKey) =>
3024
+ stat[nanosecondsKey] !== undefined
3025
+ ? String(stat[nanosecondsKey])
3026
+ : String(Math.trunc(Number(stat[millisecondsKey] || 0) * 1_000_000));
3027
+ return (
3028
+ linuxStatMatches(left, right) &&
3029
+ String(left.mode) === String(right.mode) &&
3030
+ String(left.nlink) === String(right.nlink) &&
3031
+ timestamp(left, "ctimeNs", "ctimeMs") ===
3032
+ timestamp(right, "ctimeNs", "ctimeMs") &&
3033
+ timestamp(left, "mtimeNs", "mtimeMs") ===
3034
+ timestamp(right, "mtimeNs", "mtimeMs")
3035
+ );
3036
+ }
3037
+
3038
+ function readLinuxFdExactly(runtime, fd, bytes, position) {
3039
+ const result = Buffer.allocUnsafe(bytes);
3040
+ let offset = 0;
3041
+ while (offset < bytes) {
3042
+ const read = runtime.fs.readSync(
3043
+ fd,
3044
+ result,
3045
+ offset,
3046
+ bytes - offset,
3047
+ position + offset,
3048
+ );
3049
+ if (read <= 0) throw new Error("elf_file_ended_early");
3050
+ offset += read;
3051
+ }
3052
+ return result;
3053
+ }
3054
+
3055
+ function hashLinuxOpenFile(runtime, fd, bytes) {
3056
+ const digest = crypto.createHash("sha256");
3057
+ const chunk = Buffer.allocUnsafe(
3058
+ Math.max(1, Math.min(LINUX_ATTESTATION_HASH_CHUNK_BYTES, bytes)),
3059
+ );
3060
+ let offset = 0;
3061
+ while (offset < bytes) {
3062
+ const read = runtime.fs.readSync(
3063
+ fd,
3064
+ chunk,
3065
+ 0,
3066
+ Math.min(chunk.length, bytes - offset),
3067
+ offset,
3068
+ );
3069
+ if (read <= 0) throw new Error("elf_file_ended_early");
3070
+ digest.update(chunk.subarray(0, read));
3071
+ offset += read;
3072
+ }
3073
+ return digest.digest("hex");
3074
+ }
3075
+
3076
+ /**
3077
+ * Classify one already-pinned plugin entry without invoking it or any host
3078
+ * inspection utility. The native slice accepts conventional fully static
3079
+ * ELF64 ET_EXEC images and a narrow static-PIE-shaped ELF:
3080
+ * ET_DYN without PT_INTERP, with one bounded PT_DYNAMIC table that declares
3081
+ * PIE but no external DT_NEEDED dependency.
3082
+ */
3083
+ function inspectLinuxStaticNativeElf(runtime, fd, identity) {
3084
+ const expectedMachine = LINUX_ELF_MACHINES[runtime.arch];
3085
+ if (!expectedMachine) {
3086
+ throw new Error(`unsupported_elf_architecture:${String(runtime.arch)}`);
3087
+ }
3088
+ const before = runtime.fs.fstatSync(fd);
3089
+ if (
3090
+ !before.isFile() ||
3091
+ Number(before.nlink) !== 1 ||
3092
+ (Number(before.mode) & 0o111) === 0 ||
3093
+ (Number(before.mode) & 0o6000) !== 0 ||
3094
+ String(before.dev) !== String(identity?.fileId?.dev) ||
3095
+ String(before.ino) !== String(identity?.fileId?.ino) ||
3096
+ Number(before.size) !== Number(identity?.bytes) ||
3097
+ Number(before.mtimeMs) !== Number(identity?.mtimeMs) ||
3098
+ Number(before.size) < LINUX_ELF64_HEADER_BYTES
3099
+ ) {
3100
+ throw new Error("native_entry_identity_changed");
3101
+ }
3102
+
3103
+ const header = readLinuxFdExactly(runtime, fd, LINUX_ELF64_HEADER_BYTES, 0);
3104
+ if (
3105
+ header[0] !== 0x7f ||
3106
+ header[1] !== 0x45 ||
3107
+ header[2] !== 0x4c ||
3108
+ header[3] !== 0x46
3109
+ ) {
3110
+ throw new Error("native_entry_not_elf");
3111
+ }
3112
+ if (header[4] !== 2) throw new Error("native_entry_not_elf64");
3113
+ if (header[5] !== 1) throw new Error("native_entry_not_little_endian");
3114
+ if (header[6] !== 1 || header.readUInt32LE(20) !== 1) {
3115
+ throw new Error("native_entry_invalid_elf_version");
3116
+ }
3117
+ const elfType = header.readUInt16LE(16);
3118
+ if (elfType !== LINUX_ELF_TYPE_EXEC && elfType !== LINUX_ELF_TYPE_DYN) {
3119
+ throw new Error("native_entry_unsupported_elf_type");
3120
+ }
3121
+ if (header.readUInt16LE(18) !== expectedMachine) {
3122
+ throw new Error("native_entry_architecture_mismatch");
3123
+ }
3124
+ if (header.readUInt16LE(52) !== LINUX_ELF64_HEADER_BYTES) {
3125
+ throw new Error("native_entry_invalid_elf_header");
3126
+ }
3127
+ const programHeaderBytes = header.readUInt16LE(54);
3128
+ const programHeaderCount = header.readUInt16LE(56);
3129
+ if (
3130
+ programHeaderBytes !== LINUX_ELF64_PROGRAM_HEADER_BYTES ||
3131
+ programHeaderCount < 1 ||
3132
+ programHeaderCount > LINUX_ELF_MAX_PROGRAM_HEADERS ||
3133
+ programHeaderCount === 0xffff
3134
+ ) {
3135
+ throw new Error("native_entry_invalid_program_headers");
3136
+ }
3137
+ const programHeaderOffset = header.readBigUInt64LE(32);
3138
+ const tableBytes = BigInt(programHeaderBytes) * BigInt(programHeaderCount);
3139
+ const fileBytes = BigInt(before.size);
3140
+ if (
3141
+ programHeaderOffset > fileBytes ||
3142
+ tableBytes > fileBytes - programHeaderOffset ||
3143
+ programHeaderOffset > BigInt(Number.MAX_SAFE_INTEGER)
3144
+ ) {
3145
+ throw new Error("native_entry_program_headers_out_of_bounds");
3146
+ }
3147
+
3148
+ const entryAddress = header.readBigUInt64LE(24);
3149
+ let executableLoad = false;
3150
+ let entryInExecutableLoad = false;
3151
+ let nonExecutableStack = false;
3152
+ const loadSegments = [];
3153
+ const dynamicSegments = [];
3154
+ for (let index = 0; index < programHeaderCount; index += 1) {
3155
+ const offset =
3156
+ Number(programHeaderOffset) + index * LINUX_ELF64_PROGRAM_HEADER_BYTES;
3157
+ const program = readLinuxFdExactly(
3158
+ runtime,
3159
+ fd,
3160
+ LINUX_ELF64_PROGRAM_HEADER_BYTES,
3161
+ offset,
3162
+ );
3163
+ const type = program.readUInt32LE(0);
3164
+ const flags = program.readUInt32LE(4);
3165
+ const fileOffset = program.readBigUInt64LE(8);
3166
+ const virtualAddress = program.readBigUInt64LE(16);
3167
+ const fileSize = program.readBigUInt64LE(32);
3168
+ const memorySize = program.readBigUInt64LE(40);
3169
+ if (fileOffset > fileBytes || fileSize > fileBytes - fileOffset) {
3170
+ throw new Error("native_entry_segment_out_of_bounds");
3171
+ }
3172
+ if (type === LINUX_ELF_PROGRAM_LOAD && fileSize > memorySize) {
3173
+ throw new Error("native_entry_segment_out_of_bounds");
3174
+ }
3175
+ if (type === LINUX_ELF_PROGRAM_INTERP) {
3176
+ throw new Error("native_entry_interpreter_unsupported");
3177
+ }
3178
+ if (type === LINUX_ELF_PROGRAM_DYNAMIC) {
3179
+ if (elfType === LINUX_ELF_TYPE_EXEC) {
3180
+ throw new Error("native_entry_dynamic_elf_unsupported");
3181
+ }
3182
+ dynamicSegments.push({
3183
+ flags,
3184
+ fileOffset,
3185
+ virtualAddress,
3186
+ fileSize,
3187
+ memorySize,
3188
+ });
3189
+ }
3190
+ if (type === LINUX_ELF_PROGRAM_GNU_STACK) {
3191
+ if ((flags & 0x1) !== 0) {
3192
+ throw new Error("native_entry_executable_stack_unsupported");
3193
+ }
3194
+ nonExecutableStack = true;
3195
+ }
3196
+ if (type === LINUX_ELF_PROGRAM_LOAD) {
3197
+ loadSegments.push({
3198
+ flags,
3199
+ fileOffset,
3200
+ virtualAddress,
3201
+ fileSize,
3202
+ memorySize,
3203
+ });
3204
+ }
3205
+ if (type === LINUX_ELF_PROGRAM_LOAD && (flags & 0x1) !== 0) {
3206
+ if ((flags & 0x2) !== 0) {
3207
+ throw new Error("native_entry_writable_executable_segment");
3208
+ }
3209
+ executableLoad = true;
3210
+ if (
3211
+ entryAddress >= virtualAddress &&
3212
+ entryAddress - virtualAddress < memorySize
3213
+ ) {
3214
+ entryInExecutableLoad = true;
3215
+ }
3216
+ }
3217
+ }
3218
+ if (!executableLoad || !entryInExecutableLoad) {
3219
+ throw new Error("native_entry_has_no_executable_entry_segment");
3220
+ }
3221
+ if (!nonExecutableStack) {
3222
+ throw new Error("native_entry_nonexecutable_stack_unattested");
3223
+ }
3224
+
3225
+ if (elfType === LINUX_ELF_TYPE_DYN) {
3226
+ if (dynamicSegments.length === 0) {
3227
+ throw new Error("native_entry_static_pie_dynamic_segment_missing");
3228
+ }
3229
+ if (dynamicSegments.length !== 1) {
3230
+ throw new Error("native_entry_static_pie_dynamic_segment_ambiguous");
3231
+ }
3232
+ const dynamic = dynamicSegments[0];
3233
+ if ((dynamic.flags & 0x1) !== 0) {
3234
+ throw new Error("native_entry_static_pie_dynamic_segment_executable");
3235
+ }
3236
+ if (
3237
+ dynamic.fileSize < BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES) ||
3238
+ dynamic.fileSize % BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES) !== 0n ||
3239
+ dynamic.fileSize > dynamic.memorySize
3240
+ ) {
3241
+ throw new Error("native_entry_static_pie_dynamic_segment_invalid");
3242
+ }
3243
+ const dynamicEntries =
3244
+ dynamic.fileSize / BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES);
3245
+ if (
3246
+ dynamicEntries < 1n ||
3247
+ dynamicEntries > BigInt(LINUX_ELF_MAX_DYNAMIC_ENTRIES)
3248
+ ) {
3249
+ throw new Error("native_entry_static_pie_dynamic_table_too_large");
3250
+ }
3251
+ const containingLoad = loadSegments.find((load) => {
3252
+ if (
3253
+ dynamic.fileOffset < load.fileOffset ||
3254
+ dynamic.virtualAddress < load.virtualAddress
3255
+ ) {
3256
+ return false;
3257
+ }
3258
+ const fileDelta = dynamic.fileOffset - load.fileOffset;
3259
+ const memoryDelta = dynamic.virtualAddress - load.virtualAddress;
3260
+ return (
3261
+ fileDelta === memoryDelta &&
3262
+ fileDelta <= load.fileSize &&
3263
+ dynamic.fileSize <= load.fileSize - fileDelta &&
3264
+ memoryDelta <= load.memorySize &&
3265
+ dynamic.memorySize <= load.memorySize - memoryDelta
3266
+ );
3267
+ });
3268
+ if (!containingLoad) {
3269
+ throw new Error("native_entry_static_pie_dynamic_segment_unmapped");
3270
+ }
3271
+
3272
+ const dynamicTable = readLinuxFdExactly(
3273
+ runtime,
3274
+ fd,
3275
+ Number(dynamic.fileSize),
3276
+ Number(dynamic.fileOffset),
3277
+ );
3278
+ let terminated = false;
3279
+ let flags1 = null;
3280
+ for (
3281
+ let offset = 0;
3282
+ offset < dynamicTable.length;
3283
+ offset += LINUX_ELF64_DYNAMIC_ENTRY_BYTES
3284
+ ) {
3285
+ const tag = dynamicTable.readBigUInt64LE(offset);
3286
+ const value = dynamicTable.readBigUInt64LE(offset + 8);
3287
+ if (tag === LINUX_ELF_DYNAMIC_NULL) {
3288
+ terminated = true;
3289
+ break;
3290
+ }
3291
+ if (tag === LINUX_ELF_DYNAMIC_NEEDED) {
3292
+ throw new Error("native_entry_static_pie_dependency_unsupported");
3293
+ }
3294
+ if (tag === LINUX_ELF_DYNAMIC_FLAGS_1) {
3295
+ if (flags1 !== null) {
3296
+ throw new Error("native_entry_static_pie_flags_ambiguous");
3297
+ }
3298
+ flags1 = value;
3299
+ }
3300
+ }
3301
+ if (!terminated) {
3302
+ throw new Error("native_entry_static_pie_dynamic_table_unterminated");
3303
+ }
3304
+ if (flags1 === null || (flags1 & LINUX_ELF_DYNAMIC_FLAG_PIE) === 0n) {
3305
+ throw new Error("native_entry_static_pie_flag_missing");
3306
+ }
3307
+ }
3308
+
3309
+ const sha256 = hashLinuxOpenFile(runtime, fd, Number(before.size));
3310
+ const after = runtime.fs.fstatSync(fd);
3311
+ if (!linuxOpenStatMatches(before, after) || sha256 !== identity.sha256) {
3312
+ throw new Error("native_entry_changed_during_elf_attestation");
3313
+ }
3314
+ return Object.freeze({
3315
+ format:
3316
+ elfType === LINUX_ELF_TYPE_DYN
3317
+ ? "elf64-static-pie-shaped-et-dyn"
3318
+ : "elf64-static-et-exec",
3319
+ architecture: runtime.arch,
3320
+ machine: expectedMachine,
3321
+ programHeaders: programHeaderCount,
3322
+ });
3323
+ }
3324
+
3325
+ function inspectLinuxStaticNativePath(runtime, identity) {
3326
+ let fd;
3327
+ try {
3328
+ const constants = runtime.fs.constants || fs.constants;
3329
+ const flags =
3330
+ Number(constants.O_RDONLY) |
3331
+ Number(constants.O_NOFOLLOW || 0) |
3332
+ Number(constants.O_NONBLOCK || 0);
3333
+ fd = runtime.fs.openSync(identity.realPath, flags);
3334
+ return inspectLinuxStaticNativeElf(runtime, fd, identity);
3335
+ } finally {
3336
+ if (fd !== undefined) {
3337
+ try {
3338
+ runtime.fs.closeSync(fd);
3339
+ } catch {
3340
+ // best effort
3341
+ }
3342
+ }
3343
+ }
3344
+ }
3345
+
3346
+ function linuxFdMountId(runtime, fd) {
3347
+ const contents = String(
3348
+ runtime.fs.readFileSync(`/proc/self/fdinfo/${fd}`, "utf8"),
3349
+ );
3350
+ const match = contents.match(/^mnt_id:\s*(\d+)\s*$/m);
3351
+ if (!match) throw new Error("fd_mount_identity_unavailable");
3352
+ return match[1];
3353
+ }
3354
+
3355
+ function closeLinuxPinnedMounts(runtime, mounts) {
3356
+ for (const mount of mounts || []) {
3357
+ if (!Number.isInteger(mount?.fd)) continue;
3358
+ try {
3359
+ runtime.fs.closeSync(mount.fd);
3360
+ } catch {
3361
+ // Best effort. The process is already fail-closed if a pin cannot be
3362
+ // created; cleanup must not replace the original boundary error.
3363
+ }
3364
+ }
3365
+ }
3366
+
3367
+ function pinLinuxRegularFile(
3368
+ runtime,
3369
+ source,
3370
+ destination,
3371
+ expectedStat,
3372
+ {
3373
+ requireSingleLink = false,
3374
+ openPath = source,
3375
+ verifyCurrentPath = true,
3376
+ expectedMountId = null,
3377
+ } = {},
3378
+ ) {
3379
+ let fd;
3380
+ try {
3381
+ const constants = runtime.fs.constants || fs.constants;
3382
+ const flags =
3383
+ Number(constants.O_RDONLY) |
3384
+ Number(constants.O_NOFOLLOW || 0) |
3385
+ Number(constants.O_NONBLOCK || 0);
3386
+ fd = runtime.fs.openSync(openPath, flags);
3387
+ const opened = runtime.fs.fstatSync(fd);
3388
+ const current = verifyCurrentPath ? runtime.fs.statSync(source) : opened;
3389
+ if (
3390
+ !opened.isFile() ||
3391
+ !linuxStatMatches(opened, current) ||
3392
+ (expectedStat && !linuxStatMatches(opened, expectedStat)) ||
3393
+ (requireSingleLink && Number(opened.nlink) !== 1) ||
3394
+ (expectedMountId !== null &&
3395
+ linuxFdMountId(runtime, fd) !== expectedMountId)
3396
+ ) {
3397
+ throw new Error("pinned_file_identity_changed");
3398
+ }
3399
+ return {
3400
+ source,
3401
+ destination,
3402
+ fd,
3403
+ fileId: {
3404
+ dev: String(opened.dev),
3405
+ ino: String(opened.ino),
3406
+ },
3407
+ bytes: Number(opened.size),
3408
+ mtimeMs: Number(opened.mtimeMs),
3409
+ };
3410
+ } catch (error) {
3411
+ if (fd !== undefined) {
3412
+ try {
3413
+ runtime.fs.closeSync(fd);
3414
+ } catch {
3415
+ // best effort
3416
+ }
3417
+ }
3418
+ throw error;
3419
+ }
3420
+ }
3421
+
3422
+ /**
3423
+ * Build a synthetic plugin tree from individually pinned regular files.
3424
+ * Never bind the mutable source directory itself: files added, hard-linked,
3425
+ * symlinked, or mount-injected after this scan cannot appear in the sandbox.
3426
+ */
3427
+ function pinLinuxPluginTree(runtime, pluginRoot, rootIdentity) {
3428
+ const mounts = [];
3429
+ const directoryPins = [];
3430
+ try {
3431
+ const constants = runtime.fs.constants || fs.constants;
3432
+ const directoryFlags =
3433
+ Number(constants.O_RDONLY) |
3434
+ Number(constants.O_DIRECTORY || 0) |
3435
+ Number(constants.O_NOFOLLOW || 0) |
3436
+ Number(constants.O_NONBLOCK || 0);
3437
+ const rootFd = runtime.fs.openSync(pluginRoot, directoryFlags);
3438
+ directoryPins.push({ fd: rootFd });
3439
+ const rootStat = runtime.fs.fstatSync(rootFd);
3440
+ const rootPathStat = runtime.fs.lstatSync(pluginRoot);
3441
+ if (
3442
+ rootPathStat.isSymbolicLink() ||
3443
+ !rootStat.isDirectory() ||
3444
+ !linuxStatMatches(rootStat, rootPathStat) ||
3445
+ String(rootStat.dev) !== String(rootIdentity?.fileId?.dev) ||
3446
+ String(rootStat.ino) !== String(rootIdentity?.fileId?.ino)
3447
+ ) {
3448
+ throw new Error("plugin_root_not_directory");
3449
+ }
3450
+ const rootMountId = linuxFdMountId(runtime, rootFd);
3451
+ const rootDevice = String(rootStat.dev);
3452
+ const directories = new Set(["/opt/chainless/plugin"]);
3453
+ const pending = [{ fd: rootFd, source: pluginRoot, relative: "" }];
3454
+ let entries = 0;
3455
+ while (pending.length > 0) {
3456
+ const current = pending.pop();
3457
+ const currentHandlePath = `/proc/self/fd/${current.fd}`;
3458
+ const children = runtime.fs
3459
+ .readdirSync(currentHandlePath, { withFileTypes: true })
3460
+ .map((child) => child.name)
3461
+ .sort((left, right) => left.localeCompare(right));
3462
+ for (const name of children) {
3463
+ if (
3464
+ typeof name !== "string" ||
3465
+ !name ||
3466
+ name === "." ||
3467
+ name === ".." ||
3468
+ name.includes("/") ||
3469
+ name.includes("\0")
3470
+ ) {
3471
+ throw new Error("plugin_tree_entry_invalid");
3472
+ }
3473
+ entries += 1;
3474
+ if (entries > LINUX_BWRAP_MAX_PLUGIN_ENTRIES) {
3475
+ throw new Error("plugin_tree_too_large");
3476
+ }
3477
+ const source = path.posix.join(current.source, name);
3478
+ const sourceHandlePath = `${currentHandlePath}/${name}`;
3479
+ const relative = current.relative
3480
+ ? path.posix.join(current.relative, name)
3481
+ : name;
3482
+ const destination = path.posix.join("/opt/chainless/plugin", relative);
3483
+ const stat = runtime.fs.lstatSync(sourceHandlePath);
3484
+ if (
3485
+ stat.isSymbolicLink() ||
3486
+ String(stat.dev) !== rootDevice ||
3487
+ stat.isSocket?.() ||
3488
+ stat.isFIFO?.() ||
3489
+ stat.isBlockDevice?.() ||
3490
+ stat.isCharacterDevice?.()
3491
+ ) {
3492
+ throw new Error("plugin_tree_entry_unattested");
3493
+ }
3494
+ if (stat.isDirectory()) {
3495
+ const fd = runtime.fs.openSync(sourceHandlePath, directoryFlags);
3496
+ const opened = runtime.fs.fstatSync(fd);
3497
+ directoryPins.push({ fd });
3498
+ if (
3499
+ !opened.isDirectory() ||
3500
+ !linuxStatMatches(opened, stat) ||
3501
+ linuxFdMountId(runtime, fd) !== rootMountId
3502
+ ) {
3503
+ throw new Error("plugin_tree_directory_changed");
3504
+ }
3505
+ directories.add(destination);
3506
+ pending.push({ fd, source, relative });
3507
+ } else if (stat.isFile() && Number(stat.nlink) === 1) {
3508
+ mounts.push(
3509
+ pinLinuxRegularFile(runtime, source, destination, stat, {
3510
+ openPath: sourceHandlePath,
3511
+ verifyCurrentPath: false,
3512
+ requireSingleLink: true,
3513
+ expectedMountId: rootMountId,
3514
+ }),
3515
+ );
3516
+ } else {
3517
+ throw new Error("plugin_tree_entry_unattested");
3518
+ }
3519
+ }
3520
+ }
3521
+ closeLinuxPinnedMounts(runtime, directoryPins);
3522
+ return {
3523
+ directories: [...directories].sort((left, right) => {
3524
+ const depth =
3525
+ left.split("/").filter(Boolean).length -
3526
+ right.split("/").filter(Boolean).length;
3527
+ return depth || left.localeCompare(right);
3528
+ }),
3529
+ mounts,
3530
+ };
3531
+ } catch (error) {
3532
+ closeLinuxPinnedMounts(runtime, directoryPins);
3533
+ closeLinuxPinnedMounts(runtime, mounts);
3534
+ throw error;
3535
+ }
3536
+ }
3537
+
3538
+ function buildLinuxNetworkSeccompFilter(arch) {
3539
+ const architecture = LINUX_SECCOMP_FILTERS[arch];
3540
+ if (!architecture) {
3541
+ throw new Error(`unsupported_seccomp_architecture:${String(arch)}`);
3542
+ }
3543
+ const instructions = [
3544
+ // Reject a different syscall ABI instead of interpreting overlapping
3545
+ // syscall numbers under the wrong architecture.
3546
+ [0x20, 0, 0, 4],
3547
+ [0x15, 1, 0, architecture.auditArch],
3548
+ [0x06, 0, 0, 0x80000000],
3549
+ [0x20, 0, 0, 0],
3550
+ ];
3551
+ if (architecture.x32SyscallBit) {
3552
+ instructions.push([0x54, 0, 0, ~architecture.x32SyscallBit]);
3553
+ }
3554
+ instructions.push(
3555
+ // No socket family is needed by this direct foreground Plugin Node route.
3556
+ // Blocking creation rather than relying only on CLONE_NEWNET also closes
3557
+ // non-network-namespaced transports such as AF_VSOCK.
3558
+ [0x15, 0, 1, architecture.socketSyscall],
3559
+ [0x06, 0, 0, 0x00050001],
3560
+ [0x15, 0, 1, architecture.socketpairSyscall],
3561
+ [0x06, 0, 0, 0x00050001],
3562
+ // A new io_uring can create and connect sockets without the socket(2)
3563
+ // syscall. No ring descriptor is inherited by this narrow launch route.
3564
+ [0x15, 0, 1, architecture.ioUringSetupSyscall],
3565
+ [0x06, 0, 0, 0x00050001],
3566
+ [0x06, 0, 0, 0x7fff0000],
3567
+ );
3568
+ const buffer = Buffer.alloc(instructions.length * 8);
3569
+ instructions.forEach(([code, jumpTrue, jumpFalse, value], index) => {
3570
+ const offset = index * 8;
3571
+ buffer.writeUInt16LE(code, offset);
3572
+ buffer.writeUInt8(jumpTrue, offset + 2);
3573
+ buffer.writeUInt8(jumpFalse, offset + 3);
3574
+ buffer.writeUInt32LE(value >>> 0, offset + 4);
3575
+ });
3576
+ return {
3577
+ arch,
3578
+ buffer,
3579
+ sha256: crypto.createHash("sha256").update(buffer).digest("hex"),
648
3580
  };
649
- const cleanupIdentity = () => {
650
- if (!identityPath) return;
651
- try {
652
- runtime.fs.unlinkSync(identityPath);
653
- } catch {
654
- // The post-spawn handshake normally removes this file first.
3581
+ }
3582
+
3583
+ function pinLinuxNetworkSeccompFilter(runtime) {
3584
+ const filter = buildLinuxNetworkSeccompFilter(runtime.arch);
3585
+ let fd;
3586
+ try {
3587
+ for (const method of [
3588
+ "openSync",
3589
+ "writeSync",
3590
+ "readSync",
3591
+ "fchmodSync",
3592
+ "fsyncSync",
3593
+ "fstatSync",
3594
+ ]) {
3595
+ if (typeof runtime.fs[method] !== "function") {
3596
+ throw new Error(`seccomp_filter_${method}_unavailable`);
3597
+ }
655
3598
  }
656
- };
657
- const identityContract = identityPath
658
- ? {
659
- postSpawn: { required: true, mode: "sync" },
660
- postSpawnWindows: (proc) => {
661
- try {
662
- return waitForWindowsTargetIdentity(proc, identityPath, runtime);
663
- } catch (error) {
664
- try {
665
- proc.kill?.();
666
- } catch {
667
- // Closing the helper is best-effort; the thrown failure remains
668
- // authoritative and strict mode will fail closed.
669
- }
670
- throw error;
671
- } finally {
672
- cleanupIdentity();
673
- }
674
- },
3599
+ const constants = runtime.fs.constants || fs.constants;
3600
+ const flags =
3601
+ Number(constants.O_RDWR) |
3602
+ Number(constants.O_EXCL) |
3603
+ Number(constants.O_TMPFILE ?? LINUX_O_TMPFILE) |
3604
+ Number(constants.O_NOFOLLOW || 0) |
3605
+ Number(constants.O_NONBLOCK || 0);
3606
+ // An anonymous inode avoids a same-UID process opening the filter by path.
3607
+ // Mode 0400 applies from creation; this process can still write through
3608
+ // the already-open O_RDWR descriptor, but no new writable open is allowed.
3609
+ fd = runtime.fs.openSync("/tmp", flags, 0o400);
3610
+ runtime.fs.fchmodSync(fd, 0o400);
3611
+ let offset = 0;
3612
+ while (offset < filter.buffer.length) {
3613
+ const written = runtime.fs.writeSync(
3614
+ fd,
3615
+ filter.buffer,
3616
+ offset,
3617
+ filter.buffer.length - offset,
3618
+ offset,
3619
+ );
3620
+ if (written <= 0) throw new Error("seccomp_filter_write_failed");
3621
+ offset += written;
3622
+ }
3623
+ runtime.fs.fsyncSync(fd);
3624
+ const before = runtime.fs.fstatSync(fd);
3625
+ if (!before.isFile() || Number(before.size) !== filter.buffer.length) {
3626
+ throw new Error("seccomp_filter_identity_changed");
3627
+ }
3628
+ const observed = Buffer.allocUnsafe(filter.buffer.length);
3629
+ offset = 0;
3630
+ while (offset < observed.length) {
3631
+ const read = runtime.fs.readSync(
3632
+ fd,
3633
+ observed,
3634
+ offset,
3635
+ observed.length - offset,
3636
+ offset,
3637
+ );
3638
+ if (read <= 0) throw new Error("seccomp_filter_read_failed");
3639
+ offset += read;
3640
+ }
3641
+ const after = runtime.fs.fstatSync(fd);
3642
+ const observedSha256 = crypto
3643
+ .createHash("sha256")
3644
+ .update(observed)
3645
+ .digest("hex");
3646
+ if (!linuxStatMatches(before, after) || observedSha256 !== filter.sha256) {
3647
+ throw new Error("seccomp_filter_identity_changed");
3648
+ }
3649
+ return {
3650
+ fd,
3651
+ childFd: null,
3652
+ arch: filter.arch,
3653
+ sha256: filter.sha256,
3654
+ policy: "deny-network-creation",
3655
+ };
3656
+ } catch (error) {
3657
+ if (fd !== undefined) {
3658
+ try {
3659
+ runtime.fs.closeSync(fd);
3660
+ } catch {
3661
+ // best effort
675
3662
  }
676
- : {};
3663
+ }
3664
+ throw error;
3665
+ }
3666
+ }
677
3667
 
678
- if (adapter.bootstrapRequired) {
679
- const powershell = runtime.joinPath(
680
- runtime.windowsDir(),
681
- "System32",
682
- "WindowsPowerShell",
683
- "v1.0",
684
- "powershell.exe",
685
- );
686
- if (!runtime.fs.existsSync(powershell)) {
687
- adapter.cleanup?.();
688
- cleanupIdentity();
689
- return createSandboxPlan({
690
- ...base,
691
- reason: "windows_powershell_host_unavailable",
692
- });
3668
+ function attestLinuxRootOwnedFile(
3669
+ runtime,
3670
+ filePath,
3671
+ { executable = false } = {},
3672
+ ) {
3673
+ try {
3674
+ if (!runtime.fs.existsSync(filePath)) return null;
3675
+ const source = linuxRealpath(runtime, filePath);
3676
+ const before = runtime.fs.lstatSync(source);
3677
+ if (
3678
+ !before.isFile() ||
3679
+ before.isSymbolicLink() ||
3680
+ Number(before.uid) !== 0 ||
3681
+ (Number(before.mode) & 0o022) !== 0 ||
3682
+ (executable && (Number(before.mode) & 0o111) === 0)
3683
+ ) {
3684
+ return null;
693
3685
  }
694
- const compileResult = runtime.spawnSync(
695
- powershell,
696
- [
697
- "-NoLogo",
698
- "-NoProfile",
699
- "-NonInteractive",
700
- "-ExecutionPolicy",
701
- "Bypass",
702
- "-File",
703
- adapter.scriptPath,
704
- "-CacheExecutable",
705
- adapter.cacheExecutable,
706
- "-CompileOnly",
707
- ],
708
- {
709
- shell: false,
710
- windowsHide: true,
711
- encoding: "utf8",
712
- timeout: 30_000,
713
- env: invocation.options.env || process.env,
714
- },
715
- );
716
- adapter.cleanup?.();
3686
+ const after = runtime.fs.statSync(source);
717
3687
  if (
718
- compileResult?.error ||
719
- compileResult?.status !== 0 ||
720
- !runtime.fs.existsSync(adapter.cacheExecutable)
3688
+ String(before.dev) !== String(after.dev) ||
3689
+ String(before.ino) !== String(after.ino) ||
3690
+ Number(before.size) !== Number(after.size) ||
3691
+ Number(before.mtimeMs) !== Number(after.mtimeMs)
721
3692
  ) {
722
- cleanupIdentity();
723
- return createSandboxPlan({
724
- ...base,
725
- reason: "windows_native_adapter_compile_failed",
726
- });
3693
+ return null;
727
3694
  }
3695
+ return source;
3696
+ } catch {
3697
+ return null;
728
3698
  }
3699
+ }
729
3700
 
730
- return createSandboxPlan({
731
- ...base,
732
- applied: true,
733
- enforcement: "windows-job-restricted-token",
734
- guarantees: [
735
- SANDBOX_BOUNDARIES.PROCESS_TREE,
736
- SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
737
- SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
738
- ],
739
- command: adapter.cacheExecutable,
740
- args: helperArgs,
741
- options,
742
- ...identityContract,
743
- cleanup: cleanupIdentity,
744
- });
3701
+ function attestLinuxBubblewrapBinary(runtime) {
3702
+ return (
3703
+ attestLinuxRootOwnedFile(runtime, LINUX_BWRAP_PATH, {
3704
+ executable: true,
3705
+ }) === LINUX_BWRAP_PATH
3706
+ );
745
3707
  }
746
3708
 
747
- /**
748
- * Retained for injected adapters that elect to perform synchronous post-spawn
749
- * enforcement. The built-in wrapper completes enforcement before target resume
750
- * and therefore does not require this path.
751
- *
752
- * @param {ChildProcess} proc - The spawned child process
753
- * @param {Object} sandboxResult - Result from applyWindowsSandbox
754
- */
755
- export function postSpawnWindowsSandbox(
756
- proc,
757
- sandboxResult,
758
- runtimeOverrides = {},
759
- ) {
760
- const runtime = resolveRuntime(runtimeOverrides);
761
- if (runtime.platform !== "win32" || !sandboxResult?.postSpawn?.required) {
762
- return;
3709
+ function attestLinuxBubblewrapCapabilities(runtime) {
3710
+ let result;
3711
+ try {
3712
+ result = runtime.spawnSync(LINUX_BWRAP_PATH, ["--help"], {
3713
+ shell: false,
3714
+ encoding: "utf8",
3715
+ stdio: ["ignore", "pipe", "pipe"],
3716
+ timeout: 10_000,
3717
+ env: {
3718
+ PATH: "/usr/bin:/bin",
3719
+ LANG: "C",
3720
+ LC_ALL: "C",
3721
+ },
3722
+ });
3723
+ } catch {
3724
+ return { ok: false, reason: "capability_probe_spawn_failed" };
763
3725
  }
764
- if (typeof sandboxResult.postSpawnWindows === "function") {
765
- return sandboxResult.postSpawnWindows(proc);
3726
+ if (result?.error || result?.status !== 0) {
3727
+ return { ok: false, reason: "capability_probe_failed" };
3728
+ }
3729
+ const help = `${String(result.stdout || "")}\n${String(result.stderr || "")}`;
3730
+ for (const option of [
3731
+ "--ro-bind-fd",
3732
+ "--disable-userns",
3733
+ "--assert-userns-disabled",
3734
+ "--seccomp",
3735
+ ]) {
3736
+ if (!help.includes(option)) {
3737
+ return {
3738
+ ok: false,
3739
+ reason: `required_option_missing:${option.slice(2)}`,
3740
+ };
3741
+ }
766
3742
  }
3743
+ return { ok: true, reason: null };
767
3744
  }
768
3745
 
769
- // ---------------------------------------------------------------------------
770
- // Linux resource-limit enforcement
771
- // ---------------------------------------------------------------------------
3746
+ function parseLinuxLddPaths(output) {
3747
+ const paths = new Set();
3748
+ for (const line of String(output || "").split(/\r?\n/)) {
3749
+ if (line.includes("=> not found")) {
3750
+ throw new Error("runtime_dependency_missing");
3751
+ }
3752
+ const resolved = line.match(/=>\s+(\/[^\s(]+)\s+\(/)?.[1];
3753
+ const loader = line.match(/^\s*(\/[^\s(]+)\s+\(/)?.[1];
3754
+ if (resolved) paths.add(resolved);
3755
+ if (loader) paths.add(loader);
3756
+ }
3757
+ return [...paths];
3758
+ }
772
3759
 
773
- /**
774
- * Prove only that the installed bubblewrap binary can create the namespace and
775
- * mount primitives needed by a future policy backend. This deliberately does
776
- * not attest any filesystem/network policy and therefore must never promote a
777
- * sandbox guarantee.
778
- */
779
- function probeLinuxBubblewrapRuntime(runtime) {
780
- let binaryPresent = false;
3760
+ function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
3761
+ if (
3762
+ attestLinuxRootOwnedFile(runtime, LINUX_LDD_PATH, {
3763
+ executable: true,
3764
+ }) !== LINUX_LDD_PATH
3765
+ ) {
3766
+ throw new Error("ldd_unavailable");
3767
+ }
3768
+ const runtimeExpected = {
3769
+ dev: runtimeIdentity.fileId.dev,
3770
+ ino: runtimeIdentity.fileId.ino,
3771
+ size: runtimeIdentity.bytes,
3772
+ mtimeMs: runtimeIdentity.mtimeMs,
3773
+ };
3774
+ const inspectionPin = pinLinuxRegularFile(
3775
+ runtime,
3776
+ runtimeIdentity.realPath,
3777
+ "/opt/chainless/runtime/node",
3778
+ runtimeExpected,
3779
+ );
3780
+ let result;
781
3781
  try {
782
- binaryPresent = runtime.fs.existsSync(LINUX_BWRAP_PATH);
783
- } catch {
784
- return {
785
- kind: "linux-bwrap-runtime-smoke-v1",
786
- attempted: false,
787
- runnable: false,
788
- reason: "binary_probe_failed",
789
- };
3782
+ result = runtime.spawnSync(LINUX_LDD_PATH, ["/proc/self/fd/3"], {
3783
+ shell: false,
3784
+ encoding: "utf8",
3785
+ stdio: ["ignore", "pipe", "pipe", inspectionPin.fd],
3786
+ timeout: 10_000,
3787
+ env: {
3788
+ PATH: "/usr/bin:/bin",
3789
+ LANG: "C",
3790
+ LC_ALL: "C",
3791
+ },
3792
+ });
3793
+ } finally {
3794
+ closeLinuxPinnedMounts(runtime, [inspectionPin]);
790
3795
  }
791
- if (!binaryPresent) {
792
- return {
793
- kind: "linux-bwrap-runtime-smoke-v1",
794
- attempted: false,
795
- runnable: false,
796
- reason: "binary_missing",
797
- };
3796
+ if (result?.error || result?.status !== 0) {
3797
+ throw new Error("runtime_dependency_probe_failed");
3798
+ }
3799
+ const mounts = [
3800
+ {
3801
+ source: runtimeIdentity.realPath,
3802
+ destination: "/opt/chainless/runtime/node",
3803
+ },
3804
+ ];
3805
+ for (const destination of parseLinuxLddPaths(result.stdout)) {
3806
+ if (
3807
+ path.posix.normalize(destination) !== destination ||
3808
+ !["/lib/", "/lib64/", "/usr/lib/"].some((prefix) =>
3809
+ destination.startsWith(prefix),
3810
+ )
3811
+ ) {
3812
+ throw new Error("runtime_dependency_outside_system_library_roots");
3813
+ }
3814
+ const source = attestLinuxRootOwnedFile(runtime, destination);
3815
+ if (!source) {
3816
+ throw new Error("runtime_dependency_unattested");
3817
+ }
3818
+ mounts.push({ source, destination });
3819
+ }
3820
+ if (runtime.fs.existsSync("/etc/ld.so.cache")) {
3821
+ const source = attestLinuxRootOwnedFile(runtime, "/etc/ld.so.cache");
3822
+ if (!source) {
3823
+ throw new Error("loader_cache_unattested");
3824
+ }
3825
+ mounts.push({ source, destination: "/etc/ld.so.cache" });
3826
+ }
3827
+ const byDestination = new Map();
3828
+ for (const mount of mounts) {
3829
+ const existing = byDestination.get(mount.destination);
3830
+ if (existing && existing.source !== mount.source) {
3831
+ throw new Error("runtime_mount_collision");
3832
+ }
3833
+ byDestination.set(mount.destination, mount);
3834
+ }
3835
+ return [...byDestination.values()].sort((left, right) =>
3836
+ left.destination.localeCompare(right.destination),
3837
+ );
3838
+ }
3839
+
3840
+ function pinLinuxRuntimeMounts(runtime, runtimeMounts, runtimeIdentity) {
3841
+ const mounts = [];
3842
+ try {
3843
+ for (const mount of runtimeMounts) {
3844
+ const expected =
3845
+ mount.source === runtimeIdentity.realPath
3846
+ ? {
3847
+ dev: runtimeIdentity.fileId.dev,
3848
+ ino: runtimeIdentity.fileId.ino,
3849
+ size: runtimeIdentity.bytes,
3850
+ mtimeMs: runtimeIdentity.mtimeMs,
3851
+ }
3852
+ : runtime.fs.statSync(mount.source);
3853
+ mounts.push(
3854
+ pinLinuxRegularFile(runtime, mount.source, mount.destination, expected),
3855
+ );
3856
+ }
3857
+ return mounts;
3858
+ } catch (error) {
3859
+ closeLinuxPinnedMounts(runtime, mounts);
3860
+ throw error;
3861
+ }
3862
+ }
3863
+
3864
+ function linuxSandboxEnvironment() {
3865
+ return {
3866
+ HOME: "/home/sandbox",
3867
+ TMPDIR: "/tmp",
3868
+ PATH: "/opt/chainless/runtime",
3869
+ LANG: "C.UTF-8",
3870
+ LC_ALL: "C.UTF-8",
3871
+ TZ: "UTC",
3872
+ OPENSSL_CONF: "/dev/null",
3873
+ CHAINLESS_SANDBOXED: "1",
3874
+ };
3875
+ }
3876
+
3877
+ function buildLinuxBubblewrapPolicyArgs(
3878
+ pluginDirectories,
3879
+ pinnedMounts,
3880
+ environment,
3881
+ seccompFilter,
3882
+ ) {
3883
+ const directories = new Set([
3884
+ "/opt",
3885
+ "/opt/chainless",
3886
+ "/opt/chainless/runtime",
3887
+ "/opt/chainless/plugin",
3888
+ "/home",
3889
+ "/home/sandbox",
3890
+ "/tmp",
3891
+ "/run",
3892
+ "/var",
3893
+ "/var/tmp",
3894
+ "/proc",
3895
+ "/dev",
3896
+ ...(pluginDirectories || []),
3897
+ ]);
3898
+ for (const mount of pinnedMounts) {
3899
+ let parent = path.posix.dirname(mount.destination);
3900
+ while (parent !== "/") {
3901
+ directories.add(parent);
3902
+ parent = path.posix.dirname(parent);
3903
+ }
3904
+ }
3905
+ const orderedDirectories = [...directories].sort((left, right) => {
3906
+ const depth =
3907
+ left.split("/").filter(Boolean).length -
3908
+ right.split("/").filter(Boolean).length;
3909
+ return depth || left.localeCompare(right);
3910
+ });
3911
+ const args = [
3912
+ "--die-with-parent",
3913
+ "--new-session",
3914
+ "--unshare-user",
3915
+ "--disable-userns",
3916
+ "--assert-userns-disabled",
3917
+ "--unshare-pid",
3918
+ "--unshare-ipc",
3919
+ "--unshare-net",
3920
+ "--unshare-uts",
3921
+ "--unshare-cgroup-try",
3922
+ "--cap-drop",
3923
+ "ALL",
3924
+ "--hostname",
3925
+ "chainless-sandbox",
3926
+ "--clearenv",
3927
+ ];
3928
+ for (const directory of orderedDirectories) {
3929
+ args.push("--dir", directory);
3930
+ }
3931
+ for (const mount of pinnedMounts) {
3932
+ args.push("--ro-bind-fd", String(mount.childFd), mount.destination);
3933
+ }
3934
+ args.push(
3935
+ "--seccomp",
3936
+ String(seccompFilter.childFd),
3937
+ "--remount-ro",
3938
+ "/",
3939
+ "--perms",
3940
+ "1777",
3941
+ "--size",
3942
+ String(64 * 1024 * 1024),
3943
+ "--tmpfs",
3944
+ "/tmp",
3945
+ "--perms",
3946
+ "0755",
3947
+ "--size",
3948
+ String(16 * 1024 * 1024),
3949
+ "--tmpfs",
3950
+ "/run",
3951
+ "--perms",
3952
+ "1777",
3953
+ "--size",
3954
+ String(32 * 1024 * 1024),
3955
+ "--tmpfs",
3956
+ "/var/tmp",
3957
+ "--perms",
3958
+ "0700",
3959
+ "--size",
3960
+ String(16 * 1024 * 1024),
3961
+ "--tmpfs",
3962
+ "/home/sandbox",
3963
+ "--proc",
3964
+ "/proc",
3965
+ "--dev",
3966
+ "/dev",
3967
+ );
3968
+ for (const key of Object.keys(environment).sort()) {
3969
+ args.push("--setenv", key, environment[key]);
798
3970
  }
3971
+ args.push("--chdir", "/opt/chainless/plugin");
3972
+ return args;
3973
+ }
799
3974
 
3975
+ function probeLinuxBubblewrapPolicy(
3976
+ runtime,
3977
+ policyArgs,
3978
+ pinnedDescriptors,
3979
+ targetRuntime,
3980
+ ) {
800
3981
  let result;
801
3982
  try {
802
3983
  result = runtime.spawnSync(
803
3984
  LINUX_BWRAP_PATH,
804
- [...LINUX_BWRAP_RUNTIME_PROBE_ARGS],
3985
+ [
3986
+ ...policyArgs,
3987
+ "--",
3988
+ "/opt/chainless/runtime/node",
3989
+ "-e",
3990
+ `process.stdout.write(${JSON.stringify(
3991
+ LINUX_BWRAP_NODE_PROBE_SENTINEL,
3992
+ )})`,
3993
+ ],
805
3994
  {
3995
+ cwd: "/",
806
3996
  shell: false,
807
- stdio: "ignore",
808
- timeout: 5_000,
3997
+ encoding: "utf8",
3998
+ stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
3999
+ probe: true,
4000
+ }),
4001
+ timeout: 15_000,
809
4002
  env: {
810
4003
  PATH: "/usr/bin:/bin",
811
4004
  LANG: "C",
@@ -814,28 +4007,30 @@ function probeLinuxBubblewrapRuntime(runtime) {
814
4007
  },
815
4008
  );
816
4009
  } catch {
817
- return {
818
- kind: "linux-bwrap-runtime-smoke-v1",
819
- attempted: true,
820
- runnable: false,
821
- reason: "probe_spawn_failed",
822
- };
4010
+ return linuxBubblewrapProbe(
4011
+ true,
4012
+ false,
4013
+ "probe_spawn_failed",
4014
+ targetRuntime,
4015
+ );
823
4016
  }
824
-
825
4017
  if (result?.error || result?.status !== 0) {
826
- return {
827
- kind: "linux-bwrap-runtime-smoke-v1",
828
- attempted: true,
829
- runnable: false,
830
- reason: result?.error ? "probe_spawn_failed" : "probe_failed",
831
- };
4018
+ return linuxBubblewrapProbe(
4019
+ true,
4020
+ false,
4021
+ result?.error ? "probe_spawn_failed" : "probe_failed",
4022
+ targetRuntime,
4023
+ );
832
4024
  }
833
- return {
834
- kind: "linux-bwrap-runtime-smoke-v1",
835
- attempted: true,
836
- runnable: true,
837
- reason: null,
838
- };
4025
+ if (String(result.stdout) !== LINUX_BWRAP_NODE_PROBE_SENTINEL) {
4026
+ return linuxBubblewrapProbe(
4027
+ true,
4028
+ false,
4029
+ "node_runtime_probe_failed",
4030
+ targetRuntime,
4031
+ );
4032
+ }
4033
+ return linuxBubblewrapProbe(true, true, null, targetRuntime);
839
4034
  }
840
4035
 
841
4036
  /**
@@ -886,17 +4081,370 @@ export function applyLinuxSandbox(
886
4081
  );
887
4082
 
888
4083
  if (requiresStrongLinuxBoundary) {
889
- const runtimeProbe = probeLinuxBubblewrapRuntime(runtime);
4084
+ const requestedEntryRuntime =
4085
+ sandboxOpts.executionContract?.kind ===
4086
+ "strict-plugin-native-static-elf-bin"
4087
+ ? "native-static-elf"
4088
+ : "node";
4089
+ const validation = validateLinuxPluginContract(
4090
+ command,
4091
+ args,
4092
+ spawnOpts,
4093
+ sandboxOpts.executionContract,
4094
+ runtime,
4095
+ sandboxOpts.sync,
4096
+ );
4097
+ if (!validation.ok) {
4098
+ const missing = validation.reason === "execution_contract_missing";
4099
+ return createSandboxPlan({
4100
+ ...base,
4101
+ backend: null,
4102
+ candidateBackend: LINUX_BWRAP_BACKEND,
4103
+ policyAttested: false,
4104
+ runtimeProbe: linuxBubblewrapProbe(
4105
+ false,
4106
+ false,
4107
+ validation.reason,
4108
+ requestedEntryRuntime,
4109
+ ),
4110
+ reason: missing
4111
+ ? "linux_bwrap_execution_contract_missing"
4112
+ : "linux_bwrap_execution_contract_invalid",
4113
+ guarantees: [],
4114
+ });
4115
+ }
4116
+ if (!attestLinuxBubblewrapBinary(runtime)) {
4117
+ return createSandboxPlan({
4118
+ ...base,
4119
+ backend: null,
4120
+ candidateBackend: LINUX_BWRAP_BACKEND,
4121
+ policyAttested: false,
4122
+ runtimeProbe: linuxBubblewrapProbe(
4123
+ false,
4124
+ false,
4125
+ "binary_missing_or_unattested",
4126
+ validation.entryRuntime,
4127
+ ),
4128
+ reason: "linux_bwrap_unavailable",
4129
+ guarantees: [],
4130
+ });
4131
+ }
4132
+ const capabilities = attestLinuxBubblewrapCapabilities(runtime);
4133
+ if (!capabilities.ok) {
4134
+ return createSandboxPlan({
4135
+ ...base,
4136
+ backend: null,
4137
+ candidateBackend: LINUX_BWRAP_BACKEND,
4138
+ policyAttested: false,
4139
+ runtimeProbe: linuxBubblewrapProbe(
4140
+ true,
4141
+ false,
4142
+ capabilities.reason,
4143
+ validation.entryRuntime,
4144
+ ),
4145
+ reason: "linux_bwrap_unavailable",
4146
+ guarantees: [],
4147
+ });
4148
+ }
4149
+
4150
+ let runtimeMounts;
4151
+ let pinnedRuntimeMounts = [];
4152
+ try {
4153
+ runtimeMounts = collectLinuxNodeRuntimeMounts(
4154
+ runtime,
4155
+ validation.contract.runtimeIdentity,
4156
+ );
4157
+ pinnedRuntimeMounts = pinLinuxRuntimeMounts(
4158
+ runtime,
4159
+ runtimeMounts,
4160
+ validation.contract.runtimeIdentity,
4161
+ );
4162
+ } catch (error) {
4163
+ closeLinuxPinnedMounts(runtime, pinnedRuntimeMounts);
4164
+ return createSandboxPlan({
4165
+ ...base,
4166
+ backend: null,
4167
+ candidateBackend: LINUX_BWRAP_BACKEND,
4168
+ policyAttested: false,
4169
+ runtimeProbe: linuxBubblewrapProbe(
4170
+ false,
4171
+ false,
4172
+ error.message || "runtime_mounts_unattested",
4173
+ validation.entryRuntime,
4174
+ ),
4175
+ reason: "linux_bwrap_runtime_unattested",
4176
+ guarantees: [],
4177
+ });
4178
+ }
4179
+
4180
+ let pinnedMounts = [];
4181
+ let pluginTree;
4182
+ let entryFormat = null;
4183
+ let entryMount;
4184
+ try {
4185
+ pluginTree = pinLinuxPluginTree(
4186
+ runtime,
4187
+ validation.contract.pluginRoot,
4188
+ validation.contract.rootIdentity,
4189
+ );
4190
+ pinnedMounts = assignLinuxMountChildFds([
4191
+ ...pinnedRuntimeMounts,
4192
+ ...pluginTree.mounts,
4193
+ ]);
4194
+ entryMount = pinnedMounts.find(
4195
+ (mount) => mount.source === validation.contract.entryIdentity.realPath,
4196
+ );
4197
+ if (
4198
+ !entryMount ||
4199
+ String(entryMount.fileId.dev) !==
4200
+ String(validation.contract.entryIdentity.fileId.dev) ||
4201
+ String(entryMount.fileId.ino) !==
4202
+ String(validation.contract.entryIdentity.fileId.ino) ||
4203
+ entryMount.bytes !== Number(validation.contract.entryIdentity.bytes) ||
4204
+ entryMount.mtimeMs !== Number(validation.contract.entryIdentity.mtimeMs)
4205
+ ) {
4206
+ throw new Error("plugin_entry_pin_mismatch");
4207
+ }
4208
+ if (validation.entryRuntime === "native-static-elf") {
4209
+ entryFormat = inspectLinuxStaticNativeElf(
4210
+ runtime,
4211
+ entryMount.fd,
4212
+ validation.contract.entryIdentity,
4213
+ );
4214
+ if (
4215
+ JSON.stringify(entryFormat) !== JSON.stringify(validation.entryFormat)
4216
+ ) {
4217
+ throw new Error("native_entry_format_changed_before_pin");
4218
+ }
4219
+ }
4220
+ } catch (error) {
4221
+ if (pinnedMounts.length > 0) {
4222
+ closeLinuxPinnedMounts(runtime, pinnedMounts);
4223
+ } else {
4224
+ closeLinuxPinnedMounts(runtime, pinnedRuntimeMounts);
4225
+ closeLinuxPinnedMounts(runtime, pluginTree?.mounts);
4226
+ }
4227
+ return createSandboxPlan({
4228
+ ...base,
4229
+ backend: null,
4230
+ candidateBackend: LINUX_BWRAP_BACKEND,
4231
+ policyAttested: false,
4232
+ runtimeProbe: linuxBubblewrapProbe(
4233
+ false,
4234
+ false,
4235
+ error.message || "plugin_tree_unattested",
4236
+ validation.entryRuntime,
4237
+ ),
4238
+ reason: "linux_bwrap_plugin_tree_unattested",
4239
+ guarantees: [],
4240
+ });
4241
+ }
4242
+
4243
+ let probeSeccompFilter;
4244
+ let seccompFilter;
4245
+ try {
4246
+ probeSeccompFilter = pinLinuxNetworkSeccompFilter(runtime);
4247
+ seccompFilter = pinLinuxNetworkSeccompFilter(runtime);
4248
+ const seccompChildFd = LINUX_BWRAP_FIRST_MOUNT_FD + pinnedMounts.length;
4249
+ probeSeccompFilter.childFd = seccompChildFd;
4250
+ seccompFilter.childFd = seccompChildFd;
4251
+ if (probeSeccompFilter.sha256 !== seccompFilter.sha256) {
4252
+ throw new Error("seccomp_filter_digest_mismatch");
4253
+ }
4254
+ } catch (error) {
4255
+ closeLinuxPinnedMounts(runtime, pinnedMounts);
4256
+ closeLinuxPinnedMounts(
4257
+ runtime,
4258
+ [probeSeccompFilter, seccompFilter].filter(Boolean),
4259
+ );
4260
+ return createSandboxPlan({
4261
+ ...base,
4262
+ backend: null,
4263
+ candidateBackend: LINUX_BWRAP_BACKEND,
4264
+ policyAttested: false,
4265
+ runtimeProbe: linuxBubblewrapProbe(
4266
+ false,
4267
+ false,
4268
+ error.message || "seccomp_filter_unattested",
4269
+ validation.entryRuntime,
4270
+ ),
4271
+ reason: "linux_bwrap_seccomp_unattested",
4272
+ guarantees: [],
4273
+ });
4274
+ }
4275
+ const pinnedDescriptors = [...pinnedMounts, seccompFilter];
4276
+ const probeDescriptors = [...pinnedMounts, probeSeccompFilter];
4277
+ const environment = linuxSandboxEnvironment();
4278
+ const policyArgs = buildLinuxBubblewrapPolicyArgs(
4279
+ pluginTree.directories,
4280
+ pinnedMounts,
4281
+ environment,
4282
+ seccompFilter,
4283
+ );
4284
+ const entryRelative = path.posix.relative(
4285
+ validation.contract.pluginRoot,
4286
+ validation.contract.entryIdentity.realPath,
4287
+ );
4288
+ const sandboxEntry = path.posix.join(
4289
+ "/opt/chainless/plugin",
4290
+ entryRelative,
4291
+ );
4292
+ const targetArgs =
4293
+ validation.entryRuntime === "native-static-elf"
4294
+ ? [sandboxEntry, ...args]
4295
+ : ["/opt/chainless/runtime/node", sandboxEntry, ...args.slice(1)];
4296
+ const policyDigest = crypto
4297
+ .createHash("sha256")
4298
+ .update(
4299
+ JSON.stringify({
4300
+ version: 1,
4301
+ backend: LINUX_BWRAP_BACKEND,
4302
+ contract: {
4303
+ kind: validation.contract.kind,
4304
+ pluginRoot: validation.contract.pluginRoot,
4305
+ root: validation.contract.rootIdentity,
4306
+ entry: validation.contract.entryIdentity,
4307
+ runtime: validation.contract.runtimeIdentity,
4308
+ entryRuntime: validation.entryRuntime,
4309
+ entryFormat,
4310
+ },
4311
+ mounts: pinnedMounts.map((mount) => ({
4312
+ destination: mount.destination,
4313
+ fileId: mount.fileId,
4314
+ bytes: mount.bytes,
4315
+ mtimeMs: mount.mtimeMs,
4316
+ })),
4317
+ seccomp: {
4318
+ arch: seccompFilter.arch,
4319
+ policy: seccompFilter.policy,
4320
+ sha256: seccompFilter.sha256,
4321
+ },
4322
+ policyArgs,
4323
+ target:
4324
+ validation.entryRuntime === "native-static-elf"
4325
+ ? targetArgs.slice(0, 1)
4326
+ : targetArgs.slice(0, 2),
4327
+ }),
4328
+ )
4329
+ .digest("hex");
4330
+ const runtimeProbe = Object.freeze(
4331
+ probeLinuxBubblewrapPolicy(
4332
+ runtime,
4333
+ policyArgs,
4334
+ probeDescriptors,
4335
+ validation.entryRuntime,
4336
+ ),
4337
+ );
4338
+ closeLinuxPinnedMounts(runtime, [probeSeccompFilter]);
4339
+ if (!runtimeProbe.runnable) {
4340
+ closeLinuxPinnedMounts(runtime, pinnedDescriptors);
4341
+ return createSandboxPlan({
4342
+ ...base,
4343
+ backend: null,
4344
+ candidateBackend: LINUX_BWRAP_BACKEND,
4345
+ policyAttested: false,
4346
+ policyDigest,
4347
+ runtimeProbe,
4348
+ reason: "linux_bwrap_policy_probe_failed",
4349
+ guarantees: [],
4350
+ });
4351
+ }
4352
+ const finalValidation = validateLinuxPluginContract(
4353
+ command,
4354
+ args,
4355
+ spawnOpts,
4356
+ validation.contract,
4357
+ runtime,
4358
+ sandboxOpts.sync,
4359
+ );
4360
+ if (!finalValidation.ok) {
4361
+ closeLinuxPinnedMounts(runtime, pinnedDescriptors);
4362
+ return createSandboxPlan({
4363
+ ...base,
4364
+ backend: null,
4365
+ candidateBackend: LINUX_BWRAP_BACKEND,
4366
+ policyAttested: false,
4367
+ policyDigest,
4368
+ runtimeProbe: linuxBubblewrapProbe(
4369
+ true,
4370
+ false,
4371
+ `post_probe_${finalValidation.reason}`,
4372
+ validation.entryRuntime,
4373
+ ),
4374
+ reason: "linux_bwrap_execution_contract_changed",
4375
+ guarantees: [],
4376
+ });
4377
+ }
4378
+ if (validation.entryRuntime === "native-static-elf") {
4379
+ try {
4380
+ const finalEntryFormat = inspectLinuxStaticNativeElf(
4381
+ runtime,
4382
+ entryMount.fd,
4383
+ validation.contract.entryIdentity,
4384
+ );
4385
+ if (JSON.stringify(finalEntryFormat) !== JSON.stringify(entryFormat)) {
4386
+ throw new Error("native_entry_format_changed");
4387
+ }
4388
+ if (
4389
+ JSON.stringify(finalValidation.entryFormat) !==
4390
+ JSON.stringify(entryFormat)
4391
+ ) {
4392
+ throw new Error("native_entry_path_format_changed");
4393
+ }
4394
+ } catch (error) {
4395
+ closeLinuxPinnedMounts(runtime, pinnedDescriptors);
4396
+ return createSandboxPlan({
4397
+ ...base,
4398
+ backend: null,
4399
+ candidateBackend: LINUX_BWRAP_BACKEND,
4400
+ policyAttested: false,
4401
+ policyDigest,
4402
+ runtimeProbe: linuxBubblewrapProbe(
4403
+ true,
4404
+ false,
4405
+ `post_probe_${error.message || "native_entry_changed"}`,
4406
+ validation.entryRuntime,
4407
+ ),
4408
+ reason: "linux_bwrap_execution_contract_changed",
4409
+ guarantees: [],
4410
+ });
4411
+ }
4412
+ }
4413
+ const options = {
4414
+ ...spawnOpts,
4415
+ cwd: "/",
4416
+ shell: false,
4417
+ detached: false,
4418
+ env: {
4419
+ PATH: "/usr/bin:/bin",
4420
+ LANG: "C",
4421
+ LC_ALL: "C",
4422
+ },
4423
+ stdio: Object.freeze(
4424
+ linuxStdioWithPinnedMounts(spawnOpts?.stdio, pinnedDescriptors),
4425
+ ),
4426
+ };
4427
+ let pinsClosed = false;
4428
+ const cleanup = () => {
4429
+ if (pinsClosed) return;
4430
+ pinsClosed = true;
4431
+ closeLinuxPinnedMounts(runtime, pinnedDescriptors);
4432
+ };
890
4433
  return createSandboxPlan({
891
4434
  ...base,
892
- backend: null,
893
- candidateBackend: "linux-bwrap",
894
- policyAttested: false,
4435
+ applied: true,
4436
+ enforcement: LINUX_BWRAP_BACKEND,
4437
+ backend: LINUX_BWRAP_BACKEND,
4438
+ candidateBackend: null,
4439
+ policyAttested: true,
4440
+ policyDigest,
895
4441
  runtimeProbe,
896
- reason: runtimeProbe.runnable
897
- ? "linux_bwrap_policy_unattested"
898
- : "linux_bwrap_unavailable",
899
- guarantees: [],
4442
+ reason: null,
4443
+ guarantees: [SANDBOX_BOUNDARIES.FILESYSTEM, SANDBOX_BOUNDARIES.NETWORK],
4444
+ command: LINUX_BWRAP_PATH,
4445
+ args: [...policyArgs, "--", ...targetArgs],
4446
+ options,
4447
+ cleanup,
900
4448
  });
901
4449
  }
902
4450
 
@@ -953,7 +4501,12 @@ export function applyLinuxSandbox(
953
4501
  * requiredBoundaries?: string[]
954
4502
  * }} profileOrRequest
955
4503
  * @param {Object} runtimeOverrides
956
- * @param {{profile?: string, requiredBoundaries?: string[]}|null} explicitRequest
4504
+ * @param {{
4505
+ * profile?: string,
4506
+ * requiredBoundaries?: string[],
4507
+ * sync?: boolean,
4508
+ * executionContract?: Readonly<Object>|null
4509
+ * }|null} explicitRequest
957
4510
  * @returns {ReturnType<typeof createSandboxPlan>}
958
4511
  */
959
4512
  export function applySandbox(
@@ -1011,6 +4564,8 @@ export function applySandbox(
1011
4564
  ...(profiles[profileName] || profiles.default),
1012
4565
  profileName: profiles[profileName] ? profileName : "default",
1013
4566
  requiredBoundaries: sandboxRequest.requiredBoundaries,
4567
+ sync: sandboxRequest.sync,
4568
+ executionContract: sandboxRequest.executionContract,
1014
4569
  };
1015
4570
 
1016
4571
  // Dispatch to platform handler