chainlesschain 0.163.7 → 0.164.0

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 (252) hide show
  1. package/package.json +5 -1
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/assets/{AIOps-axqQfwv8.js → AIOps-D2MEn7fI.js} +1 -1
  6. package/src/assets/web-panel/assets/{ActionButton-DV-Ro0By.js → ActionButton-lCW_4Gf0.js} +1 -1
  7. package/src/assets/web-panel/assets/{Analytics-Bt72WEVu.js → Analytics-Bs7i-nsS.js} +3 -3
  8. package/src/assets/web-panel/assets/{AppLayout-BtepG55E.js → AppLayout-CK7B6S_j.js} +5 -5
  9. package/src/assets/web-panel/assets/{Artifacts-B7KaJ7S_.js → Artifacts-C8ILjpyo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Audit-FNFaUg7A.js → Audit-Bb9cNaWc.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-DFb6J2wQ.js → BackgroundAgents-sC9ooXIf.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CC7YBhyj.js → Backup-D-DrsplT.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-RkgTNVtj.js → BaseInput-uCNZpwSK.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-hHEmGZy-.js → Chat-CsDK1uE9.js} +6 -6
  15. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CAP9YhHv.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
  16. package/src/assets/web-panel/assets/{Checkbox-z5wXOQP1.js → Checkbox-JhTXM1-K.js} +1 -1
  17. package/src/assets/web-panel/assets/{Codegen-DVaDJnxy.js → Codegen-BbzZLnyD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Col-CJmaRDR5.js → Col-D4VW4m4j.js} +1 -1
  19. package/src/assets/web-panel/assets/{Community-DF29_2bI.js → Community-BXrYawxV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compact-DtkQkBzn.js → Compact-DCihGy1u.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compliance-xoHDxp7I.js → Compliance-DlyZ-lk8.js} +1 -1
  22. package/src/assets/web-panel/assets/{Cowork-BUm-R7ht.js → Cowork-DvGZrhbO.js} +2 -2
  23. package/src/assets/web-panel/assets/{Cron-Cgl14ZkB.js → Cron-DUncZi8h.js} +2 -2
  24. package/src/assets/web-panel/assets/{Crosschain-BNcbmoqh.js → Crosschain-C_hLuUcm.js} +1 -1
  25. package/src/assets/web-panel/assets/{DID-Bwja0Zo9.js → DID-b6YSUqxr.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dashboard-Bk5XjEdJ.js → Dashboard-uvM15Cfb.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dropdown-Ci36Oitq.js → Dropdown-D1-Nd4qY.js} +1 -1
  28. package/src/assets/web-panel/assets/{EmailListRenderer--U04ZN4q.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
  29. package/src/assets/web-panel/assets/{FamilyGuardDashboard-B2gNPR2s.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
  30. package/src/assets/web-panel/assets/{Federation-C4KghYn7.js → Federation-BbI3HGpv.js} +1 -1
  31. package/src/assets/web-panel/assets/{FormItemContext-QL7x_Q_0.js → FormItemContext-DSoEKuFu.js} +1 -1
  32. package/src/assets/web-panel/assets/{GenericCardRenderer-F3Ryltwm.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
  33. package/src/assets/web-panel/assets/{Git-2HassK5k.js → Git-DIs9Zhf3.js} +2 -2
  34. package/src/assets/web-panel/assets/{Governance-DoBIp7oe.js → Governance-jzZXrxcl.js} +1 -1
  35. package/src/assets/web-panel/assets/{Inference--W01QhZ-.js → Inference-JUPjf2ZI.js} +1 -1
  36. package/src/assets/web-panel/assets/{KnowledgeGraph-DmvwqGpT.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
  37. package/src/assets/web-panel/assets/{Logs-BBJ4yAel.js → Logs-iXiz8BEE.js} +2 -2
  38. package/src/assets/web-panel/assets/{MarkdownRenderer-BLCZMzhJ.js → MarkdownRenderer-GmB6DOr6.js} +1 -1
  39. package/src/assets/web-panel/assets/{Marketplace-D5lNngSI.js → Marketplace-Y3Xch312.js} +1 -1
  40. package/src/assets/web-panel/assets/{McpTools-CkEVOMKH.js → McpTools-D7kGGbO7.js} +4 -4
  41. package/src/assets/web-panel/assets/{Memory-B1Ow1xVE.js → Memory-Do_hVxzh.js} +2 -2
  42. package/src/assets/web-panel/assets/{MobileBridge-SgYbUfb6.js → MobileBridge-CtrR0Tas.js} +1 -1
  43. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
  44. package/src/assets/web-panel/assets/{Mtc-DcTXSSma.js → Mtc-qo5QCTGd.js} +2 -2
  45. package/src/assets/web-panel/assets/{MtcAudit-Dt3WG4oj.js → MtcAudit-cq5mnjYj.js} +2 -2
  46. package/src/assets/web-panel/assets/{Multisig-B9B38UZx.js → Multisig-Bq0MAQHw.js} +3 -3
  47. package/src/assets/web-panel/assets/{NLProgramming-B0S4GGvG.js → NLProgramming-DdWE6Vi2.js} +1 -1
  48. package/src/assets/web-panel/assets/{Notes-DZ5U58Ng.js → Notes-BxMBFcIp.js} +3 -3
  49. package/src/assets/web-panel/assets/{NotificationSettings-PjL5ZW34.js → NotificationSettings-RTrAcbUe.js} +1 -1
  50. package/src/assets/web-panel/assets/{OrderTableRenderer-DrAX2ESt.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
  51. package/src/assets/web-panel/assets/{Organization-Cv1GYs-c.js → Organization-SQzIPdqC.js} +4 -4
  52. package/src/assets/web-panel/assets/{Overflow-Bbkp_PL3.js → Overflow-02yECh22.js} +1 -1
  53. package/src/assets/web-panel/assets/{P2P-BD8QUgoZ.js → P2P-BNg4pNCM.js} +2 -2
  54. package/src/assets/web-panel/assets/{PdhVaultBrowser-BO-2PeaB.js → PdhVaultBrowser-BxPJZsD3.js} +3 -3
  55. package/src/assets/web-panel/assets/{Permissions-D_OPVa02.js → Permissions-CLnDnPAz.js} +4 -4
  56. package/src/assets/web-panel/assets/{PersonalDataHub-mh-pEmaB.js → PersonalDataHub-C5m0AjGf.js} +4 -4
  57. package/src/assets/web-panel/assets/{Pipeline-6JC2EDdb.js → Pipeline-VwOf5Dhv.js} +1 -1
  58. package/src/assets/web-panel/assets/{Privacy-CUshzUAG.js → Privacy-CC0W2hZd.js} +1 -1
  59. package/src/assets/web-panel/assets/{ProjectInit-Cs2Aalri.js → ProjectInit-Dz893Bob.js} +2 -2
  60. package/src/assets/web-panel/assets/{ProjectSettings-1ak49tcj.js → ProjectSettings-CxQkvl0k.js} +2 -2
  61. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
  62. package/src/assets/web-panel/assets/{Providers-CXXxvafH.js → Providers-DeB9IkxK.js} +1 -1
  63. package/src/assets/web-panel/assets/{QrScannerModal-PTcDsF58.js → QrScannerModal-OCeoaswn.js} +1 -1
  64. package/src/assets/web-panel/assets/{QuickAsk-CDwucrJw.js → QuickAsk-DenotaAj.js} +1 -1
  65. package/src/assets/web-panel/assets/{Recommend-CJlLVRXR.js → Recommend-Cies_jjI.js} +1 -1
  66. package/src/assets/web-panel/assets/{RemoteSession-DlN-ETsO.js → RemoteSession-Dxzl5n2T.js} +3 -3
  67. package/src/assets/web-panel/assets/{Reputation-DI23LR6m.js → Reputation-CW0mvxgZ.js} +1 -1
  68. package/src/assets/web-panel/assets/{Row-Cl7UQMrM.js → Row-SEd32qZU.js} +1 -1
  69. package/src/assets/web-panel/assets/{RssFeed-CexHNfh8.js → RssFeed-BSwnJ3Lm.js} +3 -3
  70. package/src/assets/web-panel/assets/{Search-BneBWvRN.js → Search-xe2m8bkC.js} +1 -1
  71. package/src/assets/web-panel/assets/{Security-DX7Phgrp.js → Security-Mq2F9Nro.js} +4 -4
  72. package/src/assets/web-panel/assets/{Services-BWXO8Mte.js → Services-BKGw2V3P.js} +2 -2
  73. package/src/assets/web-panel/assets/{Skeleton-DCyOjKPJ.js → Skeleton-DW95-Rxf.js} +1 -1
  74. package/src/assets/web-panel/assets/{Skills-CXqERsTB.js → Skills-CHgChCv_.js} +1 -1
  75. package/src/assets/web-panel/assets/{Sla-bhbrbz8E.js → Sla-tKh6k3R7.js} +1 -1
  76. package/src/assets/web-panel/assets/{SpeechSettings-3IPU0HBZ.js → SpeechSettings-D6UL3dA5.js} +1 -1
  77. package/src/assets/web-panel/assets/{SyncSettings-DBNTQLbM.js → SyncSettings-D0Fw8fVL.js} +2 -2
  78. package/src/assets/web-panel/assets/{Tasks-quP0bkN-.js → Tasks-20x1GB3D.js} +1 -1
  79. package/src/assets/web-panel/assets/{Templates-BouWmgho.js → Templates-DJ-lts-a.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tenant-qv0D9s7z.js → Tenant-BFdGpbs7.js} +1 -1
  81. package/src/assets/web-panel/assets/{Terminal-BxvbE8BB.js → Terminal-Xpx61vHL.js} +2 -2
  82. package/src/assets/web-panel/assets/{TimelineRenderer-D-IeLnJ2.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tokens-CFMKKyVG.js → Tokens-Dellq08f.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trigger-L8as3UuJ.js → Trigger-VKw8d0J-.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trust-MxmA4VZG.js → Trust-DNftdKT5.js} +1 -1
  86. package/src/assets/web-panel/assets/{UkeySign-C0VvJYRi.js → UkeySign-DX9VAKJI.js} +1 -1
  87. package/src/assets/web-panel/assets/{VideoEditing-Bf5QZj8N.js → VideoEditing-B2h3jO8q.js} +1 -1
  88. package/src/assets/web-panel/assets/{Wallet-BIJ9riRz.js → Wallet-CjGkof7f.js} +4 -4
  89. package/src/assets/web-panel/assets/{WebAuthn-B8p9Mhkw.js → WebAuthn-2Zfcu55P.js} +4 -4
  90. package/src/assets/web-panel/assets/{WorkflowEditor-BVsuaPen.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
  91. package/src/assets/web-panel/assets/{chat-BfkjoKpV.js → chat-CQNfD-YO.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-DeAlsWK8.js → colors-D4usyFT2.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-Cs97bz5j.js → compact-item-s4TpjvjX.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-BqbG60LH.js → createContext-BRCSvYxj.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
  96. package/src/assets/web-panel/assets/{hasIn-i_RY0b1X.js → hasIn-AdkUbkV-.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BOVkf1K3.js → index--00U6vXZ.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B-3tEpTc.js → index-B2V-eKfK.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D2B4VPyP.js → index-B4JJCfQ2.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BkBi5TmC.js → index-BIoG5yYf.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-79V_zv1s.js → index-BMcimU-2.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-U4f5izHz.js → index-BPhkFZ1I.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DiSkx4UB.js → index-BQ3DXBtD.js} +3 -3
  104. package/src/assets/web-panel/assets/{index-dIHr4B_u.js → index-Bad4VNvz.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CIG8fQ2_.js → index-BgiXR_-_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DqBgBpgp.js → index-BtobShro.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-ZxO_-E8a.js → index-Bw2MEnUk.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DUUrv141.js → index-C1mJtE6q.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bm7lLR4c.js → index-C484EJiS.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-COdGA-8n.js → index-C5KAQFPQ.js} +1 -1
  111. package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
  112. package/src/assets/web-panel/assets/{index-CED2PL_y.js → index-C7Y8Aluc.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-YXgLng0K.js → index-CCsM_Ybx.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-0-agYlWL.js → index-CJCFe2Qf.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B5UkyA3c.js → index-CJVEOa4i.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-B1CbOH1i.js → index-CJdQrZth.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-s1opAL-M.js → index-CKC1t3Ic.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-yObj1O4n.js → index-CQhAVU5t.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CgsqdnRV.js → index-CYdoQsa1.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dk7-v4hP.js → index-Cduu2Hhr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-B-JVTrpr.js → index-CkwiSLhU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CeWoNHau.js → index-Cvyt-zhm.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CE2VtTlp.js → index-D2GjI2oe.js} +1 -1
  124. package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
  125. package/src/assets/web-panel/assets/{index-DmbfhJUP.js → index-DARsAyUp.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BESLFt4q.js → index-DVKfQ0Ux.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-PT_5sBMA.js → index-DZT36ZYq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-gyFpvL7l.js → index-D_XBHopw.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BYUVuwKt.js → index-DdCB70SS.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BcUXAB2s.js → index-DzRjHxEv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Dk2qouAz.js → index-MMN9V6JF.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D9evtvg2.js → index-RfMr79MW.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BuLQobzW.js → index-_IRVsV9X.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-Cf3WCLmy.js → index-nXyk8GZ9.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-CG3PRyAL.js → index-s_buFMEG.js} +1 -1
  136. package/src/assets/web-panel/assets/{initDefaultProps-C9h9yKL3.js → initDefaultProps-Blmpk7HL.js} +1 -1
  137. package/src/assets/web-panel/assets/{motion-BvmLsPdu.js → motion-DPBJMv1n.js} +1 -1
  138. package/src/assets/web-panel/assets/{move-CoL4ekU1.js → move-ButdzCM7.js} +1 -1
  139. package/src/assets/web-panel/assets/{mtc-parser-C4pMy6xV.js → mtc-parser-cyRC_QCe.js} +1 -1
  140. package/src/assets/web-panel/assets/{omit-DPuJfNxx.js → omit-1epSV7hz.js} +1 -1
  141. package/src/assets/web-panel/assets/{pickAttrs-D0KEeaK2.js → pickAttrs-DuPY8xJs.js} +1 -1
  142. package/src/assets/web-panel/assets/{placementArrow-BpkyINxt.js → placementArrow-6XZqarhe.js} +1 -1
  143. package/src/assets/web-panel/assets/{responsiveObserve-DQI_wOq8.js → responsiveObserve-B40pXBWM.js} +1 -1
  144. package/src/assets/web-panel/assets/{slide-DYfdDiRg.js → slide-jbZ9Wu4n.js} +1 -1
  145. package/src/assets/web-panel/assets/{statusUtils-B5dtvb0D.js → statusUtils-BKQQcVy4.js} +1 -1
  146. package/src/assets/web-panel/assets/{styleChecker-CF310mSj.js → styleChecker-NiKfWjUT.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFlexGapSupport-D1Qp4BLp.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFs-C7_iyvJK.js → useFs-CUGji7ph.js} +1 -1
  149. package/src/assets/web-panel/assets/{usePersonalDataHub-DZUt_fIm.js → usePersonalDataHub-4veyN2N2.js} +1 -1
  150. package/src/assets/web-panel/assets/{vnode-D3dsi0lZ.js → vnode-1-nLug-Z.js} +1 -1
  151. package/src/assets/web-panel/assets/{zoom-WdbOlvdQ.js → zoom-dhCeRDMo.js} +1 -1
  152. package/src/assets/web-panel/index.html +1 -1
  153. package/src/command-help-index.json +6 -6
  154. package/src/commands/automation.js +103 -1
  155. package/src/commands/cowork.js +174 -19
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/commands/team-merge-review.js +937 -0
  161. package/src/commands/team.js +2 -0
  162. package/src/data/changelog.json +27 -1
  163. package/src/gateways/ws/action-protocol.js +35 -10
  164. package/src/gateways/ws/remote-session-protocol.js +24 -1
  165. package/src/gateways/ws/ws-agent-handler.js +12 -0
  166. package/src/gateways/ws/ws-server.js +48 -12
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +1 -0
  170. package/src/harness/remote-session-crypto.js +210 -2
  171. package/src/harness/remote-session-registry.js +159 -6
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/agent-schedule-store.js +139 -28
  175. package/src/lib/agent-team/team-merge-review-store.js +765 -0
  176. package/src/lib/agent-team/team-merge-review-transaction.js +1223 -0
  177. package/src/lib/agent-team/team-merge-review.js +1847 -0
  178. package/src/lib/automation-center-incidents.js +314 -0
  179. package/src/lib/automation-center-runtime.js +448 -0
  180. package/src/lib/automation-center.js +182 -38
  181. package/src/lib/automation-engine.js +41 -0
  182. package/src/lib/automation-execution-authority.js +241 -12
  183. package/src/lib/automation-execution-incident.js +737 -0
  184. package/src/lib/background-agent-keeper-client.js +248 -0
  185. package/src/lib/background-agent-keeper-protocol.js +147 -0
  186. package/src/lib/background-agent-supervisor.js +520 -40
  187. package/src/lib/background-interaction-resolver.js +12 -0
  188. package/src/lib/background-turn-bootstrap-protocol.js +126 -0
  189. package/src/lib/cowork-cron.js +85 -14
  190. package/src/lib/cowork-workflow.js +336 -16
  191. package/src/lib/dynamic-workflow-facade.js +648 -0
  192. package/src/lib/execution-location-contract.js +551 -0
  193. package/src/lib/execution-location-runtime.js +130 -0
  194. package/src/lib/ide-context.js +94 -6
  195. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  196. package/src/lib/packer/pkg-config-generator.js +2 -0
  197. package/src/lib/permission-authority.js +77 -0
  198. package/src/lib/permission-side-effect-center.js +495 -0
  199. package/src/lib/plugin-runtime/install.js +514 -13
  200. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  201. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  202. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  203. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  204. package/src/lib/plugin-security.js +60 -3
  205. package/src/lib/process-execution-broker/index.js +1095 -57
  206. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  207. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  208. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  209. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  210. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  211. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  212. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  213. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  214. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  215. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  216. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  217. package/src/lib/public-ide-capability-manifest.js +3 -0
  218. package/src/lib/remote-approval-bridge.js +123 -17
  219. package/src/lib/remote-membership-authority.js +632 -0
  220. package/src/lib/remote-membership-coordinator.js +2409 -0
  221. package/src/lib/remote-membership-host-store.js +1750 -0
  222. package/src/lib/scheduler-kernel/agenda-adapter.js +17 -7
  223. package/src/lib/scheduler-kernel/automation-adapter.js +105 -11
  224. package/src/lib/scheduler-kernel/automation-event-adapter.js +87 -11
  225. package/src/lib/scheduler-kernel/contract.js +91 -0
  226. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +12 -1
  227. package/src/lib/scheduler-kernel/loop-adapter.js +7 -1
  228. package/src/lib/scheduler-kernel/routine-adapter.js +7 -1
  229. package/src/lib/scheduler-kernel/runtime-control-capabilities.js +31 -0
  230. package/src/lib/scheduler-kernel/runtime.js +176 -3
  231. package/src/lib/scheduler-kernel/store.js +1253 -45
  232. package/src/lib/scoped-permission-store.js +369 -0
  233. package/src/lib/session-anti-rollback-anchor.js +262 -5
  234. package/src/lib/sub-agent-context.js +4 -0
  235. package/src/lib/workflow-definition-contract.js +149 -0
  236. package/src/repl/agent-repl.js +41 -7
  237. package/src/repl/chat-repl.js +2 -0
  238. package/src/repl/clipboard-image.js +735 -16
  239. package/src/repl/ide-status.js +1 -1
  240. package/src/repl/prompt-interactions.js +59 -6
  241. package/src/runtime/agent-core.js +93 -3
  242. package/src/runtime/headless-runner.js +30 -25
  243. package/src/runtime/headless-stream.js +32 -25
  244. package/src/runtime/mcp-config.js +81 -10
  245. package/src/workers/background-agent-keeper.js +498 -0
  246. package/src/workers/background-agent-worker.js +358 -13
  247. package/src/workers/background-turn-bootstrap.js +143 -0
  248. package/src/assets/web-panel/assets/MobileProjects-BWdOvQAj.js +0 -1
  249. package/src/assets/web-panel/assets/Projects-BV4oApsm.js +0 -1
  250. package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +0 -1
  251. package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +0 -1
  252. package/src/assets/web-panel/assets/index-ChVT0p9u.js +0 -1
@@ -5,10 +5,13 @@
5
5
  * - macOS: Seatbelt sandbox-exec profiles
6
6
  * - Linux: prlimit resource-limit wrapper, plus a narrow bubblewrap backend
7
7
  * for an attested direct strict Plugin Node bin or a narrow static/dynamic
8
- * ELF native bin. Dynamic acceptance binds PT_INTERP plus the entry's direct
9
- * DT_NEEDED names to an attested system-file set; broader loader surfaces,
10
- * shells, broader working directories, host-writable paths, inherited
11
- * descriptors, and network egress remain fail-closed.
8
+ * ELF native bin. Dynamic acceptance recursively parses PT_INTERP and every
9
+ * startup DT_NEEDED edge against the exact descriptor-bound system graph.
10
+ * Dynamic native launches additionally use a read-only final mount namespace
11
+ * with no procfs, devfs, or writable scratch: every pathname-visible regular
12
+ * file is descriptor-pinned, hashed, and policy-bound. This closes the ELF
13
+ * loader/dlopen pathname surface, but deliberately does not claim to prevent
14
+ * anonymous JIT code or a custom in-process ELF loader.
12
15
  * - Windows: Win32 Job Object + restricted-token wrapper, with an attested
13
16
  * zero-capability AppContainer for explicit filesystem/network boundaries
14
17
  *
@@ -30,6 +33,27 @@ import {
30
33
  applyLinuxGenericWorkspaceSandbox,
31
34
  isLinuxGenericWorkspaceContract,
32
35
  } from "./linux-generic-bwrap-runtime.js";
36
+ import {
37
+ buildLinuxBwrapDescriptorScrubbedLaunch,
38
+ LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH,
39
+ linuxBwrapDescriptorScrubberPolicyBinding,
40
+ } from "./linux-bwrap-descriptor-launch.js";
41
+ import {
42
+ MCP_STDIO_FD_ENTRY_BOOTSTRAP,
43
+ MCP_STDIO_FD_ENTRY_BOOTSTRAP_SHA256,
44
+ MCP_STDIO_MACOS_GATED_ENTRY_BOOTSTRAP_SHA256,
45
+ } from "./mcp-fd-entry-bootstrap.js";
46
+ import {
47
+ MACOS_MCP_LAUNCHER_INPUTS,
48
+ macosMcpLauncherPolicyDigest,
49
+ verifyMacosMcpLauncherInstallContract,
50
+ } from "./macos-mcp-launcher-contract.js";
51
+
52
+ export {
53
+ MCP_STDIO_FD_ENTRY_BOOTSTRAP,
54
+ MCP_STDIO_FD_ENTRY_BOOTSTRAP_SHA256,
55
+ MCP_STDIO_MACOS_GATED_ENTRY_BOOTSTRAP_SHA256,
56
+ } from "./mcp-fd-entry-bootstrap.js";
33
57
 
34
58
  /**
35
59
  * Stable boundary identifiers shared by broker policies, platform plans, and
@@ -161,6 +185,8 @@ const LINUX_BWRAP_BACKEND = "linux-bwrap";
161
185
  const LINUX_BWRAP_NODE_PROBE_SENTINEL = "chainless-linux-bwrap-plugin-node-v1";
162
186
  const LINUX_BWRAP_CHILD_RUNTIME_PROBE_SENTINEL =
163
187
  "chainless-linux-bwrap-child-runtime-v1";
188
+ const LINUX_BWRAP_NATIVE_RUNTIME_CLOSURE_PROBE_SENTINEL =
189
+ "chainless-linux-bwrap-native-runtime-pathname-closure-v1";
164
190
  const LINUX_BWRAP_CHILD_RUNTIME_PROBE_FAILURE_STATUS = 86;
165
191
  const LINUX_BWRAP_MAX_PLUGIN_ENTRIES = 512;
166
192
  const LINUX_PLUGIN_TREE_SNAPSHOT_MAX_FILES = 256;
@@ -178,15 +204,12 @@ const LINUX_ENTRY_SNAPSHOT_SOURCE_MODE = 0o400;
178
204
  const LINUX_EXECUTABLE_SNAPSHOT_SOURCE_MODE = 0o500;
179
205
  const LINUX_MCP_CAPSULE_BACKEND = "linux-fd-code-snapshot";
180
206
  const LINUX_MCP_RUNTIME_SNAPSHOT_SCOPE = "mcp-node-runtime-executable";
207
+ const LINUX_PLUGIN_RUNTIME_SNAPSHOT_SCOPE =
208
+ "plugin-probe-node-runtime-executable";
181
209
  const LINUX_MCP_CAPSULE_SNAPSHOT_MECHANISM =
182
210
  "verified-o_tmpfile-copy-inherited-fd-module-compile-v1";
183
- const MACOS_MCP_CAPSULE_BACKEND = "macos-fd-code-snapshot";
184
- export const MCP_STDIO_FD_ENTRY_BOOTSTRAP =
185
- 'const fs=require("node:fs");const Module=require("node:module");const filename="/chainlesschain/mcp-capsule.cjs";const source=fs.readFileSync(4,"utf8");process.argv.splice(1,0,filename);const target=new Module(filename,module);target.filename=filename;target.paths=[];target._compile(source,filename);';
186
- export const MCP_STDIO_FD_ENTRY_BOOTSTRAP_SHA256 = crypto
187
- .createHash("sha256")
188
- .update(MCP_STDIO_FD_ENTRY_BOOTSTRAP)
189
- .digest("hex");
211
+ const MACOS_MCP_CAPSULE_CANDIDATE_BACKEND = "macos-fd-code-snapshot";
212
+ const MACOS_MCP_CAPSULE_BACKEND = MACOS_MCP_LAUNCHER_INPUTS.protocol.backend;
190
213
  const LINUX_NODE_ENTRY_SNAPSHOT_SCOPE = "plugin-entry-source";
191
214
  const LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE = "plugin-entry-executable";
192
215
  const LINUX_PLUGIN_TREE_SNAPSHOT_SCOPE = "all-pinned-plugin-regular-files";
@@ -199,6 +222,9 @@ const LINUX_ELF64_DYNAMIC_ENTRY_BYTES = 16;
199
222
  const LINUX_ELF_MAX_PROGRAM_HEADERS = 128;
200
223
  const LINUX_ELF_MAX_DYNAMIC_ENTRIES = 4096;
201
224
  const LINUX_ELF_MAX_NEEDED_ENTRIES = 128;
225
+ const LINUX_ELF_MAX_INITIAL_CLOSURE_FILES = 256;
226
+ const LINUX_ELF_MAX_INITIAL_CLOSURE_EDGES = 1024;
227
+ const LINUX_ELF_MAX_INITIAL_CLOSURE_BYTES = 512 * 1024 * 1024;
202
228
  const LINUX_ELF_MAX_INTERPRETER_BYTES = 4096;
203
229
  const LINUX_ELF_MAX_STRING_TABLE_BYTES = 1024 * 1024;
204
230
  const LINUX_AUXV_ENTRY_BYTES = 16;
@@ -217,6 +243,7 @@ const LINUX_ELF_DYNAMIC_NULL = 0n;
217
243
  const LINUX_ELF_DYNAMIC_NEEDED = 1n;
218
244
  const LINUX_ELF_DYNAMIC_STRTAB = 5n;
219
245
  const LINUX_ELF_DYNAMIC_STRSZ = 10n;
246
+ const LINUX_ELF_DYNAMIC_SONAME = 14n;
220
247
  const LINUX_ELF_DYNAMIC_RPATH = 15n;
221
248
  const LINUX_ELF_DYNAMIC_TEXTREL = 22n;
222
249
  const LINUX_ELF_DYNAMIC_FLAGS = 30n;
@@ -229,10 +256,23 @@ const LINUX_ELF_DYNAMIC_AUXILIARY = 0x7ffffffdn;
229
256
  const LINUX_ELF_DYNAMIC_FILTER = 0x7fffffffn;
230
257
  const LINUX_ELF_DYNAMIC_FLAG_TEXTREL = 0x4n;
231
258
  const LINUX_ELF_DYNAMIC_FLAG_PIE = 0x08000000n;
232
- const LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_SCOPE =
233
- "initial-pt_interp-and-direct-dt_needed-attested-system-set";
234
- const LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_MECHANISM =
235
- "parsed-elf-direct-system-set-to-attested-node-runtime-fds-v1";
259
+ const LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_SCOPE =
260
+ "initial-pt_interp-and-recursive-dt_needed-attested-system-graph";
261
+ const LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_MECHANISM =
262
+ "recursive-parsed-elf-system-graph-to-attested-runtime-fds-v1";
263
+ const LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_SCOPE =
264
+ "all-pathname-visible-regular-files-in-read-only-bwrap-namespace";
265
+ // In addition to the immutable mount set, the dynamic-only seccomp program
266
+ // blocks every descriptor-acquisition syscall that could import a loader input
267
+ // through SCM_RIGHTS, another process, or a filesystem handle. It also blocks
268
+ // mount/user namespace mutation: otherwise an untrusted target could create a
269
+ // nested user namespace, mount a writable tmpfs over /run, and dlopen a drop-in.
270
+ // This remains a pathname-loader claim: anonymous JIT/custom in-process loaders
271
+ // are excluded.
272
+ const LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MECHANISM =
273
+ "descriptor-pinned-hashed-ro-mount-set-plus-loader-fd-and-namespace-mutation-seccomp-v2";
274
+ const LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_FILES = 512;
275
+ const LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_BYTES = 1024 * 1024 * 1024;
236
276
  const LINUX_ELF_MACHINES = Object.freeze({
237
277
  x64: 62,
238
278
  arm64: 183,
@@ -242,11 +282,32 @@ const LINUX_ELF_MACHINES = Object.freeze({
242
282
  // x64, arm64, and riscv64 use this value; unsupported seccomp architectures
243
283
  // already fail closed before the anonymous filter is created.
244
284
  const LINUX_O_TMPFILE = 0x410000;
285
+ const LINUX_CLONE_NEWNS = 0x00020000;
286
+ const LINUX_CLONE_NEWUSER = 0x10000000;
287
+ const LINUX_NAMESPACE_CLONE_FLAGS = LINUX_CLONE_NEWNS | LINUX_CLONE_NEWUSER;
245
288
  const LINUX_SECCOMP_FILTERS = Object.freeze({
246
289
  x64: Object.freeze({
247
290
  auditArch: 0xc000003e,
248
291
  socketSyscall: 41,
249
292
  socketpairSyscall: 53,
293
+ recvmsgSyscall: 47,
294
+ recvmmsgSyscall: 299,
295
+ openByHandleAtSyscall: 304,
296
+ pidfdGetfdSyscall: 438,
297
+ cloneSyscall: 56,
298
+ clone3Syscall: 435,
299
+ unshareSyscall: 272,
300
+ setnsSyscall: 308,
301
+ mountSyscall: 165,
302
+ umount2Syscall: 166,
303
+ pivotRootSyscall: 155,
304
+ openTreeSyscall: 428,
305
+ moveMountSyscall: 429,
306
+ fsopenSyscall: 430,
307
+ fsconfigSyscall: 431,
308
+ fsmountSyscall: 432,
309
+ fspickSyscall: 433,
310
+ mountSetattrSyscall: 442,
250
311
  ioUringSetupSyscall: 425,
251
312
  x32SyscallBit: 0x40000000,
252
313
  }),
@@ -254,12 +315,48 @@ const LINUX_SECCOMP_FILTERS = Object.freeze({
254
315
  auditArch: 0xc00000b7,
255
316
  socketSyscall: 198,
256
317
  socketpairSyscall: 199,
318
+ recvmsgSyscall: 212,
319
+ recvmmsgSyscall: 243,
320
+ openByHandleAtSyscall: 265,
321
+ pidfdGetfdSyscall: 438,
322
+ cloneSyscall: 220,
323
+ clone3Syscall: 435,
324
+ unshareSyscall: 97,
325
+ setnsSyscall: 268,
326
+ mountSyscall: 40,
327
+ umount2Syscall: 39,
328
+ pivotRootSyscall: 41,
329
+ openTreeSyscall: 428,
330
+ moveMountSyscall: 429,
331
+ fsopenSyscall: 430,
332
+ fsconfigSyscall: 431,
333
+ fsmountSyscall: 432,
334
+ fspickSyscall: 433,
335
+ mountSetattrSyscall: 442,
257
336
  ioUringSetupSyscall: 425,
258
337
  }),
259
338
  riscv64: Object.freeze({
260
339
  auditArch: 0xc00000f3,
261
340
  socketSyscall: 198,
262
341
  socketpairSyscall: 199,
342
+ recvmsgSyscall: 212,
343
+ recvmmsgSyscall: 243,
344
+ openByHandleAtSyscall: 265,
345
+ pidfdGetfdSyscall: 438,
346
+ cloneSyscall: 220,
347
+ clone3Syscall: 435,
348
+ unshareSyscall: 97,
349
+ setnsSyscall: 268,
350
+ mountSyscall: 40,
351
+ umount2Syscall: 39,
352
+ pivotRootSyscall: 41,
353
+ openTreeSyscall: 428,
354
+ moveMountSyscall: 429,
355
+ fsopenSyscall: 430,
356
+ fsconfigSyscall: 431,
357
+ fsmountSyscall: 432,
358
+ fspickSyscall: 433,
359
+ mountSetattrSyscall: 442,
263
360
  ioUringSetupSyscall: 425,
264
361
  }),
265
362
  });
@@ -278,6 +375,8 @@ const LINUX_LOCAL_FILESYSTEM_TYPES = new Set([
278
375
  const DEFAULT_RUNTIME = Object.freeze({
279
376
  platform: os.platform(),
280
377
  arch: process.arch,
378
+ pkg: process.pkg || null,
379
+ env: process.env,
281
380
  fs,
282
381
  tmpdir: () => os.tmpdir(),
283
382
  homedir: () => os.homedir(),
@@ -292,6 +391,8 @@ const DEFAULT_RUNTIME = Object.freeze({
292
391
  sleepSync: (milliseconds) =>
293
392
  Atomics.wait(WINDOWS_IDENTITY_WAIT, 0, 0, milliseconds),
294
393
  spawnSync: nativeSpawnSync,
394
+ getuid: () => process.getuid?.() ?? null,
395
+ getgid: () => process.getgid?.() ?? null,
295
396
  warn: (message) => process.emitWarning(message),
296
397
  });
297
398
 
@@ -301,6 +402,7 @@ const windowsTemporaryCleanupBacklog = new Set();
301
402
  let windowsTemporaryCleanupRetryTimer = null;
302
403
  const windowsAppContainerCleanupBacklog = new Set();
303
404
  const issuedWindowsMcpCodeSnapshotPlans = new WeakMap();
405
+ const issuedMacMcpCodeSnapshotPlans = new WeakMap();
304
406
  // Only the unified production adapter can register a Windows MCP launch
305
407
  // capability. Direct callers of the exported platform-specific helper (and
306
408
  // callers that inject a test/runtime facade into applySandbox) may exercise
@@ -308,6 +410,7 @@ const issuedWindowsMcpCodeSnapshotPlans = new WeakMap();
308
410
  const WINDOWS_MCP_CODE_SNAPSHOT_ISSUER = Symbol(
309
411
  "windows-mcp-code-snapshot-issuer",
310
412
  );
413
+ const MACOS_MCP_CODE_SNAPSHOT_ISSUER = Symbol("macos-mcp-code-snapshot-issuer");
311
414
  // Each AppContainer retry starts a synchronous, digest-attested PowerShell
312
415
  // helper. Bound automatic retries so a permanently unsupported Windows host
313
416
  // cannot repeatedly block a long-lived CLI; explicit cleanup/reset and process
@@ -693,11 +796,55 @@ function normalizeSandboxRequest(profileOrRequest, explicitRequest) {
693
796
  * Materialize the requested POSIX shell before adding either wrapper. The
694
797
  * sandbox executable itself is always spawned directly.
695
798
  */
799
+ function snapshotWrappedInvocationArgs(args) {
800
+ if (args === null || args === undefined) return Object.freeze([]);
801
+ try {
802
+ if (
803
+ !Array.isArray(args) ||
804
+ Object.getPrototypeOf(args) !== Array.prototype
805
+ ) {
806
+ return null;
807
+ }
808
+ const descriptors = Object.getOwnPropertyDescriptors(args);
809
+ const lengthDescriptor = descriptors.length;
810
+ if (
811
+ !lengthDescriptor ||
812
+ !("value" in lengthDescriptor) ||
813
+ !Number.isSafeInteger(lengthDescriptor.value) ||
814
+ lengthDescriptor.value < 0 ||
815
+ Reflect.ownKeys(descriptors).length !== lengthDescriptor.value + 1
816
+ ) {
817
+ return null;
818
+ }
819
+ const snapshot = [];
820
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
821
+ const descriptor = descriptors[String(index)];
822
+ if (!descriptor || !("value" in descriptor)) return null;
823
+ snapshot.push(descriptor.value);
824
+ }
825
+ return Object.freeze(snapshot);
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+
696
831
  function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
832
+ const argsSnapshot = snapshotWrappedInvocationArgs(args);
833
+ if (!argsSnapshot) {
834
+ return {
835
+ command,
836
+ args: [],
837
+ inputArgs: [],
838
+ argumentsValid: false,
839
+ options: { ...(spawnOpts || {}) },
840
+ };
841
+ }
697
842
  if (!spawnOpts?.shell) {
698
843
  return {
699
844
  command,
700
- args: [...(args || [])],
845
+ args: [...argsSnapshot],
846
+ inputArgs: argsSnapshot,
847
+ argumentsValid: true,
701
848
  options: { ...(spawnOpts || {}) },
702
849
  };
703
850
  }
@@ -707,20 +854,24 @@ function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
707
854
  typeof spawnOpts.shell === "string"
708
855
  ? spawnOpts.shell
709
856
  : process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe";
710
- const shellCommand = [command, ...(args || [])].map(String).join(" ");
857
+ const shellCommand = [command, ...argsSnapshot].map(String).join(" ");
711
858
  return {
712
859
  command: shell,
713
860
  args: ["/d", "/s", "/c", shellCommand],
861
+ inputArgs: argsSnapshot,
862
+ argumentsValid: true,
714
863
  options: { ...spawnOpts, shell: false },
715
864
  };
716
865
  }
717
866
 
718
867
  const shell =
719
868
  typeof spawnOpts.shell === "string" ? spawnOpts.shell : "/bin/sh";
720
- const shellCommand = [command, ...(args || [])].map(String).join(" ");
869
+ const shellCommand = [command, ...argsSnapshot].map(String).join(" ");
721
870
  return {
722
871
  command: shell,
723
872
  args: ["-c", shellCommand],
873
+ inputArgs: argsSnapshot,
874
+ argumentsValid: true,
724
875
  options: { ...spawnOpts, shell: false },
725
876
  };
726
877
  }
@@ -857,6 +1008,650 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
857
1008
  return lines.join("\n");
858
1009
  }
859
1010
 
1011
+ function macMode(stat) {
1012
+ return Number(stat.mode) & 0o7777;
1013
+ }
1014
+
1015
+ function macSameOpenIdentity(left, right) {
1016
+ return (
1017
+ linuxOpenStatMatches(left, right) &&
1018
+ String(left.uid) === String(right.uid) &&
1019
+ String(left.gid) === String(right.gid)
1020
+ );
1021
+ }
1022
+
1023
+ function macFixedPathStat(runtime, candidate, options) {
1024
+ const resolved = linuxRealpath(runtime, candidate);
1025
+ const stat = runtime.fs.lstatSync(candidate);
1026
+ const kindMatches =
1027
+ options.kind === "directory" ? stat.isDirectory() : stat.isFile();
1028
+ if (
1029
+ resolved !== candidate ||
1030
+ stat.isSymbolicLink?.() ||
1031
+ !kindMatches ||
1032
+ Number(stat.uid) !== 0 ||
1033
+ Number(stat.gid) !== 0 ||
1034
+ (options.mode !== undefined && macMode(stat) !== options.mode) ||
1035
+ (options.mode === undefined && (macMode(stat) & 0o022) !== 0) ||
1036
+ (options.nlink !== undefined && Number(stat.nlink) !== options.nlink)
1037
+ ) {
1038
+ throw new Error(options.reason || "macos_fixed_path_untrusted");
1039
+ }
1040
+ return stat;
1041
+ }
1042
+
1043
+ function macOpenPinnedFile(runtime, candidate, options = {}) {
1044
+ let fd;
1045
+ try {
1046
+ const namedBefore = macFixedPathStat(runtime, candidate, {
1047
+ kind: "file",
1048
+ mode: options.mode,
1049
+ nlink: options.nlink,
1050
+ reason: options.reason,
1051
+ });
1052
+ const constants = runtime.fs.constants || fs.constants;
1053
+ fd = runtime.fs.openSync(
1054
+ candidate,
1055
+ Number(constants.O_RDONLY) |
1056
+ Number(constants.O_NOFOLLOW || 0) |
1057
+ Number(constants.O_CLOEXEC || 0) |
1058
+ Number(constants.O_NONBLOCK || 0),
1059
+ );
1060
+ const before = runtime.fs.fstatSync(fd);
1061
+ const bytes = Number(before.size);
1062
+ if (
1063
+ !before.isFile() ||
1064
+ !macSameOpenIdentity(namedBefore, before) ||
1065
+ (options.executable === true && (Number(before.mode) & 0o111) === 0) ||
1066
+ !Number.isSafeInteger(bytes) ||
1067
+ bytes < (options.minimumBytes ?? 1) ||
1068
+ bytes > (options.maximumBytes ?? 16 * 1024 * 1024) ||
1069
+ (options.expectedBytes !== undefined && bytes !== options.expectedBytes)
1070
+ ) {
1071
+ throw new Error(options.reason || "macos_fixed_file_untrusted");
1072
+ }
1073
+ const digest = hashLinuxOpenFile(runtime, fd, bytes);
1074
+ const after = runtime.fs.fstatSync(fd);
1075
+ const namedAfter = runtime.fs.lstatSync(candidate);
1076
+ if (
1077
+ !macSameOpenIdentity(before, after) ||
1078
+ !macSameOpenIdentity(after, namedAfter) ||
1079
+ linuxRealpath(runtime, candidate) !== candidate ||
1080
+ (options.expectedSha256 !== undefined &&
1081
+ digest !== options.expectedSha256)
1082
+ ) {
1083
+ throw new Error(options.reason || "macos_fixed_file_changed");
1084
+ }
1085
+ return {
1086
+ fd,
1087
+ stat: after,
1088
+ sha256: digest,
1089
+ bytes,
1090
+ fileId: { dev: String(after.dev), ino: String(after.ino) },
1091
+ mtimeMs: Number(after.mtimeMs),
1092
+ };
1093
+ } catch (error) {
1094
+ if (fd !== undefined) {
1095
+ try {
1096
+ runtime.fs.closeSync(fd);
1097
+ } catch {
1098
+ // best effort
1099
+ }
1100
+ }
1101
+ throw error;
1102
+ }
1103
+ }
1104
+
1105
+ function macReadPinnedUtf8(runtime, pin, maximumBytes) {
1106
+ if (pin.bytes > maximumBytes) throw new Error("macos_contract_too_large");
1107
+ const buffer = Buffer.alloc(pin.bytes);
1108
+ let offset = 0;
1109
+ while (offset < buffer.length) {
1110
+ const read = runtime.fs.readSync(
1111
+ pin.fd,
1112
+ buffer,
1113
+ offset,
1114
+ buffer.length - offset,
1115
+ offset,
1116
+ );
1117
+ if (read <= 0) throw new Error("macos_contract_ended_early");
1118
+ offset += read;
1119
+ }
1120
+ return buffer.toString("utf8");
1121
+ }
1122
+
1123
+ function macSpawnAttestation(runtime, command, args) {
1124
+ const result = runtime.spawnSync(command, args, {
1125
+ cwd: "/",
1126
+ shell: false,
1127
+ encoding: "utf8",
1128
+ timeout: 15_000,
1129
+ env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" },
1130
+ });
1131
+ if (result?.error || result?.signal || result?.status !== 0) {
1132
+ throw new Error("macos_helper_attestation_command_failed");
1133
+ }
1134
+ return {
1135
+ stdout: String(result.stdout || ""),
1136
+ stderr: String(result.stderr || ""),
1137
+ };
1138
+ }
1139
+
1140
+ function macExactObjectKeys(value, keys) {
1141
+ return (
1142
+ value &&
1143
+ typeof value === "object" &&
1144
+ !Array.isArray(value) &&
1145
+ JSON.stringify(Object.keys(value).sort()) ===
1146
+ JSON.stringify([...keys].sort())
1147
+ );
1148
+ }
1149
+
1150
+ function verifyMacHelperProbe(probe, nonce, contract, runtime) {
1151
+ const keys = [
1152
+ "schema",
1153
+ "nonce",
1154
+ "protocolVersion",
1155
+ "protocolSha256",
1156
+ "sourceSha256",
1157
+ "gateBootstrapSha256",
1158
+ "selfSha256",
1159
+ "realUid",
1160
+ "effectiveUid",
1161
+ "realGid",
1162
+ "effectiveGid",
1163
+ "installedMode",
1164
+ "snapshotRootMode",
1165
+ "readyGate",
1166
+ "rootWatchdog",
1167
+ "staleCleanupBounded",
1168
+ "maximumStaleSnapshots",
1169
+ "globalLaunchLock",
1170
+ "callerLifelineFd",
1171
+ "callerLifelineWatched",
1172
+ "signalRelayNonblocking",
1173
+ "relayParentCredentialsDropped",
1174
+ "entryRootOwnedAnonymousSnapshot",
1175
+ "entrySourcePrePostStat",
1176
+ "entryWriterClosedBeforeReadonlyReopen",
1177
+ "entryReadonlyIdentityRechecked",
1178
+ "entryUnlinkedAndDirectoryFsyncedBeforeTarget",
1179
+ "targetInheritedEntrySnapshotOnly",
1180
+ "runtimeAndCapsuleSlotsNullBeforeExec",
1181
+ "bootstrapClosesNullAndReadyDescriptors",
1182
+ "processForkExplicitlyDenied",
1183
+ ];
1184
+ const callerUid = runtime.getuid();
1185
+ const callerGid = runtime.getgid();
1186
+ if (
1187
+ !macExactObjectKeys(probe, keys) ||
1188
+ !Number.isSafeInteger(callerUid) ||
1189
+ callerUid <= 0 ||
1190
+ !Number.isSafeInteger(callerGid) ||
1191
+ callerGid <= 0 ||
1192
+ probe.schema !== "chainlesschain.macos-mcp-launcher-probe.v1" ||
1193
+ probe.nonce !== nonce ||
1194
+ probe.protocolVersion !== 1 ||
1195
+ probe.protocolSha256 !== MACOS_MCP_LAUNCHER_INPUTS.protocolSha256 ||
1196
+ probe.sourceSha256 !== MACOS_MCP_LAUNCHER_INPUTS.sourceSha256 ||
1197
+ probe.gateBootstrapSha256 !==
1198
+ MCP_STDIO_MACOS_GATED_ENTRY_BOOTSTRAP_SHA256 ||
1199
+ probe.selfSha256 !== contract.helperSha256 ||
1200
+ probe.realUid !== callerUid ||
1201
+ probe.effectiveUid !== 0 ||
1202
+ probe.realGid !== callerGid ||
1203
+ probe.effectiveGid !== callerGid ||
1204
+ probe.installedMode !== "4555" ||
1205
+ probe.snapshotRootMode !== "0711" ||
1206
+ probe.readyGate !== true ||
1207
+ probe.rootWatchdog !== true ||
1208
+ probe.staleCleanupBounded !== true ||
1209
+ probe.maximumStaleSnapshots !==
1210
+ MACOS_MCP_LAUNCHER_INPUTS.protocol.maximumStaleSnapshots ||
1211
+ probe.globalLaunchLock !== true ||
1212
+ probe.callerLifelineFd !==
1213
+ MACOS_MCP_LAUNCHER_INPUTS.protocol.callerLifelineFd ||
1214
+ probe.callerLifelineWatched !== true ||
1215
+ probe.signalRelayNonblocking !== true ||
1216
+ probe.relayParentCredentialsDropped !== true ||
1217
+ probe.entryRootOwnedAnonymousSnapshot !== true ||
1218
+ probe.entrySourcePrePostStat !== true ||
1219
+ probe.entryWriterClosedBeforeReadonlyReopen !== true ||
1220
+ probe.entryReadonlyIdentityRechecked !== true ||
1221
+ probe.entryUnlinkedAndDirectoryFsyncedBeforeTarget !== true ||
1222
+ probe.targetInheritedEntrySnapshotOnly !== true ||
1223
+ probe.runtimeAndCapsuleSlotsNullBeforeExec !== true ||
1224
+ probe.bootstrapClosesNullAndReadyDescriptors !== true ||
1225
+ probe.processForkExplicitlyDenied !== true
1226
+ ) {
1227
+ throw new Error("macos_helper_probe_contract_mismatch");
1228
+ }
1229
+ return probe;
1230
+ }
1231
+
1232
+ function attestInstalledMacMcpLauncher(runtime) {
1233
+ const protocol = MACOS_MCP_LAUNCHER_INPUTS.protocol;
1234
+ let contractPin;
1235
+ let helperPin;
1236
+ try {
1237
+ for (const directory of [
1238
+ "/Library",
1239
+ "/Library/PrivilegedHelperTools",
1240
+ "/Library/Application Support",
1241
+ "/Library/Application Support/ChainlessChain",
1242
+ "/Library/Application Support/ChainlessChain/McpLauncher",
1243
+ ]) {
1244
+ macFixedPathStat(runtime, directory, {
1245
+ kind: "directory",
1246
+ mode: 0o755,
1247
+ reason: "macos_helper_install_directory_untrusted",
1248
+ });
1249
+ }
1250
+ macFixedPathStat(runtime, protocol.snapshotRoot, {
1251
+ kind: "directory",
1252
+ mode: 0o711,
1253
+ reason: "macos_helper_snapshot_root_untrusted",
1254
+ });
1255
+ macFixedPathStat(
1256
+ runtime,
1257
+ path.posix.join(protocol.snapshotRoot, protocol.snapshotLockName),
1258
+ {
1259
+ kind: "file",
1260
+ mode: 0o600,
1261
+ nlink: 1,
1262
+ reason: "macos_helper_snapshot_lock_untrusted",
1263
+ },
1264
+ );
1265
+ macFixedPathStat(runtime, protocol.sandboxExecutable, {
1266
+ kind: "file",
1267
+ reason: "macos_sandbox_exec_untrusted",
1268
+ });
1269
+ for (const tool of [
1270
+ "/usr/bin/codesign",
1271
+ "/usr/sbin/pkgutil",
1272
+ "/usr/sbin/spctl",
1273
+ ]) {
1274
+ macFixedPathStat(runtime, tool, {
1275
+ kind: "file",
1276
+ reason: "macos_signature_tool_untrusted",
1277
+ });
1278
+ }
1279
+ contractPin = macOpenPinnedFile(runtime, protocol.installContractPath, {
1280
+ mode: 0o444,
1281
+ nlink: 1,
1282
+ maximumBytes: 64 * 1024,
1283
+ reason: "macos_helper_install_contract_untrusted",
1284
+ });
1285
+ const contractText = macReadPinnedUtf8(runtime, contractPin, 64 * 1024);
1286
+ const contract = verifyMacosMcpLauncherInstallContract(
1287
+ JSON.parse(contractText),
1288
+ );
1289
+ const receiptResult = macSpawnAttestation(runtime, "/usr/sbin/pkgutil", [
1290
+ "--pkg-info",
1291
+ contract.packageIdentifier,
1292
+ ]);
1293
+ const receipt = Object.fromEntries(
1294
+ receiptResult.stdout
1295
+ .split(/\r?\n/u)
1296
+ .map((line) => line.match(/^([^:]+):\s*(.*)$/u))
1297
+ .filter(Boolean)
1298
+ .map((match) => [match[1], match[2]]),
1299
+ );
1300
+ if (
1301
+ receipt["package-id"] !== contract.packageIdentifier ||
1302
+ receipt.version !== contract.packageVersion ||
1303
+ receipt.volume !== "/"
1304
+ ) {
1305
+ throw new Error("macos_helper_package_receipt_mismatch");
1306
+ }
1307
+ helperPin = macOpenPinnedFile(runtime, protocol.helperInstallPath, {
1308
+ mode: 0o4555,
1309
+ nlink: 1,
1310
+ executable: true,
1311
+ expectedBytes: contract.helperBytes,
1312
+ expectedSha256: contract.helperSha256,
1313
+ reason: "macos_helper_binary_untrusted",
1314
+ });
1315
+ macSpawnAttestation(runtime, "/usr/bin/codesign", [
1316
+ "--verify",
1317
+ "--strict",
1318
+ "--verbose=4",
1319
+ `-R=${contract.designatedRequirement}`,
1320
+ protocol.helperInstallPath,
1321
+ ]);
1322
+ const description = macSpawnAttestation(runtime, "/usr/bin/codesign", [
1323
+ "-dvvv",
1324
+ protocol.helperInstallPath,
1325
+ ]);
1326
+ const details = `${description.stdout}\n${description.stderr}`;
1327
+ const teamIdentifier = details.match(/^TeamIdentifier=(.+)$/mu)?.[1];
1328
+ const signingIdentifier = details.match(/^Identifier=(.+)$/mu)?.[1];
1329
+ if (
1330
+ teamIdentifier !== contract.teamIdentifier ||
1331
+ signingIdentifier !== contract.signingIdentifier ||
1332
+ !/^CodeDirectory .+ flags=.+\(runtime\)/mu.test(details)
1333
+ ) {
1334
+ throw new Error("macos_helper_signature_identity_mismatch");
1335
+ }
1336
+ const requirementOutput = macSpawnAttestation(
1337
+ runtime,
1338
+ "/usr/bin/codesign",
1339
+ ["-dr", "-", protocol.helperInstallPath],
1340
+ );
1341
+ const designatedRequirement =
1342
+ `${requirementOutput.stdout}\n${requirementOutput.stderr}`.match(
1343
+ /^designated => (.+)$/mu,
1344
+ )?.[1];
1345
+ if (designatedRequirement !== contract.designatedRequirement) {
1346
+ throw new Error("macos_helper_designated_requirement_mismatch");
1347
+ }
1348
+ const gatekeeper = macSpawnAttestation(runtime, "/usr/sbin/spctl", [
1349
+ "--assess",
1350
+ "--type",
1351
+ "execute",
1352
+ "--verbose=4",
1353
+ protocol.helperInstallPath,
1354
+ ]);
1355
+ const gatekeeperEvidence = `${gatekeeper.stdout}\n${gatekeeper.stderr}`;
1356
+ if (
1357
+ !/\baccepted\b/iu.test(gatekeeperEvidence) ||
1358
+ !/source=Notarized Developer ID/iu.test(gatekeeperEvidence)
1359
+ ) {
1360
+ throw new Error("macos_helper_notarization_unattested");
1361
+ }
1362
+ const nonce = runtime.randomBytes(32).toString("hex");
1363
+ const probeResult = macSpawnAttestation(
1364
+ runtime,
1365
+ protocol.helperInstallPath,
1366
+ ["--probe-v1", nonce],
1367
+ );
1368
+ if (probeResult.stderr.trim() !== "") {
1369
+ throw new Error("macos_helper_probe_stderr");
1370
+ }
1371
+ const probe = verifyMacHelperProbe(
1372
+ JSON.parse(probeResult.stdout),
1373
+ nonce,
1374
+ contract,
1375
+ runtime,
1376
+ );
1377
+ if (
1378
+ hashLinuxOpenFile(runtime, helperPin.fd, helperPin.bytes) !==
1379
+ helperPin.sha256 ||
1380
+ !macSameOpenIdentity(
1381
+ helperPin.stat,
1382
+ runtime.fs.fstatSync(helperPin.fd),
1383
+ ) ||
1384
+ !macSameOpenIdentity(
1385
+ helperPin.stat,
1386
+ macFixedPathStat(runtime, protocol.helperInstallPath, {
1387
+ kind: "file",
1388
+ mode: 0o4555,
1389
+ nlink: 1,
1390
+ reason: "macos_helper_binary_changed",
1391
+ }),
1392
+ ) ||
1393
+ !macSameOpenIdentity(
1394
+ contractPin.stat,
1395
+ macFixedPathStat(runtime, protocol.installContractPath, {
1396
+ kind: "file",
1397
+ mode: 0o444,
1398
+ nlink: 1,
1399
+ reason: "macos_helper_install_contract_changed",
1400
+ }),
1401
+ )
1402
+ ) {
1403
+ throw new Error("macos_helper_installation_changed_during_attestation");
1404
+ }
1405
+ const installContractSha256 = crypto
1406
+ .createHash("sha256")
1407
+ .update(contractText)
1408
+ .digest("hex");
1409
+ return Object.freeze({
1410
+ contract,
1411
+ probe,
1412
+ helper: Object.freeze({
1413
+ path: protocol.helperInstallPath,
1414
+ sha256: helperPin.sha256,
1415
+ bytes: helperPin.bytes,
1416
+ fileId: Object.freeze({ ...helperPin.fileId }),
1417
+ mode: macMode(helperPin.stat),
1418
+ uid: Number(helperPin.stat.uid),
1419
+ gid: Number(helperPin.stat.gid),
1420
+ signingIdentifier,
1421
+ teamIdentifier,
1422
+ packageIdentifier: contract.packageIdentifier,
1423
+ packageVersion: contract.packageVersion,
1424
+ designatedRequirementSha256: crypto
1425
+ .createHash("sha256")
1426
+ .update(designatedRequirement)
1427
+ .digest("hex"),
1428
+ }),
1429
+ installContractSha256,
1430
+ });
1431
+ } finally {
1432
+ for (const pin of [helperPin, contractPin]) {
1433
+ if (pin?.fd !== undefined) {
1434
+ try {
1435
+ runtime.fs.closeSync(pin.fd);
1436
+ } catch {
1437
+ // best effort; launch admission still fails if later pinning fails
1438
+ }
1439
+ }
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ function macPinContractFile(runtime, identity, options = {}) {
1445
+ let fd;
1446
+ try {
1447
+ if (
1448
+ !identity ||
1449
+ typeof identity.realPath !== "string" ||
1450
+ !path.posix.isAbsolute(identity.realPath) ||
1451
+ !/^[a-f0-9]{64}$/u.test(identity.sha256 || "") ||
1452
+ !identity.fileId ||
1453
+ !Number.isSafeInteger(identity.bytes) ||
1454
+ identity.bytes < (options.minimumBytes ?? 0) ||
1455
+ identity.bytes > options.maximumBytes ||
1456
+ linuxRealpath(runtime, identity.realPath) !== identity.realPath
1457
+ ) {
1458
+ throw new Error("macos_contract_file_identity_invalid");
1459
+ }
1460
+ const namedBefore = runtime.fs.lstatSync(identity.realPath);
1461
+ const constants = runtime.fs.constants || fs.constants;
1462
+ if (namedBefore.isSymbolicLink?.() || !namedBefore.isFile()) {
1463
+ throw new Error("macos_contract_file_identity_invalid");
1464
+ }
1465
+ fd = runtime.fs.openSync(
1466
+ identity.realPath,
1467
+ Number(constants.O_RDONLY) |
1468
+ Number(constants.O_NOFOLLOW || 0) |
1469
+ Number(constants.O_CLOEXEC || 0) |
1470
+ Number(constants.O_NONBLOCK || 0),
1471
+ );
1472
+ const before = runtime.fs.fstatSync(fd);
1473
+ if (
1474
+ !before.isFile() ||
1475
+ !macSameOpenIdentity(namedBefore, before) ||
1476
+ String(before.dev) !== String(identity.fileId.dev) ||
1477
+ String(before.ino) !== String(identity.fileId.ino) ||
1478
+ Number(before.size) !== identity.bytes ||
1479
+ Number(before.mtimeMs) !== Number(identity.mtimeMs) ||
1480
+ (options.executable === true && (Number(before.mode) & 0o111) === 0)
1481
+ ) {
1482
+ throw new Error("macos_contract_file_identity_changed");
1483
+ }
1484
+ const sha256 = hashLinuxOpenFile(runtime, fd, identity.bytes);
1485
+ const after = runtime.fs.fstatSync(fd);
1486
+ const namedAfter = runtime.fs.lstatSync(identity.realPath);
1487
+ if (
1488
+ sha256 !== identity.sha256 ||
1489
+ !macSameOpenIdentity(before, after) ||
1490
+ !macSameOpenIdentity(after, namedAfter) ||
1491
+ linuxRealpath(runtime, identity.realPath) !== identity.realPath
1492
+ ) {
1493
+ throw new Error("macos_contract_file_identity_changed");
1494
+ }
1495
+ return {
1496
+ fd,
1497
+ sha256,
1498
+ bytes: identity.bytes,
1499
+ fileId: { dev: String(after.dev), ino: String(after.ino) },
1500
+ mtimeMs: Number(after.mtimeMs),
1501
+ mode: Number(after.mode),
1502
+ };
1503
+ } catch (error) {
1504
+ if (fd !== undefined) {
1505
+ try {
1506
+ runtime.fs.closeSync(fd);
1507
+ } catch {
1508
+ // best effort
1509
+ }
1510
+ }
1511
+ throw error;
1512
+ }
1513
+ }
1514
+
1515
+ function macPinCapsuleDirectory(runtime, contract, callerUid) {
1516
+ let fd;
1517
+ try {
1518
+ const root = contract.pluginRoot;
1519
+ const identity = contract.rootIdentity;
1520
+ if (
1521
+ typeof root !== "string" ||
1522
+ linuxRealpath(runtime, root) !== root ||
1523
+ !identity?.fileId ||
1524
+ identity.realPath !== root
1525
+ ) {
1526
+ throw new Error("macos_capsule_root_identity_invalid");
1527
+ }
1528
+ const namedBefore = runtime.fs.lstatSync(root);
1529
+ const constants = runtime.fs.constants || fs.constants;
1530
+ if (
1531
+ namedBefore.isSymbolicLink?.() ||
1532
+ !namedBefore.isDirectory() ||
1533
+ Number(namedBefore.uid) !== callerUid ||
1534
+ (Number(namedBefore.mode) & 0o022) !== 0
1535
+ ) {
1536
+ throw new Error("macos_capsule_root_identity_invalid");
1537
+ }
1538
+ fd = runtime.fs.openSync(
1539
+ root,
1540
+ Number(constants.O_RDONLY) |
1541
+ Number(constants.O_DIRECTORY || 0) |
1542
+ Number(constants.O_NOFOLLOW || 0) |
1543
+ Number(constants.O_CLOEXEC || 0) |
1544
+ Number(constants.O_NONBLOCK || 0),
1545
+ );
1546
+ const before = runtime.fs.fstatSync(fd);
1547
+ const namedAfter = runtime.fs.lstatSync(root);
1548
+ if (
1549
+ !before.isDirectory() ||
1550
+ !macSameOpenIdentity(namedBefore, before) ||
1551
+ !macSameOpenIdentity(before, namedAfter) ||
1552
+ String(before.dev) !== String(identity.fileId.dev) ||
1553
+ String(before.ino) !== String(identity.fileId.ino) ||
1554
+ linuxRealpath(runtime, root) !== root
1555
+ ) {
1556
+ throw new Error("macos_capsule_root_identity_changed");
1557
+ }
1558
+ return {
1559
+ fd,
1560
+ fileId: { dev: String(before.dev), ino: String(before.ino) },
1561
+ mode: Number(before.mode),
1562
+ uid: Number(before.uid),
1563
+ gid: Number(before.gid),
1564
+ };
1565
+ } catch (error) {
1566
+ if (fd !== undefined) {
1567
+ try {
1568
+ runtime.fs.closeSync(fd);
1569
+ } catch {
1570
+ // best effort
1571
+ }
1572
+ }
1573
+ throw error;
1574
+ }
1575
+ }
1576
+
1577
+ function macStandardStdio(stdio) {
1578
+ if (Array.isArray(stdio)) {
1579
+ return [stdio[0] ?? "pipe", stdio[1] ?? "pipe", stdio[2] ?? "pipe"];
1580
+ }
1581
+ const value = stdio ?? "pipe";
1582
+ return [value, value, value];
1583
+ }
1584
+
1585
+ function macEnvironmentDigest(environment) {
1586
+ const normalized = Object.fromEntries(
1587
+ Object.entries(environment || {})
1588
+ .map(([key, value]) => [key, String(value)])
1589
+ .sort(([left], [right]) => left.localeCompare(right)),
1590
+ );
1591
+ return crypto
1592
+ .createHash("sha256")
1593
+ .update(JSON.stringify(normalized))
1594
+ .digest("hex");
1595
+ }
1596
+
1597
+ export const MACOS_PKG_EXECPATH_MAGIC = "PKG_INVOKE_NODEJS";
1598
+
1599
+ export function macMcpTargetEnvironment(
1600
+ environment,
1601
+ { packaged = false, inheritedEnvironment = process.env } = {},
1602
+ ) {
1603
+ if (!packaged) return environment;
1604
+ const base = environment ?? inheritedEnvironment;
1605
+ if (!base || typeof base !== "object" || Array.isArray(base)) {
1606
+ throw new TypeError("packaged macOS MCP environment must be an object");
1607
+ }
1608
+ return {
1609
+ ...base,
1610
+ PKG_EXECPATH: MACOS_PKG_EXECPATH_MAGIC,
1611
+ };
1612
+ }
1613
+
1614
+ function sameMacStdio(left, right) {
1615
+ return (
1616
+ Array.isArray(left) &&
1617
+ Array.isArray(right) &&
1618
+ left.length === 9 &&
1619
+ right.length === 9 &&
1620
+ left.every((value, index) => value === right[index])
1621
+ );
1622
+ }
1623
+
1624
+ export function consumeMacMcpCodeSnapshotPlanBinding(plan, expected = {}) {
1625
+ const issued = issuedMacMcpCodeSnapshotPlans.get(plan);
1626
+ if (!issued) return false;
1627
+ issuedMacMcpCodeSnapshotPlans.delete(plan);
1628
+ return (
1629
+ issued.executionContract === expected.executionContract &&
1630
+ issued.originalCommand === expected.command &&
1631
+ sameStringArray(issued.originalArgs, expected.args) &&
1632
+ issued.originalCwd === (expected.cwd ?? null) &&
1633
+ issued.originalShell === (expected.shell ?? null) &&
1634
+ issued.originalDetached === (expected.detached ?? null) &&
1635
+ issued.profile === expected.profile &&
1636
+ sameStringArray(issued.requiredBoundaries, expected.requiredBoundaries) &&
1637
+ issued.sync === (expected.sync === true) &&
1638
+ plan.backend === MACOS_MCP_CAPSULE_BACKEND &&
1639
+ plan.enforcement === MACOS_MCP_CAPSULE_BACKEND &&
1640
+ plan.policyAttested === true &&
1641
+ plan.policyDigest === issued.policyDigest &&
1642
+ plan.command === issued.helperCommand &&
1643
+ sameStringArray(plan.args, issued.helperArgs) &&
1644
+ plan.options?.cwd === "/" &&
1645
+ plan.options?.shell === false &&
1646
+ plan.options?.detached !== true &&
1647
+ macEnvironmentDigest(plan.options?.env) === issued.environmentDigest &&
1648
+ sameMacStdio(issued.stdio, plan.options?.stdio) &&
1649
+ plan.postSpawn?.required === false &&
1650
+ plan.postSpawn?.mode === "none" &&
1651
+ plan.runtimeProbe?.planBindingDigest === issued.planBindingDigest
1652
+ );
1653
+ }
1654
+
860
1655
  function validateMacMcpCapsuleContract(
861
1656
  command,
862
1657
  args,
@@ -873,6 +1668,7 @@ function validateMacMcpCapsuleContract(
873
1668
  !linuxStdioIsNarrow(spawnOpts?.stdio) ||
874
1669
  spawnOpts?.serialization !== undefined ||
875
1670
  spawnOpts?.argv0 !== undefined ||
1671
+ spawnOpts?.detached === true ||
876
1672
  spawnOpts?.uid !== undefined ||
877
1673
  spawnOpts?.gid !== undefined ||
878
1674
  !Array.isArray(args) ||
@@ -936,6 +1732,7 @@ function applyMacMcpCapsuleCodeSnapshot(
936
1732
  sandboxOpts,
937
1733
  runtime,
938
1734
  base,
1735
+ planBindingAuthority = null,
939
1736
  ) {
940
1737
  const contract = sandboxOpts.executionContract;
941
1738
  const validation = validateMacMcpCapsuleContract(
@@ -950,7 +1747,7 @@ function applyMacMcpCapsuleCodeSnapshot(
950
1747
  createSandboxPlan({
951
1748
  ...base,
952
1749
  backend: null,
953
- candidateBackend: MACOS_MCP_CAPSULE_BACKEND,
1750
+ candidateBackend: MACOS_MCP_CAPSULE_CANDIDATE_BACKEND,
954
1751
  policyAttested: false,
955
1752
  runtimeProbe: runtimeProbe
956
1753
  ? {
@@ -968,31 +1765,288 @@ function applyMacMcpCapsuleCodeSnapshot(
968
1765
  return unavailable("macos_mcp_capsule_execution_contract_invalid", {
969
1766
  attempted: false,
970
1767
  runnable: false,
971
- reason: validation.reason,
1768
+ reason: validation.reason,
1769
+ contentSnapshot: false,
1770
+ handleAtomic: false,
1771
+ entrySnapshotAtomic: false,
1772
+ runtimeLaunchAtomic: false,
1773
+ sharedLibraryClosure: false,
1774
+ });
1775
+ }
1776
+
1777
+ const protocol = MACOS_MCP_LAUNCHER_INPUTS.protocol;
1778
+ const legacyUnavailable = () =>
1779
+ unavailable("macos_atomic_runtime_exec_unavailable", {
1780
+ attempted: true,
1781
+ runnable: false,
1782
+ reason: "public_api_has_no_descriptor_bound_exec",
1783
+ contentSnapshot: false,
1784
+ handleAtomic: false,
1785
+ entrySnapshotAtomic: false,
1786
+ runtimeLaunchAtomic: false,
1787
+ runtimeLaunchMechanism: "darwin-public-api-pathname-exec-only-v1",
1788
+ sharedLibraryClosure: false,
1789
+ });
1790
+ if (
1791
+ typeof runtime.fs.existsSync !== "function" ||
1792
+ !runtime.fs.existsSync(protocol.helperInstallPath) ||
1793
+ !runtime.fs.existsSync(protocol.installContractPath)
1794
+ ) {
1795
+ // Darwin public APIs still provide no descriptor-bound exec. Production is
1796
+ // enabled only by the separately signed, root-installed release contract.
1797
+ return legacyUnavailable();
1798
+ }
1799
+
1800
+ let runtimePin;
1801
+ let entryPin;
1802
+ let capsulePin;
1803
+ try {
1804
+ const callerUid = runtime.getuid();
1805
+ const callerGid = runtime.getgid();
1806
+ if (
1807
+ !Number.isSafeInteger(callerUid) ||
1808
+ callerUid <= 0 ||
1809
+ !Number.isSafeInteger(callerGid) ||
1810
+ callerGid <= 0
1811
+ ) {
1812
+ throw new Error("macos_helper_caller_credentials_invalid");
1813
+ }
1814
+ const installation = attestInstalledMacMcpLauncher(runtime);
1815
+ runtimePin = macPinContractFile(runtime, contract.runtimeIdentity, {
1816
+ executable: true,
1817
+ minimumBytes: 1,
1818
+ maximumBytes: protocol.maximumRuntimeBytes,
1819
+ });
1820
+ entryPin = macPinContractFile(runtime, contract.entryIdentity, {
1821
+ minimumBytes: 0,
1822
+ maximumBytes: protocol.maximumEntryBytes,
1823
+ });
1824
+ capsulePin = macPinCapsuleDirectory(runtime, contract, callerUid);
1825
+ const nonce = runtime.randomBytes(32).toString("hex");
1826
+ if (!/^[a-f0-9]{64}$/u.test(nonce)) {
1827
+ throw new Error("macos_helper_nonce_generation_failed");
1828
+ }
1829
+ const snapshotPath = path.posix.join(protocol.snapshotRoot, nonce, "node");
1830
+ const policyDigest = macosMcpLauncherPolicyDigest({
1831
+ snapshotPath,
1832
+ capsulePath: contract.pluginRoot,
1833
+ });
1834
+ const helperArgs = [
1835
+ "--launch-v1",
1836
+ nonce,
1837
+ MACOS_MCP_LAUNCHER_INPUTS.protocolSha256,
1838
+ runtimePin.sha256,
1839
+ String(runtimePin.bytes),
1840
+ entryPin.sha256,
1841
+ String(entryPin.bytes),
1842
+ String(callerUid),
1843
+ String(callerGid),
1844
+ policyDigest,
1845
+ ...args.slice(1),
1846
+ ];
1847
+ const stdio = [
1848
+ ...macStandardStdio(spawnOpts?.stdio),
1849
+ runtimePin.fd,
1850
+ entryPin.fd,
1851
+ capsulePin.fd,
1852
+ "ignore",
1853
+ "ignore",
1854
+ "pipe",
1855
+ ];
1856
+ const packagedRuntime = Boolean(runtime.pkg);
1857
+ const targetEnvironment = macMcpTargetEnvironment(spawnOpts?.env, {
1858
+ packaged: packagedRuntime,
1859
+ inheritedEnvironment: runtime.env,
1860
+ });
1861
+ const options = {
1862
+ ...(spawnOpts || {}),
1863
+ cwd: "/",
1864
+ shell: false,
1865
+ detached: false,
1866
+ stdio,
1867
+ ...(targetEnvironment === undefined ? {} : { env: targetEnvironment }),
1868
+ };
1869
+ const installAttestationDigest = crypto
1870
+ .createHash("sha256")
1871
+ .update(
1872
+ JSON.stringify({
1873
+ version: 1,
1874
+ protocolSha256: MACOS_MCP_LAUNCHER_INPUTS.protocolSha256,
1875
+ sourceSha256: MACOS_MCP_LAUNCHER_INPUTS.sourceSha256,
1876
+ gateBootstrapSha256: MCP_STDIO_MACOS_GATED_ENTRY_BOOTSTRAP_SHA256,
1877
+ helper: installation.helper,
1878
+ installContractSha256: installation.installContractSha256,
1879
+ }),
1880
+ )
1881
+ .digest("hex");
1882
+ const environmentDigest = macEnvironmentDigest(options.env);
1883
+ const planBindingDigest = crypto
1884
+ .createHash("sha256")
1885
+ .update(
1886
+ JSON.stringify({
1887
+ version: 1,
1888
+ backend: MACOS_MCP_CAPSULE_BACKEND,
1889
+ helperArgs,
1890
+ policyDigest,
1891
+ installAttestationDigest,
1892
+ environmentDigest,
1893
+ runtimeFd: runtimePin.fd,
1894
+ entryFd: entryPin.fd,
1895
+ capsuleRootFd: capsulePin.fd,
1896
+ callerLifelineFd: protocol.callerLifelineFd,
1897
+ }),
1898
+ )
1899
+ .digest("hex");
1900
+ let cleaned = false;
1901
+ const cleanup = () => {
1902
+ if (cleaned) return;
1903
+ cleaned = true;
1904
+ for (const pin of [runtimePin, entryPin, capsulePin]) {
1905
+ if (pin?.fd !== undefined) {
1906
+ try {
1907
+ runtime.fs.closeSync(pin.fd);
1908
+ } catch {
1909
+ // Closing an already-duplicated parent descriptor is best effort.
1910
+ }
1911
+ }
1912
+ }
1913
+ };
1914
+ const runtimeProbe = {
1915
+ kind: "darwin-mcp-capsule-code-snapshot-v2",
1916
+ attempted: true,
1917
+ runnable: true,
1918
+ reason: null,
1919
+ probeRuntime: "node",
1920
+ targetRuntime: "node",
1921
+ targetRuntimeInvocationMode: packagedRuntime
1922
+ ? "pkg-copied-executable-eval-v1"
1923
+ : "node-executable-eval-v1",
1924
+ pkgExecPathMagicBound: packagedRuntime,
1925
+ contentSnapshot: true,
1926
+ contentSnapshotScope: "mcp-capsule-entry-and-node-runtime",
1927
+ contentSnapshotMechanism:
1928
+ "signed-root-runtime-path-and-anonymous-entry-fd-snapshots-v1",
1929
+ handleAtomic: true,
1930
+ entrySnapshotAtomic: true,
1931
+ runtimeLaunchAtomic: true,
1932
+ runtimeLaunchMechanism:
1933
+ "signed-root-helper-fd-copy-protected-path-ready-gate-v1",
1934
+ runtimeLaunchPath: snapshotPath,
1935
+ entrySnapshotPath: "anonymous-root-owned-fd4",
1936
+ runtimeSnapshotSha256: runtimePin.sha256,
1937
+ runtimeSnapshotBytes: runtimePin.bytes,
1938
+ entrySnapshotSha256: entryPin.sha256,
1939
+ entrySnapshotBytes: entryPin.bytes,
1940
+ entrySnapshotBootstrapSha256:
1941
+ MCP_STDIO_MACOS_GATED_ENTRY_BOOTSTRAP_SHA256,
1942
+ sharedLibraryClosure: false,
1943
+ rootProtectedRuntimeSnapshot: true,
1944
+ entryRootOwnedAnonymousSnapshot: true,
1945
+ entrySourcePrePostStat: true,
1946
+ entryWriterClosedBeforeReadonlyReopen: true,
1947
+ entryReadonlyIdentityRechecked: true,
1948
+ entryUnlinkedAndDirectoryFsyncedBeforeTarget: true,
1949
+ targetInheritedEntrySnapshotOnly: true,
1950
+ runtimeAndCapsuleSlotsNullBeforeExec: true,
1951
+ bootstrapClosesNullAndReadyDescriptors: true,
1952
+ actualRuntimeReadyHandshake: true,
1953
+ runtimeSnapshotUnlinkedBeforeEntryRelease: true,
1954
+ callerCredentialDropIrreversible: true,
1955
+ relayParentCredentialsDropped: true,
1956
+ callerLifelineWatched: true,
1957
+ signalRelayNonblocking: true,
1958
+ targetDescriptorAllowlist:
1959
+ "stdio-fd3-null-fd4-entry-fd5-null-fd6-gate-fd7-ready",
1960
+ capsuleRootDescriptorBound: true,
1961
+ capsulePathObjectAtomic: false,
1962
+ sandboxProfileFixedAndDigestBound: true,
1963
+ processForkExplicitlyDenied: true,
1964
+ sandboxExecLiveGateContract: true,
1965
+ globalLaunchSerialization: true,
1966
+ maximumStaleSnapshots: protocol.maximumStaleSnapshots,
1967
+ helperSha256: installation.helper.sha256,
1968
+ helperSourceSha256: MACOS_MCP_LAUNCHER_INPUTS.sourceSha256,
1969
+ helperProtocolSha256: MACOS_MCP_LAUNCHER_INPUTS.protocolSha256,
1970
+ helperInstallContractSha256: installation.installContractSha256,
1971
+ helperDesignatedRequirementSha256:
1972
+ installation.helper.designatedRequirementSha256,
1973
+ helperTeamIdentifier: installation.helper.teamIdentifier,
1974
+ helperPackageIdentifier: installation.helper.packageIdentifier,
1975
+ helperPackageVersion: installation.helper.packageVersion,
1976
+ installAttestationDigest,
1977
+ planBindingMechanism: "macos-mcp-code-snapshot-plan-binding-v1",
1978
+ planBindingDigest,
1979
+ };
1980
+ const plan = createSandboxPlan({
1981
+ ...base,
1982
+ applied: true,
1983
+ enforcement: MACOS_MCP_CAPSULE_BACKEND,
1984
+ backend: MACOS_MCP_CAPSULE_BACKEND,
1985
+ candidateBackend: null,
1986
+ policyAttested: true,
1987
+ policyDigest,
1988
+ command: protocol.helperInstallPath,
1989
+ args: helperArgs,
1990
+ options,
1991
+ runtimeProbe,
1992
+ guarantees: [
1993
+ SANDBOX_BOUNDARIES.CODE_SNAPSHOT,
1994
+ SANDBOX_BOUNDARIES.FILESYSTEM,
1995
+ SANDBOX_BOUNDARIES.NETWORK,
1996
+ SANDBOX_BOUNDARIES.PROCESS_EXEC,
1997
+ SANDBOX_BOUNDARIES.PROCESS_TREE,
1998
+ SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
1999
+ ],
2000
+ cleanup,
2001
+ });
2002
+ if (planBindingAuthority === MACOS_MCP_CODE_SNAPSHOT_ISSUER) {
2003
+ issuedMacMcpCodeSnapshotPlans.set(
2004
+ plan,
2005
+ Object.freeze({
2006
+ executionContract: contract,
2007
+ originalCommand: command,
2008
+ originalArgs: Object.freeze([...args]),
2009
+ originalCwd: spawnOpts?.cwd ?? null,
2010
+ originalShell: spawnOpts?.shell ?? null,
2011
+ originalDetached: spawnOpts?.detached ?? null,
2012
+ profile: base.profile,
2013
+ requiredBoundaries: Object.freeze([
2014
+ ...(sandboxOpts.requiredBoundaries || []),
2015
+ ]),
2016
+ sync: sandboxOpts.sync === true,
2017
+ policyDigest,
2018
+ helperCommand: protocol.helperInstallPath,
2019
+ helperArgs: Object.freeze([...helperArgs]),
2020
+ environmentDigest,
2021
+ stdio: Object.freeze([...stdio]),
2022
+ planBindingDigest,
2023
+ }),
2024
+ );
2025
+ }
2026
+ return plan;
2027
+ } catch (error) {
2028
+ for (const pin of [runtimePin, entryPin, capsulePin]) {
2029
+ if (pin?.fd !== undefined) {
2030
+ try {
2031
+ runtime.fs.closeSync(pin.fd);
2032
+ } catch {
2033
+ // best effort
2034
+ }
2035
+ }
2036
+ }
2037
+ return unavailable("macos_atomic_runtime_exec_unavailable", {
2038
+ kind: "darwin-mcp-capsule-code-snapshot-v2",
2039
+ attempted: true,
2040
+ runnable: false,
2041
+ reason: error?.message || "signed_root_helper_unattested",
972
2042
  contentSnapshot: false,
973
2043
  handleAtomic: false,
974
2044
  entrySnapshotAtomic: false,
975
2045
  runtimeLaunchAtomic: false,
2046
+ runtimeLaunchMechanism: "signed-root-helper-unattested-v1",
976
2047
  sharedLibraryClosure: false,
977
2048
  });
978
2049
  }
979
-
980
- // Darwin exposes only pathname-based exec/spawn APIs to an unprivileged
981
- // process. An inherited descriptor can bind the entry source, but public
982
- // APIs cannot bind the verified Node runtime descriptor to the image that
983
- // executes. Private temporary paths and Seatbelt still leave a same-UID
984
- // pathname-replacement window, so they cannot satisfy CODE_SNAPSHOT.
985
- return unavailable("macos_atomic_runtime_exec_unavailable", {
986
- attempted: true,
987
- runnable: false,
988
- reason: "public_api_has_no_descriptor_bound_exec",
989
- contentSnapshot: false,
990
- handleAtomic: false,
991
- entrySnapshotAtomic: false,
992
- runtimeLaunchAtomic: false,
993
- runtimeLaunchMechanism: "darwin-public-api-pathname-exec-only-v1",
994
- sharedLibraryClosure: false,
995
- });
996
2050
  }
997
2051
 
998
2052
  /**
@@ -1013,6 +2067,7 @@ export function applyMacSandbox(
1013
2067
  spawnOpts,
1014
2068
  sandboxOpts = {},
1015
2069
  runtimeOverrides = {},
2070
+ planBindingAuthority = null,
1016
2071
  ) {
1017
2072
  const runtime = resolveRuntime(runtimeOverrides);
1018
2073
  const invocation = normalizeWrappedInvocation(
@@ -1047,6 +2102,7 @@ export function applyMacSandbox(
1047
2102
  sandboxOpts,
1048
2103
  runtime,
1049
2104
  base,
2105
+ planBindingAuthority,
1050
2106
  );
1051
2107
  }
1052
2108
 
@@ -3882,6 +4938,7 @@ function linuxBubblewrapProbe(
3882
4938
  supervisorBinding = null,
3883
4939
  pluginTreeSnapshot = null,
3884
4940
  nativeDynamicClosure = null,
4941
+ runtimeLoadSet = null,
3885
4942
  runtimeDetachedChildSpawnVerified = false,
3886
4943
  ) {
3887
4944
  const native =
@@ -3923,18 +4980,42 @@ function linuxBubblewrapProbe(
3923
4980
  reason === null &&
3924
4981
  supervisorDescriptorBound &&
3925
4982
  nativeDynamicClosure?.mechanism ===
3926
- LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_MECHANISM &&
4983
+ LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_MECHANISM &&
3927
4984
  nativeDynamicClosure?.scope ===
3928
- LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_SCOPE &&
4985
+ LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_SCOPE &&
3929
4986
  typeof nativeDynamicClosure?.interpreter === "string" &&
3930
4987
  path.posix.isAbsolute(nativeDynamicClosure.interpreter) &&
3931
4988
  Number.isSafeInteger(nativeDynamicClosure?.dependencies) &&
3932
4989
  nativeDynamicClosure.dependencies >= 0 &&
3933
- nativeDynamicClosure.dependencies <= LINUX_ELF_MAX_NEEDED_ENTRIES &&
4990
+ nativeDynamicClosure.dependencies <= LINUX_ELF_MAX_INITIAL_CLOSURE_EDGES &&
3934
4991
  Number.isSafeInteger(nativeDynamicClosure?.files) &&
3935
4992
  nativeDynamicClosure.files >= 1 &&
4993
+ nativeDynamicClosure.files <= LINUX_ELF_MAX_INITIAL_CLOSURE_FILES &&
4994
+ Number.isSafeInteger(nativeDynamicClosure?.bytes) &&
4995
+ nativeDynamicClosure.bytes > 0 &&
4996
+ nativeDynamicClosure.bytes <= LINUX_ELF_MAX_INITIAL_CLOSURE_BYTES &&
3936
4997
  typeof nativeDynamicClosure?.digest === "string" &&
3937
4998
  /^[a-f0-9]{64}$/.test(nativeDynamicClosure.digest);
4999
+ const runtimeSharedLibraryPathnameClosure =
5000
+ initialDynamicLoadClosureDescriptorBound &&
5001
+ pluginTreeContentSnapshot &&
5002
+ runtimeLoadSet?.scope === LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_SCOPE &&
5003
+ runtimeLoadSet?.mechanism ===
5004
+ LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MECHANISM &&
5005
+ Number.isSafeInteger(runtimeLoadSet?.files) &&
5006
+ runtimeLoadSet.files > 0 &&
5007
+ runtimeLoadSet.files <= LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_FILES &&
5008
+ Number.isSafeInteger(runtimeLoadSet?.bytes) &&
5009
+ runtimeLoadSet.bytes > 0 &&
5010
+ runtimeLoadSet.bytes <= LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_BYTES &&
5011
+ typeof runtimeLoadSet?.digest === "string" &&
5012
+ /^[a-f0-9]{64}$/.test(runtimeLoadSet.digest) &&
5013
+ runtimeLoadSet.policyBound === true &&
5014
+ runtimeLoadSet.writableFilesystems === false &&
5015
+ runtimeLoadSet.procfsMounted === false &&
5016
+ runtimeLoadSet.devfsMounted === false &&
5017
+ runtimeLoadSet.scratchWritable === false &&
5018
+ runtimeLoadSet.descriptorReopenPaths === false;
3938
5019
  return {
3939
5020
  kind: dynamicNative
3940
5021
  ? "linux-bwrap-plugin-native-dynamic-elf-policy-v1"
@@ -3977,7 +5058,31 @@ function linuxBubblewrapProbe(
3977
5058
  initialDynamicInterpreter: nativeDynamicClosure.interpreter,
3978
5059
  initialDynamicDependencyCount: nativeDynamicClosure.dependencies,
3979
5060
  initialDynamicRuntimeFileCount: nativeDynamicClosure.files,
5061
+ initialDynamicRuntimeBytes: nativeDynamicClosure.bytes,
3980
5062
  initialDynamicLoadClosureDigest: nativeDynamicClosure.digest,
5063
+ // The legacy aggregate field is intentionally kept false: this
5064
+ // production slice closes libc/ELF-loader pathname resolution, not
5065
+ // anonymous JIT mappings or a malicious custom in-process loader.
5066
+ sharedLibraryClosure: false,
5067
+ ...(runtimeSharedLibraryPathnameClosure
5068
+ ? {
5069
+ runtimeSharedLibraryPathnameClosure: true,
5070
+ runtimeSharedLibraryPathnameClosureExcludes:
5071
+ "anonymous-jit-and-custom-in-process-loader",
5072
+ runtimeSharedLibraryClosureScope: runtimeLoadSet.scope,
5073
+ runtimeSharedLibraryClosureMechanism: runtimeLoadSet.mechanism,
5074
+ runtimeSharedLibraryLoadSetFiles: runtimeLoadSet.files,
5075
+ runtimeSharedLibraryLoadSetBytes: runtimeLoadSet.bytes,
5076
+ runtimeSharedLibraryLoadSetDigest: runtimeLoadSet.digest,
5077
+ runtimeLoadSetPolicyBound: runtimeLoadSet.policyBound,
5078
+ runtimeWritableFilesystems: runtimeLoadSet.writableFilesystems,
5079
+ runtimeProcfsMounted: runtimeLoadSet.procfsMounted,
5080
+ runtimeDevfsMounted: runtimeLoadSet.devfsMounted,
5081
+ runtimeScratchWritable: runtimeLoadSet.scratchWritable,
5082
+ runtimeDescriptorReopenPaths:
5083
+ runtimeLoadSet.descriptorReopenPaths,
5084
+ }
5085
+ : {}),
3981
5086
  }
3982
5087
  : {}),
3983
5088
  supervisorDescriptorBound,
@@ -3990,8 +5095,9 @@ function linuxBubblewrapProbe(
3990
5095
  supervisorDescriptorConsumedBeforeTarget: runnable,
3991
5096
  supervisorStagingPathHidden: runnable,
3992
5097
  supervisorTemporaryCopyObscured: runnable,
3993
- supervisorPid1ExecutableExposure:
3994
- supervisorBinding.pid1ExecutableExposure,
5098
+ supervisorPid1ExecutableExposure: runtimeSharedLibraryPathnameClosure
5099
+ ? "procfs-not-mounted"
5100
+ : supervisorBinding.pid1ExecutableExposure,
3995
5101
  supervisorExecutableIdentity: {
3996
5102
  path: supervisorBinding.path,
3997
5103
  fileId: supervisorBinding.fileId,
@@ -4246,7 +5352,7 @@ function assignLinuxMountChildFds(mounts) {
4246
5352
  function linuxStdioWithPinnedMounts(
4247
5353
  stdio,
4248
5354
  descriptors,
4249
- { probe = false, supervisorFd = null } = {},
5355
+ { probe = false, supervisorFd = null, scrubberFd = null } = {},
4250
5356
  ) {
4251
5357
  let standard;
4252
5358
  if (probe) {
@@ -4260,10 +5366,12 @@ function linuxStdioWithPinnedMounts(
4260
5366
  }
4261
5367
  while (standard.length < 3) standard.push(undefined);
4262
5368
  const supervisor = Number.isInteger(supervisorFd) ? [supervisorFd] : [];
5369
+ const scrubber = Number.isInteger(scrubberFd) ? [scrubberFd] : [];
4263
5370
  return [
4264
5371
  ...standard.slice(0, 3),
4265
5372
  ...supervisor,
4266
5373
  ...descriptors.map((descriptor) => descriptor.fd),
5374
+ ...scrubber,
4267
5375
  ];
4268
5376
  }
4269
5377
 
@@ -4306,16 +5414,51 @@ function validateLinuxPluginContract(
4306
5414
  ) {
4307
5415
  return { ok: false, reason: "unsupported_launch_options" };
4308
5416
  }
4309
- const invalidArgs =
4310
- !Array.isArray(args) ||
4311
- args.some((arg) => typeof arg !== "string" || arg.includes("\0"));
5417
+ let argsSnapshot = null;
5418
+ try {
5419
+ if (
5420
+ Array.isArray(args) &&
5421
+ Object.getPrototypeOf(args) === Array.prototype
5422
+ ) {
5423
+ const descriptors = Object.getOwnPropertyDescriptors(args);
5424
+ const lengthDescriptor = descriptors.length;
5425
+ if (
5426
+ lengthDescriptor &&
5427
+ "value" in lengthDescriptor &&
5428
+ Number.isSafeInteger(lengthDescriptor.value) &&
5429
+ lengthDescriptor.value >= 0 &&
5430
+ Reflect.ownKeys(descriptors).length === lengthDescriptor.value + 1
5431
+ ) {
5432
+ const snapshot = [];
5433
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
5434
+ const descriptor = descriptors[String(index)];
5435
+ if (
5436
+ !descriptor ||
5437
+ !("value" in descriptor) ||
5438
+ typeof descriptor.value !== "string" ||
5439
+ descriptor.value.includes("\0")
5440
+ ) {
5441
+ argsSnapshot = null;
5442
+ break;
5443
+ }
5444
+ snapshot.push(descriptor.value);
5445
+ }
5446
+ if (snapshot.length === lengthDescriptor.value) {
5447
+ argsSnapshot = Object.freeze(snapshot);
5448
+ }
5449
+ }
5450
+ }
5451
+ } catch {
5452
+ argsSnapshot = null;
5453
+ }
5454
+ const invalidArgs = argsSnapshot === null;
4312
5455
  const invalidNodeLaunch =
4313
5456
  nodeContract &&
4314
- (!Array.isArray(args) ||
5457
+ (!argsSnapshot ||
4315
5458
  command !== contract.runtimePath ||
4316
5459
  command !== contract.runtimeIdentity?.realPath ||
4317
- args.length < 1 ||
4318
- args[0] !== contract.entryIdentity?.realPath);
5460
+ argsSnapshot.length < 1 ||
5461
+ argsSnapshot[0] !== contract.entryIdentity?.realPath);
4319
5462
  const invalidNativeLaunch =
4320
5463
  nativeContract && command !== contract.entryIdentity?.realPath;
4321
5464
  if (
@@ -4433,6 +5576,7 @@ function validateLinuxPluginContract(
4433
5576
  return {
4434
5577
  ok: true,
4435
5578
  contract,
5579
+ argsSnapshot,
4436
5580
  entryRuntime: nativeContract
4437
5581
  ? entryFormat?.runtime ||
4438
5582
  (legacyStaticNativeContract
@@ -4555,6 +5699,16 @@ function linuxMcpRuntimeSnapshotContract() {
4555
5699
  };
4556
5700
  }
4557
5701
 
5702
+ function linuxPluginRuntimeSnapshotContract() {
5703
+ return {
5704
+ scope: LINUX_PLUGIN_RUNTIME_SNAPSHOT_SCOPE,
5705
+ targetMode: "0500",
5706
+ sourceMode: LINUX_EXECUTABLE_SNAPSHOT_SOURCE_MODE,
5707
+ minimumBytes: 1,
5708
+ errorPrefix: "plugin_node_runtime_snapshot",
5709
+ };
5710
+ }
5711
+
4558
5712
  function createLinuxRegularFileSnapshot(
4559
5713
  runtime,
4560
5714
  entryMount,
@@ -4630,7 +5784,8 @@ function createLinuxRegularFileSnapshot(
4630
5784
  }
4631
5785
  if (
4632
5786
  (snapshotContract.scope === LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE ||
4633
- snapshotContract.scope === LINUX_MCP_RUNTIME_SNAPSHOT_SCOPE) &&
5787
+ snapshotContract.scope === LINUX_MCP_RUNTIME_SNAPSHOT_SCOPE ||
5788
+ snapshotContract.scope === LINUX_PLUGIN_RUNTIME_SNAPSHOT_SCOPE) &&
4634
5789
  (sourceFileMode & 0o111) === 0
4635
5790
  ) {
4636
5791
  throw snapshotError("source_mode_changed");
@@ -5406,9 +6561,14 @@ function linuxSystemDynamicPath(value) {
5406
6561
  * to a host inspection utility. Besides conventional fully static ET_EXEC and
5407
6562
  * static-PIE-shaped ET_DYN images, the parser can describe a narrow dynamic
5408
6563
  * executable. The caller must separately prove that its PT_INTERP and every
5409
- * direct DT_NEEDED member are present in a pinned, attested system-file set.
6564
+ * recursively reachable startup DT_NEEDED member resolve through a pinned,
6565
+ * attested system-file graph.
5410
6566
  */
5411
- function inspectLinuxNativeElf(runtime, fd, identity) {
6567
+ function inspectLinuxNativeElf(runtime, fd, identity, { role = "entry" } = {}) {
6568
+ const dependencyObject = role === "dependency";
6569
+ if (!dependencyObject && role !== "entry") {
6570
+ throw new Error("native_elf_inspection_role_invalid");
6571
+ }
5412
6572
  const expectedMachine = LINUX_ELF_MACHINES[runtime.arch];
5413
6573
  if (!expectedMachine) {
5414
6574
  throw new Error(`unsupported_elf_architecture:${String(runtime.arch)}`);
@@ -5417,8 +6577,11 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5417
6577
  const before = runtime.fs.fstatSync(fd);
5418
6578
  if (
5419
6579
  !before.isFile() ||
5420
- Number(before.nlink) !== 1 ||
5421
- (Number(before.mode) & 0o111) === 0 ||
6580
+ (dependencyObject
6581
+ ? Number(before.nlink) < 1 ||
6582
+ Number(before.uid) !== 0 ||
6583
+ (Number(before.mode) & 0o022) !== 0
6584
+ : Number(before.nlink) !== 1 || (Number(before.mode) & 0o111) === 0) ||
5422
6585
  (Number(before.mode) & 0o6000) !== 0 ||
5423
6586
  String(before.dev) !== String(identity?.fileId?.dev) ||
5424
6587
  String(before.ino) !== String(identity?.fileId?.ino) ||
@@ -5447,6 +6610,9 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5447
6610
  if (elfType !== LINUX_ELF_TYPE_EXEC && elfType !== LINUX_ELF_TYPE_DYN) {
5448
6611
  throw new Error("native_entry_unsupported_elf_type");
5449
6612
  }
6613
+ if (dependencyObject && elfType !== LINUX_ELF_TYPE_DYN) {
6614
+ throw new Error("native_dependency_not_shared_object");
6615
+ }
5450
6616
  if (header.readUInt16LE(18) !== expectedMachine) {
5451
6617
  throw new Error("native_entry_architecture_mismatch");
5452
6618
  }
@@ -5551,7 +6717,7 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5551
6717
  }
5552
6718
  }
5553
6719
  attestLinuxElfLoadMappings(loadSegments, runtimePageSize);
5554
- if (!executableLoad || !entryInExecutableLoad) {
6720
+ if (!executableLoad || (!dependencyObject && !entryInExecutableLoad)) {
5555
6721
  throw new Error("native_entry_has_no_executable_entry_segment");
5556
6722
  }
5557
6723
  if (!nonExecutableStack) {
@@ -5561,6 +6727,12 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5561
6727
  if (interpreterSegments.length > 1) {
5562
6728
  throw new Error("native_entry_interpreter_ambiguous");
5563
6729
  }
6730
+ // A shared object can itself be directly executable: Ubuntu 24.04's
6731
+ // libc.so.6, for example, carries PT_INTERP. The dynamic loader ignores
6732
+ // that segment when it maps the object as a dependency, so rejecting it
6733
+ // would exclude a legitimate recursive startup graph. Still parse and
6734
+ // validate the segment below; only an entry object's interpreter becomes
6735
+ // part of the kernel-exec contract and returned closure evidence.
5564
6736
  let interpreter = null;
5565
6737
  if (interpreterSegments.length === 1) {
5566
6738
  const segment = interpreterSegments[0];
@@ -5590,17 +6762,24 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5590
6762
  }
5591
6763
  }
5592
6764
 
5593
- const staticPie = elfType === LINUX_ELF_TYPE_DYN && interpreter === null;
5594
- const dynamicExecutable = interpreter !== null;
6765
+ const staticPie =
6766
+ !dependencyObject && elfType === LINUX_ELF_TYPE_DYN && interpreter === null;
6767
+ const dynamicExecutable = !dependencyObject && interpreter !== null;
5595
6768
  let dynamicMetadata = null;
5596
- if (elfType === LINUX_ELF_TYPE_EXEC && !dynamicExecutable) {
6769
+ if (
6770
+ !dependencyObject &&
6771
+ elfType === LINUX_ELF_TYPE_EXEC &&
6772
+ !dynamicExecutable
6773
+ ) {
5597
6774
  if (dynamicSegments.length > 0) {
5598
6775
  throw new Error("native_entry_dynamic_elf_unsupported");
5599
6776
  }
5600
6777
  } else {
5601
- const prefix = staticPie
5602
- ? "native_entry_static_pie"
5603
- : "native_entry_dynamic";
6778
+ const prefix = dependencyObject
6779
+ ? "native_dependency_dynamic"
6780
+ : staticPie
6781
+ ? "native_entry_static_pie"
6782
+ : "native_entry_dynamic";
5604
6783
  if (dynamicSegments.length === 0) {
5605
6784
  throw new Error(`${prefix}_dynamic_segment_missing`);
5606
6785
  }
@@ -5668,6 +6847,7 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5668
6847
  Number(dynamic.fileOffset),
5669
6848
  );
5670
6849
  const neededOffsets = [];
6850
+ let sonameOffset = null;
5671
6851
  const forbiddenDependencyTags = new Set([
5672
6852
  LINUX_ELF_DYNAMIC_RPATH,
5673
6853
  LINUX_ELF_DYNAMIC_RUNPATH,
@@ -5704,6 +6884,11 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5704
6884
  if (neededOffsets.length > LINUX_ELF_MAX_NEEDED_ENTRIES) {
5705
6885
  throw new Error(`${prefix}_dependency_count_exceeded`);
5706
6886
  }
6887
+ } else if (tag === LINUX_ELF_DYNAMIC_SONAME) {
6888
+ if (sonameOffset !== null) {
6889
+ throw new Error(`${prefix}_soname_ambiguous`);
6890
+ }
6891
+ sonameOffset = value;
5707
6892
  } else if (tag === LINUX_ELF_DYNAMIC_STRTAB) {
5708
6893
  if (stringTableAddress !== null) {
5709
6894
  throw new Error(`${prefix}_string_table_ambiguous`);
@@ -5733,17 +6918,19 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5733
6918
  throw new Error(`${prefix}_text_relocation_unsupported`);
5734
6919
  }
5735
6920
  if (
6921
+ !dependencyObject &&
5736
6922
  elfType === LINUX_ELF_TYPE_DYN &&
5737
6923
  (flags1 === null || (flags1 & LINUX_ELF_DYNAMIC_FLAG_PIE) === 0n)
5738
6924
  ) {
5739
6925
  throw new Error(`${prefix}_flag_missing`);
5740
6926
  }
5741
- if (staticPie && neededOffsets.length > 0) {
6927
+ if (!dependencyObject && staticPie && neededOffsets.length > 0) {
5742
6928
  throw new Error("native_entry_static_pie_dependency_unsupported");
5743
6929
  }
5744
6930
 
5745
6931
  let needed = [];
5746
- if (neededOffsets.length > 0) {
6932
+ let soname = null;
6933
+ if (neededOffsets.length > 0 || sonameOffset !== null) {
5747
6934
  if (
5748
6935
  stringTableAddress === null ||
5749
6936
  stringTableBytes === null ||
@@ -5793,34 +6980,61 @@ function inspectLinuxNativeElf(runtime, fd, identity) {
5793
6980
  if (new Set(needed).size !== needed.length) {
5794
6981
  throw new Error("native_entry_dynamic_dependency_ambiguous");
5795
6982
  }
6983
+ if (sonameOffset !== null) {
6984
+ if (sonameOffset > BigInt(Number.MAX_SAFE_INTEGER)) {
6985
+ throw new Error(`${prefix}_soname_string_out_of_bounds`);
6986
+ }
6987
+ soname = readLinuxElfString(
6988
+ stringTable,
6989
+ Number(sonameOffset),
6990
+ "dynamic_soname",
6991
+ );
6992
+ if (
6993
+ !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/.test(soname) ||
6994
+ path.posix.basename(soname) !== soname
6995
+ ) {
6996
+ throw new Error(`${prefix}_soname_invalid`);
6997
+ }
6998
+ }
5796
6999
  }
5797
7000
 
5798
- if (dynamicExecutable) {
7001
+ if (dynamicExecutable || dependencyObject) {
5799
7002
  dynamicMetadata = Object.freeze({
5800
- interpreter,
7003
+ ...(dynamicExecutable ? { interpreter } : {}),
5801
7004
  needed: Object.freeze(needed),
7005
+ soname,
5802
7006
  });
5803
7007
  }
5804
7008
  }
5805
7009
 
5806
7010
  const sha256 = hashLinuxOpenFile(runtime, fd, Number(before.size));
5807
7011
  const after = runtime.fs.fstatSync(fd);
5808
- if (!linuxOpenStatMatches(before, after) || sha256 !== identity.sha256) {
7012
+ if (
7013
+ !linuxOpenStatMatches(before, after) ||
7014
+ (identity.sha256 !== undefined && sha256 !== identity.sha256)
7015
+ ) {
5809
7016
  throw new Error("native_entry_changed_during_elf_attestation");
5810
7017
  }
5811
7018
  return Object.freeze({
5812
- runtime: dynamicExecutable ? "native-dynamic-elf" : "native-static-elf",
5813
- format: dynamicExecutable
5814
- ? elfType === LINUX_ELF_TYPE_DYN
5815
- ? "elf64-dynamic-pie-et-dyn"
5816
- : "elf64-dynamic-et-exec"
5817
- : elfType === LINUX_ELF_TYPE_DYN
5818
- ? "elf64-static-pie-shaped-et-dyn"
5819
- : "elf64-static-et-exec",
7019
+ runtime: dependencyObject
7020
+ ? "native-shared-object"
7021
+ : dynamicExecutable
7022
+ ? "native-dynamic-elf"
7023
+ : "native-static-elf",
7024
+ format: dependencyObject
7025
+ ? "elf64-shared-object-et-dyn"
7026
+ : dynamicExecutable
7027
+ ? elfType === LINUX_ELF_TYPE_DYN
7028
+ ? "elf64-dynamic-pie-et-dyn"
7029
+ : "elf64-dynamic-et-exec"
7030
+ : elfType === LINUX_ELF_TYPE_DYN
7031
+ ? "elf64-static-pie-shaped-et-dyn"
7032
+ : "elf64-static-et-exec",
5820
7033
  architecture: runtime.arch,
5821
7034
  machine: expectedMachine,
5822
7035
  loaderPageBytes: Number(runtimePageSize),
5823
7036
  programHeaders: programHeaderCount,
7037
+ sha256,
5824
7038
  ...(dynamicMetadata || {}),
5825
7039
  });
5826
7040
  }
@@ -5856,17 +7070,22 @@ function linuxFdMountId(runtime, fd) {
5856
7070
  }
5857
7071
 
5858
7072
  function closeLinuxPinnedMounts(runtime, mounts) {
7073
+ const closed = new Set();
5859
7074
  for (const mount of mounts || []) {
5860
- if (!Number.isInteger(mount?.fd)) continue;
5861
- const fd = mount.fd;
5862
- // Clear ownership before close so repeated cleanup cannot close an
5863
- // unrelated descriptor if the OS has already reused this number.
5864
- mount.fd = null;
5865
- try {
5866
- runtime.fs.closeSync(fd);
5867
- } catch {
5868
- // Best effort. The process is already fail-closed if a pin cannot be
5869
- // created; cleanup must not replace the original boundary error.
7075
+ for (const field of ["fd", "scrubberFd"]) {
7076
+ if (!Number.isInteger(mount?.[field])) continue;
7077
+ const fd = mount[field];
7078
+ // Clear ownership before close so repeated cleanup cannot close an
7079
+ // unrelated descriptor if the OS has already reused this number.
7080
+ mount[field] = null;
7081
+ if (closed.has(fd)) continue;
7082
+ closed.add(fd);
7083
+ try {
7084
+ runtime.fs.closeSync(fd);
7085
+ } catch {
7086
+ // Best effort. The process is already fail-closed if a pin cannot be
7087
+ // created; cleanup must not replace the original boundary error.
7088
+ }
5870
7089
  }
5871
7090
  }
5872
7091
  }
@@ -6042,7 +7261,10 @@ function pinLinuxPluginTree(runtime, pluginRoot, rootIdentity) {
6042
7261
  }
6043
7262
  }
6044
7263
 
6045
- function buildLinuxNetworkSeccompFilter(arch) {
7264
+ function buildLinuxNetworkSeccompFilter(
7265
+ arch,
7266
+ { runtimePathnameClosure = false } = {},
7267
+ ) {
6046
7268
  const architecture = LINUX_SECCOMP_FILTERS[arch];
6047
7269
  if (!architecture) {
6048
7270
  throw new Error(`unsupported_seccomp_architecture:${String(arch)}`);
@@ -6071,8 +7293,54 @@ function buildLinuxNetworkSeccompFilter(arch) {
6071
7293
  // syscall. No ring descriptor is inherited by this narrow launch route.
6072
7294
  [0x15, 0, 1, architecture.ioUringSetupSyscall],
6073
7295
  [0x06, 0, 0, 0x00050001],
6074
- [0x06, 0, 0, 0x7fff0000],
6075
7296
  );
7297
+ if (runtimePathnameClosure) {
7298
+ // clone3 receives a pointer to struct clone_args, which classic seccomp BPF
7299
+ // cannot safely dereference. Make the syscall unavailable so glibc can
7300
+ // fall back to classic clone for ordinary threads; the classic call below
7301
+ // rejects only namespace-producing flags.
7302
+ instructions.push(
7303
+ [0x15, 0, 1, architecture.clone3Syscall],
7304
+ [0x06, 0, 0, 0x00050026],
7305
+ );
7306
+ for (const syscall of [
7307
+ // recvmsg(2) and recvmmsg(2) are the two receive APIs capable of
7308
+ // importing SCM_RIGHTS descriptors. recvfrom/read cannot receive
7309
+ // ancillary descriptors, and sendmsg does not import one.
7310
+ architecture.recvmsgSyscall,
7311
+ architecture.recvmmsgSyscall,
7312
+ architecture.pidfdGetfdSyscall,
7313
+ architecture.openByHandleAtSyscall,
7314
+ // A read-only outer namespace is not a closure if the target can create
7315
+ // or join another namespace and mount writable bytes over a visible path.
7316
+ architecture.unshareSyscall,
7317
+ architecture.setnsSyscall,
7318
+ architecture.mountSyscall,
7319
+ architecture.umount2Syscall,
7320
+ architecture.pivotRootSyscall,
7321
+ architecture.openTreeSyscall,
7322
+ architecture.moveMountSyscall,
7323
+ architecture.fsopenSyscall,
7324
+ architecture.fsconfigSyscall,
7325
+ architecture.fsmountSyscall,
7326
+ architecture.fspickSyscall,
7327
+ architecture.mountSetattrSyscall,
7328
+ ]) {
7329
+ instructions.push([0x15, 0, 1, syscall], [0x06, 0, 0, 0x00050001]);
7330
+ }
7331
+ instructions.push(
7332
+ // seccomp_data.args[0] starts at byte 16. All supported ABIs are
7333
+ // little-endian, so loading this word inspects the low classic-clone
7334
+ // flags. Preserve fork/pthread clone calls while rejecting NEWUSER/NEWNS.
7335
+ [0x15, 0, 5, architecture.cloneSyscall],
7336
+ [0x20, 0, 0, 16],
7337
+ [0x54, 0, 0, LINUX_NAMESPACE_CLONE_FLAGS],
7338
+ [0x15, 1, 0, 0],
7339
+ [0x06, 0, 0, 0x00050001],
7340
+ [0x06, 0, 0, 0x7fff0000],
7341
+ );
7342
+ }
7343
+ instructions.push([0x06, 0, 0, 0x7fff0000]);
6076
7344
  const buffer = Buffer.alloc(instructions.length * 8);
6077
7345
  instructions.forEach(([code, jumpTrue, jumpFalse, value], index) => {
6078
7346
  const offset = index * 8;
@@ -6088,8 +7356,8 @@ function buildLinuxNetworkSeccompFilter(arch) {
6088
7356
  };
6089
7357
  }
6090
7358
 
6091
- function pinLinuxNetworkSeccompFilter(runtime) {
6092
- const filter = buildLinuxNetworkSeccompFilter(runtime.arch);
7359
+ function pinLinuxNetworkSeccompFilter(runtime, options = {}) {
7360
+ const filter = buildLinuxNetworkSeccompFilter(runtime.arch, options);
6093
7361
  let fd;
6094
7362
  try {
6095
7363
  for (const method of [
@@ -6159,7 +7427,9 @@ function pinLinuxNetworkSeccompFilter(runtime) {
6159
7427
  childFd: null,
6160
7428
  arch: filter.arch,
6161
7429
  sha256: filter.sha256,
6162
- policy: "deny-network-creation",
7430
+ policy: options.runtimePathnameClosure
7431
+ ? "deny-network-creation-loader-fd-acquisition-and-namespace-mutation"
7432
+ : "deny-network-creation",
6163
7433
  };
6164
7434
  } catch (error) {
6165
7435
  if (fd !== undefined) {
@@ -6336,42 +7606,119 @@ function pinLinuxBubblewrapSupervisor(runtime) {
6336
7606
  }
6337
7607
  }
6338
7608
 
6339
- function openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin) {
7609
+ function pinLinuxBubblewrapDescriptorScrubber(runtime) {
6340
7610
  let fd;
6341
7611
  try {
7612
+ for (const method of [
7613
+ "openSync",
7614
+ "readSync",
7615
+ "fstatSync",
7616
+ "statSync",
7617
+ "lstatSync",
7618
+ "closeSync",
7619
+ ]) {
7620
+ if (typeof runtime.fs?.[method] !== "function") {
7621
+ throw new Error(`descriptor_scrubber_${method}_unavailable`);
7622
+ }
7623
+ }
7624
+ if (
7625
+ linuxRealpath(runtime, LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH) !==
7626
+ LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH
7627
+ ) {
7628
+ throw new Error("descriptor_scrubber_realpath_changed");
7629
+ }
7630
+ const pathBefore = runtime.fs.lstatSync(
7631
+ LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH,
7632
+ { bigint: true },
7633
+ );
6342
7634
  if (
6343
- !Number.isInteger(supervisorPin?.fd) ||
6344
- !supervisorPin.openedStat ||
6345
- !supervisorPin.attestation
7635
+ pathBefore.isSymbolicLink?.() === true ||
7636
+ !linuxBubblewrapSupervisorStatValid(pathBefore)
6346
7637
  ) {
6347
- throw new Error("supervisor_pin_missing");
7638
+ throw new Error("descriptor_scrubber_path_unattested");
6348
7639
  }
6349
7640
  const constants = runtime.fs.constants || fs.constants;
6350
7641
  const flags =
6351
- Number(constants.O_RDONLY) | Number(constants.O_NONBLOCK || 0);
6352
- fd = runtime.fs.openSync(`/proc/self/fd/${supervisorPin.fd}`, flags);
7642
+ Number(constants.O_RDONLY) |
7643
+ Number(constants.O_NOFOLLOW || 0) |
7644
+ Number(constants.O_NONBLOCK || 0);
7645
+ fd = runtime.fs.openSync(LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH, flags);
6353
7646
  const openedBefore = runtime.fs.fstatSync(fd, { bigint: true });
7647
+ const currentPath = runtime.fs.statSync(
7648
+ LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH,
7649
+ { bigint: true },
7650
+ );
6354
7651
  if (
6355
- !linuxBubblewrapPinnedStatMatches(supervisorPin.openedStat, openedBefore)
7652
+ !linuxBubblewrapSupervisorStatValid(openedBefore) ||
7653
+ !linuxOpenStatMatches(pathBefore, openedBefore) ||
7654
+ !linuxOpenStatMatches(openedBefore, currentPath)
6356
7655
  ) {
6357
- throw new Error("supervisor_launch_reader_identity_changed");
7656
+ throw new Error("descriptor_scrubber_identity_changed");
6358
7657
  }
6359
- const sha256 = hashLinuxOpenFile(
6360
- runtime,
6361
- fd,
6362
- supervisorPin.attestation.bytes,
6363
- );
7658
+ const sha256 = hashLinuxOpenFile(runtime, fd, Number(openedBefore.size));
6364
7659
  const openedAfter = runtime.fs.fstatSync(fd, { bigint: true });
7660
+ const pathAfter = runtime.fs.statSync(
7661
+ LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH,
7662
+ { bigint: true },
7663
+ );
6365
7664
  if (
6366
7665
  !linuxOpenStatMatches(openedBefore, openedAfter) ||
6367
- sha256 !== supervisorPin.attestation.sha256
7666
+ !linuxOpenStatMatches(openedAfter, pathAfter)
6368
7667
  ) {
6369
- throw new Error("supervisor_launch_reader_identity_changed");
7668
+ throw new Error("descriptor_scrubber_identity_changed");
6370
7669
  }
6371
7670
  return {
6372
7671
  fd,
6373
- command: `/proc/self/fd/${LINUX_BWRAP_SUPERVISOR_CHILD_FD}`,
7672
+ openedStat: openedAfter,
7673
+ attestation: Object.freeze({
7674
+ path: LINUX_BWRAP_DESCRIPTOR_SCRUBBER_PATH,
7675
+ fileId: Object.freeze({
7676
+ dev: String(openedAfter.dev),
7677
+ ino: String(openedAfter.ino),
7678
+ }),
7679
+ sha256,
7680
+ bytes: Number(openedAfter.size),
7681
+ mtimeMs: Number(openedAfter.mtimeMs),
7682
+ mode: Number(openedAfter.mode),
7683
+ uid: Number(openedAfter.uid),
7684
+ gid: Number(openedAfter.gid),
7685
+ }),
6374
7686
  };
7687
+ } catch (error) {
7688
+ if (fd !== undefined) {
7689
+ try {
7690
+ runtime.fs.closeSync(fd);
7691
+ } catch {
7692
+ // Preserve the original attestation error.
7693
+ }
7694
+ }
7695
+ throw error;
7696
+ }
7697
+ }
7698
+
7699
+ function openLinuxBubblewrapPinnedExecutableReader(runtime, pin, label) {
7700
+ let fd;
7701
+ try {
7702
+ if (!Number.isInteger(pin?.fd) || !pin.openedStat || !pin.attestation) {
7703
+ throw new Error(`${label}_pin_missing`);
7704
+ }
7705
+ const constants = runtime.fs.constants || fs.constants;
7706
+ const flags =
7707
+ Number(constants.O_RDONLY) | Number(constants.O_NONBLOCK || 0);
7708
+ fd = runtime.fs.openSync(`/proc/self/fd/${pin.fd}`, flags);
7709
+ const openedBefore = runtime.fs.fstatSync(fd, { bigint: true });
7710
+ if (!linuxBubblewrapPinnedStatMatches(pin.openedStat, openedBefore)) {
7711
+ throw new Error(`${label}_launch_reader_identity_changed`);
7712
+ }
7713
+ const sha256 = hashLinuxOpenFile(runtime, fd, pin.attestation.bytes);
7714
+ const openedAfter = runtime.fs.fstatSync(fd, { bigint: true });
7715
+ if (
7716
+ !linuxOpenStatMatches(openedBefore, openedAfter) ||
7717
+ sha256 !== pin.attestation.sha256
7718
+ ) {
7719
+ throw new Error(`${label}_launch_reader_identity_changed`);
7720
+ }
7721
+ return fd;
6375
7722
  } catch (error) {
6376
7723
  if (fd !== undefined) {
6377
7724
  try {
@@ -6384,17 +7731,46 @@ function openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin) {
6384
7731
  }
6385
7732
  }
6386
7733
 
6387
- function attestLinuxBubblewrapSupervisorPin(runtime, supervisorPin) {
7734
+ function openLinuxBubblewrapSupervisorLaunch(
7735
+ runtime,
7736
+ supervisorPin,
7737
+ descriptorScrubberPin,
7738
+ ) {
7739
+ let fd;
7740
+ let scrubberFd;
7741
+ try {
7742
+ fd = openLinuxBubblewrapPinnedExecutableReader(
7743
+ runtime,
7744
+ supervisorPin,
7745
+ "supervisor",
7746
+ );
7747
+ scrubberFd = openLinuxBubblewrapPinnedExecutableReader(
7748
+ runtime,
7749
+ descriptorScrubberPin,
7750
+ "descriptor_scrubber",
7751
+ );
7752
+ return { fd, scrubberFd };
7753
+ } catch (error) {
7754
+ closeLinuxPinnedMounts(runtime, [{ fd }, { fd: scrubberFd }]);
7755
+ throw error;
7756
+ }
7757
+ }
7758
+
7759
+ function attestLinuxBubblewrapSupervisorPin(
7760
+ runtime,
7761
+ supervisorPin,
7762
+ label = "supervisor",
7763
+ ) {
6388
7764
  if (
6389
7765
  !Number.isInteger(supervisorPin?.fd) ||
6390
7766
  !supervisorPin.openedStat ||
6391
7767
  !supervisorPin.attestation
6392
7768
  ) {
6393
- throw new Error("supervisor_pin_missing");
7769
+ throw new Error(`${label}_pin_missing`);
6394
7770
  }
6395
7771
  const before = runtime.fs.fstatSync(supervisorPin.fd, { bigint: true });
6396
7772
  if (!linuxBubblewrapPinnedStatMatches(supervisorPin.openedStat, before)) {
6397
- throw new Error("supervisor_identity_changed");
7773
+ throw new Error(`${label}_identity_changed`);
6398
7774
  }
6399
7775
  const sha256 = hashLinuxOpenFile(
6400
7776
  runtime,
@@ -6406,17 +7782,72 @@ function attestLinuxBubblewrapSupervisorPin(runtime, supervisorPin) {
6406
7782
  !linuxOpenStatMatches(before, after) ||
6407
7783
  sha256 !== supervisorPin.attestation.sha256
6408
7784
  ) {
6409
- throw new Error("supervisor_identity_changed");
7785
+ throw new Error(`${label}_identity_changed`);
7786
+ }
7787
+ }
7788
+
7789
+ function linuxBubblewrapScrubbedInvocation(
7790
+ supervisorLaunch,
7791
+ executableArgs,
7792
+ stdio,
7793
+ ) {
7794
+ const scrubberChildFd = stdio.length - 1;
7795
+ let explicitLayout = true;
7796
+ const parentFds = new Set();
7797
+ for (
7798
+ let index = LINUX_BWRAP_SUPERVISOR_CHILD_FD;
7799
+ index <= scrubberChildFd;
7800
+ index += 1
7801
+ ) {
7802
+ const parentFd = stdio[index];
7803
+ if (
7804
+ !Object.hasOwn(stdio, index) ||
7805
+ !Number.isSafeInteger(parentFd) ||
7806
+ parentFd < 0 ||
7807
+ parentFds.has(parentFd)
7808
+ ) {
7809
+ explicitLayout = false;
7810
+ break;
7811
+ }
7812
+ parentFds.add(parentFd);
7813
+ }
7814
+ if (
7815
+ !Number.isInteger(supervisorLaunch?.fd) ||
7816
+ !Number.isInteger(supervisorLaunch?.scrubberFd) ||
7817
+ stdio.length < 5 ||
7818
+ stdio[LINUX_BWRAP_SUPERVISOR_CHILD_FD] !== supervisorLaunch.fd ||
7819
+ stdio[scrubberChildFd] !== supervisorLaunch.scrubberFd ||
7820
+ !explicitLayout
7821
+ ) {
7822
+ throw new Error("descriptor_scrubber_launch_layout_invalid");
6410
7823
  }
7824
+ return buildLinuxBwrapDescriptorScrubbedLaunch({
7825
+ scrubberChildFd,
7826
+ preservedMaxFd: scrubberChildFd - 1,
7827
+ executableChildFd: LINUX_BWRAP_SUPERVISOR_CHILD_FD,
7828
+ executableArgs,
7829
+ });
6411
7830
  }
6412
7831
 
6413
7832
  function attestLinuxBubblewrapCapabilities(runtime, supervisorLaunch) {
6414
7833
  let result;
6415
7834
  try {
6416
- result = runtime.spawnSync(supervisorLaunch.command, ["--help"], {
7835
+ const stdio = [
7836
+ "ignore",
7837
+ "pipe",
7838
+ "pipe",
7839
+ supervisorLaunch.fd,
7840
+ supervisorLaunch.scrubberFd,
7841
+ ];
7842
+ const invocation = linuxBubblewrapScrubbedInvocation(
7843
+ supervisorLaunch,
7844
+ ["--help"],
7845
+ stdio,
7846
+ );
7847
+ result = runtime.spawnSync(invocation.command, invocation.args, {
6417
7848
  shell: false,
6418
7849
  encoding: "utf8",
6419
- stdio: ["ignore", "pipe", "pipe", supervisorLaunch.fd],
7850
+ stdio,
6420
7851
  timeout: 10_000,
6421
7852
  env: {
6422
7853
  PATH: "/usr/bin:/bin",
@@ -6452,18 +7883,65 @@ function attestLinuxBubblewrapCapabilities(runtime, supervisorLaunch) {
6452
7883
  return { ok: true, reason: null };
6453
7884
  }
6454
7885
 
6455
- function parseLinuxLddPaths(output) {
6456
- const paths = new Set();
6457
- for (const line of String(output || "").split(/\r?\n/)) {
7886
+ function parseLinuxLddResolution(output) {
7887
+ const byDestination = new Map();
7888
+ const byLoaderName = new Map();
7889
+ const byBasename = new Map();
7890
+ for (const rawLine of String(output || "").split(/\r?\n/)) {
7891
+ const line = rawLine.trim();
7892
+ if (!line) continue;
6458
7893
  if (line.includes("=> not found")) {
6459
7894
  throw new Error("runtime_dependency_missing");
6460
7895
  }
6461
- const resolved = line.match(/=>\s+(\/[^\s(]+)\s+\(/)?.[1];
6462
- const loader = line.match(/^\s*(\/[^\s(]+)\s+\(/)?.[1];
6463
- if (resolved) paths.add(resolved);
6464
- if (loader) paths.add(loader);
7896
+ if (/^linux-vdso(?:\.so(?:\.\d+)?)?\s+\(/.test(line)) continue;
7897
+ const resolved = line.match(
7898
+ /^([A-Za-z0-9][A-Za-z0-9._+-]{0,254})\s+=>\s+(\/[^\s(]+)\s+\(/,
7899
+ );
7900
+ const direct = line.match(/^(\/[^\s(]+)\s+\(/);
7901
+ if (!resolved && !direct) {
7902
+ throw new Error("runtime_dependency_resolution_output_invalid");
7903
+ }
7904
+ const loaderName = resolved?.[1] || path.posix.basename(direct[1]);
7905
+ const destination = resolved?.[2] || direct[1];
7906
+ if (path.posix.normalize(destination) !== destination) {
7907
+ throw new Error("runtime_dependency_outside_system_library_roots");
7908
+ }
7909
+ if (
7910
+ !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/.test(loaderName) ||
7911
+ path.posix.basename(destination) !== loaderName
7912
+ ) {
7913
+ throw new Error("runtime_dependency_resolution_alias_unsupported");
7914
+ }
7915
+ const priorLoaderDestination = byLoaderName.get(loaderName);
7916
+ if (
7917
+ priorLoaderDestination !== undefined &&
7918
+ priorLoaderDestination !== destination
7919
+ ) {
7920
+ throw new Error("runtime_dependency_resolution_ambiguous");
7921
+ }
7922
+ const basename = path.posix.basename(destination);
7923
+ const priorBasenameDestination = byBasename.get(basename);
7924
+ if (
7925
+ priorBasenameDestination !== undefined &&
7926
+ priorBasenameDestination !== destination
7927
+ ) {
7928
+ throw new Error("runtime_dependency_resolution_ambiguous");
7929
+ }
7930
+ byLoaderName.set(loaderName, destination);
7931
+ byBasename.set(basename, destination);
7932
+ const existing = byDestination.get(destination) || new Set();
7933
+ existing.add(loaderName);
7934
+ byDestination.set(destination, existing);
7935
+ }
7936
+ if (byDestination.size === 0) {
7937
+ throw new Error("runtime_dependency_resolution_empty");
6465
7938
  }
6466
- return [...paths];
7939
+ return [...byDestination.entries()]
7940
+ .map(([destination, loaderNames]) => ({
7941
+ destination,
7942
+ loaderNames: [...loaderNames].sort(),
7943
+ }))
7944
+ .sort((left, right) => left.destination.localeCompare(right.destination));
6467
7945
  }
6468
7946
 
6469
7947
  function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
@@ -6509,12 +7987,15 @@ function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
6509
7987
  {
6510
7988
  source: runtimeIdentity.realPath,
6511
7989
  destination: "/opt/chainless/runtime/node",
7990
+ loaderNames: [],
6512
7991
  },
6513
7992
  ];
6514
- for (const destination of parseLinuxLddPaths(result.stdout)) {
7993
+ for (const { destination, loaderNames } of parseLinuxLddResolution(
7994
+ result.stdout,
7995
+ )) {
6515
7996
  if (
6516
7997
  path.posix.normalize(destination) !== destination ||
6517
- !["/lib/", "/lib64/", "/usr/lib/"].some((prefix) =>
7998
+ !["/lib/", "/lib64/", "/usr/lib/", "/usr/lib64/"].some((prefix) =>
6518
7999
  destination.startsWith(prefix),
6519
8000
  )
6520
8001
  ) {
@@ -6524,14 +8005,18 @@ function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
6524
8005
  if (!source) {
6525
8006
  throw new Error("runtime_dependency_unattested");
6526
8007
  }
6527
- mounts.push({ source, destination });
8008
+ mounts.push({ source, destination, loaderNames });
6528
8009
  }
6529
8010
  if (runtime.fs.existsSync("/etc/ld.so.cache")) {
6530
8011
  const source = attestLinuxRootOwnedFile(runtime, "/etc/ld.so.cache");
6531
8012
  if (!source) {
6532
8013
  throw new Error("loader_cache_unattested");
6533
8014
  }
6534
- mounts.push({ source, destination: "/etc/ld.so.cache" });
8015
+ mounts.push({
8016
+ source,
8017
+ destination: "/etc/ld.so.cache",
8018
+ loaderNames: [],
8019
+ });
6535
8020
  }
6536
8021
  const byDestination = new Map();
6537
8022
  for (const mount of mounts) {
@@ -6539,7 +8024,16 @@ function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
6539
8024
  if (existing && existing.source !== mount.source) {
6540
8025
  throw new Error("runtime_mount_collision");
6541
8026
  }
6542
- byDestination.set(mount.destination, mount);
8027
+ if (existing) {
8028
+ existing.loaderNames = [
8029
+ ...new Set([
8030
+ ...(existing.loaderNames || []),
8031
+ ...(mount.loaderNames || []),
8032
+ ]),
8033
+ ].sort();
8034
+ } else {
8035
+ byDestination.set(mount.destination, mount);
8036
+ }
6543
8037
  }
6544
8038
  return [...byDestination.values()].sort((left, right) =>
6545
8039
  left.destination.localeCompare(right.destination),
@@ -6559,9 +8053,15 @@ function pinLinuxRuntimeMounts(runtime, runtimeMounts, runtimeIdentity) {
6559
8053
  mtimeMs: runtimeIdentity.mtimeMs,
6560
8054
  }
6561
8055
  : runtime.fs.statSync(mount.source);
6562
- mounts.push(
6563
- pinLinuxRegularFile(runtime, mount.source, mount.destination, expected),
6564
- );
8056
+ mounts.push({
8057
+ ...pinLinuxRegularFile(
8058
+ runtime,
8059
+ mount.source,
8060
+ mount.destination,
8061
+ expected,
8062
+ ),
8063
+ loaderNames: Object.freeze([...(mount.loaderNames || [])]),
8064
+ });
6565
8065
  }
6566
8066
  return mounts;
6567
8067
  } catch (error) {
@@ -6570,15 +8070,22 @@ function pinLinuxRuntimeMounts(runtime, runtimeMounts, runtimeIdentity) {
6570
8070
  }
6571
8071
  }
6572
8072
 
6573
- // Bind only the kernel-selected interpreter and the entry ELF's direct
6574
- // DT_NEEDED names. This deliberately does not claim transitive or dlopen-time
6575
- // closure; handleAtomic remains false for the wider launch chain.
6576
- function bindLinuxNativeDynamicRuntime(entryFormat, pinnedRuntimeMounts) {
8073
+ // Bind the kernel-selected interpreter and recursively parse every DT_NEEDED
8074
+ // edge reachable from the entry through the exact root-owned descriptors that
8075
+ // will be mounted into the sandbox. This proves only the loader's initial ELF
8076
+ // graph. It deliberately does not claim runtime dlopen closure; writable
8077
+ // scratch mounts and executable-created code remain outside this evidence.
8078
+ function bindLinuxNativeDynamicRuntime(
8079
+ runtime,
8080
+ entryFormat,
8081
+ pinnedRuntimeMounts,
8082
+ ) {
6577
8083
  if (entryFormat?.runtime !== "native-dynamic-elf") return null;
6578
8084
  if (
6579
8085
  !linuxSystemDynamicPath(entryFormat.interpreter) ||
6580
8086
  !Array.isArray(entryFormat.needed) ||
6581
- entryFormat.needed.length > LINUX_ELF_MAX_NEEDED_ENTRIES
8087
+ entryFormat.needed.length > LINUX_ELF_MAX_NEEDED_ENTRIES ||
8088
+ entryFormat.soname !== null
6582
8089
  ) {
6583
8090
  throw new Error("native_dynamic_metadata_invalid");
6584
8091
  }
@@ -6590,64 +8097,290 @@ function bindLinuxNativeDynamicRuntime(entryFormat, pinnedRuntimeMounts) {
6590
8097
  typeof mount?.fileId?.ino === "string" &&
6591
8098
  Number.isSafeInteger(mount?.bytes) &&
6592
8099
  mount.bytes > 0 &&
6593
- Number.isFinite(mount?.mtimeMs),
8100
+ Number.isFinite(mount?.mtimeMs) &&
8101
+ Array.isArray(mount?.loaderNames) &&
8102
+ mount.loaderNames.every(
8103
+ (name) =>
8104
+ typeof name === "string" &&
8105
+ /^[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/.test(name),
8106
+ ),
6594
8107
  );
8108
+ const resolveDependency = (dependency, { direct = false } = {}) => {
8109
+ if (
8110
+ typeof dependency !== "string" ||
8111
+ !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/.test(dependency) ||
8112
+ path.posix.basename(dependency) !== dependency
8113
+ ) {
8114
+ throw new Error("native_dynamic_dependency_name_invalid");
8115
+ }
8116
+ const matches = systemMounts.filter(
8117
+ (mount) =>
8118
+ path.posix.basename(mount.destination) === dependency &&
8119
+ mount.loaderNames.includes(dependency),
8120
+ );
8121
+ if (matches.length === 0) {
8122
+ throw new Error(
8123
+ direct
8124
+ ? "native_dynamic_dependency_outside_direct_system_set"
8125
+ : "native_dynamic_recursive_dependency_outside_system_graph",
8126
+ );
8127
+ }
8128
+ if (matches.length !== 1) {
8129
+ throw new Error(
8130
+ direct
8131
+ ? "native_dynamic_dependency_runtime_ambiguous"
8132
+ : "native_dynamic_recursive_dependency_runtime_ambiguous",
8133
+ );
8134
+ }
8135
+ return matches[0];
8136
+ };
6595
8137
  const interpreterMount = systemMounts.find(
6596
8138
  (mount) => mount.destination === entryFormat.interpreter,
6597
8139
  );
6598
8140
  if (!interpreterMount) {
6599
8141
  throw new Error("native_dynamic_interpreter_outside_direct_system_set");
6600
8142
  }
6601
- const selected = new Map([[interpreterMount.destination, interpreterMount]]);
8143
+ const selected = new Map();
8144
+ const pending = [];
8145
+ let selectedBytes = 0;
8146
+ const select = (mount) => {
8147
+ if (selected.has(mount.destination)) return;
8148
+ if (selected.size >= LINUX_ELF_MAX_INITIAL_CLOSURE_FILES) {
8149
+ throw new Error("native_dynamic_recursive_file_count_exceeded");
8150
+ }
8151
+ if (
8152
+ !Number.isSafeInteger(mount.bytes) ||
8153
+ mount.bytes <= 0 ||
8154
+ selectedBytes > LINUX_ELF_MAX_INITIAL_CLOSURE_BYTES - mount.bytes
8155
+ ) {
8156
+ throw new Error("native_dynamic_recursive_bytes_exceeded");
8157
+ }
8158
+ selectedBytes += mount.bytes;
8159
+ selected.set(mount.destination, mount);
8160
+ pending.push(mount);
8161
+ };
8162
+ select(interpreterMount);
8163
+ const edges = [];
6602
8164
  for (const dependency of entryFormat.needed) {
8165
+ const target = resolveDependency(dependency, { direct: true });
8166
+ edges.push({ from: "$entry", name: dependency, to: target.destination });
8167
+ select(target);
8168
+ }
8169
+ const parsed = new Map();
8170
+ for (let index = 0; index < pending.length; index += 1) {
8171
+ const mount = pending[index];
8172
+ const metadata = inspectLinuxNativeElf(runtime, mount.fd, mount, {
8173
+ role: "dependency",
8174
+ });
8175
+ if (
8176
+ metadata.soname !== null &&
8177
+ (metadata.soname !== path.posix.basename(mount.destination) ||
8178
+ !mount.loaderNames.includes(metadata.soname))
8179
+ ) {
8180
+ throw new Error("native_dependency_soname_alias_unsupported");
8181
+ }
8182
+ parsed.set(mount.destination, metadata);
8183
+ for (const dependency of metadata.needed || []) {
8184
+ if (edges.length >= LINUX_ELF_MAX_INITIAL_CLOSURE_EDGES) {
8185
+ throw new Error("native_dynamic_recursive_edge_count_exceeded");
8186
+ }
8187
+ const target = resolveDependency(dependency);
8188
+ edges.push({
8189
+ from: mount.destination,
8190
+ name: dependency,
8191
+ to: target.destination,
8192
+ });
8193
+ select(target);
8194
+ }
8195
+ }
8196
+ const members = [...selected.values()]
8197
+ .map((mount) => {
8198
+ const metadata = parsed.get(mount.destination);
8199
+ if (!metadata) {
8200
+ throw new Error("native_dynamic_recursive_member_unparsed");
8201
+ }
8202
+ return {
8203
+ destination: mount.destination,
8204
+ fileId: {
8205
+ dev: mount.fileId.dev,
8206
+ ino: mount.fileId.ino,
8207
+ },
8208
+ bytes: mount.bytes,
8209
+ mtimeMs: mount.mtimeMs,
8210
+ sha256: metadata.sha256,
8211
+ loaderNames: [...mount.loaderNames],
8212
+ needed: [...(metadata.needed || [])].sort(),
8213
+ soname: metadata.soname,
8214
+ };
8215
+ })
8216
+ .sort((left, right) => left.destination.localeCompare(right.destination));
8217
+ const orderedEdges = edges.sort((left, right) =>
8218
+ `${left.from}\0${left.name}\0${left.to}`.localeCompare(
8219
+ `${right.from}\0${right.name}\0${right.to}`,
8220
+ ),
8221
+ );
8222
+ const digest = crypto
8223
+ .createHash("sha256")
8224
+ .update(
8225
+ JSON.stringify({
8226
+ version: 1,
8227
+ scope: LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_SCOPE,
8228
+ mechanism: LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_MECHANISM,
8229
+ interpreter: entryFormat.interpreter,
8230
+ directNeeded: [...entryFormat.needed].sort(),
8231
+ members,
8232
+ edges: orderedEdges,
8233
+ }),
8234
+ )
8235
+ .digest("hex");
8236
+ return Object.freeze({
8237
+ scope: LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_SCOPE,
8238
+ mechanism: LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_MECHANISM,
8239
+ interpreter: entryFormat.interpreter,
8240
+ dependencies: orderedEdges.length,
8241
+ files: members.length,
8242
+ bytes: selectedBytes,
8243
+ digest,
8244
+ });
8245
+ }
8246
+
8247
+ /**
8248
+ * Hash every regular file that will remain pathname-visible to a dynamic
8249
+ * native target. The final namespace is built only from these descriptor
8250
+ * mounts and a read-only synthetic root, so this set is the complete input to
8251
+ * the kernel ELF loader and libc dlopen pathname resolution. Anonymous
8252
+ * executable memory and a malicious program's own ELF loader are intentionally
8253
+ * outside this narrower, enforceable claim.
8254
+ */
8255
+ function buildLinuxRuntimePathnameLoadSet(
8256
+ runtime,
8257
+ pinnedMounts,
8258
+ pluginTreeSnapshot,
8259
+ nativeDynamicClosure,
8260
+ ) {
8261
+ if (
8262
+ nativeDynamicClosure?.scope !==
8263
+ LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_SCOPE ||
8264
+ nativeDynamicClosure?.mechanism !==
8265
+ LINUX_NATIVE_DYNAMIC_RECURSIVE_SYSTEM_GRAPH_MECHANISM ||
8266
+ pluginTreeSnapshot?.scope !== LINUX_PLUGIN_TREE_SNAPSHOT_SCOPE ||
8267
+ !Array.isArray(pinnedMounts) ||
8268
+ pinnedMounts.length < 1 ||
8269
+ pinnedMounts.length > LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_FILES
8270
+ ) {
8271
+ throw new Error("native_runtime_pathname_load_set_invalid");
8272
+ }
8273
+ const destinations = new Set();
8274
+ let totalBytes = 0;
8275
+ const members = pinnedMounts.map((mount) => {
8276
+ const destination = mount?.destination;
8277
+ const pluginFile =
8278
+ typeof destination === "string" &&
8279
+ linuxPathWithin("/opt/chainless/plugin", destination);
8280
+ const runtimeFile = destination === "/opt/chainless/runtime/node";
8281
+ const systemLibrary = linuxSystemDynamicPath(destination);
8282
+ const loaderCache = destination === "/etc/ld.so.cache";
6603
8283
  if (
6604
- typeof dependency !== "string" ||
6605
- !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/.test(dependency)
8284
+ typeof destination !== "string" ||
8285
+ !path.posix.isAbsolute(destination) ||
8286
+ path.posix.normalize(destination) !== destination ||
8287
+ destinations.has(destination) ||
8288
+ (!pluginFile && !runtimeFile && !systemLibrary && !loaderCache) ||
8289
+ !Number.isInteger(mount?.fd)
6606
8290
  ) {
6607
- throw new Error("native_dynamic_dependency_name_invalid");
8291
+ throw new Error("native_runtime_pathname_load_set_member_invalid");
6608
8292
  }
6609
- const matches = systemMounts.filter(
6610
- (mount) => path.posix.basename(mount.destination) === dependency,
6611
- );
6612
- if (matches.length === 0) {
6613
- throw new Error("native_dynamic_dependency_outside_direct_system_set");
8293
+ destinations.add(destination);
8294
+ const before = runtime.fs.fstatSync(mount.fd);
8295
+ const bytes = Number(before.size);
8296
+ if (
8297
+ !before.isFile() ||
8298
+ !Number.isSafeInteger(bytes) ||
8299
+ bytes < 0 ||
8300
+ bytes > LINUX_ATTESTED_FILE_MAX_BYTES ||
8301
+ String(before.dev) !== String(mount.fileId?.dev) ||
8302
+ String(before.ino) !== String(mount.fileId?.ino) ||
8303
+ bytes !== Number(mount.bytes) ||
8304
+ Number(before.mtimeMs) !== Number(mount.mtimeMs) ||
8305
+ totalBytes > LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MAX_BYTES - bytes
8306
+ ) {
8307
+ throw new Error("native_runtime_pathname_load_set_member_changed");
6614
8308
  }
6615
- if (matches.length !== 1) {
6616
- throw new Error("native_dynamic_dependency_runtime_ambiguous");
8309
+ const sha256 = hashLinuxOpenFile(runtime, mount.fd, bytes);
8310
+ const after = runtime.fs.fstatSync(mount.fd);
8311
+ if (
8312
+ !linuxOpenStatMatches(before, after) ||
8313
+ ((pluginFile || runtimeFile) &&
8314
+ (mount.contentSnapshot?.mechanism !== LINUX_ENTRY_SNAPSHOT_MECHANISM ||
8315
+ mount.contentSnapshot?.sha256 !== sha256 ||
8316
+ (runtimeFile &&
8317
+ mount.contentSnapshot?.scope !==
8318
+ LINUX_PLUGIN_RUNTIME_SNAPSHOT_SCOPE)))
8319
+ ) {
8320
+ throw new Error("native_runtime_pathname_load_set_member_changed");
6617
8321
  }
6618
- selected.set(matches[0].destination, matches[0]);
6619
- }
6620
- const members = [...selected.values()]
6621
- .map((mount) => ({
6622
- destination: mount.destination,
8322
+ totalBytes += bytes;
8323
+ return {
8324
+ destination,
8325
+ sourceKind: pluginFile
8326
+ ? "plugin-content-snapshot"
8327
+ : runtimeFile
8328
+ ? "broker-node-runtime"
8329
+ : systemLibrary
8330
+ ? "root-owned-system-library"
8331
+ : "root-owned-loader-cache",
6623
8332
  fileId: {
6624
- dev: mount.fileId.dev,
6625
- ino: mount.fileId.ino,
8333
+ dev: String(after.dev),
8334
+ ino: String(after.ino),
6626
8335
  },
6627
- bytes: mount.bytes,
6628
- mtimeMs: mount.mtimeMs,
6629
- }))
6630
- .sort((left, right) => left.destination.localeCompare(right.destination));
8336
+ sha256,
8337
+ bytes,
8338
+ mtimeMs: Number(after.mtimeMs),
8339
+ mountMode:
8340
+ mount.mountMode === "ro-bind-data" ? "ro-bind-data" : "ro-bind-fd",
8341
+ };
8342
+ });
8343
+ members.sort((left, right) =>
8344
+ left.destination.localeCompare(right.destination),
8345
+ );
8346
+ const loaderCacheVisible = members.some(
8347
+ ({ sourceKind }) => sourceKind === "root-owned-loader-cache",
8348
+ );
8349
+ if (
8350
+ totalBytes < 1 ||
8351
+ !members.some(
8352
+ ({ sourceKind }) => sourceKind === "plugin-content-snapshot",
8353
+ ) ||
8354
+ !members.some(({ sourceKind }) => sourceKind === "broker-node-runtime") ||
8355
+ loaderCacheVisible !== runtime.fs.existsSync("/etc/ld.so.cache")
8356
+ ) {
8357
+ throw new Error("native_runtime_pathname_load_set_incomplete");
8358
+ }
6631
8359
  const digest = crypto
6632
8360
  .createHash("sha256")
6633
8361
  .update(
6634
8362
  JSON.stringify({
6635
8363
  version: 1,
6636
- scope: LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_SCOPE,
6637
- mechanism: LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_MECHANISM,
6638
- interpreter: entryFormat.interpreter,
6639
- needed: [...entryFormat.needed].sort(),
8364
+ scope: LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_SCOPE,
8365
+ mechanism: LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MECHANISM,
8366
+ initialDynamicLoadClosureDigest: nativeDynamicClosure.digest,
8367
+ pluginTreeContentSnapshotDigest: pluginTreeSnapshot.digest,
6640
8368
  members,
6641
8369
  }),
6642
8370
  )
6643
8371
  .digest("hex");
6644
8372
  return Object.freeze({
6645
- scope: LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_SCOPE,
6646
- mechanism: LINUX_NATIVE_DYNAMIC_DIRECT_SYSTEM_SET_MECHANISM,
6647
- interpreter: entryFormat.interpreter,
6648
- dependencies: entryFormat.needed.length,
8373
+ scope: LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_SCOPE,
8374
+ mechanism: LINUX_NATIVE_RUNTIME_PATHNAME_CLOSURE_MECHANISM,
6649
8375
  files: members.length,
8376
+ bytes: totalBytes,
6650
8377
  digest,
8378
+ policyBound: true,
8379
+ writableFilesystems: false,
8380
+ procfsMounted: false,
8381
+ devfsMounted: false,
8382
+ scratchWritable: false,
8383
+ descriptorReopenPaths: false,
6651
8384
  });
6652
8385
  }
6653
8386
 
@@ -6669,6 +8402,7 @@ function buildLinuxBubblewrapPolicyArgs(
6669
8402
  pinnedMounts,
6670
8403
  environment,
6671
8404
  seccompFilter,
8405
+ { runtimePathnameClosure = false } = {},
6672
8406
  ) {
6673
8407
  const directories = new Set([
6674
8408
  "/opt",
@@ -6720,9 +8454,9 @@ function buildLinuxBubblewrapPolicyArgs(
6720
8454
  }
6721
8455
  // FD 3 is also the descriptor-backed executable used to start bubblewrap.
6722
8456
  // Consume and close it during setup, then obscure this staging copy with the
6723
- // later /run tmpfs. The target can still observe bubblewrap itself as the
6724
- // PID-namespace supervisor through procfs; this only contains the inherited
6725
- // launch descriptor and its temporary setup path.
8457
+ // later /run tmpfs. The ordinary profile can still observe bubblewrap itself
8458
+ // as the PID-namespace supervisor through procfs. The dynamic-native runtime
8459
+ // closure profile does not mount procfs at all.
6726
8460
  args.push(
6727
8461
  "--perms",
6728
8462
  "0000",
@@ -6743,40 +8477,56 @@ function buildLinuxBubblewrapPolicyArgs(
6743
8477
  args.push("--ro-bind-fd", String(mount.childFd), mount.destination);
6744
8478
  }
6745
8479
  }
6746
- args.push(
6747
- "--seccomp",
6748
- String(seccompFilter.childFd),
6749
- "--remount-ro",
6750
- "/",
6751
- "--perms",
6752
- "1777",
6753
- "--size",
6754
- String(64 * 1024 * 1024),
6755
- "--tmpfs",
6756
- "/tmp",
6757
- "--perms",
6758
- "0755",
6759
- "--size",
6760
- String(16 * 1024 * 1024),
6761
- "--tmpfs",
6762
- "/run",
6763
- "--perms",
6764
- "1777",
6765
- "--size",
6766
- String(32 * 1024 * 1024),
6767
- "--tmpfs",
6768
- "/var/tmp",
6769
- "--perms",
6770
- "0700",
6771
- "--size",
6772
- String(16 * 1024 * 1024),
6773
- "--tmpfs",
6774
- "/home/sandbox",
6775
- "--proc",
6776
- "/proc",
6777
- "--dev",
6778
- "/dev",
6779
- );
8480
+ args.push("--seccomp", String(seccompFilter.childFd), "--remount-ro", "/");
8481
+ if (runtimePathnameClosure) {
8482
+ // bubblewrap creates its root tmpfs with MS_NOSUID|MS_NODEV, not NOEXEC.
8483
+ // Keeping any writable tmpfs would therefore let an untrusted native write
8484
+ // a new .so and dlopen it. Only an empty /run overlay is retained to hide
8485
+ // the consumed supervisor staging copy, and it is remounted read-only
8486
+ // before the target starts. /tmp, /var/tmp, HOME, procfs, and devfs remain
8487
+ // empty directories on the read-only synthetic root.
8488
+ args.push(
8489
+ "--perms",
8490
+ "0755",
8491
+ "--size",
8492
+ String(16 * 1024 * 1024),
8493
+ "--tmpfs",
8494
+ "/run",
8495
+ "--remount-ro",
8496
+ "/run",
8497
+ );
8498
+ } else {
8499
+ args.push(
8500
+ "--perms",
8501
+ "1777",
8502
+ "--size",
8503
+ String(64 * 1024 * 1024),
8504
+ "--tmpfs",
8505
+ "/tmp",
8506
+ "--perms",
8507
+ "0755",
8508
+ "--size",
8509
+ String(16 * 1024 * 1024),
8510
+ "--tmpfs",
8511
+ "/run",
8512
+ "--perms",
8513
+ "1777",
8514
+ "--size",
8515
+ String(32 * 1024 * 1024),
8516
+ "--tmpfs",
8517
+ "/var/tmp",
8518
+ "--perms",
8519
+ "0700",
8520
+ "--size",
8521
+ String(16 * 1024 * 1024),
8522
+ "--tmpfs",
8523
+ "/home/sandbox",
8524
+ "--proc",
8525
+ "/proc",
8526
+ "--dev",
8527
+ "/dev",
8528
+ );
8529
+ }
6780
8530
  for (const key of Object.keys(environment).sort()) {
6781
8531
  args.push("--setenv", key, environment[key]);
6782
8532
  }
@@ -6886,25 +8636,28 @@ function probeLinuxBubblewrapPolicy(
6886
8636
  // These pipes connect the host Broker to bwrap and are created before
6887
8637
  // bwrap installs the target seccomp filter. The nested runtime probe above
6888
8638
  // inherits a regular-file descriptor and never requests UV_CREATE_PIPE.
6889
- result = runtime.spawnSync(
6890
- supervisorLaunch.command,
8639
+ const stdio = linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
8640
+ probe: true,
8641
+ supervisorFd: supervisorLaunch.fd,
8642
+ scrubberFd: supervisorLaunch.scrubberFd,
8643
+ });
8644
+ const invocation = linuxBubblewrapScrubbedInvocation(
8645
+ supervisorLaunch,
6891
8646
  [...policyArgs, "--", "/opt/chainless/runtime/node", "-e", probeSource],
6892
- {
6893
- cwd: "/",
6894
- shell: false,
6895
- encoding: "utf8",
6896
- stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
6897
- probe: true,
6898
- supervisorFd: supervisorLaunch.fd,
6899
- }),
6900
- timeout: 15_000,
6901
- env: {
6902
- PATH: "/usr/bin:/bin",
6903
- LANG: "C",
6904
- LC_ALL: "C",
6905
- },
6906
- },
8647
+ stdio,
6907
8648
  );
8649
+ result = runtime.spawnSync(invocation.command, invocation.args, {
8650
+ cwd: "/",
8651
+ shell: false,
8652
+ encoding: "utf8",
8653
+ stdio,
8654
+ timeout: 15_000,
8655
+ env: {
8656
+ PATH: "/usr/bin:/bin",
8657
+ LANG: "C",
8658
+ LC_ALL: "C",
8659
+ },
8660
+ });
6908
8661
  } catch {
6909
8662
  return linuxBubblewrapProbe(
6910
8663
  true,
@@ -6948,7 +8701,99 @@ function probeLinuxBubblewrapPolicy(
6948
8701
  supervisorBinding,
6949
8702
  null,
6950
8703
  null,
8704
+ null,
8705
+ true,
8706
+ );
8707
+ }
8708
+
8709
+ function probeLinuxRuntimePathnameClosurePolicy(
8710
+ runtime,
8711
+ supervisorLaunch,
8712
+ supervisorBinding,
8713
+ policyArgs,
8714
+ pinnedDescriptors,
8715
+ targetRuntime,
8716
+ contentSnapshot = null,
8717
+ ) {
8718
+ const probeSource = [
8719
+ 'const fs = require("node:fs");',
8720
+ `const sentinel = ${JSON.stringify(
8721
+ LINUX_BWRAP_NATIVE_RUNTIME_CLOSURE_PROBE_SENTINEL,
8722
+ )};`,
8723
+ "const writableCandidates = [",
8724
+ ' "/unapproved-runtime.so", "/tmp/unapproved-runtime.so",',
8725
+ ' "/run/unapproved-runtime.so", "/var/tmp/unapproved-runtime.so",',
8726
+ ' "/home/sandbox/unapproved-runtime.so",',
8727
+ ' "/opt/chainless/plugin/unapproved-runtime.so",',
8728
+ "];",
8729
+ "for (const candidate of writableCandidates) {",
8730
+ " try {",
8731
+ ' fs.writeFileSync(candidate, "not-approved");',
8732
+ " process.exit(71);",
8733
+ " } catch {}",
8734
+ "}",
8735
+ "const absentPaths = [",
8736
+ ' "/proc/self/exe", "/proc/1/exe", "/proc/self/fd/0",',
8737
+ ' "/dev/null", "/dev/fd/0",',
8738
+ ` ${JSON.stringify(LINUX_BWRAP_SUPERVISOR_HIDDEN_PATH)},`,
8739
+ "];",
8740
+ "for (const candidate of absentPaths) {",
8741
+ " try {",
8742
+ " fs.lstatSync(candidate);",
8743
+ " process.exit(72);",
8744
+ " } catch (error) {",
8745
+ ' if (error?.code !== "ENOENT") process.exit(73);',
8746
+ " }",
8747
+ "}",
8748
+ 'if (fs.readdirSync("/proc").length !== 0) process.exit(74);',
8749
+ 'if (fs.readdirSync("/dev").length !== 0) process.exit(75);',
8750
+ "process.stdout.write(sentinel);",
8751
+ ].join("\n");
8752
+ let result;
8753
+ try {
8754
+ const stdio = linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
8755
+ probe: true,
8756
+ supervisorFd: supervisorLaunch.fd,
8757
+ scrubberFd: supervisorLaunch.scrubberFd,
8758
+ });
8759
+ const invocation = linuxBubblewrapScrubbedInvocation(
8760
+ supervisorLaunch,
8761
+ [...policyArgs, "--", "/opt/chainless/runtime/node", "-e", probeSource],
8762
+ stdio,
8763
+ );
8764
+ result = runtime.spawnSync(invocation.command, invocation.args, {
8765
+ cwd: "/",
8766
+ shell: false,
8767
+ encoding: "utf8",
8768
+ stdio,
8769
+ timeout: 15_000,
8770
+ env: {
8771
+ PATH: "/usr/bin:/bin",
8772
+ LANG: "C",
8773
+ LC_ALL: "C",
8774
+ },
8775
+ });
8776
+ } catch {
8777
+ return linuxBubblewrapProbe(
8778
+ true,
8779
+ false,
8780
+ "runtime_pathname_closure_probe_spawn_failed",
8781
+ targetRuntime,
8782
+ contentSnapshot,
8783
+ supervisorBinding,
8784
+ );
8785
+ }
8786
+ const runnable =
8787
+ !result?.error &&
8788
+ result?.status === 0 &&
8789
+ String(result.stdout) === LINUX_BWRAP_NATIVE_RUNTIME_CLOSURE_PROBE_SENTINEL;
8790
+ return linuxBubblewrapProbe(
6951
8791
  true,
8792
+ runnable,
8793
+ runnable ? null : "runtime_pathname_closure_probe_failed",
8794
+ targetRuntime,
8795
+ contentSnapshot,
8796
+ supervisorBinding,
6952
8797
  );
6953
8798
  }
6954
8799
 
@@ -6979,7 +8824,7 @@ export function applyLinuxSandbox(
6979
8824
  profile: sandboxOpts.profileName || "default",
6980
8825
  backend: "linux-prlimit",
6981
8826
  command,
6982
- args,
8827
+ args: invocation.inputArgs,
6983
8828
  options: { ...(spawnOpts || {}) },
6984
8829
  };
6985
8830
  if (runtime.platform !== "linux") {
@@ -7106,6 +8951,7 @@ export function applyLinuxSandbox(
7106
8951
  let nativeDynamicClosure = null;
7107
8952
  try {
7108
8953
  nativeDynamicClosure = bindLinuxNativeDynamicRuntime(
8954
+ runtime,
7109
8955
  validation.entryFormat,
7110
8956
  pinnedRuntimeMounts,
7111
8957
  );
@@ -7119,7 +8965,7 @@ export function applyLinuxSandbox(
7119
8965
  runtimeProbe: linuxBubblewrapProbe(
7120
8966
  false,
7121
8967
  false,
7122
- error.message || "native_dynamic_direct_system_set_unattested",
8968
+ error.message || "native_dynamic_recursive_system_graph_unattested",
7123
8969
  validation.entryRuntime,
7124
8970
  ),
7125
8971
  reason: "linux_bwrap_native_runtime_unattested",
@@ -7199,6 +9045,7 @@ export function applyLinuxSandbox(
7199
9045
  }
7200
9046
 
7201
9047
  let entrySnapshot = null;
9048
+ let runtimeFileSnapshot = null;
7202
9049
  let pluginTreeSnapshot = null;
7203
9050
  const pluginTreeFileSnapshots = [];
7204
9051
  let probePinnedMounts = pinnedMounts;
@@ -7278,6 +9125,24 @@ export function applyLinuxSandbox(
7278
9125
  finalMounts[index] = fileSnapshot.finalMount;
7279
9126
  probeMounts[index] = fileSnapshot.probeMount;
7280
9127
  }
9128
+ let rawRuntimeMount = null;
9129
+ if (validation.entryRuntime === "native-dynamic-elf") {
9130
+ const runtimeIndex = pinnedMounts.findIndex(
9131
+ (mount) => mount.destination === "/opt/chainless/runtime/node",
9132
+ );
9133
+ if (runtimeIndex < 0) {
9134
+ throw new Error("native_runtime_snapshot_mount_missing");
9135
+ }
9136
+ rawRuntimeMount = pinnedMounts[runtimeIndex];
9137
+ runtimeFileSnapshot = createLinuxRegularFileSnapshot(
9138
+ runtime,
9139
+ rawRuntimeMount,
9140
+ validation.contract.runtimeIdentity,
9141
+ linuxPluginRuntimeSnapshotContract(),
9142
+ );
9143
+ finalMounts[runtimeIndex] = runtimeFileSnapshot.finalMount;
9144
+ probeMounts[runtimeIndex] = runtimeFileSnapshot.probeMount;
9145
+ }
7281
9146
  pluginTreeSnapshot = buildLinuxPluginTreeSnapshotAttestation(
7282
9147
  finalMounts,
7283
9148
  sandboxEntry,
@@ -7286,12 +9151,14 @@ export function applyLinuxSandbox(
7286
9151
  pinnedMounts = finalMounts;
7287
9152
  probePinnedMounts = probeMounts;
7288
9153
  entryMount = entrySnapshot.finalMount;
7289
- closeLinuxPinnedMounts(runtime, rawPluginMounts);
9154
+ closeLinuxPinnedMounts(runtime, [...rawPluginMounts, rawRuntimeMount]);
7290
9155
  } catch (error) {
7291
9156
  closeLinuxPinnedMounts(runtime, pinnedMounts);
7292
9157
  closeLinuxPinnedMounts(runtime, [
7293
9158
  entrySnapshot?.probeMount,
7294
9159
  entrySnapshot?.finalMount,
9160
+ runtimeFileSnapshot?.probeMount,
9161
+ runtimeFileSnapshot?.finalMount,
7295
9162
  ...pluginTreeFileSnapshots.flatMap((snapshot) => [
7296
9163
  snapshot.probeMount,
7297
9164
  snapshot.finalMount,
@@ -7313,14 +9180,53 @@ export function applyLinuxSandbox(
7313
9180
  });
7314
9181
  }
7315
9182
 
9183
+ let runtimePathnameLoadSet = null;
9184
+ if (validation.entryRuntime === "native-dynamic-elf") {
9185
+ try {
9186
+ runtimePathnameLoadSet = buildLinuxRuntimePathnameLoadSet(
9187
+ runtime,
9188
+ pinnedMounts,
9189
+ pluginTreeSnapshot,
9190
+ nativeDynamicClosure,
9191
+ );
9192
+ } catch (error) {
9193
+ closeLinuxPinnedMounts(runtime, pinnedMounts);
9194
+ closeLinuxPinnedMounts(runtime, [
9195
+ entrySnapshot?.probeMount,
9196
+ runtimeFileSnapshot?.probeMount,
9197
+ ...pluginTreeFileSnapshots.map((snapshot) => snapshot.probeMount),
9198
+ ]);
9199
+ return createSandboxPlan({
9200
+ ...base,
9201
+ backend: null,
9202
+ candidateBackend: LINUX_BWRAP_BACKEND,
9203
+ policyAttested: false,
9204
+ runtimeProbe: linuxBubblewrapProbe(
9205
+ false,
9206
+ false,
9207
+ error.message || "native_runtime_pathname_load_set_unattested",
9208
+ validation.entryRuntime,
9209
+ ),
9210
+ reason: "linux_bwrap_native_runtime_load_set_unattested",
9211
+ guarantees: [],
9212
+ });
9213
+ }
9214
+ }
9215
+
7316
9216
  let probeSeccompFilter;
7317
9217
  let seccompFilter;
7318
9218
  const pluginTreeProbeMounts = pluginTreeFileSnapshots.map(
7319
9219
  (snapshot) => snapshot.probeMount,
7320
9220
  );
7321
9221
  try {
7322
- probeSeccompFilter = pinLinuxNetworkSeccompFilter(runtime);
7323
- seccompFilter = pinLinuxNetworkSeccompFilter(runtime);
9222
+ const runtimePathnameClosure =
9223
+ validation.entryRuntime === "native-dynamic-elf";
9224
+ probeSeccompFilter = pinLinuxNetworkSeccompFilter(runtime, {
9225
+ runtimePathnameClosure,
9226
+ });
9227
+ seccompFilter = pinLinuxNetworkSeccompFilter(runtime, {
9228
+ runtimePathnameClosure,
9229
+ });
7324
9230
  const seccompChildFd = LINUX_BWRAP_FIRST_MOUNT_FD + pinnedMounts.length;
7325
9231
  probeSeccompFilter.childFd = seccompChildFd;
7326
9232
  seccompFilter.childFd = seccompChildFd;
@@ -7333,6 +9239,7 @@ export function applyLinuxSandbox(
7333
9239
  runtime,
7334
9240
  [
7335
9241
  entrySnapshot?.probeMount,
9242
+ runtimeFileSnapshot?.probeMount,
7336
9243
  ...pluginTreeProbeMounts,
7337
9244
  probeSeccompFilter,
7338
9245
  seccompFilter,
@@ -7355,12 +9262,15 @@ export function applyLinuxSandbox(
7355
9262
  }
7356
9263
  const pinnedDescriptors = [...pinnedMounts, seccompFilter];
7357
9264
  const probeDescriptors = [...probePinnedMounts, probeSeccompFilter];
9265
+ let descriptorScrubberPin;
7358
9266
  const closeStrongLinuxResources = (...extra) => {
7359
9267
  closeLinuxPinnedMounts(runtime, [
7360
9268
  ...pinnedDescriptors,
7361
9269
  probeSeccompFilter,
7362
9270
  entrySnapshot?.probeMount,
9271
+ runtimeFileSnapshot?.probeMount,
7363
9272
  ...pluginTreeProbeMounts,
9273
+ descriptorScrubberPin,
7364
9274
  ...extra,
7365
9275
  ]);
7366
9276
  };
@@ -7370,9 +9280,11 @@ export function applyLinuxSandbox(
7370
9280
  let finalLaunch;
7371
9281
  try {
7372
9282
  supervisorPin = pinLinuxBubblewrapSupervisor(runtime);
9283
+ descriptorScrubberPin = pinLinuxBubblewrapDescriptorScrubber(runtime);
7373
9284
  capabilityLaunch = openLinuxBubblewrapSupervisorLaunch(
7374
9285
  runtime,
7375
9286
  supervisorPin,
9287
+ descriptorScrubberPin,
7376
9288
  );
7377
9289
  } catch (error) {
7378
9290
  closeStrongLinuxResources(supervisorPin);
@@ -7417,7 +9329,11 @@ export function applyLinuxSandbox(
7417
9329
  });
7418
9330
  }
7419
9331
  try {
7420
- probeLaunch = openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin);
9332
+ probeLaunch = openLinuxBubblewrapSupervisorLaunch(
9333
+ runtime,
9334
+ supervisorPin,
9335
+ descriptorScrubberPin,
9336
+ );
7421
9337
  } catch (error) {
7422
9338
  closeStrongLinuxResources(supervisorPin, probeLaunch);
7423
9339
  return createSandboxPlan({
@@ -7438,15 +9354,56 @@ export function applyLinuxSandbox(
7438
9354
  });
7439
9355
  }
7440
9356
  const environment = linuxSandboxEnvironment();
9357
+ const runtimePathnameClosure =
9358
+ validation.entryRuntime === "native-dynamic-elf" &&
9359
+ runtimePathnameLoadSet !== null;
7441
9360
  const policyArgs = buildLinuxBubblewrapPolicyArgs(
7442
9361
  pluginTree.directories,
7443
9362
  pinnedMounts,
7444
9363
  environment,
7445
9364
  seccompFilter,
9365
+ { runtimePathnameClosure },
9366
+ );
9367
+ const probePolicyArgs = buildLinuxBubblewrapPolicyArgs(
9368
+ pluginTree.directories,
9369
+ probePinnedMounts,
9370
+ environment,
9371
+ probeSeccompFilter,
9372
+ { runtimePathnameClosure },
7446
9373
  );
9374
+ // The closure probe must exercise the exact ordered mount policy later
9375
+ // used for the untrusted target. Only the backing parent descriptors,
9376
+ // stdio, and command differ between the two launches; their child FD
9377
+ // numbers and every bwrap policy option are identical.
9378
+ if (
9379
+ runtimePathnameClosure &&
9380
+ JSON.stringify(probePolicyArgs) !== JSON.stringify(policyArgs)
9381
+ ) {
9382
+ closeStrongLinuxResources(supervisorPin, probeLaunch);
9383
+ return createSandboxPlan({
9384
+ ...base,
9385
+ backend: null,
9386
+ candidateBackend: LINUX_BWRAP_BACKEND,
9387
+ policyAttested: false,
9388
+ runtimeProbe: linuxBubblewrapProbe(
9389
+ true,
9390
+ false,
9391
+ "runtime_pathname_closure_probe_policy_mismatch",
9392
+ validation.entryRuntime,
9393
+ null,
9394
+ supervisorPin.attestation,
9395
+ ),
9396
+ reason: "linux_bwrap_policy_probe_failed",
9397
+ guarantees: [],
9398
+ });
9399
+ }
7447
9400
  const targetArgs = validation.entryRuntime.startsWith("native-")
7448
- ? [sandboxEntry, ...args]
7449
- : ["/opt/chainless/runtime/node", sandboxEntry, ...args.slice(1)];
9401
+ ? [sandboxEntry, ...validation.argsSnapshot]
9402
+ : [
9403
+ "/opt/chainless/runtime/node",
9404
+ sandboxEntry,
9405
+ ...validation.argsSnapshot.slice(1),
9406
+ ];
7450
9407
  const snapshotPolicyBinding = entrySnapshot
7451
9408
  ? {
7452
9409
  scope: entrySnapshot.attestation.scope,
@@ -7462,13 +9419,23 @@ export function applyLinuxSandbox(
7462
9419
  targetMode: entrySnapshot.attestation.targetMode,
7463
9420
  }
7464
9421
  : null;
9422
+ const descriptorScrubberPolicy = linuxBwrapDescriptorScrubberPolicyBinding(
9423
+ descriptorScrubberPin.attestation,
9424
+ {
9425
+ scrubberChildFd: LINUX_BWRAP_FIRST_MOUNT_FD + pinnedDescriptors.length,
9426
+ preservedMaxFd:
9427
+ LINUX_BWRAP_FIRST_MOUNT_FD + pinnedDescriptors.length - 1,
9428
+ executableChildFd: LINUX_BWRAP_SUPERVISOR_CHILD_FD,
9429
+ },
9430
+ );
7465
9431
  const policyDigest = crypto
7466
9432
  .createHash("sha256")
7467
9433
  .update(
7468
9434
  JSON.stringify({
7469
- version: 4,
9435
+ version: 6,
7470
9436
  backend: LINUX_BWRAP_BACKEND,
7471
9437
  supervisor: supervisorPin.attestation,
9438
+ descriptorScrubber: descriptorScrubberPolicy,
7472
9439
  contract: {
7473
9440
  kind: validation.contract.kind,
7474
9441
  pluginRoot: validation.contract.pluginRoot,
@@ -7510,6 +9477,9 @@ export function applyLinuxSandbox(
7510
9477
  ...(nativeDynamicClosure
7511
9478
  ? { initialDynamicLoadClosure: nativeDynamicClosure }
7512
9479
  : {}),
9480
+ ...(runtimePathnameLoadSet
9481
+ ? { runtimeSharedLibraryPathnameLoadSet: runtimePathnameLoadSet }
9482
+ : {}),
7513
9483
  pluginTreeContentSnapshot: pluginTreeSnapshot,
7514
9484
  seccomp: {
7515
9485
  arch: seccompFilter.arch,
@@ -7524,19 +9494,30 @@ export function applyLinuxSandbox(
7524
9494
  )
7525
9495
  .digest("hex");
7526
9496
  const policyProbe = Object.freeze(
7527
- probeLinuxBubblewrapPolicy(
7528
- runtime,
7529
- probeLaunch,
7530
- supervisorPin.attestation,
7531
- policyArgs,
7532
- probeDescriptors,
7533
- validation.entryRuntime,
7534
- ),
9497
+ runtimePathnameClosure
9498
+ ? probeLinuxRuntimePathnameClosurePolicy(
9499
+ runtime,
9500
+ probeLaunch,
9501
+ supervisorPin.attestation,
9502
+ probePolicyArgs,
9503
+ probeDescriptors,
9504
+ validation.entryRuntime,
9505
+ entrySnapshot?.attestation,
9506
+ )
9507
+ : probeLinuxBubblewrapPolicy(
9508
+ runtime,
9509
+ probeLaunch,
9510
+ supervisorPin.attestation,
9511
+ probePolicyArgs,
9512
+ probeDescriptors,
9513
+ validation.entryRuntime,
9514
+ ),
7535
9515
  );
7536
9516
  closeLinuxPinnedMounts(runtime, [
7537
9517
  probeLaunch,
7538
9518
  probeSeccompFilter,
7539
9519
  entrySnapshot?.probeMount,
9520
+ runtimeFileSnapshot?.probeMount,
7540
9521
  ...pluginTreeProbeMounts,
7541
9522
  ]);
7542
9523
  probeLaunch = null;
@@ -7562,7 +9543,13 @@ export function applyLinuxSandbox(
7562
9543
  sandboxOpts.sync,
7563
9544
  { sealedEntry: entrySnapshot !== null },
7564
9545
  );
7565
- if (!finalValidation.ok) {
9546
+ const launchArgumentsChanged =
9547
+ finalValidation.ok &&
9548
+ (finalValidation.argsSnapshot.length !== validation.argsSnapshot.length ||
9549
+ finalValidation.argsSnapshot.some(
9550
+ (value, index) => value !== validation.argsSnapshot[index],
9551
+ ));
9552
+ if (!finalValidation.ok || launchArgumentsChanged) {
7566
9553
  closeStrongLinuxResources(supervisorPin, finalLaunch);
7567
9554
  return createSandboxPlan({
7568
9555
  ...base,
@@ -7573,7 +9560,11 @@ export function applyLinuxSandbox(
7573
9560
  runtimeProbe: linuxBubblewrapProbe(
7574
9561
  true,
7575
9562
  false,
7576
- `post_probe_${finalValidation.reason}`,
9563
+ `post_probe_${
9564
+ finalValidation.ok
9565
+ ? "launch_arguments_changed"
9566
+ : finalValidation.reason
9567
+ }`,
7577
9568
  validation.entryRuntime,
7578
9569
  null,
7579
9570
  supervisorPin.attestation,
@@ -7583,6 +9574,14 @@ export function applyLinuxSandbox(
7583
9574
  });
7584
9575
  }
7585
9576
  try {
9577
+ if (runtimeFileSnapshot) {
9578
+ attestLinuxRegularFileSnapshot(
9579
+ runtime,
9580
+ runtimeFileSnapshot.finalMount,
9581
+ runtimeFileSnapshot.attestation,
9582
+ linuxPluginRuntimeSnapshotContract(),
9583
+ );
9584
+ }
7586
9585
  attestLinuxPluginTreeSnapshot(
7587
9586
  runtime,
7588
9587
  pinnedMounts,
@@ -7612,6 +9611,11 @@ export function applyLinuxSandbox(
7612
9611
  }
7613
9612
  try {
7614
9613
  attestLinuxBubblewrapSupervisorPin(runtime, supervisorPin);
9614
+ attestLinuxBubblewrapSupervisorPin(
9615
+ runtime,
9616
+ descriptorScrubberPin,
9617
+ "descriptor_scrubber",
9618
+ );
7615
9619
  } catch (error) {
7616
9620
  closeStrongLinuxResources(supervisorPin, finalLaunch);
7617
9621
  return createSandboxPlan({
@@ -7633,7 +9637,11 @@ export function applyLinuxSandbox(
7633
9637
  });
7634
9638
  }
7635
9639
  try {
7636
- finalLaunch = openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin);
9640
+ finalLaunch = openLinuxBubblewrapSupervisorLaunch(
9641
+ runtime,
9642
+ supervisorPin,
9643
+ descriptorScrubberPin,
9644
+ );
7637
9645
  } catch (error) {
7638
9646
  closeStrongLinuxResources(supervisorPin, finalLaunch);
7639
9647
  return createSandboxPlan({
@@ -7655,8 +9663,9 @@ export function applyLinuxSandbox(
7655
9663
  });
7656
9664
  }
7657
9665
  const supervisorBinding = supervisorPin.attestation;
7658
- closeLinuxPinnedMounts(runtime, [supervisorPin]);
9666
+ closeLinuxPinnedMounts(runtime, [supervisorPin, descriptorScrubberPin]);
7659
9667
  supervisorPin = null;
9668
+ descriptorScrubberPin = null;
7660
9669
  const capsuleContract =
7661
9670
  validation.contract.kind === MCP_STDIO_CAPSULE_SANDBOX_CONTRACT_KIND;
7662
9671
  const baseRuntimeProbe = linuxBubblewrapProbe(
@@ -7668,12 +9677,17 @@ export function applyLinuxSandbox(
7668
9677
  supervisorBinding,
7669
9678
  pluginTreeSnapshot,
7670
9679
  nativeDynamicClosure,
9680
+ runtimePathnameLoadSet,
7671
9681
  policyProbe.runtimeDetachedChildSpawnVerified === true,
7672
9682
  );
9683
+ const descriptorBoundRuntimeProbe = {
9684
+ ...baseRuntimeProbe,
9685
+ descriptorScrubber: descriptorScrubberPolicy,
9686
+ };
7673
9687
  const runtimeProbe = Object.freeze(
7674
9688
  capsuleContract
7675
9689
  ? {
7676
- ...baseRuntimeProbe,
9690
+ ...descriptorBoundRuntimeProbe,
7677
9691
  mcpCapsuleCodeSnapshot: true,
7678
9692
  entrySnapshotAtomic: true,
7679
9693
  runtimeLaunchAtomic: true,
@@ -7687,51 +9701,81 @@ export function applyLinuxSandbox(
7687
9701
  runtimeLaunchPath: "/opt/chainless/runtime/node",
7688
9702
  entrySnapshotPath: sandboxEntry,
7689
9703
  }
7690
- : baseRuntimeProbe,
9704
+ : descriptorBoundRuntimeProbe,
7691
9705
  );
7692
- const options = {
7693
- ...spawnOpts,
7694
- cwd: "/",
7695
- shell: false,
7696
- detached: false,
7697
- env: {
7698
- PATH: "/usr/bin:/bin",
7699
- LANG: "C",
7700
- LC_ALL: "C",
7701
- },
7702
- stdio: Object.freeze(
7703
- linuxStdioWithPinnedMounts(spawnOpts?.stdio, pinnedDescriptors, {
7704
- supervisorFd: finalLaunch.fd,
7705
- }),
7706
- ),
7707
- };
7708
9706
  let pinsClosed = false;
7709
9707
  const cleanup = () => {
7710
9708
  if (pinsClosed) return;
7711
9709
  pinsClosed = true;
7712
9710
  closeLinuxPinnedMounts(runtime, [...pinnedDescriptors, finalLaunch]);
7713
9711
  };
7714
- return createSandboxPlan({
7715
- ...base,
7716
- applied: true,
7717
- enforcement: LINUX_BWRAP_BACKEND,
7718
- backend: LINUX_BWRAP_BACKEND,
7719
- candidateBackend: null,
7720
- policyAttested: true,
7721
- policyDigest,
7722
- runtimeProbe,
7723
- reason: null,
7724
- guarantees: [
7725
- SANDBOX_BOUNDARIES.FILESYSTEM,
7726
- SANDBOX_BOUNDARIES.NETWORK,
7727
- SANDBOX_BOUNDARIES.PROCESS_TREE,
7728
- ...(capsuleContract ? [SANDBOX_BOUNDARIES.CODE_SNAPSHOT] : []),
7729
- ],
7730
- command: finalLaunch.command,
7731
- args: [...policyArgs, "--", ...targetArgs],
7732
- options,
7733
- cleanup,
7734
- });
9712
+ try {
9713
+ const stdio = linuxStdioWithPinnedMounts(
9714
+ spawnOpts?.stdio,
9715
+ pinnedDescriptors,
9716
+ {
9717
+ supervisorFd: finalLaunch.fd,
9718
+ scrubberFd: finalLaunch.scrubberFd,
9719
+ },
9720
+ );
9721
+ const invocation = linuxBubblewrapScrubbedInvocation(
9722
+ finalLaunch,
9723
+ [...policyArgs, "--", ...targetArgs],
9724
+ stdio,
9725
+ );
9726
+ const options = {
9727
+ ...spawnOpts,
9728
+ cwd: "/",
9729
+ shell: false,
9730
+ detached: false,
9731
+ env: {
9732
+ PATH: "/usr/bin:/bin",
9733
+ LANG: "C",
9734
+ LC_ALL: "C",
9735
+ },
9736
+ stdio: Object.freeze(stdio),
9737
+ };
9738
+ return createSandboxPlan({
9739
+ ...base,
9740
+ applied: true,
9741
+ enforcement: LINUX_BWRAP_BACKEND,
9742
+ backend: LINUX_BWRAP_BACKEND,
9743
+ candidateBackend: null,
9744
+ policyAttested: true,
9745
+ policyDigest,
9746
+ runtimeProbe,
9747
+ reason: null,
9748
+ guarantees: [
9749
+ SANDBOX_BOUNDARIES.FILESYSTEM,
9750
+ SANDBOX_BOUNDARIES.NETWORK,
9751
+ SANDBOX_BOUNDARIES.PROCESS_TREE,
9752
+ ...(capsuleContract ? [SANDBOX_BOUNDARIES.CODE_SNAPSHOT] : []),
9753
+ ],
9754
+ command: invocation.command,
9755
+ args: invocation.args,
9756
+ options,
9757
+ cleanup,
9758
+ });
9759
+ } catch (error) {
9760
+ cleanup();
9761
+ return createSandboxPlan({
9762
+ ...base,
9763
+ backend: null,
9764
+ candidateBackend: LINUX_BWRAP_BACKEND,
9765
+ policyAttested: false,
9766
+ policyDigest,
9767
+ runtimeProbe: linuxBubblewrapProbe(
9768
+ true,
9769
+ false,
9770
+ `post_probe_${error.message || "final_plan_construction_failed"}`,
9771
+ validation.entryRuntime,
9772
+ null,
9773
+ supervisorBinding,
9774
+ ),
9775
+ reason: "linux_bwrap_execution_contract_changed",
9776
+ guarantees: [],
9777
+ });
9778
+ }
7735
9779
  }
7736
9780
 
7737
9781
  const prlimitParts = [];
@@ -7864,7 +9908,14 @@ export function applySandbox(
7864
9908
 
7865
9909
  // Dispatch to platform handler
7866
9910
  if (runtime.platform === "darwin") {
7867
- return applyMacSandbox(command, args, spawnOpts, profile, runtimeOverrides);
9911
+ return applyMacSandbox(
9912
+ command,
9913
+ args,
9914
+ spawnOpts,
9915
+ profile,
9916
+ runtimeOverrides,
9917
+ runtimeInjected ? null : MACOS_MCP_CODE_SNAPSHOT_ISSUER,
9918
+ );
7868
9919
  }
7869
9920
  if (runtime.platform === "win32") {
7870
9921
  return applyWindowsSandbox(