chainlesschain 0.162.183 → 0.162.185

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 (205) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BapS7lz4.js → AIOps-Co4ipAKx.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CXYdXEUI.js → ActionButton-BpsoDrDq.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-CKR5Lexn.js → Analytics-DMUqeBik.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DATVUOXB.js → AppLayout-C6fD_dXr.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-vb_0ZlV1.js → Artifacts-DW0YlQlV.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-BI1l9GWt.js → Audit-DXtHiiFK.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BFXbaUEf.js → BackgroundAgents-BgmF62U-.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-DEYBJJ6H.js → Backup-D8vhVLQC.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-COdzeO0M.js → BaseInput-D7nCtY2X.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-oSN6sfM5.js → Chat-CBZLEV2G.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CI_jT0ru.js → ChatBubbleRenderer-CtDb-2Qu.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-BezEa4tm.js → Checkbox-DJLkeD4M.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CNEL0Ife.js → Codegen-Cs6xO8Fd.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Bf2Y2Bn1.js → Col-Dtx9wxfT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-BfUlu4Hc.js → Community-DiigJg4n.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-BN4xWP9e.js → Compact-B67oMDDL.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-Bt6BKmEP.js → Compliance-BBDl-Jos.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DKLp8YxO.js → Cowork-Ba98hSsa.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-o7Hhw6ky.js → Cron-DBWQZIz7.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DaNXWJIw.js → Crosschain-OcrKzxAt.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-B-Qa2mha.js → DID-mW2eSbjM.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-CMX0Qnxw.js → Dashboard-BmsDPmKJ.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BLuLyRu_.js → Dropdown-CVzmCyRx.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CFBAeMij.js → EmailListRenderer-vVgqKzhh.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DWzXQ67V.js → FamilyGuardDashboard-CWY5t_ZJ.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-DB1QSLgr.js → Federation-ZGomhjxr.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-C-OKCToP.js → FormItemContext-DyaknbLr.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DMJFtFZf.js → GenericCardRenderer-CmIvN1qr.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DsGSujOV.js → Git-DQOJIYkG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-B0iS9NEW.js → Governance-CMiY-EEp.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CH004H7c.js → Inference-D6IXuucf.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-Q9MTQ83c.js → KnowledgeGraph-6jkTL0hV.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-B_fyjZK9.js → Logs-N0jD-349.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-BIStF9d1.js → Marketplace-WhYuhBxm.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-pGfal768.js → McpTools-DTeO28xy.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-xbNqpIqB.js → Memory-MSHzPSE-.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-B5EQGrIK.js → MobileBridge-Cyqs3J3W.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-a_rmfxJH.js → Mtc-MWBnezNM.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-BmKuofUr.js → MtcAudit-DQu35Nu-.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-BQbJ-CAN.js → Multisig-CQS_M-DP.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-JtPJKX25.js → NLProgramming-WD16vQvY.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-ZFKznTSG.js → Notes-DqsiSwOq.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-B6aHRBBR.js → NotificationSettings-D3LoxO-P.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-DdcsOqEz.js → OrderTableRenderer-C5jPoTd6.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-BFDTAUgS.js → Organization-48aMBJso.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-0FoTfbWH.js → Overflow-j-a2XkhR.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CWd90etw.js → P2P-BYpBNYnh.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-DtCGC-W-.js → PdhVaultBrowser-CYCsrrLg.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-CvQ7nr7T.js → Permissions-ZRXGXDFb.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-rdNPcR-Z.js → PersonalDataHub-6OWUPHyL.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-CmrfiNOs.js → Pipeline-FcjlDK9E.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-D7ep1h_t.js → Privacy-vD_6Tldp.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-C0xKINHS.js → ProjectInit-B_CHOLXp.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-AOmZFp-F.js → ProjectSettings-rEiqQ2da.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-BWHSkXZP.js → Projects-Bg8obRAM.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-Cb7JCTPL.js → Providers-Dgtt97B4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-BDMehcXC.js → QrScannerModal-DaGVylZO.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-BF6_1nvx.js → QuickAsk-DP2_2x3u.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-BHfT-A28.js → Recommend-D29yj4ki.js} +1 -1
  64. package/src/assets/web-panel/assets/RemoteSession-Ue2wnBLp.js +5 -0
  65. package/src/assets/web-panel/assets/{Reputation-CsWHLEcj.js → Reputation-CrxIHqeC.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-D6srnWhm.js → Row-BgSzJPCn.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-Xhr-KyVd.js → RssFeed-AwhYOrP9.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-vIbIJvwL.js → Search-D2shpEcm.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-Bzi5Aiko.js → Security-BtKzhTbT.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BHtVX52P.js → Services-BY0DsUSm.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-C0R8B2jf.js → Skeleton-B3Sb0UI9.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-DtT6rWOX.js → Skills-Ne5w0bDs.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-0B37mxVU.js → Sla-C8VkaP4T.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-0V2jE1Bs.js → SpeechSettings-BQdIEYP9.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CYedRzaj.js → SyncSettings-CwFafrUg.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-yJuKoDlR.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-BKoHztGT.js → Templates-DYn9qCmx.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant--17nwVNZ.js → Tenant-DkKReHvn.js} +1 -1
  79. package/src/assets/web-panel/assets/Terminal-r6bd-owt.js +3 -0
  80. package/src/assets/web-panel/assets/{TimelineRenderer-C5jx4AE3.js → TimelineRenderer-Mq2juUC3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-DYJKYa9t.js → Tokens-BnMw7MRi.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DChpWiQ9.js → Trigger-CTzKp4o9.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DfWG52EA.js → Trust-BBrSZm9U.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-CMugYcyA.js → UkeySign-D9fDvEMq.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-BOSmrHAd.js → VideoEditing-DUi5ve-0.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-D5g3k3Sa.js → Wallet-C8Fi1P8_.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-D-7u4Mkc.js → WebAuthn-s1LppYi2.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-CFzZMSUJ.js → WorkflowEditor-DJL2C4lj.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-VbR6l6mc.js → chat-D3F2ndOB.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BSZn8EDS.js → colors-B7739-e3.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-y74obMEq.js → compact-item-CcJbFTF4.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-C326bjex.js → createContext-CuP_DEFS.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-D_INJQT_.js → hasIn-6YEXfIik.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DuIkfnTF.js → index-4h3lpWJ3.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DBkzrpiL.js → index-BIID4XcP.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-B7SrC-On.js → index-BOHUo3aM.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BnSfnSMP.js → index-BYYSLEjn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-KF9Zmqln.js → index-Bh4hcNWW.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-C327pLbw.js → index-BhIKnQAW.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-E_v1ZCTP.js → index-Bru_kpZl.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Bfw2hf2s.js → index-BsdSnHQE.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-jYaogFE8.js → index-BxSqVMGh.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dlxh9D8D.js → index-C3ZA_h1j.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DdyjrCAu.js → index-C3pctdCb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-abmn_8Yz.js → index-CRuoRapN.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-qMA4GYdQ.js → index-CY0zd_jc.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CezFrAmD.js +1 -0
  109. package/src/assets/web-panel/assets/index-Cxgguez6.js +1 -0
  110. package/src/assets/web-panel/assets/{index-Cr3uuz0C.js → index-CxradkS6.js} +19 -19
  111. package/src/assets/web-panel/assets/{index-DwHSZLXg.js → index-D0K_rFC1.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CMHqs3Jb.js → index-D0zdPyTD.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bnua8IX3.js → index-D3haorUt.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BY8-nCgs.js → index-D6Ij_2uw.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-FQBsuExX.js → index-DDRlBaDt.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C-ZCaUJc.js → index-DGsE7MQz.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BjcWKg1S.js → index-DHvxXXNA.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-GA80ir_u.js → index-DV11jpeO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CsovTpx9.js → index-DXdnfDVK.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-nSn2-C4N.js → index-D_75jYWk.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BJi3UqBt.js → index-DjI9MkId.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-gINyI1rh.js → index-DlSvpNMf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CfLJ42kv.js → index-Dox44j29.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C2NyllnW.js → index-DpEKmjoa.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Iw0FfqUz.js → index-Dsy7_x4s.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-4eyH-eC2.js → index-Gxw09e9R.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-4PIeTM--.js → index-LfRlSmqe.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-IZAWt0H8.js → index-Nz-MUHD1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DUU2606o.js → index-b9cuvFEv.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CzE53HiB.js → index-eR6H6NR7.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-9PIVuXqo.js → index-fWgUQZ9d.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DkH1JvEt.js → index-mfWQRSLn.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DNMxIsDg.js → index-yOZYSNmG.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-DI0Zaf8-.js → initDefaultProps-CJ1Bq2L3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BZDKSy3Z.js → motion-rYJN_vzf.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DsIW01Vp.js → move-DnDK2gTP.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CohkDhsf.js → mtc-parser-CKnXy7ES.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-DpiN5rzg.js → omit-3nRc3hWT.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-qbuqni3y.js → pickAttrs-Ddjj3sLu.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-mBky6714.js → placementArrow-CbsbphQY.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-Bas3eQ2K.js → responsiveObserve-D9aZ4VDJ.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-CBx_SKoK.js → slide-D4pvdFj0.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BlYguJSV.js → statusUtils-C2W-vY-S.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BCYQmRGv.js → styleChecker-Cbq5150d.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-Dp5dybLM.js → useFlexGapSupport-BA2B7R82.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-DHAMBgA2.js → useFs-CLrwg3xg.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-W6jsSw76.js → usePersonalDataHub-DQeAI5qs.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-BkI6aayc.js → vnode-8BDGy-YU.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-C_IMGueq.js → zoom-B2SzFprl.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agenda.js +74 -1
  152. package/src/commands/background-session.js +11 -1
  153. package/src/commands/batch.js +160 -5
  154. package/src/commands/plugin.js +90 -27
  155. package/src/commands/session.js +31 -1
  156. package/src/commands/team.js +212 -6
  157. package/src/data/changelog.json +26 -0
  158. package/src/gateways/terminal/PtyManager.js +45 -15
  159. package/src/gateways/ws/ws-agent-handler.js +15 -1
  160. package/src/gateways/ws/ws-session-gateway.js +88 -0
  161. package/src/harness/background-task-command-runner.js +234 -0
  162. package/src/harness/background-task-manager.js +150 -20
  163. package/src/harness/background-task-worker.js +17 -11
  164. package/src/harness/jsonl-session-store.js +57 -6
  165. package/src/harness/mcp-client.js +65 -43
  166. package/src/lib/agent-batch.js +4 -1
  167. package/src/lib/agent-team/team-worktree.js +3 -0
  168. package/src/lib/async-hook-supervisor.cjs +82 -10
  169. package/src/lib/async-hook-supervisor.js +50 -4
  170. package/src/lib/background-agent-supervisor.js +46 -0
  171. package/src/lib/collaboration-run-store.js +362 -0
  172. package/src/lib/event-runtime-host.js +12 -10
  173. package/src/lib/hook-runner.cjs +105 -18
  174. package/src/lib/hook-runner.js +79 -6
  175. package/src/lib/hook-shell-command.cjs +148 -0
  176. package/src/lib/hooks-v2-runtime.js +280 -81
  177. package/src/lib/hooks-v2-workspace-context.js +258 -0
  178. package/src/lib/jsonl-session-store.js +2 -0
  179. package/src/lib/lsp/__tests__/lsp-client.test.js +15 -0
  180. package/src/lib/lsp/lsp-client.js +12 -1
  181. package/src/lib/plugin-monitor-supervisor.js +53 -16
  182. package/src/lib/plugin-runtime/bin.js +21 -5
  183. package/src/lib/plugin-runtime/install.js +249 -57
  184. package/src/lib/plugin-runtime/mcp.js +21 -2
  185. package/src/lib/plugin-runtime/monitors.js +19 -3
  186. package/src/lib/plugin-runtime/sandbox-policy.js +100 -4
  187. package/src/lib/process-execution-broker/index.js +1151 -44
  188. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +1557 -0
  189. package/src/lib/process-execution-broker/linux-generic-bwrap.js +1378 -0
  190. package/src/lib/process-execution-broker/platform-sandbox.js +586 -58
  191. package/src/lib/session-usage.js +202 -2
  192. package/src/lib/stream-retry.js +29 -0
  193. package/src/repl/agent-repl.js +40 -3
  194. package/src/runtime/agent-core.js +304 -42
  195. package/src/runtime/headless-runner.js +48 -1
  196. package/src/runtime/headless-stream.js +42 -2
  197. package/src/workers/background-agent-worker.js +5 -11
  198. package/src/assets/web-panel/assets/MarkdownRenderer-C0o4FtpD.js +0 -1
  199. package/src/assets/web-panel/assets/MobileProjects-VTc3gT1y.js +0 -1
  200. package/src/assets/web-panel/assets/RemoteSession-B6EEf8hh.js +0 -5
  201. package/src/assets/web-panel/assets/Tasks-D-R1x8vm.js +0 -1
  202. package/src/assets/web-panel/assets/Terminal-Dnkos7vW.js +0 -3
  203. package/src/assets/web-panel/assets/devWarning-D3Bho_6u.js +0 -1
  204. package/src/assets/web-panel/assets/index-5iAFwYx1.js +0 -1
  205. package/src/assets/web-panel/assets/index-BfnB7LPx.js +0 -1
@@ -0,0 +1,1557 @@
1
+ /**
2
+ * Production resource acquisition for the generic Linux bubblewrap backend.
3
+ *
4
+ * Caller-supplied callbacks never cross this boundary. Every mount, the
5
+ * bubblewrap executable, and the seccomp program is opened by this module and
6
+ * independently re-attested before the final spawn plan is returned.
7
+ */
8
+
9
+ import crypto from "node:crypto";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { spawnSync as nativeSpawnSync } from "node:child_process";
14
+ import {
15
+ LINUX_GENERIC_BWRAP_BACKEND,
16
+ LINUX_GENERIC_CONTRACT_KIND,
17
+ LINUX_GENERIC_PTY_LAUNCHER_PATH,
18
+ planLinuxGenericBubblewrap,
19
+ } from "./linux-generic-bwrap.js";
20
+
21
+ const BWRAP_PATH = "/usr/bin/bwrap";
22
+ const MAX_ATTESTED_FILE_BYTES = 256 * 1024 * 1024;
23
+ const MAX_MOUNTINFO_BYTES = 4 * 1024 * 1024;
24
+ const MOUNTINFO_PATH = "/proc/self/mountinfo";
25
+ const MOUNT_TOPOLOGY_VERSION = 1;
26
+ const O_TMPFILE = 0x410000;
27
+ const SYSTEM_DESTINATIONS = [
28
+ "/usr",
29
+ "/bin",
30
+ "/sbin",
31
+ "/lib",
32
+ "/lib32",
33
+ "/lib64",
34
+ ];
35
+ const SYSTEM_ROOTS = new Set(SYSTEM_DESTINATIONS);
36
+ const SYSTEM_SYMLINKS = new Map([
37
+ ["/bin", "usr/bin"],
38
+ ["/sbin", "usr/sbin"],
39
+ ["/lib", "usr/lib"],
40
+ ["/lib32", "usr/lib32"],
41
+ ["/lib64", "usr/lib64"],
42
+ ]);
43
+ const ETC_ALLOWLIST = [
44
+ "/etc/group",
45
+ "/etc/hosts",
46
+ "/etc/ld.so.cache",
47
+ "/etc/nsswitch.conf",
48
+ "/etc/passwd",
49
+ ];
50
+ const FORBIDDEN_EXACT_WORKSPACE_ALIASES = [
51
+ "/",
52
+ "/home",
53
+ "/home/sandbox",
54
+ "/media",
55
+ "/mnt",
56
+ "/opt",
57
+ "/root",
58
+ "/run",
59
+ "/srv",
60
+ "/tmp",
61
+ "/var",
62
+ "/var/tmp",
63
+ ];
64
+ const FORBIDDEN_WORKSPACE_ALIAS_SUBTREES = [
65
+ "/bin",
66
+ "/boot",
67
+ "/dev",
68
+ "/etc",
69
+ "/lib",
70
+ "/lib32",
71
+ "/lib64",
72
+ "/proc",
73
+ "/sbin",
74
+ "/sys",
75
+ "/usr",
76
+ ];
77
+ const OPAQUE_NON_ROOT_FILESYSTEMS = new Set([
78
+ "9p",
79
+ "aufs",
80
+ "ceph",
81
+ "cifs",
82
+ "ecryptfs",
83
+ "glusterfs",
84
+ "nfs",
85
+ "nfs4",
86
+ "overlay",
87
+ "smb3",
88
+ "virtiofs",
89
+ ]);
90
+ const REQUIRED_BWRAP_OPTIONS = [
91
+ "--assert-userns-disabled",
92
+ "--bind-fd",
93
+ "--disable-userns",
94
+ "--file",
95
+ "--perms",
96
+ "--remount-ro",
97
+ "--ro-bind-fd",
98
+ "--seccomp",
99
+ ];
100
+ const SECCOMP_ARCHITECTURES = Object.freeze({
101
+ x64: Object.freeze({
102
+ auditArch: 0xc000003e,
103
+ socketSyscall: 41,
104
+ socketpairSyscall: 53,
105
+ ioUringSetupSyscall: 425,
106
+ x32SyscallBit: 0x40000000,
107
+ }),
108
+ arm64: Object.freeze({
109
+ auditArch: 0xc00000b7,
110
+ socketSyscall: 198,
111
+ socketpairSyscall: 199,
112
+ ioUringSetupSyscall: 425,
113
+ }),
114
+ riscv64: Object.freeze({
115
+ auditArch: 0xc00000f3,
116
+ socketSyscall: 198,
117
+ socketpairSyscall: 199,
118
+ ioUringSetupSyscall: 425,
119
+ }),
120
+ });
121
+
122
+ const DEFAULT_RUNTIME = Object.freeze({
123
+ platform: os.platform(),
124
+ arch: process.arch,
125
+ execPath: process.execPath,
126
+ fs,
127
+ homedir: () => os.homedir(),
128
+ tmpdir: () => os.tmpdir(),
129
+ randomBytes: (size) => crypto.randomBytes(size),
130
+ spawnSync: nativeSpawnSync,
131
+ });
132
+
133
+ function runtimeFor(overrides = {}) {
134
+ return { ...DEFAULT_RUNTIME, ...overrides };
135
+ }
136
+
137
+ function realpath(runtime, value) {
138
+ const implementation =
139
+ runtime.fs.realpathSync?.native || runtime.fs.realpathSync;
140
+ if (typeof implementation !== "function") {
141
+ throw new Error("realpath_unavailable");
142
+ }
143
+ return implementation.call(runtime.fs.realpathSync, value);
144
+ }
145
+
146
+ function fileId(stat) {
147
+ return { dev: String(stat.dev), ino: String(stat.ino) };
148
+ }
149
+
150
+ function identity(realPath, stat) {
151
+ return Object.freeze({
152
+ realPath,
153
+ fileId: Object.freeze(fileId(stat)),
154
+ mode: Number(stat.mode),
155
+ uid: Number(stat.uid),
156
+ gid: Number(stat.gid),
157
+ });
158
+ }
159
+
160
+ function identityMatches(left, right) {
161
+ return (
162
+ left?.realPath === right?.realPath &&
163
+ String(left?.fileId?.dev) === String(right?.fileId?.dev) &&
164
+ String(left?.fileId?.ino) === String(right?.fileId?.ino) &&
165
+ Number(left?.mode) === Number(right?.mode) &&
166
+ Number(left?.uid) === Number(right?.uid) &&
167
+ Number(left?.gid) === Number(right?.gid)
168
+ );
169
+ }
170
+
171
+ function statMatchesIdentity(stat, expected, { directory }) {
172
+ const expectedType = directory ? 0o040000 : 0o100000;
173
+ return (
174
+ String(stat?.dev) === String(expected?.fileId?.dev) &&
175
+ String(stat?.ino) === String(expected?.fileId?.ino) &&
176
+ Number(stat?.mode) === Number(expected?.mode) &&
177
+ Number(stat?.uid) === Number(expected?.uid) &&
178
+ Number(stat?.gid) === Number(expected?.gid) &&
179
+ (Number(stat?.mode) & 0o170000) === expectedType
180
+ );
181
+ }
182
+
183
+ function isWithin(root, target) {
184
+ const relative = path.posix.relative(root, target);
185
+ return (
186
+ relative === "" ||
187
+ (relative !== ".." &&
188
+ !relative.startsWith("../") &&
189
+ !path.posix.isAbsolute(relative))
190
+ );
191
+ }
192
+
193
+ function rootOwned(stat, { directory, executable = false }) {
194
+ const expectedType = directory ? 0o040000 : 0o100000;
195
+ return (
196
+ (Number(stat?.mode) & 0o170000) === expectedType &&
197
+ Number(stat?.uid) === 0 &&
198
+ (Number(stat?.mode) & 0o022) === 0 &&
199
+ (!executable || (Number(stat?.mode) & 0o111) !== 0)
200
+ );
201
+ }
202
+
203
+ function hashOpenFile(runtime, fd, bytes) {
204
+ if (
205
+ !Number.isSafeInteger(bytes) ||
206
+ bytes < 1 ||
207
+ bytes > MAX_ATTESTED_FILE_BYTES
208
+ ) {
209
+ throw new Error("attested_file_size_invalid");
210
+ }
211
+ const digest = crypto.createHash("sha256");
212
+ const buffer = Buffer.allocUnsafe(Math.min(1024 * 1024, bytes));
213
+ let offset = 0;
214
+ while (offset < bytes) {
215
+ const requested = Math.min(buffer.length, bytes - offset);
216
+ const read = runtime.fs.readSync(fd, buffer, 0, requested, offset);
217
+ if (read <= 0) throw new Error("attested_file_read_failed");
218
+ digest.update(buffer.subarray(0, read));
219
+ offset += read;
220
+ }
221
+ return digest.digest("hex");
222
+ }
223
+
224
+ function buildNetworkSeccompFilter(arch) {
225
+ const selected = SECCOMP_ARCHITECTURES[arch];
226
+ if (!selected) {
227
+ throw new Error(`unsupported_seccomp_architecture:${String(arch)}`);
228
+ }
229
+ const instructions = [
230
+ [0x20, 0, 0, 4],
231
+ [0x15, 1, 0, selected.auditArch],
232
+ [0x06, 0, 0, 0x80000000],
233
+ [0x20, 0, 0, 0],
234
+ ];
235
+ if (selected.x32SyscallBit) {
236
+ instructions.push([0x54, 0, 0, ~selected.x32SyscallBit]);
237
+ }
238
+ instructions.push(
239
+ [0x15, 0, 1, selected.socketSyscall],
240
+ [0x06, 0, 0, 0x00050001],
241
+ [0x15, 0, 1, selected.socketpairSyscall],
242
+ [0x06, 0, 0, 0x00050001],
243
+ [0x15, 0, 1, selected.ioUringSetupSyscall],
244
+ [0x06, 0, 0, 0x00050001],
245
+ [0x06, 0, 0, 0x7fff0000],
246
+ );
247
+ const buffer = Buffer.alloc(instructions.length * 8);
248
+ instructions.forEach(([code, jumpTrue, jumpFalse, value], index) => {
249
+ const offset = index * 8;
250
+ buffer.writeUInt16LE(code, offset);
251
+ buffer.writeUInt8(jumpTrue, offset + 2);
252
+ buffer.writeUInt8(jumpFalse, offset + 3);
253
+ buffer.writeUInt32LE(value >>> 0, offset + 4);
254
+ });
255
+ return Object.freeze({
256
+ arch,
257
+ buffer,
258
+ sha256: crypto.createHash("sha256").update(buffer).digest("hex"),
259
+ });
260
+ }
261
+
262
+ function shellQuote(value) {
263
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
264
+ }
265
+
266
+ function stableJson(value) {
267
+ if (Array.isArray(value)) {
268
+ return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
269
+ }
270
+ if (value && typeof value === "object") {
271
+ return `{${Object.keys(value)
272
+ .sort()
273
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
274
+ .join(",")}}`;
275
+ }
276
+ return JSON.stringify(value);
277
+ }
278
+
279
+ function sha256(value) {
280
+ return crypto.createHash("sha256").update(value).digest("hex");
281
+ }
282
+
283
+ function decodeMountInfoPath(value) {
284
+ if (typeof value !== "string" || !value) {
285
+ throw new Error("mountinfo_path_invalid");
286
+ }
287
+ const decoded = [];
288
+ const escapes = new Map([
289
+ ["040", " "],
290
+ ["011", "\t"],
291
+ ["012", "\n"],
292
+ ["134", "\\"],
293
+ ]);
294
+ for (let index = 0; index < value.length; index += 1) {
295
+ if (value[index] !== "\\") {
296
+ decoded.push(value[index]);
297
+ continue;
298
+ }
299
+ const octal = value.slice(index + 1, index + 4);
300
+ if (!escapes.has(octal)) {
301
+ throw new Error("mountinfo_path_escape_invalid");
302
+ }
303
+ decoded.push(escapes.get(octal));
304
+ index += 3;
305
+ }
306
+ const result = decoded.join("");
307
+ if (
308
+ result.includes("\0") ||
309
+ !path.posix.isAbsolute(result) ||
310
+ path.posix.normalize(result) !== result
311
+ ) {
312
+ throw new Error("mountinfo_path_invalid");
313
+ }
314
+ return result;
315
+ }
316
+
317
+ function encodeMountInfoPath(value) {
318
+ return String(value)
319
+ .replaceAll("\\", "\\134")
320
+ .replaceAll(" ", "\\040")
321
+ .replaceAll("\t", "\\011")
322
+ .replaceAll("\n", "\\012");
323
+ }
324
+
325
+ function parseMountInfo(runtime) {
326
+ const raw = String(runtime.fs.readFileSync(MOUNTINFO_PATH, "utf8"));
327
+ if (
328
+ Buffer.byteLength(raw, "utf8") < 1 ||
329
+ Buffer.byteLength(raw, "utf8") > MAX_MOUNTINFO_BYTES
330
+ ) {
331
+ throw new Error("mountinfo_size_invalid");
332
+ }
333
+ const entries = [];
334
+ for (const line of raw.split("\n")) {
335
+ if (!line) continue;
336
+ const fields = line.split(" ");
337
+ const separator = fields.indexOf("-");
338
+ if (
339
+ separator < 6 ||
340
+ fields.length < separator + 4 ||
341
+ !/^\d+$/.test(fields[0] || "") ||
342
+ !/^\d+$/.test(fields[1] || "") ||
343
+ !/^\d+:\d+$/.test(fields[2] || "")
344
+ ) {
345
+ throw new Error("mountinfo_entry_invalid");
346
+ }
347
+ const root = decodeMountInfoPath(fields[3]);
348
+ const mountPoint = decodeMountInfoPath(fields[4]);
349
+ const optionalFields = fields.slice(6, separator);
350
+ const fsType = fields[separator + 1];
351
+ if (
352
+ !fields[5] ||
353
+ fields[5].includes("\0") ||
354
+ optionalFields.some((field) => !field || field.includes("\0")) ||
355
+ !fsType ||
356
+ fsType.includes("\0") ||
357
+ fields.slice(separator + 1).some((field) => !field)
358
+ ) {
359
+ throw new Error("mountinfo_entry_invalid");
360
+ }
361
+ entries.push(
362
+ Object.freeze({
363
+ mountId: fields[0],
364
+ parentId: fields[1],
365
+ majorMinor: fields[2],
366
+ root,
367
+ mountPoint,
368
+ fsType,
369
+ optionalFields: Object.freeze(optionalFields),
370
+ lineDigest: sha256(line),
371
+ }),
372
+ );
373
+ }
374
+ if (entries.length === 0) throw new Error("mountinfo_empty");
375
+ return entries;
376
+ }
377
+
378
+ function mountDepth(value) {
379
+ return value === "/" ? 0 : value.split("/").filter(Boolean).length;
380
+ }
381
+
382
+ function containingMount(entries, target) {
383
+ const candidates = entries.filter((entry) =>
384
+ isWithin(entry.mountPoint, target),
385
+ );
386
+ if (candidates.length === 0) throw new Error("mountinfo_root_unavailable");
387
+ const deepest = Math.max(
388
+ ...candidates.map((entry) => mountDepth(entry.mountPoint)),
389
+ );
390
+ const matches = candidates.filter(
391
+ (entry) => mountDepth(entry.mountPoint) === deepest,
392
+ );
393
+ if (matches.length !== 1) {
394
+ throw new Error("mountinfo_workspace_mount_ambiguous");
395
+ }
396
+ return matches[0];
397
+ }
398
+
399
+ function sameDirectoryIdentity(left, right) {
400
+ return (
401
+ String(left?.dev ?? left?.fileId?.dev) ===
402
+ String(right?.dev ?? right?.fileId?.dev) &&
403
+ String(left?.ino ?? left?.fileId?.ino) ===
404
+ String(right?.ino ?? right?.fileId?.ino)
405
+ );
406
+ }
407
+
408
+ function bindSourceMountTopology(entries, bindSources) {
409
+ const normalizedSources = bindSources
410
+ .map((source) => ({
411
+ kind: source.kind,
412
+ source: source.source,
413
+ recursive: source.recursive === true,
414
+ }))
415
+ .sort(
416
+ (left, right) =>
417
+ left.source.localeCompare(right.source) ||
418
+ left.kind.localeCompare(right.kind),
419
+ );
420
+ const sourceKeys = new Set();
421
+ for (const source of normalizedSources) {
422
+ if (
423
+ typeof source.kind !== "string" ||
424
+ !source.kind ||
425
+ typeof source.source !== "string" ||
426
+ !path.posix.isAbsolute(source.source) ||
427
+ path.posix.normalize(source.source) !== source.source
428
+ ) {
429
+ throw new Error("bind_source_mount_topology_invalid");
430
+ }
431
+ const key = `${source.kind}\0${source.source}`;
432
+ if (sourceKeys.has(key)) {
433
+ throw new Error("bind_source_mount_topology_duplicate");
434
+ }
435
+ sourceKeys.add(key);
436
+ }
437
+
438
+ let importedMountEntryCount = 0;
439
+ const sourceMountBindings = normalizedSources.map((source) => {
440
+ const mountedBy = containingMount(entries, source.source);
441
+ const importedMounts = new Map([[mountedBy.mountId, mountedBy]]);
442
+ if (source.recursive) {
443
+ for (const entry of entries) {
444
+ if (isWithin(source.source, entry.mountPoint)) {
445
+ importedMounts.set(entry.mountId, entry);
446
+ }
447
+ }
448
+ }
449
+ const imported = [...importedMounts.values()];
450
+ if (imported.some((entry) => entry.optionalFields.length !== 0)) {
451
+ throw new Error("bind_source_mount_propagation_not_private");
452
+ }
453
+ importedMountEntryCount += imported.length;
454
+ return {
455
+ ...source,
456
+ containingMount: mountedBy.lineDigest,
457
+ importedMountEntryDigests: imported
458
+ .map((entry) => entry.lineDigest)
459
+ .sort(),
460
+ };
461
+ });
462
+ return {
463
+ sourceMountSetDigest: sha256(stableJson(normalizedSources)),
464
+ sourceMountCount: normalizedSources.length,
465
+ importedMountEntryCount,
466
+ sourceMountBindings,
467
+ sourceMountPropagationPrivateAtAttestation: true,
468
+ };
469
+ }
470
+
471
+ function workspaceMountTopology(
472
+ runtime,
473
+ workspaceRoot,
474
+ workspaceIdentity,
475
+ bindSources,
476
+ ) {
477
+ const entries = parseMountInfo(runtime);
478
+ const descendants = entries.filter(
479
+ (entry) =>
480
+ entry.mountPoint !== workspaceRoot &&
481
+ isWithin(workspaceRoot, entry.mountPoint),
482
+ );
483
+ if (descendants.length > 0) {
484
+ throw new Error("workspace_descendant_mount_detected");
485
+ }
486
+
487
+ const mountedBy = containingMount(entries, workspaceRoot);
488
+ const rootMount = containingMount(entries, "/");
489
+ const workspaceRelative = path.posix.relative(
490
+ mountedBy.mountPoint,
491
+ workspaceRoot,
492
+ );
493
+ const filesystemPath = path.posix.join(mountedBy.root, workspaceRelative);
494
+ const sameFilesystem = entries.filter(
495
+ (entry) => entry.majorMinor === mountedBy.majorMinor,
496
+ );
497
+ const aliases = new Set();
498
+ for (const entry of sameFilesystem) {
499
+ if (!isWithin(entry.root, filesystemPath)) continue;
500
+ aliases.add(
501
+ path.posix.join(
502
+ entry.mountPoint,
503
+ path.posix.relative(entry.root, filesystemPath),
504
+ ),
505
+ );
506
+ }
507
+ if (aliases.size === 0) {
508
+ throw new Error("workspace_mount_provenance_unavailable");
509
+ }
510
+ if (
511
+ mountedBy.mountPoint !== "/" &&
512
+ mountedBy.root !== "/" &&
513
+ mountedBy.mountPoint === workspaceRoot
514
+ ) {
515
+ throw new Error("workspace_root_bind_alias_forbidden");
516
+ }
517
+ if ([...aliases].some((alias) => alias !== workspaceRoot)) {
518
+ throw new Error("workspace_root_alias_forbidden");
519
+ }
520
+
521
+ let home;
522
+ try {
523
+ home = realpath(runtime, runtime.homedir());
524
+ } catch {
525
+ throw new Error("workspace_home_identity_unavailable");
526
+ }
527
+ const forbiddenExact = new Set([
528
+ ...FORBIDDEN_EXACT_WORKSPACE_ALIASES,
529
+ ...FORBIDDEN_WORKSPACE_ALIAS_SUBTREES,
530
+ home,
531
+ ]);
532
+ for (const alias of aliases) {
533
+ if (
534
+ forbiddenExact.has(alias) ||
535
+ FORBIDDEN_WORKSPACE_ALIAS_SUBTREES.some((root) => isWithin(root, alias))
536
+ ) {
537
+ throw new Error("workspace_root_alias_forbidden");
538
+ }
539
+ }
540
+
541
+ const forbiddenIdentities = [];
542
+ for (const candidate of forbiddenExact) {
543
+ try {
544
+ const canonical = realpath(runtime, candidate);
545
+ const stat = runtime.fs.statSync(canonical);
546
+ if (!stat.isDirectory?.()) continue;
547
+ const observed = {
548
+ path: candidate,
549
+ canonical,
550
+ dev: String(stat.dev),
551
+ ino: String(stat.ino),
552
+ };
553
+ forbiddenIdentities.push(observed);
554
+ if (sameDirectoryIdentity(stat, workspaceIdentity)) {
555
+ throw new Error("workspace_root_identity_alias_forbidden");
556
+ }
557
+ } catch (error) {
558
+ if (error?.message === "workspace_root_identity_alias_forbidden") {
559
+ throw error;
560
+ }
561
+ // Missing optional system roots do not weaken the paths that exist.
562
+ }
563
+ }
564
+
565
+ const opaqueFilesystem =
566
+ mountedBy.fsType.startsWith("fuse") ||
567
+ OPAQUE_NON_ROOT_FILESYSTEMS.has(mountedBy.fsType);
568
+ if (mountedBy.mountPoint !== "/" && opaqueFilesystem) {
569
+ throw new Error("workspace_mount_provenance_opaque");
570
+ }
571
+
572
+ const lineage = entries.filter((entry) =>
573
+ isWithin(entry.mountPoint, workspaceRoot),
574
+ );
575
+ const bindSourceTopology = bindSourceMountTopology(entries, bindSources);
576
+ const binding = {
577
+ version: MOUNT_TOPOLOGY_VERSION,
578
+ source: "proc-self-mountinfo",
579
+ workspaceRoot,
580
+ containingMount: mountedBy.lineDigest,
581
+ rootMount: rootMount.lineDigest,
582
+ lineageEntryDigests: lineage.map((entry) => entry.lineDigest).sort(),
583
+ filesystemEntryDigests: sameFilesystem
584
+ .map((entry) => entry.lineDigest)
585
+ .sort(),
586
+ aliasDigests: [...aliases].map(sha256).sort(),
587
+ forbiddenIdentityDigests: forbiddenIdentities
588
+ .map((entry) => sha256(stableJson(entry)))
589
+ .sort(),
590
+ ...bindSourceTopology,
591
+ strictDescendantMountsAtAttestation: 0,
592
+ recursiveBind: true,
593
+ mountTopologyAtomic: false,
594
+ };
595
+ return Object.freeze({
596
+ version: binding.version,
597
+ source: binding.source,
598
+ workspaceRoot: binding.workspaceRoot,
599
+ digest: sha256(stableJson(binding)),
600
+ lineageEntryCount: binding.lineageEntryDigests.length,
601
+ filesystemEntryCount: binding.filesystemEntryDigests.length,
602
+ aliasCount: binding.aliasDigests.length,
603
+ forbiddenIdentityCount: binding.forbiddenIdentityDigests.length,
604
+ sourceMountSetDigest: binding.sourceMountSetDigest,
605
+ sourceMountCount: binding.sourceMountCount,
606
+ importedMountEntryCount: binding.importedMountEntryCount,
607
+ sourceMountPropagationPrivateAtAttestation:
608
+ binding.sourceMountPropagationPrivateAtAttestation,
609
+ strictDescendantMountsAtAttestation:
610
+ binding.strictDescendantMountsAtAttestation,
611
+ rootAliasAttested: true,
612
+ recursiveBind: binding.recursiveBind,
613
+ mountTopologyAtomic: binding.mountTopologyAtomic,
614
+ });
615
+ }
616
+
617
+ function normalizedStdio(stdio) {
618
+ if (stdio === undefined || stdio === null || stdio === "pipe") {
619
+ return ["pipe", "pipe", "pipe"];
620
+ }
621
+ if (stdio === "ignore") return ["ignore", "ignore", "ignore"];
622
+ if (!Array.isArray(stdio)) return null;
623
+ const result = [...stdio];
624
+ while (result.length < 3) result.push(undefined);
625
+ return result.map((entry, index) => {
626
+ if (entry === undefined || entry === null) {
627
+ return index < 3 ? "pipe" : "ignore";
628
+ }
629
+ return entry;
630
+ });
631
+ }
632
+
633
+ function resolveExecutable(runtime, requested, cwd, environment) {
634
+ if (typeof requested !== "string" || !requested || requested.includes("\0")) {
635
+ throw new Error("generic_target_invalid");
636
+ }
637
+ const candidates = [];
638
+ if (requested.includes("/")) {
639
+ candidates.push(
640
+ path.posix.isAbsolute(requested)
641
+ ? requested
642
+ : path.posix.resolve(cwd, requested),
643
+ );
644
+ } else {
645
+ for (const entry of String(
646
+ environment?.PATH || "/usr/local/bin:/usr/bin:/bin",
647
+ ).split(":")) {
648
+ if (!entry) continue;
649
+ const directory = path.posix.isAbsolute(entry)
650
+ ? entry
651
+ : path.posix.resolve(cwd, entry);
652
+ candidates.push(path.posix.join(directory, requested));
653
+ }
654
+ }
655
+ for (const candidate of candidates) {
656
+ try {
657
+ runtime.fs.accessSync(
658
+ candidate,
659
+ runtime.fs.constants?.X_OK ?? fs.constants.X_OK,
660
+ );
661
+ const canonical = realpath(runtime, candidate);
662
+ const stat = runtime.fs.statSync(canonical);
663
+ if (!stat.isFile() || (Number(stat.mode) & 0o111) === 0) continue;
664
+ return { canonical, stat };
665
+ } catch {
666
+ // Continue PATH resolution exactly as child_process would.
667
+ }
668
+ }
669
+ throw new Error("generic_target_unavailable");
670
+ }
671
+
672
+ function systemTarget(canonical) {
673
+ return [...SYSTEM_ROOTS].some((root) => isWithin(root, canonical));
674
+ }
675
+
676
+ function createTrustedResources(
677
+ contract,
678
+ command,
679
+ args,
680
+ spawnOpts,
681
+ sandboxOpts,
682
+ runtime,
683
+ ) {
684
+ const ownedFds = new Set();
685
+ const probeFds = new Set();
686
+ const finalFds = new Set();
687
+ const closeFd = (fd) => {
688
+ if (!Number.isInteger(fd) || !ownedFds.delete(fd)) return;
689
+ probeFds.delete(fd);
690
+ finalFds.delete(fd);
691
+ try {
692
+ runtime.fs.closeSync(fd);
693
+ } catch {
694
+ // Cleanup must preserve the fail-closed boundary result.
695
+ }
696
+ };
697
+ const closeSet = (set) => {
698
+ for (const fd of [...set]) closeFd(fd);
699
+ };
700
+ const cleanup = () => closeSet(ownedFds);
701
+ const track = (fd, phase) => {
702
+ ownedFds.add(fd);
703
+ (phase === "probe" ? probeFds : finalFds).add(fd);
704
+ return fd;
705
+ };
706
+ const openFlags = ({ directory, readWrite = false }) => {
707
+ const constants = runtime.fs.constants || fs.constants;
708
+ return (
709
+ Number(readWrite ? constants.O_RDWR : constants.O_RDONLY) |
710
+ Number(directory ? constants.O_DIRECTORY || 0 : 0) |
711
+ Number(constants.O_NOFOLLOW || 0) |
712
+ Number(constants.O_NONBLOCK || 0)
713
+ );
714
+ };
715
+ const openPair = (
716
+ source,
717
+ { directory, rootRequired = false, executable = false },
718
+ ) => {
719
+ const canonical = realpath(runtime, source);
720
+ if (canonical !== source) throw new Error("resource_path_not_canonical");
721
+ const pathStat = runtime.fs.lstatSync(source);
722
+ if (
723
+ pathStat.isSymbolicLink?.() ||
724
+ (directory ? !pathStat.isDirectory() : !pathStat.isFile()) ||
725
+ (rootRequired && !rootOwned(pathStat, { directory, executable }))
726
+ ) {
727
+ throw new Error("resource_path_unattested");
728
+ }
729
+ const probeFd = track(
730
+ runtime.fs.openSync(source, openFlags({ directory })),
731
+ "probe",
732
+ );
733
+ const finalFd = track(
734
+ runtime.fs.openSync(source, openFlags({ directory })),
735
+ "final",
736
+ );
737
+ const probeStat = runtime.fs.fstatSync(probeFd);
738
+ const finalStat = runtime.fs.fstatSync(finalFd);
739
+ const observedIdentity = identity(source, pathStat);
740
+ if (
741
+ !statMatchesIdentity(probeStat, observedIdentity, { directory }) ||
742
+ !statMatchesIdentity(finalStat, observedIdentity, { directory })
743
+ ) {
744
+ throw new Error("resource_descriptor_identity_changed");
745
+ }
746
+ return {
747
+ source,
748
+ probeFd,
749
+ finalFd,
750
+ identity: observedIdentity,
751
+ };
752
+ };
753
+
754
+ try {
755
+ if (
756
+ runtime.platform !== "linux" ||
757
+ contract?.kind !== LINUX_GENERIC_CONTRACT_KIND ||
758
+ spawnOpts?.shell === true ||
759
+ typeof spawnOpts?.shell === "string" ||
760
+ spawnOpts?.detached === true ||
761
+ spawnOpts?.uid !== undefined ||
762
+ spawnOpts?.gid !== undefined ||
763
+ spawnOpts?.argv0 !== undefined ||
764
+ spawnOpts?.serialization !== undefined
765
+ ) {
766
+ throw new Error("generic_launch_options_unsupported");
767
+ }
768
+ const hostNetworkNamespace = String(
769
+ runtime.fs.readlinkSync("/proc/self/ns/net"),
770
+ );
771
+ if (!/^net:\[\d+\]$/.test(hostNetworkNamespace)) {
772
+ throw new Error("host_network_namespace_identity_unavailable");
773
+ }
774
+
775
+ const workspace = openPair(contract.workspaceRoot, {
776
+ directory: true,
777
+ });
778
+ const cwdPins = openPair(contract.workingDirectory, {
779
+ directory: true,
780
+ });
781
+ // Fail before executing any helper when the writable recursive source is
782
+ // already propagation-capable. The complete exact source set is attested
783
+ // after the actual system and /etc descriptors have been acquired.
784
+ workspaceMountTopology(
785
+ runtime,
786
+ contract.workspaceRoot,
787
+ workspace.identity,
788
+ [
789
+ {
790
+ kind: "workspace",
791
+ source: contract.workspaceRoot,
792
+ recursive: true,
793
+ },
794
+ ],
795
+ );
796
+
797
+ const supervisor = openPair(BWRAP_PATH, {
798
+ directory: false,
799
+ rootRequired: true,
800
+ executable: true,
801
+ });
802
+ const supervisorBytes = Number(
803
+ runtime.fs.fstatSync(supervisor.probeFd).size,
804
+ );
805
+ const supervisorProbeDigest = hashOpenFile(
806
+ runtime,
807
+ supervisor.probeFd,
808
+ supervisorBytes,
809
+ );
810
+ const supervisorFinalDigest = hashOpenFile(
811
+ runtime,
812
+ supervisor.finalFd,
813
+ supervisorBytes,
814
+ );
815
+ if (supervisorProbeDigest !== supervisorFinalDigest) {
816
+ throw new Error("bubblewrap_supervisor_digest_mismatch");
817
+ }
818
+ supervisor.identity = Object.freeze({
819
+ ...supervisor.identity,
820
+ sha256: supervisorFinalDigest,
821
+ bytes: supervisorBytes,
822
+ });
823
+
824
+ const capabilityFd = runtime.fs.openSync(
825
+ BWRAP_PATH,
826
+ openFlags({ directory: false }),
827
+ );
828
+ ownedFds.add(capabilityFd);
829
+ const capabilityStat = runtime.fs.fstatSync(capabilityFd);
830
+ if (
831
+ !statMatchesIdentity(capabilityStat, supervisor.identity, {
832
+ directory: false,
833
+ }) ||
834
+ hashOpenFile(runtime, capabilityFd, supervisorBytes) !==
835
+ supervisorFinalDigest
836
+ ) {
837
+ throw new Error("bubblewrap_capability_reader_unattested");
838
+ }
839
+ const capabilityResult = runtime.spawnSync("/proc/self/fd/3", ["--help"], {
840
+ shell: false,
841
+ encoding: "utf8",
842
+ stdio: ["ignore", "pipe", "pipe", capabilityFd],
843
+ timeout: 10_000,
844
+ env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" },
845
+ });
846
+ closeFd(capabilityFd);
847
+ if (capabilityResult?.error || capabilityResult?.status !== 0) {
848
+ const error = new Error("bubblewrap_capability_probe_failed");
849
+ error.code = "LINUX_GENERIC_BWRAP_UNAVAILABLE";
850
+ throw error;
851
+ }
852
+ const supported = new Set(
853
+ `${String(capabilityResult.stdout || "")}\n${String(
854
+ capabilityResult.stderr || "",
855
+ )}`
856
+ .split(/\s+/)
857
+ .filter(Boolean),
858
+ );
859
+ if (REQUIRED_BWRAP_OPTIONS.some((option) => !supported.has(option))) {
860
+ const error = new Error("bubblewrap_required_option_missing");
861
+ error.code = "LINUX_GENERIC_BWRAP_UNAVAILABLE";
862
+ throw error;
863
+ }
864
+
865
+ let ptyLauncher = null;
866
+ let ptyLauncherBytes = 0;
867
+ let ptyLauncherDigest = null;
868
+ if (sandboxOpts?.pty === true) {
869
+ ptyLauncher = openPair(LINUX_GENERIC_PTY_LAUNCHER_PATH, {
870
+ directory: false,
871
+ rootRequired: true,
872
+ executable: true,
873
+ });
874
+ ptyLauncherBytes = Number(runtime.fs.fstatSync(ptyLauncher.probeFd).size);
875
+ const probeDigest = hashOpenFile(
876
+ runtime,
877
+ ptyLauncher.probeFd,
878
+ ptyLauncherBytes,
879
+ );
880
+ ptyLauncherDigest = hashOpenFile(
881
+ runtime,
882
+ ptyLauncher.finalFd,
883
+ ptyLauncherBytes,
884
+ );
885
+ if (probeDigest !== ptyLauncherDigest) {
886
+ throw new Error("pty_launcher_digest_mismatch");
887
+ }
888
+ ptyLauncher.identity = Object.freeze({
889
+ ...ptyLauncher.identity,
890
+ sha256: ptyLauncherDigest,
891
+ bytes: ptyLauncherBytes,
892
+ });
893
+
894
+ const launcherCapabilityFd = runtime.fs.openSync(
895
+ LINUX_GENERIC_PTY_LAUNCHER_PATH,
896
+ openFlags({ directory: false }),
897
+ );
898
+ ownedFds.add(launcherCapabilityFd);
899
+ const launcherCapabilityStat = runtime.fs.fstatSync(launcherCapabilityFd);
900
+ if (
901
+ !statMatchesIdentity(launcherCapabilityStat, ptyLauncher.identity, {
902
+ directory: false,
903
+ }) ||
904
+ hashOpenFile(runtime, launcherCapabilityFd, ptyLauncherBytes) !==
905
+ ptyLauncherDigest
906
+ ) {
907
+ throw new Error("pty_launcher_capability_reader_unattested");
908
+ }
909
+ const launcherCapabilityResult = runtime.spawnSync(
910
+ "/proc/self/fd/3",
911
+ ["--help"],
912
+ {
913
+ shell: false,
914
+ encoding: "utf8",
915
+ stdio: ["ignore", "pipe", "pipe", launcherCapabilityFd],
916
+ timeout: 10_000,
917
+ env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" },
918
+ },
919
+ );
920
+ closeFd(launcherCapabilityFd);
921
+ const launcherHelp =
922
+ `${String(launcherCapabilityResult?.stdout || "")}\n` +
923
+ String(launcherCapabilityResult?.stderr || "");
924
+ if (
925
+ launcherCapabilityResult?.error ||
926
+ launcherCapabilityResult?.status !== 0 ||
927
+ !launcherHelp.split(/\s+/).includes("--ctty")
928
+ ) {
929
+ const error = new Error("pty_launcher_capability_probe_failed");
930
+ error.code = "LINUX_GENERIC_BWRAP_UNAVAILABLE";
931
+ throw error;
932
+ }
933
+ }
934
+
935
+ const system = [];
936
+ const systemSymlinks = [];
937
+ for (const destination of SYSTEM_DESTINATIONS) {
938
+ if (!runtime.fs.existsSync(destination)) continue;
939
+ const entryStat = runtime.fs.lstatSync(destination);
940
+ if (entryStat.isSymbolicLink?.()) {
941
+ const expectedTarget = SYSTEM_SYMLINKS.get(destination);
942
+ const observedTarget = runtime.fs.readlinkSync(destination);
943
+ const normalizedTarget = observedTarget.startsWith("/")
944
+ ? observedTarget.slice(1)
945
+ : observedTarget;
946
+ if (
947
+ !expectedTarget ||
948
+ normalizedTarget !== expectedTarget ||
949
+ !runtime.fs.existsSync(`/${expectedTarget}`)
950
+ ) {
951
+ throw new Error("system_runtime_symlink_unattested");
952
+ }
953
+ systemSymlinks.push({
954
+ destination,
955
+ target: expectedTarget,
956
+ });
957
+ continue;
958
+ }
959
+ const pair = openPair(destination, {
960
+ directory: true,
961
+ rootRequired: true,
962
+ });
963
+ system.push({
964
+ destination,
965
+ probeFd: pair.probeFd,
966
+ finalFd: pair.finalFd,
967
+ identity: pair.identity,
968
+ });
969
+ }
970
+ if (!system.some((entry) => entry.destination === "/usr")) {
971
+ throw new Error("system_usr_runtime_unavailable");
972
+ }
973
+
974
+ const etc = [];
975
+ for (const destination of ETC_ALLOWLIST) {
976
+ if (!runtime.fs.existsSync(destination)) continue;
977
+ const pair = openPair(destination, {
978
+ directory: false,
979
+ rootRequired: true,
980
+ });
981
+ etc.push({
982
+ destination,
983
+ probeFd: pair.probeFd,
984
+ finalFd: pair.finalFd,
985
+ identity: pair.identity,
986
+ });
987
+ }
988
+ const sourceMountSources = Object.freeze([
989
+ Object.freeze({
990
+ kind: "workspace",
991
+ source: contract.workspaceRoot,
992
+ recursive: true,
993
+ }),
994
+ ...system.map((entry) =>
995
+ Object.freeze({
996
+ kind: "system",
997
+ source: entry.destination,
998
+ recursive: true,
999
+ }),
1000
+ ),
1001
+ ...etc.map((entry) =>
1002
+ Object.freeze({
1003
+ kind: "etc",
1004
+ source: entry.destination,
1005
+ recursive: false,
1006
+ }),
1007
+ ),
1008
+ ]);
1009
+ const initialWorkspaceMountTopology = workspaceMountTopology(
1010
+ runtime,
1011
+ contract.workspaceRoot,
1012
+ workspace.identity,
1013
+ sourceMountSources,
1014
+ );
1015
+
1016
+ const filter = buildNetworkSeccompFilter(runtime.arch);
1017
+ const openSeccomp = (phase) => {
1018
+ const constants = runtime.fs.constants || fs.constants;
1019
+ const flags =
1020
+ Number(constants.O_RDWR) |
1021
+ Number(constants.O_EXCL) |
1022
+ Number(constants.O_TMPFILE ?? O_TMPFILE) |
1023
+ Number(constants.O_NOFOLLOW || 0) |
1024
+ Number(constants.O_NONBLOCK || 0);
1025
+ const fd = track(runtime.fs.openSync("/tmp", flags, 0o400), phase);
1026
+ runtime.fs.fchmodSync(fd, 0o400);
1027
+ let offset = 0;
1028
+ while (offset < filter.buffer.length) {
1029
+ const written = runtime.fs.writeSync(
1030
+ fd,
1031
+ filter.buffer,
1032
+ offset,
1033
+ filter.buffer.length - offset,
1034
+ offset,
1035
+ );
1036
+ if (written <= 0) throw new Error("seccomp_filter_write_failed");
1037
+ offset += written;
1038
+ }
1039
+ runtime.fs.fsyncSync(fd);
1040
+ const stat = runtime.fs.fstatSync(fd);
1041
+ if (
1042
+ !stat.isFile() ||
1043
+ Number(stat.size) !== filter.buffer.length ||
1044
+ hashOpenFile(runtime, fd, filter.buffer.length) !== filter.sha256
1045
+ ) {
1046
+ throw new Error("seccomp_filter_unattested");
1047
+ }
1048
+ return fd;
1049
+ };
1050
+ const seccomp = {
1051
+ probeFd: openSeccomp("probe"),
1052
+ finalFd: openSeccomp("final"),
1053
+ sha256: filter.sha256,
1054
+ policy: "deny-network-creation",
1055
+ };
1056
+
1057
+ const environment = spawnOpts?.env || process.env;
1058
+ const resolvedTarget = resolveExecutable(
1059
+ runtime,
1060
+ command,
1061
+ contract.workingDirectory,
1062
+ environment,
1063
+ );
1064
+ let targetScope;
1065
+ if (isWithin(contract.workspaceRoot, resolvedTarget.canonical)) {
1066
+ targetScope = "workspace";
1067
+ } else if (
1068
+ systemTarget(resolvedTarget.canonical) &&
1069
+ rootOwned(resolvedTarget.stat, {
1070
+ directory: false,
1071
+ executable: true,
1072
+ })
1073
+ ) {
1074
+ targetScope = "system";
1075
+ } else {
1076
+ throw new Error("generic_target_outside_trusted_runtime");
1077
+ }
1078
+ const target = {
1079
+ attestedContractDigest: contract.contractDigest,
1080
+ requestedCommand: command,
1081
+ resolvedCommand: resolvedTarget.canonical,
1082
+ args: [...(args || [])],
1083
+ scope: targetScope,
1084
+ ...(targetScope === "system"
1085
+ ? {
1086
+ identity: identity(resolvedTarget.canonical, resolvedTarget.stat),
1087
+ }
1088
+ : {}),
1089
+ };
1090
+
1091
+ const socketProbeCandidates = [
1092
+ { command: runtime.execPath, kind: "node" },
1093
+ { command: "/usr/local/bin/node", kind: "node" },
1094
+ { command: "/usr/bin/node", kind: "node" },
1095
+ { command: "/usr/bin/python3", kind: "python" },
1096
+ { command: "/usr/bin/python", kind: "python" },
1097
+ ];
1098
+ let socketProbeRuntime = null;
1099
+ for (const candidate of socketProbeCandidates) {
1100
+ if (typeof candidate.command !== "string" || !candidate.command) continue;
1101
+ try {
1102
+ const resolved = resolveExecutable(
1103
+ runtime,
1104
+ candidate.command,
1105
+ contract.workingDirectory,
1106
+ environment,
1107
+ );
1108
+ if (
1109
+ systemTarget(resolved.canonical) &&
1110
+ rootOwned(resolved.stat, {
1111
+ directory: false,
1112
+ executable: true,
1113
+ })
1114
+ ) {
1115
+ socketProbeRuntime = Object.freeze({
1116
+ command: resolved.canonical,
1117
+ kind: candidate.kind,
1118
+ });
1119
+ break;
1120
+ }
1121
+ } catch {
1122
+ // Try the next root-owned Node runtime.
1123
+ }
1124
+ }
1125
+ if (!socketProbeRuntime) {
1126
+ throw new Error("socket_probe_runtime_unavailable");
1127
+ }
1128
+
1129
+ const descriptorAttested = (fd, expected, directory) => {
1130
+ if (!ownedFds.has(fd)) return false;
1131
+ try {
1132
+ return statMatchesIdentity(runtime.fs.fstatSync(fd), expected, {
1133
+ directory,
1134
+ });
1135
+ } catch {
1136
+ return false;
1137
+ }
1138
+ };
1139
+ const pathAttested = (source, expected, directory) => {
1140
+ try {
1141
+ return (
1142
+ realpath(runtime, source) === source &&
1143
+ statMatchesIdentity(runtime.fs.lstatSync(source), expected, {
1144
+ directory,
1145
+ })
1146
+ );
1147
+ } catch {
1148
+ return false;
1149
+ }
1150
+ };
1151
+ const launchStaysBound = (issued) => {
1152
+ const stdio = normalizedStdio(spawnOpts?.stdio);
1153
+ return (
1154
+ issued?.contractDigest === contract.contractDigest &&
1155
+ issued.workspaceRoot === contract.workspaceRoot &&
1156
+ issued.workingDirectory === contract.workingDirectory &&
1157
+ issued.origin === (spawnOpts?.origin || "unknown") &&
1158
+ issued.command === command &&
1159
+ issued.sync === (sandboxOpts?.sync === true) &&
1160
+ issued.pty === (sandboxOpts?.pty === true) &&
1161
+ issued.shell === false &&
1162
+ Array.isArray(stdio) &&
1163
+ issued.stdio.length === stdio.length &&
1164
+ issued.stdio.every((value, index) => value === stdio[index]) &&
1165
+ issued.args.length === (args || []).length &&
1166
+ issued.args.every((value, index) => value === args[index]) &&
1167
+ identityMatches(issued.rootIdentity, workspace.identity) &&
1168
+ identityMatches(issued.cwdIdentity, cwdPins.identity)
1169
+ );
1170
+ };
1171
+ const attestFinal = (issued) => {
1172
+ let finalWorkspaceMountTopology;
1173
+ try {
1174
+ finalWorkspaceMountTopology = workspaceMountTopology(
1175
+ runtime,
1176
+ contract.workspaceRoot,
1177
+ workspace.identity,
1178
+ sourceMountSources,
1179
+ );
1180
+ } catch {
1181
+ return false;
1182
+ }
1183
+ if (
1184
+ !launchStaysBound(issued) ||
1185
+ finalWorkspaceMountTopology.digest !==
1186
+ initialWorkspaceMountTopology.digest ||
1187
+ !pathAttested(contract.workspaceRoot, workspace.identity, true) ||
1188
+ !pathAttested(contract.workingDirectory, cwdPins.identity, true) ||
1189
+ !descriptorAttested(workspace.finalFd, workspace.identity, true) ||
1190
+ !descriptorAttested(cwdPins.finalFd, cwdPins.identity, true) ||
1191
+ !descriptorAttested(supervisor.finalFd, supervisor.identity, false) ||
1192
+ hashOpenFile(runtime, supervisor.finalFd, supervisorBytes) !==
1193
+ supervisorFinalDigest ||
1194
+ (sandboxOpts?.pty === true &&
1195
+ (!descriptorAttested(
1196
+ ptyLauncher?.finalFd,
1197
+ ptyLauncher?.identity,
1198
+ false,
1199
+ ) ||
1200
+ hashOpenFile(runtime, ptyLauncher.finalFd, ptyLauncherBytes) !==
1201
+ ptyLauncherDigest)) ||
1202
+ hashOpenFile(runtime, seccomp.finalFd, filter.buffer.length) !==
1203
+ filter.sha256
1204
+ ) {
1205
+ return false;
1206
+ }
1207
+ for (const entry of [...system, ...etc]) {
1208
+ if (
1209
+ !descriptorAttested(
1210
+ entry.finalFd,
1211
+ entry.identity,
1212
+ system.includes(entry),
1213
+ )
1214
+ ) {
1215
+ return false;
1216
+ }
1217
+ }
1218
+ try {
1219
+ const currentTarget = resolveExecutable(
1220
+ runtime,
1221
+ command,
1222
+ contract.workingDirectory,
1223
+ environment,
1224
+ );
1225
+ return (
1226
+ currentTarget.canonical === target.resolvedCommand &&
1227
+ (target.scope === "workspace"
1228
+ ? isWithin(contract.workspaceRoot, currentTarget.canonical)
1229
+ : rootOwned(currentTarget.stat, {
1230
+ directory: false,
1231
+ executable: true,
1232
+ }) &&
1233
+ statMatchesIdentity(currentTarget.stat, target.identity, {
1234
+ directory: false,
1235
+ }))
1236
+ );
1237
+ } catch {
1238
+ return false;
1239
+ }
1240
+ };
1241
+
1242
+ const probe = (call) => {
1243
+ const nonce = runtime.randomBytes(12).toString("hex");
1244
+ const workspaceMarker = path.posix.join(
1245
+ contract.workspaceRoot,
1246
+ `.chainless-bwrap-probe-${nonce}`,
1247
+ );
1248
+ const outsideMarker = path.posix.join(
1249
+ runtime.tmpdir(),
1250
+ `.chainless-bwrap-outside-${nonce}`,
1251
+ );
1252
+ const homeMarker = path.posix.join(
1253
+ runtime.homedir(),
1254
+ `.chainless-bwrap-home-${nonce}`,
1255
+ );
1256
+ const workspaceHomeAliasMarker = path.posix.join(
1257
+ contract.workspaceRoot,
1258
+ path.posix.basename(homeMarker),
1259
+ );
1260
+ const devShmMarker = path.posix.join(
1261
+ "/dev/shm",
1262
+ `.chainless-bwrap-dev-shm-${nonce}`,
1263
+ );
1264
+ const outsideContents = `host-only-${nonce}`;
1265
+ const homeContents = `home-only-${nonce}`;
1266
+ let outsideCreated = false;
1267
+ let homeCreated = false;
1268
+ let devShmCreated = false;
1269
+ try {
1270
+ runtime.fs.writeFileSync(outsideMarker, outsideContents, {
1271
+ encoding: "utf8",
1272
+ flag: "wx",
1273
+ mode: 0o600,
1274
+ });
1275
+ outsideCreated = true;
1276
+ runtime.fs.writeFileSync(homeMarker, homeContents, {
1277
+ encoding: "utf8",
1278
+ flag: "wx",
1279
+ mode: 0o600,
1280
+ });
1281
+ homeCreated = true;
1282
+ runtime.fs.writeFileSync(devShmMarker, `host-dev-shm-${nonce}`, {
1283
+ encoding: "utf8",
1284
+ flag: "wx",
1285
+ mode: 0o600,
1286
+ });
1287
+ devShmCreated = true;
1288
+ const socketProbe =
1289
+ socketProbeRuntime.kind === "node"
1290
+ ? {
1291
+ args: [
1292
+ "-e",
1293
+ [
1294
+ 'const net = require("node:net");',
1295
+ 'const socket = net.connect({host:"127.0.0.1",port:9});',
1296
+ "const timer = setTimeout(() => process.exit(4), 1000);",
1297
+ 'socket.once("connect", () => { clearTimeout(timer); process.exit(5); });',
1298
+ 'socket.once("error", (error) => {',
1299
+ " clearTimeout(timer);",
1300
+ ' process.exit(error?.code === "EPERM" ? 0 : 6);',
1301
+ "});",
1302
+ ].join(""),
1303
+ ],
1304
+ }
1305
+ : {
1306
+ args: [
1307
+ "-I",
1308
+ "-S",
1309
+ "-c",
1310
+ [
1311
+ "import errno,socket,sys",
1312
+ "try:",
1313
+ " s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)",
1314
+ "except OSError as e:",
1315
+ " sys.exit(0 if e.errno==errno.EPERM else 6)",
1316
+ "else:",
1317
+ " s.close()",
1318
+ " sys.exit(5)",
1319
+ ].join("\n"),
1320
+ ],
1321
+ };
1322
+ const socketProbeCommand = [
1323
+ shellQuote(socketProbeRuntime.command),
1324
+ ...socketProbe.args.map(shellQuote),
1325
+ ].join(" ");
1326
+ const script = [
1327
+ "set -eu",
1328
+ "mount_is_read_only() {",
1329
+ ' want="$1"',
1330
+ " seen=0",
1331
+ " while IFS=' ' read -r _ _ _ _ mountpoint mountopts _; do",
1332
+ ' [ "$mountpoint" = "$want" ] || continue',
1333
+ " seen=1",
1334
+ ' case ",$mountopts," in',
1335
+ " *,ro,*) ;;",
1336
+ " *) return 1 ;;",
1337
+ " esac",
1338
+ " done < /proc/self/mountinfo",
1339
+ ' [ "$seen" = 1 ]',
1340
+ "}",
1341
+ "workspace_mount_topology_is_attested() {",
1342
+ ` want=${shellQuote(encodeMountInfoPath(contract.workspaceRoot))}`,
1343
+ ' prefix="${want}/"',
1344
+ " seen=0",
1345
+ " while IFS=' ' read -r _ _ _ _ mountpoint _; do",
1346
+ ' [ "$mountpoint" = "$want" ] && seen=1',
1347
+ ' case "$mountpoint" in',
1348
+ ' "$prefix"*) return 1 ;;',
1349
+ " esac",
1350
+ ` done < ${MOUNTINFO_PATH}`,
1351
+ ' [ "$seen" = 1 ]',
1352
+ "}",
1353
+ "mount_is_read_only /",
1354
+ "mount_is_read_only /usr",
1355
+ "workspace_mount_topology_is_attested",
1356
+ `test ! -e ${shellQuote("/etc/shadow")}`,
1357
+ `test ! -e ${shellQuote(homeMarker)}`,
1358
+ `test ! -e ${shellQuote(workspaceHomeAliasMarker)}`,
1359
+ `test ! -e ${shellQuote(outsideMarker)}`,
1360
+ `test ! -e ${shellQuote(devShmMarker)}`,
1361
+ `printf %s ${shellQuote(nonce)} > ${shellQuote(devShmMarker)}`,
1362
+ `test "$(cat ${shellQuote(devShmMarker)})" = ${shellQuote(nonce)}`,
1363
+ `rm -f ${shellQuote(devShmMarker)}`,
1364
+ "if printf x > /chainless-undeclared-root 2>/dev/null; then exit 12; fi",
1365
+ "test ! -e /chainless-undeclared-root",
1366
+ "if printf x > /usr/.chainless-system-write 2>/dev/null; then exit 13; fi",
1367
+ "test ! -e /usr/.chainless-system-write",
1368
+ `test "$(readlink /proc/self/ns/net)" != ${shellQuote(
1369
+ hostNetworkNamespace,
1370
+ )}`,
1371
+ `printf %s ${shellQuote(nonce)} > ${shellQuote(workspaceMarker)}`,
1372
+ `test "$(cat ${shellQuote(workspaceMarker)})" = ${shellQuote(nonce)}`,
1373
+ `rm -f ${shellQuote(workspaceMarker)}`,
1374
+ socketProbeCommand,
1375
+ `printf '%s\\n%s\\n' ${shellQuote(call.policyDigest)} ${shellQuote(
1376
+ call.contractDigest,
1377
+ )}`,
1378
+ ].join("\n");
1379
+ const result = runtime.spawnSync(
1380
+ call.command,
1381
+ [...call.args, "--", "/bin/sh", "-c", script],
1382
+ call.options,
1383
+ );
1384
+ const outsideUnchanged =
1385
+ outsideCreated &&
1386
+ String(runtime.fs.readFileSync(outsideMarker, "utf8")) ===
1387
+ outsideContents;
1388
+ const homeUnchanged =
1389
+ homeCreated &&
1390
+ String(runtime.fs.readFileSync(homeMarker, "utf8")) === homeContents;
1391
+ const devShmUnchanged =
1392
+ devShmCreated &&
1393
+ String(runtime.fs.readFileSync(devShmMarker, "utf8")) ===
1394
+ `host-dev-shm-${nonce}`;
1395
+ const expectedOutput = `${call.policyDigest}\n${call.contractDigest}\n`;
1396
+ const runnable =
1397
+ !result?.error &&
1398
+ result?.status === 0 &&
1399
+ String(result.stdout) === expectedOutput &&
1400
+ outsideUnchanged &&
1401
+ homeUnchanged &&
1402
+ devShmUnchanged;
1403
+ return {
1404
+ runnable,
1405
+ policyDigest: runnable ? call.policyDigest : null,
1406
+ contractDigest: runnable ? call.contractDigest : null,
1407
+ emptyRoot: runnable,
1408
+ undeclaredRootReadOnly: runnable,
1409
+ workspaceReadWrite: runnable,
1410
+ workspaceMountTopologyAttested: runnable,
1411
+ anonymousDevWritable: runnable,
1412
+ systemReadOnly: runnable,
1413
+ hostHomeHidden: runnable,
1414
+ outsideMarkerHidden: runnable,
1415
+ networkNamespace: runnable,
1416
+ networkNamespaceChanged: runnable,
1417
+ socketCreationDenied: runnable,
1418
+ };
1419
+ } catch {
1420
+ return { runnable: false };
1421
+ } finally {
1422
+ for (const marker of [
1423
+ workspaceMarker,
1424
+ outsideMarker,
1425
+ homeMarker,
1426
+ devShmMarker,
1427
+ ]) {
1428
+ try {
1429
+ runtime.fs.unlinkSync(marker);
1430
+ } catch {
1431
+ // Probe cleanup is best effort; a failed probe stays fail-closed.
1432
+ }
1433
+ }
1434
+ }
1435
+ };
1436
+
1437
+ return {
1438
+ attestedContractDigest: contract.contractDigest,
1439
+ attestContract: launchStaysBound,
1440
+ attestFinal,
1441
+ closeProbe: () => closeSet(probeFds),
1442
+ cleanup,
1443
+ supervisor: {
1444
+ probeFd: supervisor.probeFd,
1445
+ finalFd: supervisor.finalFd,
1446
+ identity: supervisor.identity,
1447
+ sha256: supervisorFinalDigest,
1448
+ bytes: supervisorBytes,
1449
+ },
1450
+ ...(ptyLauncher
1451
+ ? {
1452
+ ptyLauncher: {
1453
+ probeFd: ptyLauncher.probeFd,
1454
+ finalFd: ptyLauncher.finalFd,
1455
+ identity: ptyLauncher.identity,
1456
+ sha256: ptyLauncherDigest,
1457
+ bytes: ptyLauncherBytes,
1458
+ },
1459
+ }
1460
+ : {}),
1461
+ workspace: {
1462
+ probeFd: workspace.probeFd,
1463
+ finalFd: workspace.finalFd,
1464
+ identity: workspace.identity,
1465
+ mountTopology: initialWorkspaceMountTopology,
1466
+ },
1467
+ system,
1468
+ systemSymlinks,
1469
+ etc,
1470
+ seccomp,
1471
+ target,
1472
+ probe,
1473
+ };
1474
+ } catch (error) {
1475
+ cleanup();
1476
+ throw error;
1477
+ }
1478
+ }
1479
+
1480
+ /**
1481
+ * Apply the generic strong backend. This function is called only by the Linux
1482
+ * branch in platform-sandbox; all runtime overrides are internal test seams.
1483
+ */
1484
+ export function applyLinuxGenericWorkspaceSandbox(
1485
+ command,
1486
+ args,
1487
+ spawnOpts,
1488
+ sandboxOpts,
1489
+ runtimeOverrides = {},
1490
+ ) {
1491
+ const runtime = runtimeFor(runtimeOverrides);
1492
+ const contract = sandboxOpts?.executionContract;
1493
+ const requiredBoundaries = Array.isArray(sandboxOpts?.requiredBoundaries)
1494
+ ? sandboxOpts.requiredBoundaries
1495
+ : [];
1496
+ const provenance = {
1497
+ origin: spawnOpts?.origin || "unknown",
1498
+ command,
1499
+ args: [...(args || [])],
1500
+ cwd: spawnOpts?.cwd || process.cwd(),
1501
+ shell: spawnOpts?.shell,
1502
+ sync: sandboxOpts?.sync === true,
1503
+ pty: sandboxOpts?.pty === true,
1504
+ stdio: spawnOpts?.stdio,
1505
+ requiredBoundaries,
1506
+ };
1507
+ let resources;
1508
+ try {
1509
+ resources = createTrustedResources(
1510
+ contract,
1511
+ command,
1512
+ args || [],
1513
+ spawnOpts || {},
1514
+ sandboxOpts || {},
1515
+ runtime,
1516
+ );
1517
+ } catch (error) {
1518
+ const preflightFailure =
1519
+ error?.code === "LINUX_GENERIC_BWRAP_UNAVAILABLE"
1520
+ ? "linux_generic_bwrap_unavailable"
1521
+ : "linux_generic_resources_unattested";
1522
+ return planLinuxGenericBubblewrap(
1523
+ {
1524
+ contract,
1525
+ provenance,
1526
+ resources: null,
1527
+ environment: spawnOpts?.env || process.env,
1528
+ spawnOptions: spawnOpts,
1529
+ preflightFailure,
1530
+ },
1531
+ { platform: runtime.platform },
1532
+ );
1533
+ }
1534
+ return planLinuxGenericBubblewrap(
1535
+ {
1536
+ contract,
1537
+ provenance,
1538
+ resources,
1539
+ environment: spawnOpts?.env || process.env,
1540
+ spawnOptions: spawnOpts,
1541
+ probe: resources.probe,
1542
+ },
1543
+ { platform: runtime.platform },
1544
+ );
1545
+ }
1546
+
1547
+ export function isLinuxGenericWorkspaceContract(contract) {
1548
+ return (
1549
+ contract?.contractVersion === 1 &&
1550
+ contract?.kind === LINUX_GENERIC_CONTRACT_KIND &&
1551
+ typeof contract?.contractDigest === "string" &&
1552
+ contract?.workspaceRoot?.startsWith("/") &&
1553
+ contract?.workingDirectory?.startsWith("/")
1554
+ );
1555
+ }
1556
+
1557
+ export { LINUX_GENERIC_BWRAP_BACKEND };