chainlesschain 0.162.180 → 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 (263) hide show
  1. package/package.json +7 -3
  2. package/scripts/build-windows-sandbox-helper.ps1 +163 -0
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D_TIL54O.js → AIOps-DzZ5_xoj.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-eQwy9r-f.js → ActionButton-DeLXddIg.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-D2eNuDgT.js → Analytics-rvDCmet1.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CFhwOFsf.js → AppLayout-Yt6QVDnp.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-CJUMw6PB.js → Artifacts-BS112K5L.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-CDB9V58o.js → Audit-0wJuQrNu.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-TBppYFjN.js → BackgroundAgents-B90PMgbn.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-CYEOUpzJ.js → Backup-TkgMglQQ.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-C88fFT2u.js → BaseInput-CQVS9p1l.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-mkr1vC3N.js → Chat-YTjQHoC7.js} +6 -6
  14. package/src/assets/web-panel/assets/ChatBubbleRenderer-PR074hUN.js +1 -0
  15. package/src/assets/web-panel/assets/{Checkbox-CVU4xqti.js → Checkbox-5bwaOabq.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-rrNcByz3.js → Codegen-BFmNQ_Xe.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-Cqu-zCvh.js → Col-BT_u4Dzp.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-RGRlSgXV.js → Community-CJP52u5b.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DBm6epOK.js → Compact-BpzhJE9i.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-BgKw5iaW.js → Compliance-Cg7rfslp.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-BqQcNO3Z.js → Cowork-JisxL6Yy.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DO5gTFNb.js → Cron-Bu7Ug_MC.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-B8hbJNuN.js → Crosschain-CvuqoLdj.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-B08bFJ2C.js → DID-C2u9G8Pu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-xNdy_ifo.js → Dashboard-BFt5Yv3d.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D-KIZgWL.js → Dropdown-DIaT9Wxq.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-DKCvsaGo.js → EmailListRenderer-D_A601Y4.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D_cfqama.js → FamilyGuardDashboard--IyQ8I8w.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-B-UqQD68.js → Federation-BihWHxuY.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-BWWvWuJW.js → FormItemContext-BB8dOXc_.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-CPchx6N7.js → GenericCardRenderer-DQYhj1HH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-Vx4co9Ff.js → Git-B-2QZxGf.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-D4x0pp_m.js → Governance-CusSb-nG.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-T3KvwPYR.js → Inference-BAjmvPPL.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-Ddv3KUv3.js → KnowledgeGraph-DoNYWKYq.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-DfH-Dkae.js → Logs-B8fKWD-n.js} +2 -2
  37. package/src/assets/web-panel/assets/{MarkdownRenderer-DHW3PbCq.js → MarkdownRenderer-BlJm1J7E.js} +1 -1
  38. package/src/assets/web-panel/assets/{Marketplace-tjgb0R3D.js → Marketplace-CyOscbSL.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-DHAk5nYX.js → McpTools-Ex6Om5da.js} +5 -5
  40. package/src/assets/web-panel/assets/{Memory-A2Yx7thM.js → Memory-CDcD8l8r.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-BEmTVfN5.js → MobileBridge-xfBqoFDg.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-DuFHUgE4.js → MobileProjects-BwObMMA2.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-BDSll0OZ.js → Mtc-CIWd36OV.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-CuBvluaS.js → MtcAudit-CPX0ouuA.js} +4 -4
  45. package/src/assets/web-panel/assets/{Multisig-BmXAA-Sb.js → Multisig-BDJsIUuW.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-ci2stZAe.js → NLProgramming-DEkbuD3N.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-D-1RpqnS.js → Notes-DJpHRhH1.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-C0GyitIT.js → NotificationSettings-C1UZ-Qd5.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-U2bN-hj5.js → OrderTableRenderer-GlJtqLjT.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-BNZgpPci.js → Organization-Bbhj0fac.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-CDidKmUN.js → Overflow-1H04x3kx.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BnreBaXq.js → P2P-CDpPe3et.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-DwJUxO9j.js → PdhVaultBrowser-MUX0Bpvw.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-BQGgJPH0.js → Permissions-A07-0Ci1.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-k_fSkCwL.js → PersonalDataHub-DhhDWxAZ.js} +5 -5
  56. package/src/assets/web-panel/assets/{Pipeline-WGqKMqau.js → Pipeline-CyfbJfe-.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-DtHj9Ymq.js → Privacy-CNFps3ED.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-B3wH2SFC.js → ProjectInit--T-u3Dr5.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-Cd0cgcA1.js → ProjectSettings-DJmrK2fC.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-Qx_MdX9k.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DTpxEXur.js → Providers-DLzzbOZd.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-B2v6gR-C.js → QrScannerModal-CexOomKS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-D3Nr9xXR.js → QuickAsk-QklGi19_.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-C9inoMky.js → Recommend-Bb9kwSw8.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-DZuVNNN5.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-41KFG646.js → Reputation-Dr5cLP0N.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-DqAaqqCv.js → Row-oCQHYv5t.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-CMDpCDKc.js → RssFeed-IFDEykIR.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-B6mtegNL.js → Search-CspVIxDc.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-BaePFVZ8.js → Security-Bm66o_DH.js} +3 -3
  71. package/src/assets/web-panel/assets/{Services-DeG3Qscl.js → Services-DvanNqXl.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-D_QP7F90.js → Skeleton-CHjEe9_O.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-DLGyUgIA.js → Skills-Cb8jWjMX.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-BN2pxW1Q.js → Sla-DxZm1491.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-BWsZPd8B.js → SpeechSettings-DWxEEclC.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-VOZRPODW.js → SyncSettings-FIvOFw5p.js} +2 -2
  77. package/src/assets/web-panel/assets/Tasks-CyuLnDFR.js +1 -0
  78. package/src/assets/web-panel/assets/{Templates-eJSob-q0.js → Templates-CaNjzV_t.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-Bdwk8Rwy.js → Tenant-DjlP6TmH.js} +1 -1
  80. package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +3 -0
  81. package/src/assets/web-panel/assets/{TimelineRenderer-D5qkbPKT.js → TimelineRenderer-DoZ8y7y8.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DKgwn8hI.js → Tokens-390cg2TC.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-HlEHniGM.js → Trigger-CLrtCj5_.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-CdPcsBpN.js → Trust-DpESfSPn.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-vAdXd-vG.js → UkeySign-CN6279O0.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-9hHBYFnH.js → VideoEditing-DPtAUlSe.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-Dxh5d1yR.js → Wallet-nKOMV_1n.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-BsFmLHkp.js → WebAuthn-HuXlSOc4.js} +5 -5
  89. package/src/assets/web-panel/assets/{WorkflowEditor-Cn0OIwLn.js → WorkflowEditor-CUfFjARi.js} +1 -1
  90. package/src/assets/web-panel/assets/chat-6Eq0PcZJ.js +1 -0
  91. package/src/assets/web-panel/assets/{colors-Du9jzLN0.js → colors-BP_Sf6Iy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-DhEscfzw.js → compact-item-BufMKIh5.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-CwzEfF3i.js → createContext-BBMXxWSW.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DT_GSE6J.js → hasIn-fUK_tRO2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-tND8-TDU.js → index-2A5ph8b2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-B8s5FxJY.js → index-3FuwBewR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-53z1YUxX.js → index-6GmBa4Kg.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BFD0cEna.js → index-B2vS66eI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQWxDOvM.js → index-B7Etk0Te.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-VeHs6qH0.js → index-B9WYSn4m.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CGvgnfl9.js → index-BHy6yNiF.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D3llLkYM.js → index-BMLHv389.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dny_ZnAO.js → index-BWKoBU8R.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DzjzrOli.js → index-Bkg4ZePE.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DN60VBn5.js → index-C44atv4k.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-v5HLYCVa.js → index-CE2DdTP2.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BF-ZCVGE.js → index-CGjmILDm.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-gFjYdstW.js → index-CW_G-MXw.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-NA2oBLSB.js → index-CZ6Yt17L.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Mobw3PHs.js → index-CeASi4fr.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DWL3SYN3.js → index-CgiECHu2.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-1LoutPJd.js → index-Cp8vhyZy.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BnHhFnwt.js → index-CwSf5pnf.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B6QDWRLk.js → index-D4_MuGkd.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-D-QkW1fp.js → index-D9vU25ij.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C4SN3by5.js → index-DASluSqY.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CyEhUJcu.js → index-DLpIs_P_.js} +1 -1
  119. package/src/assets/web-panel/assets/index-DOZTSoS7.js +1 -0
  120. package/src/assets/web-panel/assets/{index-BrkJ7JPm.js → index-DPdUNk1i.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Cnok8mhZ.js → index-DVsqvI4M.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DhpvjpqI.js +1 -0
  123. package/src/assets/web-panel/assets/{index-CAVW0cWM.js → index-DmkMXwnR.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Cc9aPkv-.js → index-DocdRcbd.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DnVVYKCE.js → index-Dyngbd29.js} +20 -20
  126. package/src/assets/web-panel/assets/{index-D8gt0j7r.js → index-GwdRUZ-x.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BIYTjbv_.js → index-LvCBfTLo.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DhXay17G.js → index-cpEli44X.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQWENaZf.js → index-fzaMtSJ8.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DfRDyY1q.js → index-pum7HYBA.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BgSR2WMG.js → index-t8N1tSeX.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D5IytTND.js → index-uCbEN7cY.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DtRwLhee.js → index-wKTLbKyc.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-D4kZoXNr.js → index-wV6iGdpY.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-Cvt59c10.js → initDefaultProps-V5Fo95FM.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-D15pPEzl.js → motion-BYWU0hUT.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-N55KeXJ7.js → move-Cr1M-J2h.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-CyFO1bYP.js → mtc-parser-BysQKi2N.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-B5IXfxU2.js → omit-D1zHU8GC.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-CpVqsulV.js → pickAttrs-CUArxtfi.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-ME36sMF3.js → placementArrow-ChoYEys0.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-BXAIR1r-.js → responsiveObserve-BQRs3Mo7.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-BkOX3-Dp.js → slide-B8zbsCkg.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-Cy4Aklpt.js → statusUtils-C2U8Gs6h.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-gUd2T206.js → styleChecker-Dd8MgL_V.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-CaVD4o99.js → useFlexGapSupport-hknqY5NH.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs--pFGl0KD.js → useFs-CLTAmcfw.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-D45iP86i.js → usePersonalDataHub-ByegHbiB.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-BayoChH_.js → vnode-DqlAy5DP.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-CpP38poi.js → zoom-DiXiztQn.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/commands/agenda.js +115 -3
  153. package/src/commands/agent.js +10 -1
  154. package/src/commands/background-session.js +2 -1
  155. package/src/commands/context.js +69 -2
  156. package/src/data/changelog.json +26 -0
  157. package/src/gateways/terminal/PtyManager.js +45 -4
  158. package/src/gateways/ws/background-agent-protocol.js +38 -0
  159. package/src/gateways/ws/message-dispatcher.js +1 -0
  160. package/src/gateways/ws/remote-session-protocol.js +65 -6
  161. package/src/gateways/ws/ws-server.js +18 -2
  162. package/src/harness/background-task-manager.js +70 -2
  163. package/src/harness/feature-flags.js +15 -8
  164. package/src/harness/mcp-client.js +618 -43
  165. package/src/harness/worktree-isolator.js +14 -3
  166. package/src/lazy-dispatch.js +30 -1
  167. package/src/lib/agent-authority.js +22 -0
  168. package/src/lib/agent-core.js +3 -0
  169. package/src/lib/agent-sandbox.js +161 -0
  170. package/src/lib/agent-schedule-store.js +45 -22
  171. package/src/lib/agent-team/team-runner.js +22 -0
  172. package/src/lib/artifact-store.js +35 -0
  173. package/src/lib/async-hook-supervisor.cjs +13 -2
  174. package/src/lib/audit-mtc.js +3 -2
  175. package/src/lib/background-agent-supervisor.js +104 -3
  176. package/src/lib/background-interaction-journal.js +407 -0
  177. package/src/lib/background-interaction-resolver.js +23 -34
  178. package/src/lib/background-session-transport.js +12 -2
  179. package/src/lib/capability-manifest.js +41 -3
  180. package/src/lib/capability-negotiation.js +1 -1
  181. package/src/lib/channels/channel-manager.js +33 -0
  182. package/src/lib/channels/telegram-channel.js +23 -5
  183. package/src/lib/channels/webhook-channel.js +38 -15
  184. package/src/lib/chrome-connector.js +169 -23
  185. package/src/lib/config-manager.js +23 -9
  186. package/src/lib/context-breakdown.js +31 -0
  187. package/src/lib/cowork-cron.js +274 -32
  188. package/src/lib/cross-chain-mtc.js +3 -2
  189. package/src/lib/durable-security-store.js +124 -0
  190. package/src/lib/event-runtime-host.js +67 -6
  191. package/src/lib/event-runtime-store.js +163 -0
  192. package/src/lib/goal-store.js +32 -12
  193. package/src/lib/headless-manifest.js +2 -1
  194. package/src/lib/hook-environment.cjs +58 -0
  195. package/src/lib/hook-manager.js +12 -5
  196. package/src/lib/hook-runner.cjs +122 -28
  197. package/src/lib/hook-runner.js +7 -2
  198. package/src/lib/hooks-v2-producers.js +36 -0
  199. package/src/lib/hooks-v2-runtime.js +329 -52
  200. package/src/lib/interaction-adapter.js +60 -19
  201. package/src/lib/interaction-binding.js +64 -0
  202. package/src/lib/json-schema-output.js +439 -14
  203. package/src/lib/json-schema-validate.js +514 -883
  204. package/src/lib/lan-pairing-tokens.js +115 -57
  205. package/src/lib/lsp/__tests__/lsp-client.test.js +32 -0
  206. package/src/lib/lsp/__tests__/lsp-manager.test.js +25 -0
  207. package/src/lib/lsp/lsp-client.js +4 -0
  208. package/src/lib/lsp/lsp-manager.js +3 -0
  209. package/src/lib/lsp/lsp-server-registry.js +18 -0
  210. package/src/lib/mcp-client.js +1 -0
  211. package/src/lib/mcp-oauth.js +35 -21
  212. package/src/lib/packer/pkg-config-generator.js +3 -0
  213. package/src/lib/permission-decision.js +99 -0
  214. package/src/lib/plugin-monitor-supervisor.js +3 -0
  215. package/src/lib/plugin-runtime/bin.js +1148 -70
  216. package/src/lib/plugin-runtime/capabilities.js +50 -0
  217. package/src/lib/plugin-runtime/capability-consent.js +42 -35
  218. package/src/lib/plugin-runtime/hooks.js +36 -9
  219. package/src/lib/plugin-runtime/install.js +23 -7
  220. package/src/lib/plugin-runtime/lsp.js +4 -0
  221. package/src/lib/plugin-runtime/manifest.js +112 -8
  222. package/src/lib/plugin-runtime/mcp.js +30 -5
  223. package/src/lib/plugin-runtime/monitors.js +11 -0
  224. package/src/lib/plugin-runtime/plugin-options.js +94 -38
  225. package/src/lib/plugin-runtime/policy.js +29 -2
  226. package/src/lib/plugin-runtime/sandbox-policy.js +84 -0
  227. package/src/lib/plugin-runtime/scopes.js +27 -13
  228. package/src/lib/plugin-runtime/trust.js +36 -31
  229. package/src/lib/process-execution-broker/credential-agent.js +114 -16
  230. package/src/lib/process-execution-broker/credential-transport-worker.js +312 -0
  231. package/src/lib/process-execution-broker/credential-transport.js +378 -0
  232. package/src/lib/process-execution-broker/index.js +765 -49
  233. package/src/lib/process-execution-broker/platform-sandbox.js +4233 -138
  234. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  235. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  236. package/src/lib/process-execution-broker/windows-sandbox.cs +3856 -0
  237. package/src/lib/project-mcp-trust.js +21 -26
  238. package/src/lib/protocol-replay.js +19 -12
  239. package/src/lib/repl-bang-memorize.js +67 -10
  240. package/src/lib/settings-hooks.cjs +13 -0
  241. package/src/lib/skill-loader.js +83 -10
  242. package/src/lib/slash-commands.js +27 -4
  243. package/src/lib/sync-credentials.js +5 -4
  244. package/src/lib/turn-binding.js +8 -6
  245. package/src/repl/agent-repl.js +117 -31
  246. package/src/repl/repl-turn-binding.js +5 -2
  247. package/src/runtime/agent-core.js +699 -70
  248. package/src/runtime/agent-runtime.js +13 -1
  249. package/src/runtime/coding-agent-contract-shared.cjs +3 -3
  250. package/src/runtime/headless-runner.js +105 -7
  251. package/src/runtime/headless-stream.js +209 -23
  252. package/src/runtime/mcp-config.js +29 -17
  253. package/src/runtime/policies/agent-policy.js +2 -0
  254. package/src/workers/background-agent-worker.js +128 -13
  255. package/src/assets/web-panel/assets/ChatBubbleRenderer-C7bofJuA.js +0 -1
  256. package/src/assets/web-panel/assets/Projects-BbgmyQnS.js +0 -1
  257. package/src/assets/web-panel/assets/RemoteSession-Bqk3EYSu.js +0 -4
  258. package/src/assets/web-panel/assets/Tasks-3bDAUz8K.js +0 -1
  259. package/src/assets/web-panel/assets/Terminal-CsMLKIoV.js +0 -3
  260. package/src/assets/web-panel/assets/chat-CJ19O6Dh.js +0 -1
  261. package/src/assets/web-panel/assets/devWarning-BBMFnsja.js +0 -1
  262. package/src/assets/web-panel/assets/index-Do8JMV4N.js +0 -1
  263. package/src/assets/web-panel/assets/index-UYAJ6iNM.js +0 -1
@@ -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
7
- * - Windows: explicitly unavailable until a native Job Object + restricted
8
- * token adapter is installed
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
@@ -19,20 +23,268 @@ import os from "node:os";
19
23
  import path from "node:path";
20
24
  import * as fs from "node:fs";
21
25
  import crypto from "node:crypto";
26
+ import { spawnSync as nativeSpawnSync } from "node:child_process";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ /**
30
+ * Stable boundary identifiers shared by broker policies, platform plans, and
31
+ * audit records. A guarantee means that the named restriction is enforced by
32
+ * the selected backend; it does not merely describe a configured intent.
33
+ */
34
+ export const SANDBOX_BOUNDARIES = Object.freeze({
35
+ FILESYSTEM: "filesystem",
36
+ NETWORK: "network",
37
+ PROCESS_EXEC: "process-exec",
38
+ PROCESS_TREE: "process-tree",
39
+ RESOURCE_LIMITS: "resource-limits",
40
+ PRIVILEGE_REDUCTION: "privilege-reduction",
41
+ });
42
+
43
+ /**
44
+ * @typedef {typeof SANDBOX_BOUNDARIES[keyof typeof SANDBOX_BOUNDARIES]} SandboxBoundary
45
+ *
46
+ * @typedef {Object} SandboxGuaranteePlan
47
+ * @property {1} contractVersion
48
+ * @property {boolean} applied
49
+ * @property {string} platform
50
+ * @property {string} profile
51
+ * @property {string} command
52
+ * @property {readonly string[]} args
53
+ * @property {Readonly<Object>} options
54
+ * @property {string|null} enforcement
55
+ * @property {string|null} backend
56
+ * @property {readonly SandboxBoundary[]} guarantees
57
+ * @property {string|null} reason
58
+ * @property {{required: boolean, mode: "none"|"sync"|"async"}} postSpawn
59
+ */
60
+
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";
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
+ ]);
144
+ const LINUX_BWRAP_PATH = "/usr/bin/bwrap";
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",
208
+ ]);
22
209
  const DEFAULT_RUNTIME = Object.freeze({
23
210
  platform: os.platform(),
211
+ arch: process.arch,
24
212
  fs,
25
213
  tmpdir: () => os.tmpdir(),
26
214
  homedir: () => os.homedir(),
27
215
  randomBytes: (size) => crypto.randomBytes(size),
28
216
  resolvePath: (value) => path.resolve(value),
29
217
  joinPath: (...parts) => path.join(...parts),
218
+ // GLOBALROOT resolves the kernel-owned SystemRoot object rather than the
219
+ // caller-controlled WINDIR/SystemRoot environment variables.
220
+ windowsDir: () => WINDOWS_TRUSTED_SYSTEM_ROOT,
221
+ moduleDir: MODULE_DIR,
222
+ now: () => Date.now(),
223
+ sleepSync: (milliseconds) =>
224
+ Atomics.wait(WINDOWS_IDENTITY_WAIT, 0, 0, milliseconds),
225
+ spawnSync: nativeSpawnSync,
226
+ warn: (message) => process.emitWarning(message),
30
227
  });
31
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
+
32
243
  function resolveRuntime(overrides = {}) {
33
244
  return { ...DEFAULT_RUNTIME, ...overrides };
34
245
  }
35
246
 
247
+ function normalizeSandboxRequest(profileOrRequest, explicitRequest) {
248
+ const legacyRequest =
249
+ profileOrRequest &&
250
+ typeof profileOrRequest === "object" &&
251
+ !Array.isArray(profileOrRequest)
252
+ ? profileOrRequest
253
+ : { profile: profileOrRequest };
254
+ const request =
255
+ explicitRequest &&
256
+ typeof explicitRequest === "object" &&
257
+ !Array.isArray(explicitRequest)
258
+ ? explicitRequest
259
+ : legacyRequest;
260
+ const profile =
261
+ typeof request.profile === "string"
262
+ ? request.profile
263
+ : typeof legacyRequest.profile === "string"
264
+ ? legacyRequest.profile
265
+ : "default";
266
+ const requiredBoundaries = Array.isArray(request.requiredBoundaries)
267
+ ? [
268
+ ...new Set(
269
+ request.requiredBoundaries.filter(
270
+ (boundary) => typeof boundary === "string",
271
+ ),
272
+ ),
273
+ ]
274
+ : [];
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
+ };
286
+ }
287
+
36
288
  /**
37
289
  * A native `spawn(..., { shell: true })` asks Node to execute one command
38
290
  * string through the platform shell. Once a sandbox wrapper becomes the
@@ -45,7 +297,7 @@ function resolveRuntime(overrides = {}) {
45
297
  * sandbox executable itself is always spawned directly.
46
298
  */
47
299
  function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
48
- if (!spawnOpts?.shell || platform === "win32") {
300
+ if (!spawnOpts?.shell) {
49
301
  return {
50
302
  command,
51
303
  args: [...(args || [])],
@@ -53,6 +305,19 @@ function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
53
305
  };
54
306
  }
55
307
 
308
+ if (platform === "win32") {
309
+ const shell =
310
+ typeof spawnOpts.shell === "string"
311
+ ? spawnOpts.shell
312
+ : process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe";
313
+ const shellCommand = [command, ...(args || [])].map(String).join(" ");
314
+ return {
315
+ command: shell,
316
+ args: ["/d", "/s", "/c", shellCommand],
317
+ options: { ...spawnOpts, shell: false },
318
+ };
319
+ }
320
+
56
321
  const shell =
57
322
  typeof spawnOpts.shell === "string" ? spawnOpts.shell : "/bin/sh";
58
323
  const shellCommand = [command, ...(args || [])].map(String).join(" ");
@@ -82,6 +347,8 @@ function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
82
347
  * args: string[],
83
348
  * options: Object,
84
349
  * enforcement: string|null,
350
+ * backend: string|null,
351
+ * guarantees: readonly SandboxBoundary[],
85
352
  * reason: string|null,
86
353
  * cleanup?: Function,
87
354
  * postSpawn: {required: boolean, mode: "none"|"sync"|"async"}
@@ -97,10 +364,15 @@ function createSandboxPlan(input) {
97
364
  contractVersion: 1,
98
365
  applied: false,
99
366
  enforcement: null,
367
+ backend: null,
100
368
  reason: null,
101
369
  ...input,
102
370
  args: Object.freeze([...(input.args || [])]),
103
371
  options: Object.freeze({ ...(input.options || {}) }),
372
+ guarantees: Object.freeze([...(input.guarantees || [])]),
373
+ runtimeProbe: input.runtimeProbe
374
+ ? Object.freeze({ ...input.runtimeProbe })
375
+ : null,
104
376
  postSpawn,
105
377
  });
106
378
  }
@@ -132,6 +404,10 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
132
404
  const lines = [
133
405
  "(version 1)",
134
406
  "(deny default)",
407
+ // Apple's system baseline supplies the loader/device primitives required
408
+ // for ordinary command startup. Our explicit rules below still own
409
+ // process, filesystem write, and network policy.
410
+ '(import "system.sb")',
135
411
  // Basic system operations always allowed
136
412
  "(allow signal (target self))",
137
413
  '(allow process-exec (literal "/usr/bin/env"))',
@@ -148,6 +424,10 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
148
424
 
149
425
  if (allowNetwork) {
150
426
  lines.push("(allow network*)");
427
+ } else {
428
+ // Keep the imported system baseline from widening the caller's explicit
429
+ // no-network policy.
430
+ lines.push("(deny network*)");
151
431
  }
152
432
 
153
433
  // Allow read/write to specific paths
@@ -162,9 +442,17 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
162
442
  }
163
443
 
164
444
  // Always allow read access to system paths needed for basic execution
445
+ lines.push('(allow file-read* (subpath "/bin"))');
446
+ lines.push('(allow file-read* (subpath "/usr/bin"))');
165
447
  lines.push('(allow file-read* (subpath "/usr/lib"))');
448
+ lines.push('(allow file-read* (subpath "/usr/libexec"))');
166
449
  lines.push('(allow file-read* (subpath "/System/Library"))');
450
+ lines.push('(allow file-read* (subpath "/Library/Frameworks"))');
167
451
  lines.push('(allow file-read* (subpath "/usr/local/lib"))');
452
+ lines.push(
453
+ '(allow file-read* file-write* (literal "/dev/null") (literal "/dev/stdin") (literal "/dev/stdout") (literal "/dev/stderr"))',
454
+ );
455
+ lines.push('(allow file-read* (literal "/dev/urandom"))');
168
456
  lines.push('(allow file-read* (literal "/etc/passwd"))');
169
457
 
170
458
  return lines.join("\n");
@@ -199,6 +487,7 @@ export function applyMacSandbox(
199
487
  const base = {
200
488
  platform: runtime.platform,
201
489
  profile: sandboxOpts.profileName || "default",
490
+ backend: "macos-seatbelt",
202
491
  command,
203
492
  args,
204
493
  options: { ...(spawnOpts || {}) },
@@ -250,6 +539,11 @@ export function applyMacSandbox(
250
539
  ...base,
251
540
  applied: true,
252
541
  enforcement: "macos-seatbelt",
542
+ guarantees: [
543
+ SANDBOX_BOUNDARIES.FILESYSTEM,
544
+ ...(sandboxOpts.allowNetwork ? [] : [SANDBOX_BOUNDARIES.NETWORK]),
545
+ ...(sandboxOpts.allowExec ? [] : [SANDBOX_BOUNDARIES.PROCESS_EXEC]),
546
+ ],
253
547
  command: sandboxExecutable,
254
548
  args: newArgs,
255
549
  options: invocation.options,
@@ -264,173 +558,3971 @@ export function applyMacSandbox(
264
558
  }
265
559
 
266
560
  // ---------------------------------------------------------------------------
267
- // Windows Job Object availability
561
+ // Windows Job Object + Restricted Token
268
562
  // ---------------------------------------------------------------------------
269
563
 
270
- /**
271
- * Report Windows sandbox availability. Node's child_process API cannot create
272
- * and synchronously assign a Job Object or restricted token by itself.
273
- *
274
- * @param {string} command
275
- * @param {string[]} args
276
- * @param {Object} spawnOpts
277
- * @param {Object} sandboxOpts
278
- */
279
- export function applyWindowsSandbox(
280
- command,
281
- args,
282
- spawnOpts,
283
- sandboxOpts = {},
284
- runtimeOverrides = {},
564
+ function cleanupWindowsTemporaryPath(
565
+ runtime,
566
+ targetPath,
567
+ attempts = 100,
568
+ delayMs = 10,
285
569
  ) {
286
- const runtime = resolveRuntime(runtimeOverrides);
287
- const base = {
288
- platform: runtime.platform,
289
- profile: sandboxOpts.profileName || "default",
290
- command,
291
- args,
292
- options: spawnOpts,
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));
293
687
  };
294
- if (runtime.platform !== "win32") {
295
- return createSandboxPlan({
296
- ...base,
297
- reason: "platform_mismatch",
298
- });
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;
299
759
  }
760
+ }
300
761
 
301
- // Node does not expose Job Object or restricted-token primitives. Mark the
302
- // platform unavailable instead of treating CREATE_NO_WINDOW or an
303
- // environment marker as a security boundary. A future native adapter may
304
- // return an applied plan with a synchronous postSpawn step.
305
- return createSandboxPlan({
306
- ...base,
307
- reason: "windows_native_job_object_unavailable",
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();
308
813
  });
309
814
  }
310
815
 
311
816
  /**
312
- * Reject an attempted Windows post-spawn association until a native adapter is
313
- * available.
314
- *
315
- * @param {ChildProcess} proc - The spawned child process
316
- * @param {Object} sandboxResult - Result from applyWindowsSandbox
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.
317
820
  */
318
- export function postSpawnWindowsSandbox(
319
- proc,
320
- sandboxResult,
321
- runtimeOverrides = {},
322
- ) {
323
- const runtime = resolveRuntime(runtimeOverrides);
324
- if (runtime.platform !== "win32" || !sandboxResult?.postSpawn?.required) {
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
+ ) {
325
840
  return;
326
841
  }
327
- const error = new Error(
328
- "Windows native Job Object enforcement is unavailable",
329
- );
330
- error.code = "ERR_WINDOWS_SANDBOX_UNAVAILABLE";
331
- throw error;
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);
856
+ }
857
+ }, idleTtlMs);
858
+ entry.idleTimer.unref?.();
332
859
  }
333
860
 
334
- // ---------------------------------------------------------------------------
335
- // Linux resource-limit enforcement
336
- // ---------------------------------------------------------------------------
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
+ }
337
880
 
338
881
  /**
339
- * Apply Linux resource limits through the util-linux `prlimit` wrapper.
340
- *
341
- * @param {string} command
342
- * @param {string[]} args
343
- * @param {Object} spawnOpts
344
- * @param {Object} sandboxOpts
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[]).
345
886
  */
346
- export function applyLinuxSandbox(
347
- command,
348
- args,
349
- spawnOpts,
350
- sandboxOpts = {},
351
- runtimeOverrides = {},
352
- ) {
353
- const runtime = resolveRuntime(runtimeOverrides);
354
- const invocation = normalizeWrappedInvocation(
355
- command,
356
- args,
357
- spawnOpts,
358
- runtime.platform,
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`,
359
900
  );
360
- const base = {
361
- platform: runtime.platform,
362
- profile: sandboxOpts.profileName || "default",
363
- command,
364
- args,
365
- options: { ...(spawnOpts || {}) },
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" };
928
+ }
929
+ if (error?.code === "EEXIST") {
930
+ return { reason: "windows_native_adapter_random_path_collision" };
931
+ }
932
+ return { reason: "windows_native_adapter_materialize_failed" };
933
+ }
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
+ },
366
947
  };
367
- if (runtime.platform !== "linux") {
368
- return createSandboxPlan({
369
- ...base,
370
- reason: "platform_mismatch",
948
+ }
949
+
950
+ function materializeWindowsAdapterInvocation(runtime, helperArgs) {
951
+ const payloadPath = runtime.joinPath(
952
+ runtime.tmpdir(),
953
+ `chainless-win-sandbox-invocation-${runtime
954
+ .randomBytes(24)
955
+ .toString("hex")}.json`,
956
+ );
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",
371
982
  });
983
+ } catch (error) {
984
+ if (error?.code === "EEXIST") {
985
+ return { reason: "windows_adapter_invocation_random_path_collision" };
986
+ }
987
+ return {
988
+ reason: cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath)
989
+ ? "windows_adapter_invocation_materialize_failed"
990
+ : "windows_adapter_invocation_cleanup_unverified",
991
+ };
372
992
  }
993
+ return {
994
+ payloadPath,
995
+ payloadDigest: sha256(content),
996
+ cleanup: () => cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath),
997
+ };
998
+ }
373
999
 
374
- const { limits = {} } = sandboxOpts;
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
+ }
375
1040
 
376
- const prlimitParts = [];
377
- if (limits.cpu) prlimitParts.push(`--cpu=${limits.cpu}`);
378
- if (limits.as) prlimitParts.push(`--as=${limits.as}`);
379
- if (limits.nofile) prlimitParts.push(`--nofile=${limits.nofile}`);
380
- if (limits.nproc) prlimitParts.push(`--nproc=${limits.nproc}`);
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
+ }
381
1069
 
382
- if (prlimitParts.length === 0) {
383
- return createSandboxPlan({
384
- ...base,
385
- reason: "linux_resource_limits_not_configured",
386
- });
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
+ }
387
1094
  }
388
- if (!runtime.fs.existsSync("/usr/bin/prlimit")) {
389
- return createSandboxPlan({
390
- ...base,
391
- reason: "linux_prlimit_unavailable",
392
- });
1095
+
1096
+ const inboxHost = runtime.joinPath(
1097
+ windowsDirectory,
1098
+ "System32",
1099
+ "WindowsPowerShell",
1100
+ "v1.0",
1101
+ "powershell.exe",
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
+ };
393
1118
  }
394
1119
 
395
- const options = {
396
- ...invocation.options,
397
- env: {
398
- ...(invocation.options.env || process.env),
399
- CHAINLESS_SANDBOXED: "1",
400
- },
1120
+ let attestation;
1121
+ try {
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
+ }
1129
+ } catch {
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,
401
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
+ }
402
1213
 
403
- return createSandboxPlan({
404
- ...base,
405
- applied: true,
406
- enforcement: "linux-prlimit",
407
- command: "/usr/bin/prlimit",
408
- args: [...prlimitParts, "--", invocation.command, ...invocation.args],
409
- options,
410
- });
1214
+ windowsAdapterCacheEntries.add(entry);
1215
+ windowsAdapterCache = entry;
1216
+ registerWindowsAdapterExitCleanup();
1217
+ return { entry };
411
1218
  }
412
1219
 
413
- // ---------------------------------------------------------------------------
414
- // Unified sandbox apply
415
- // ---------------------------------------------------------------------------
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
+ }
416
1235
 
417
- /**
418
- * Apply platform-appropriate sandbox for a child process.
419
- *
420
- * @param {string} command
421
- * @param {string[]} args
422
- * @param {Object} spawnOpts - Original spawn options
423
- * @param {"default"|"strict"|"network-only"} profileName
424
- * @returns {ReturnType<typeof createSandboxPlan>}
425
- */
426
- export function applySandbox(
427
- command,
428
- args,
429
- spawnOpts,
430
- profileName = "default",
431
- runtimeOverrides = {},
432
- ) {
433
- const runtime = resolveRuntime(runtimeOverrides);
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);
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
+
1298
+ return {
1299
+ ensureExecutable: ensureEntry,
1300
+ createInvocation,
1301
+ spawnSync: (args, options) => {
1302
+ const invocation = createInvocation(args);
1303
+ let result;
1304
+ let failure;
1305
+ try {
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;
1321
+ }
1322
+ if (failure) throw failure;
1323
+ return result;
1324
+ },
1325
+ release: () => {
1326
+ if (released) return;
1327
+ released = true;
1328
+ releaseWindowsAdapterLease(lease);
1329
+ },
1330
+ };
1331
+ }
1332
+
1333
+ function waitForWindowsTargetIdentity(
1334
+ proc,
1335
+ identityPath,
1336
+ runtime,
1337
+ expectedAppContainer = null,
1338
+ timeoutMs = 30_000,
1339
+ ) {
1340
+ const deadline = runtime.now() + timeoutMs;
1341
+ let lastError;
1342
+ while (runtime.now() < deadline) {
1343
+ try {
1344
+ const identity = JSON.parse(
1345
+ runtime.fs.readFileSync(identityPath, "utf8"),
1346
+ );
1347
+ if (identity?.error) {
1348
+ throw new Error(
1349
+ `Windows sandbox helper rejected the target: ${identity.error}`,
1350
+ );
1351
+ }
1352
+ const targetPid = Number(identity?.targetPid);
1353
+ const helperPid = Number(identity?.helperPid);
1354
+ if (!Number.isSafeInteger(targetPid) || targetPid <= 0) {
1355
+ throw new Error("Windows sandbox identity omitted a valid target PID");
1356
+ }
1357
+ if (
1358
+ Number.isSafeInteger(helperPid) &&
1359
+ helperPid > 0 &&
1360
+ helperPid !== proc.pid
1361
+ ) {
1362
+ throw new Error(
1363
+ `Windows sandbox identity named unexpected helper PID ${helperPid}`,
1364
+ );
1365
+ }
1366
+ if (targetPid === proc.pid) {
1367
+ throw new Error(
1368
+ "Windows sandbox target PID unexpectedly equals its helper PID",
1369
+ );
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
+ }
1390
+
1391
+ const wrapperPid = proc.pid;
1392
+ proc.sandboxWrapperPid = wrapperPid;
1393
+ proc.sandboxTargetPid = targetPid;
1394
+ if (expectedAppContainer) {
1395
+ proc.sandboxAppContainerProfile =
1396
+ expectedAppContainer.appContainerProfileName;
1397
+ proc.sandboxAppContainerSid = expectedAppContainer.appContainerSid;
1398
+ }
1399
+ // ChildProcess.pid is a writable data property. The native ChildProcess
1400
+ // handle still points at the helper, so kill()/ref()/unref() retain Job
1401
+ // lifetime semantics while callers observe and persist the real target.
1402
+ proc.pid = targetPid;
1403
+ return {
1404
+ targetPid,
1405
+ wrapperPid,
1406
+ ...(expectedAppContainer
1407
+ ? {
1408
+ appContainerProfileName:
1409
+ expectedAppContainer.appContainerProfileName,
1410
+ appContainerSid: expectedAppContainer.appContainerSid,
1411
+ capabilityCount: 0,
1412
+ }
1413
+ : {}),
1414
+ };
1415
+ } catch (error) {
1416
+ lastError = error;
1417
+ if (
1418
+ error?.code !== "ENOENT" &&
1419
+ !(error instanceof SyntaxError) &&
1420
+ !String(error?.message || "").includes("omitted a valid target PID")
1421
+ ) {
1422
+ throw error;
1423
+ }
1424
+ }
1425
+ runtime.sleepSync(10);
1426
+ }
1427
+ throw new Error(
1428
+ `Timed out waiting for Windows sandbox target identity${
1429
+ lastError?.message ? `: ${lastError.message}` : ""
1430
+ }`,
1431
+ );
1432
+ }
1433
+
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,
2019
+ command,
2020
+ args,
2021
+ options: spawnOpts,
2022
+ };
2023
+ let appContainerPolicyDigest = null;
2024
+ const unavailablePlan = (reason, extra = {}) =>
2025
+ createSandboxPlan({
2026
+ ...base,
2027
+ ...(requiresAppContainer
2028
+ ? {
2029
+ backend: null,
2030
+ candidateBackend: WINDOWS_APPCONTAINER_BACKEND,
2031
+ policyAttested: false,
2032
+ policyDigest: appContainerPolicyDigest,
2033
+ }
2034
+ : {}),
2035
+ reason,
2036
+ ...extra,
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");
2065
+ }
2066
+
2067
+ // The hosted Windows restricted-token path cannot reliably preserve file
2068
+ // descriptor backed stdout/stderr when its helper is itself detached. Mark
2069
+ // that combination unavailable before launch: strict/required callers fail
2070
+ // closed, while ordinary trusted control-plane callers retain the broker's
2071
+ // explicit audited fallback instead of receiving a phantom target PID.
2072
+ if (
2073
+ spawnOpts?.detached === true &&
2074
+ Array.isArray(spawnOpts?.stdio) &&
2075
+ spawnOpts.stdio.slice(0, 3).some((entry) => Number.isInteger(entry))
2076
+ ) {
2077
+ return unavailablePlan("windows_detached_file_stdio_unsupported");
2078
+ }
2079
+
2080
+ const nodeIpcFd = Array.isArray(spawnOpts?.stdio)
2081
+ ? spawnOpts.stdio.findIndex((entry) => entry === "ipc")
2082
+ : -1;
2083
+ if (
2084
+ Array.isArray(spawnOpts?.stdio) &&
2085
+ spawnOpts.stdio
2086
+ .slice(3)
2087
+ .some(
2088
+ (entry, offset) =>
2089
+ offset + 3 !== nodeIpcFd &&
2090
+ entry !== "ignore" &&
2091
+ entry !== null &&
2092
+ entry !== undefined,
2093
+ )
2094
+ ) {
2095
+ return unavailablePlan("windows_extra_descriptor_unsupported");
2096
+ }
2097
+
2098
+ const invocation = normalizeWrappedInvocation(
2099
+ command,
2100
+ args,
2101
+ spawnOpts,
2102
+ runtime.platform,
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
+ }
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
+ );
2177
+ const limits = sandboxOpts.limits || {};
2178
+ const targetWorkingDirectory = runtime.resolvePath(
2179
+ invocation.options.cwd || process.cwd(),
2180
+ );
2181
+ const identityPath =
2182
+ spawnOpts?.detached === true ||
2183
+ (requiresAppContainer && sandboxOpts.sync !== true)
2184
+ ? runtime.joinPath(
2185
+ runtime.tmpdir(),
2186
+ `chainless-win-sandbox-identity-${runtime
2187
+ .randomBytes(24)
2188
+ .toString("hex")}.json`,
2189
+ )
2190
+ : null;
2191
+ const launchSpec = {
2192
+ cpuSeconds: Number(limits.cpu || 0),
2193
+ processMemoryBytes: Number(
2194
+ limits.as || (profile === "strict" ? 256 * 1024 * 1024 : 0),
2195
+ ),
2196
+ activeProcessLimit: profile === "strict" ? 16 : 64,
2197
+ allowReparsePaths: loaderMode === "managed-executable",
2198
+ disableAdministratorSids:
2199
+ profile === "strict" ||
2200
+ requiresAppContainer ||
2201
+ Boolean(entrySnapshot.locks),
2202
+ command: invocation.command,
2203
+ args: invocation.args,
2204
+ nodeIpcFd,
2205
+ detached: invocation.options.detached === true,
2206
+ windowsHide: invocation.options.windowsHide === true,
2207
+ workingDirectory: targetWorkingDirectory,
2208
+ environment: targetEnvironment,
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
+ }
2228
+ if (identityPath) launchSpec.identityPath = identityPath;
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,
2240
+ );
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"),
3580
+ };
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
+ }
3598
+ }
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
3662
+ }
3663
+ }
3664
+ throw error;
3665
+ }
3666
+ }
3667
+
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;
3685
+ }
3686
+ const after = runtime.fs.statSync(source);
3687
+ if (
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)
3692
+ ) {
3693
+ return null;
3694
+ }
3695
+ return source;
3696
+ } catch {
3697
+ return null;
3698
+ }
3699
+ }
3700
+
3701
+ function attestLinuxBubblewrapBinary(runtime) {
3702
+ return (
3703
+ attestLinuxRootOwnedFile(runtime, LINUX_BWRAP_PATH, {
3704
+ executable: true,
3705
+ }) === LINUX_BWRAP_PATH
3706
+ );
3707
+ }
3708
+
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" };
3725
+ }
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
+ }
3742
+ }
3743
+ return { ok: true, reason: null };
3744
+ }
3745
+
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
+ }
3759
+
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;
3781
+ try {
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]);
3795
+ }
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]);
3970
+ }
3971
+ args.push("--chdir", "/opt/chainless/plugin");
3972
+ return args;
3973
+ }
3974
+
3975
+ function probeLinuxBubblewrapPolicy(
3976
+ runtime,
3977
+ policyArgs,
3978
+ pinnedDescriptors,
3979
+ targetRuntime,
3980
+ ) {
3981
+ let result;
3982
+ try {
3983
+ result = runtime.spawnSync(
3984
+ LINUX_BWRAP_PATH,
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
+ ],
3994
+ {
3995
+ cwd: "/",
3996
+ shell: false,
3997
+ encoding: "utf8",
3998
+ stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
3999
+ probe: true,
4000
+ }),
4001
+ timeout: 15_000,
4002
+ env: {
4003
+ PATH: "/usr/bin:/bin",
4004
+ LANG: "C",
4005
+ LC_ALL: "C",
4006
+ },
4007
+ },
4008
+ );
4009
+ } catch {
4010
+ return linuxBubblewrapProbe(
4011
+ true,
4012
+ false,
4013
+ "probe_spawn_failed",
4014
+ targetRuntime,
4015
+ );
4016
+ }
4017
+ if (result?.error || result?.status !== 0) {
4018
+ return linuxBubblewrapProbe(
4019
+ true,
4020
+ false,
4021
+ result?.error ? "probe_spawn_failed" : "probe_failed",
4022
+ targetRuntime,
4023
+ );
4024
+ }
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);
4034
+ }
4035
+
4036
+ /**
4037
+ * Apply Linux resource limits through the util-linux `prlimit` wrapper.
4038
+ *
4039
+ * @param {string} command
4040
+ * @param {string[]} args
4041
+ * @param {Object} spawnOpts
4042
+ * @param {Object} sandboxOpts
4043
+ */
4044
+ export function applyLinuxSandbox(
4045
+ command,
4046
+ args,
4047
+ spawnOpts,
4048
+ sandboxOpts = {},
4049
+ runtimeOverrides = {},
4050
+ ) {
4051
+ const runtime = resolveRuntime(runtimeOverrides);
4052
+ const invocation = normalizeWrappedInvocation(
4053
+ command,
4054
+ args,
4055
+ spawnOpts,
4056
+ runtime.platform,
4057
+ );
4058
+ const base = {
4059
+ platform: runtime.platform,
4060
+ profile: sandboxOpts.profileName || "default",
4061
+ backend: "linux-prlimit",
4062
+ command,
4063
+ args,
4064
+ options: { ...(spawnOpts || {}) },
4065
+ };
4066
+ if (runtime.platform !== "linux") {
4067
+ return createSandboxPlan({
4068
+ ...base,
4069
+ reason: "platform_mismatch",
4070
+ });
4071
+ }
4072
+
4073
+ const { limits = {} } = sandboxOpts;
4074
+ const requiredBoundaries = Array.isArray(sandboxOpts.requiredBoundaries)
4075
+ ? sandboxOpts.requiredBoundaries
4076
+ : [];
4077
+ const requiresStrongLinuxBoundary = requiredBoundaries.some(
4078
+ (boundary) =>
4079
+ boundary === SANDBOX_BOUNDARIES.FILESYSTEM ||
4080
+ boundary === SANDBOX_BOUNDARIES.NETWORK,
4081
+ );
4082
+
4083
+ if (requiresStrongLinuxBoundary) {
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
+ };
4433
+ return createSandboxPlan({
4434
+ ...base,
4435
+ applied: true,
4436
+ enforcement: LINUX_BWRAP_BACKEND,
4437
+ backend: LINUX_BWRAP_BACKEND,
4438
+ candidateBackend: null,
4439
+ policyAttested: true,
4440
+ policyDigest,
4441
+ runtimeProbe,
4442
+ reason: null,
4443
+ guarantees: [SANDBOX_BOUNDARIES.FILESYSTEM, SANDBOX_BOUNDARIES.NETWORK],
4444
+ command: LINUX_BWRAP_PATH,
4445
+ args: [...policyArgs, "--", ...targetArgs],
4446
+ options,
4447
+ cleanup,
4448
+ });
4449
+ }
4450
+
4451
+ const prlimitParts = [];
4452
+ if (limits.cpu) prlimitParts.push(`--cpu=${limits.cpu}`);
4453
+ if (limits.as) prlimitParts.push(`--as=${limits.as}`);
4454
+ if (limits.nofile) prlimitParts.push(`--nofile=${limits.nofile}`);
4455
+ if (limits.nproc) prlimitParts.push(`--nproc=${limits.nproc}`);
4456
+
4457
+ if (prlimitParts.length === 0) {
4458
+ return createSandboxPlan({
4459
+ ...base,
4460
+ reason: "linux_resource_limits_not_configured",
4461
+ });
4462
+ }
4463
+ if (!runtime.fs.existsSync("/usr/bin/prlimit")) {
4464
+ return createSandboxPlan({
4465
+ ...base,
4466
+ reason: "linux_prlimit_unavailable",
4467
+ });
4468
+ }
4469
+
4470
+ const options = {
4471
+ ...invocation.options,
4472
+ env: {
4473
+ ...(invocation.options.env || process.env),
4474
+ CHAINLESS_SANDBOXED: "1",
4475
+ },
4476
+ };
4477
+
4478
+ return createSandboxPlan({
4479
+ ...base,
4480
+ applied: true,
4481
+ enforcement: "linux-prlimit",
4482
+ guarantees: [SANDBOX_BOUNDARIES.RESOURCE_LIMITS],
4483
+ command: "/usr/bin/prlimit",
4484
+ args: [...prlimitParts, "--", invocation.command, ...invocation.args],
4485
+ options,
4486
+ });
4487
+ }
4488
+
4489
+ // ---------------------------------------------------------------------------
4490
+ // Unified sandbox apply
4491
+ // ---------------------------------------------------------------------------
4492
+
4493
+ /**
4494
+ * Apply platform-appropriate sandbox for a child process.
4495
+ *
4496
+ * @param {string} command
4497
+ * @param {string[]} args
4498
+ * @param {Object} spawnOpts - Original spawn options
4499
+ * @param {"default"|"strict"|"network-only"|{
4500
+ * profile?: "default"|"strict"|"network-only",
4501
+ * requiredBoundaries?: string[]
4502
+ * }} profileOrRequest
4503
+ * @param {Object} runtimeOverrides
4504
+ * @param {{
4505
+ * profile?: string,
4506
+ * requiredBoundaries?: string[],
4507
+ * sync?: boolean,
4508
+ * executionContract?: Readonly<Object>|null
4509
+ * }|null} explicitRequest
4510
+ * @returns {ReturnType<typeof createSandboxPlan>}
4511
+ */
4512
+ export function applySandbox(
4513
+ command,
4514
+ args,
4515
+ spawnOpts,
4516
+ profileOrRequest = "default",
4517
+ runtimeOverrides = {},
4518
+ explicitRequest = null,
4519
+ ) {
4520
+ const runtime = resolveRuntime(runtimeOverrides);
4521
+ const sandboxRequest = normalizeSandboxRequest(
4522
+ profileOrRequest,
4523
+ explicitRequest,
4524
+ );
4525
+ const profileName = sandboxRequest.profile;
434
4526
  const profiles = {
435
4527
  default: {
436
4528
  allowNetwork: false,
@@ -471,6 +4563,9 @@ export function applySandbox(
471
4563
  const profile = {
472
4564
  ...(profiles[profileName] || profiles.default),
473
4565
  profileName: profiles[profileName] ? profileName : "default",
4566
+ requiredBoundaries: sandboxRequest.requiredBoundaries,
4567
+ sync: sandboxRequest.sync,
4568
+ executionContract: sandboxRequest.executionContract,
474
4569
  };
475
4570
 
476
4571
  // Dispatch to platform handler