chainlesschain 0.162.182 → 0.162.183

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 (176) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DzZ5_xoj.js → AIOps-BapS7lz4.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DeLXddIg.js → ActionButton-CXYdXEUI.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-rvDCmet1.js → Analytics-CKR5Lexn.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Yt6QVDnp.js → AppLayout-DATVUOXB.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BS112K5L.js → Artifacts-vb_0ZlV1.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-0wJuQrNu.js → Audit-BI1l9GWt.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-B90PMgbn.js → BackgroundAgents-BFXbaUEf.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-TkgMglQQ.js → Backup-DEYBJJ6H.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CQVS9p1l.js → BaseInput-COdzeO0M.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-YTjQHoC7.js → Chat-oSN6sfM5.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-CI_jT0ru.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-5bwaOabq.js → Checkbox-BezEa4tm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BFmNQ_Xe.js → Codegen-CNEL0Ife.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-BT_u4Dzp.js → Col-Bf2Y2Bn1.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CJP52u5b.js → Community-BfUlu4Hc.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BpzhJE9i.js → Compact-BN4xWP9e.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Cg7rfslp.js → Compliance-Bt6BKmEP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-JisxL6Yy.js → Cowork-DKLp8YxO.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-Bu7Ug_MC.js → Cron-o7Hhw6ky.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-CvuqoLdj.js → Crosschain-DaNXWJIw.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-C2u9G8Pu.js → DID-B-Qa2mha.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-BFt5Yv3d.js → Dashboard-CMX0Qnxw.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DIaT9Wxq.js → Dropdown-BLuLyRu_.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D_A601Y4.js → EmailListRenderer-CFBAeMij.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard--IyQ8I8w.js → FamilyGuardDashboard-DWzXQ67V.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BihWHxuY.js → Federation-DB1QSLgr.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-BB8dOXc_.js → FormItemContext-C-OKCToP.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DQYhj1HH.js → GenericCardRenderer-DMJFtFZf.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-B-2QZxGf.js → Git-DsGSujOV.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CusSb-nG.js → Governance-B0iS9NEW.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BAjmvPPL.js → Inference-CH004H7c.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DoNYWKYq.js → KnowledgeGraph-Q9MTQ83c.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-B8fKWD-n.js → Logs-B_fyjZK9.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BlJm1J7E.js → MarkdownRenderer-C0o4FtpD.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-CyOscbSL.js → Marketplace-BIStF9d1.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-Ex6Om5da.js → McpTools-pGfal768.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-CDcD8l8r.js → Memory-xbNqpIqB.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-xfBqoFDg.js → MobileBridge-B5EQGrIK.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-VTc3gT1y.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-CIWd36OV.js → Mtc-a_rmfxJH.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CPX0ouuA.js → MtcAudit-BmKuofUr.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-BDJsIUuW.js → Multisig-BQbJ-CAN.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DEkbuD3N.js → NLProgramming-JtPJKX25.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-DJpHRhH1.js → Notes-ZFKznTSG.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C1UZ-Qd5.js → NotificationSettings-B6aHRBBR.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-GlJtqLjT.js → OrderTableRenderer-DdcsOqEz.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-Bbhj0fac.js → Organization-BFDTAUgS.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-1H04x3kx.js → Overflow-0FoTfbWH.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CDpPe3et.js → P2P-CWd90etw.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-MUX0Bpvw.js → PdhVaultBrowser-DtCGC-W-.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-A07-0Ci1.js → Permissions-CvQ7nr7T.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DhhDWxAZ.js → PersonalDataHub-rdNPcR-Z.js} +5 -5
  54. package/src/assets/web-panel/assets/{Pipeline-CyfbJfe-.js → Pipeline-CmrfiNOs.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-CNFps3ED.js → Privacy-D7ep1h_t.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit--T-u3Dr5.js → ProjectInit-C0xKINHS.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DJmrK2fC.js → ProjectSettings-AOmZFp-F.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BWHSkXZP.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-DLzzbOZd.js → Providers-Cb7JCTPL.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CexOomKS.js → QrScannerModal-BDMehcXC.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-QklGi19_.js → QuickAsk-BF6_1nvx.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Bb9kwSw8.js → Recommend-BHfT-A28.js} +1 -1
  63. package/src/assets/web-panel/assets/RemoteSession-B6EEf8hh.js +5 -0
  64. package/src/assets/web-panel/assets/{Reputation-Dr5cLP0N.js → Reputation-CsWHLEcj.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-oCQHYv5t.js → Row-D6srnWhm.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-IFDEykIR.js → RssFeed-Xhr-KyVd.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-CspVIxDc.js → Search-vIbIJvwL.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-Bm66o_DH.js → Security-Bzi5Aiko.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-DvanNqXl.js → Services-BHtVX52P.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-CHjEe9_O.js → Skeleton-C0R8B2jf.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-Cb8jWjMX.js → Skills-DtT6rWOX.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DxZm1491.js → Sla-0B37mxVU.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-DWxEEclC.js → SpeechSettings-0V2jE1Bs.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-FIvOFw5p.js → SyncSettings-CYedRzaj.js} +2 -2
  75. package/src/assets/web-panel/assets/Tasks-D-R1x8vm.js +1 -0
  76. package/src/assets/web-panel/assets/{Templates-CaNjzV_t.js → Templates-BKoHztGT.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DjlP6TmH.js → Tenant--17nwVNZ.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-Dnkos7vW.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-DoZ8y7y8.js → TimelineRenderer-C5jx4AE3.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-390cg2TC.js → Tokens-DYJKYa9t.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-CLrtCj5_.js → Trigger-DChpWiQ9.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DpESfSPn.js → Trust-DfWG52EA.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CN6279O0.js → UkeySign-CMugYcyA.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DPtAUlSe.js → VideoEditing-BOSmrHAd.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-nKOMV_1n.js → Wallet-D5g3k3Sa.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-HuXlSOc4.js → WebAuthn-D-7u4Mkc.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-CUfFjARi.js → WorkflowEditor-CFzZMSUJ.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-6Eq0PcZJ.js → chat-VbR6l6mc.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BP_Sf6Iy.js → colors-BSZn8EDS.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-BufMKIh5.js → compact-item-y74obMEq.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BBMXxWSW.js → createContext-C326bjex.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-D3Bho_6u.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-fUK_tRO2.js → hasIn-D_INJQT_.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-LvCBfTLo.js → index-4PIeTM--.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CgiECHu2.js → index-4eyH-eC2.js} +1 -1
  96. package/src/assets/web-panel/assets/index-5iAFwYx1.js +1 -0
  97. package/src/assets/web-panel/assets/{index-DLpIs_P_.js → index-9PIVuXqo.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-cpEli44X.js → index-B7SrC-On.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-6GmBa4Kg.js → index-BJi3UqBt.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-D9vU25ij.js → index-BY8-nCgs.js} +1 -1
  101. package/src/assets/web-panel/assets/index-BfnB7LPx.js +1 -0
  102. package/src/assets/web-panel/assets/{index-B2vS66eI.js → index-Bfw2hf2s.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DVsqvI4M.js → index-BjcWKg1S.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C44atv4k.js → index-BnSfnSMP.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BWKoBU8R.js → index-Bnua8IX3.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-GwdRUZ-x.js → index-C-ZCaUJc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-pum7HYBA.js → index-C2NyllnW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-B9WYSn4m.js → index-C327pLbw.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bkg4ZePE.js → index-CMHqs3Jb.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DmkMXwnR.js → index-CfLJ42kv.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Dyngbd29.js → index-Cr3uuz0C.js} +19 -19
  112. package/src/assets/web-panel/assets/{index-t8N1tSeX.js → index-CsovTpx9.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DocdRcbd.js → index-CzE53HiB.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-fzaMtSJ8.js → index-DBkzrpiL.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CE2DdTP2.js → index-DNMxIsDg.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-wKTLbKyc.js → index-DUU2606o.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-uCbEN7cY.js → index-DdyjrCAu.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CZ6Yt17L.js → index-DkH1JvEt.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CwSf5pnf.js → index-Dlxh9D8D.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-3FuwBewR.js → index-DuIkfnTF.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CW_G-MXw.js → index-DwHSZLXg.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BHy6yNiF.js → index-E_v1ZCTP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-wV6iGdpY.js → index-FQBsuExX.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DASluSqY.js → index-GA80ir_u.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMLHv389.js → index-IZAWt0H8.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B7Etk0Te.js → index-Iw0FfqUz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D4_MuGkd.js → index-KF9Zmqln.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Cp8vhyZy.js → index-abmn_8Yz.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-2A5ph8b2.js → index-gINyI1rh.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CeASi4fr.js → index-jYaogFE8.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CGjmILDm.js → index-nSn2-C4N.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DPdUNk1i.js → index-qMA4GYdQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-V5Fo95FM.js → initDefaultProps-DI0Zaf8-.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BYWU0hUT.js → motion-BZDKSy3Z.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Cr1M-J2h.js → move-DsIW01Vp.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-BysQKi2N.js → mtc-parser-CohkDhsf.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-D1zHU8GC.js → omit-DpiN5rzg.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-CUArxtfi.js → pickAttrs-qbuqni3y.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-ChoYEys0.js → placementArrow-mBky6714.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BQRs3Mo7.js → responsiveObserve-Bas3eQ2K.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B8zbsCkg.js → slide-CBx_SKoK.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-C2U8Gs6h.js → statusUtils-BlYguJSV.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-Dd8MgL_V.js → styleChecker-BCYQmRGv.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-hknqY5NH.js → useFlexGapSupport-Dp5dybLM.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CLTAmcfw.js → useFs-DHAMBgA2.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-ByegHbiB.js → usePersonalDataHub-W6jsSw76.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DqlAy5DP.js → vnode-BkI6aayc.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DiXiztQn.js → zoom-C_IMGueq.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +9 -0
  151. package/src/commands/background-session.js +49 -0
  152. package/src/commands/plugin.js +235 -23
  153. package/src/commands/session.js +13 -0
  154. package/src/data/changelog.json +13 -0
  155. package/src/harness/jsonl-session-store.js +6 -4
  156. package/src/lib/agent-team/team-runner.js +19 -1
  157. package/src/lib/background-agent-supervisor.js +49 -0
  158. package/src/lib/plugin-runtime/install.js +226 -7
  159. package/src/lib/plugin-runtime/scopes.js +13 -2
  160. package/src/lib/plugin-usage-attribution.js +44 -0
  161. package/src/lib/process-execution-broker/index.js +252 -1
  162. package/src/lib/process-execution-broker/platform-sandbox.js +1251 -101
  163. package/src/lib/session-usage.js +77 -0
  164. package/src/repl/agent-repl.js +6 -6
  165. package/src/runtime/headless-runner.js +9 -1
  166. package/src/runtime/headless-stream.js +20 -1
  167. package/src/runtime/mcp-config.js +15 -5
  168. package/src/assets/web-panel/assets/ChatBubbleRenderer-PR074hUN.js +0 -1
  169. package/src/assets/web-panel/assets/MobileProjects-BwObMMA2.js +0 -1
  170. package/src/assets/web-panel/assets/Projects-Qx_MdX9k.js +0 -1
  171. package/src/assets/web-panel/assets/RemoteSession-DZuVNNN5.js +0 -5
  172. package/src/assets/web-panel/assets/Tasks-CyuLnDFR.js +0 -1
  173. package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +0 -3
  174. package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +0 -1
  175. package/src/assets/web-panel/assets/index-DOZTSoS7.js +0 -1
  176. package/src/assets/web-panel/assets/index-DhpvjpqI.js +0 -1
@@ -146,9 +146,24 @@ const LINUX_LDD_PATH = "/usr/bin/ldd";
146
146
  const LINUX_BWRAP_BACKEND = "linux-bwrap";
147
147
  const LINUX_BWRAP_NODE_PROBE_SENTINEL = "chainless-linux-bwrap-plugin-node-v1";
148
148
  const LINUX_BWRAP_MAX_PLUGIN_ENTRIES = 512;
149
- const LINUX_BWRAP_FIRST_MOUNT_FD = 3;
149
+ const LINUX_PLUGIN_TREE_SNAPSHOT_MAX_FILES = 256;
150
+ const LINUX_PLUGIN_TREE_SNAPSHOT_MAX_BYTES = 256 * 1024 * 1024;
151
+ const LINUX_BWRAP_SUPERVISOR_CHILD_FD = 3;
152
+ const LINUX_BWRAP_FIRST_MOUNT_FD = 4;
153
+ const LINUX_BWRAP_SUPERVISOR_HIDDEN_PATH = "/run/.chainless-bwrap-supervisor";
154
+ const LINUX_BWRAP_SUPERVISOR_BINDING_MECHANISM =
155
+ "pinned-child-fd3-file-consume-run-overmount-v1";
150
156
  const LINUX_ATTESTED_FILE_MAX_BYTES = 256 * 1024 * 1024;
151
157
  const LINUX_ATTESTATION_HASH_CHUNK_BYTES = 1024 * 1024;
158
+ const LINUX_ENTRY_SNAPSHOT_MECHANISM =
159
+ "verified-o_tmpfile-copy-bwrap-ro-bind-data-v1";
160
+ const LINUX_ENTRY_SNAPSHOT_SOURCE_MODE = 0o400;
161
+ const LINUX_NODE_ENTRY_SNAPSHOT_SCOPE = "plugin-entry-source";
162
+ const LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE = "plugin-entry-executable";
163
+ const LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE = "all-pinned-plugin-regular-files";
164
+ const LINUX_NODE_PLUGIN_TREE_SNAPSHOT_CONSISTENCY = "per-file-pin-to-launch";
165
+ const LINUX_NODE_ENTRY_SNAPSHOT_TARGET_MODE = "0400";
166
+ const LINUX_NATIVE_ENTRY_SNAPSHOT_TARGET_MODE = "0500";
152
167
  const LINUX_ELF64_HEADER_BYTES = 64;
153
168
  const LINUX_ELF64_PROGRAM_HEADER_BYTES = 56;
154
169
  const LINUX_ELF64_DYNAMIC_ENTRY_BYTES = 16;
@@ -2582,8 +2597,40 @@ function linuxBubblewrapProbe(
2582
2597
  runnable,
2583
2598
  reason,
2584
2599
  targetRuntime = "node",
2600
+ contentSnapshot = null,
2601
+ supervisorBinding = null,
2602
+ pluginTreeSnapshot = null,
2585
2603
  ) {
2586
2604
  const native = targetRuntime === "native-static-elf";
2605
+ const expectedSnapshotScope = native
2606
+ ? LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE
2607
+ : LINUX_NODE_ENTRY_SNAPSHOT_SCOPE;
2608
+ const snapshot =
2609
+ contentSnapshot?.scope === expectedSnapshotScope &&
2610
+ contentSnapshot?.mechanism === LINUX_ENTRY_SNAPSHOT_MECHANISM;
2611
+ const supervisorDescriptorBound =
2612
+ supervisorBinding?.mechanism === LINUX_BWRAP_SUPERVISOR_BINDING_MECHANISM;
2613
+ const pluginTreeContentSnapshot =
2614
+ !native &&
2615
+ attempted &&
2616
+ snapshot &&
2617
+ runnable &&
2618
+ reason === null &&
2619
+ supervisorDescriptorBound &&
2620
+ pluginTreeSnapshot?.scope === LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE &&
2621
+ pluginTreeSnapshot?.mechanism === LINUX_ENTRY_SNAPSHOT_MECHANISM &&
2622
+ Number.isSafeInteger(pluginTreeSnapshot?.files) &&
2623
+ pluginTreeSnapshot.files > 0 &&
2624
+ pluginTreeSnapshot.files <= LINUX_PLUGIN_TREE_SNAPSHOT_MAX_FILES &&
2625
+ Number.isSafeInteger(pluginTreeSnapshot?.bytes) &&
2626
+ pluginTreeSnapshot.bytes >= 0 &&
2627
+ pluginTreeSnapshot.bytes <= LINUX_PLUGIN_TREE_SNAPSHOT_MAX_BYTES &&
2628
+ typeof pluginTreeSnapshot?.digest === "string" &&
2629
+ /^[a-f0-9]{64}$/.test(pluginTreeSnapshot.digest) &&
2630
+ pluginTreeSnapshot?.consistency ===
2631
+ LINUX_NODE_PLUGIN_TREE_SNAPSHOT_CONSISTENCY &&
2632
+ pluginTreeSnapshot?.contractBound === false &&
2633
+ pluginTreeSnapshot?.atomic === false;
2587
2634
  return {
2588
2635
  kind: native
2589
2636
  ? "linux-bwrap-plugin-native-static-elf-policy-v1"
@@ -2593,7 +2640,50 @@ function linuxBubblewrapProbe(
2593
2640
  reason,
2594
2641
  probeRuntime: "node",
2595
2642
  targetRuntime,
2596
- contentSnapshot: false,
2643
+ contentSnapshot: snapshot,
2644
+ ...(snapshot
2645
+ ? {
2646
+ contentSnapshotScope: contentSnapshot.scope,
2647
+ contentSnapshotMechanism: contentSnapshot.mechanism,
2648
+ }
2649
+ : {}),
2650
+ ...(pluginTreeContentSnapshot
2651
+ ? {
2652
+ pluginTreeContentSnapshot: true,
2653
+ pluginTreeContentSnapshotScope: pluginTreeSnapshot.scope,
2654
+ pluginTreeContentSnapshotMechanism: pluginTreeSnapshot.mechanism,
2655
+ pluginTreeContentSnapshotFiles: pluginTreeSnapshot.files,
2656
+ pluginTreeContentSnapshotBytes: pluginTreeSnapshot.bytes,
2657
+ pluginTreeContentSnapshotDigest: pluginTreeSnapshot.digest,
2658
+ pluginTreeSnapshotConsistency: pluginTreeSnapshot.consistency,
2659
+ pluginTreeSnapshotContractBound: pluginTreeSnapshot.contractBound,
2660
+ pluginTreeSnapshotAtomic: pluginTreeSnapshot.atomic,
2661
+ }
2662
+ : {}),
2663
+ supervisorDescriptorBound,
2664
+ ...(supervisorDescriptorBound
2665
+ ? {
2666
+ supervisorExecutablePinned: true,
2667
+ supervisorBindingScope: supervisorBinding.scope,
2668
+ supervisorDescriptorBindingMechanism: supervisorBinding.mechanism,
2669
+ supervisorDescriptorContained: runnable,
2670
+ supervisorDescriptorConsumedBeforeTarget: runnable,
2671
+ supervisorStagingPathHidden: runnable,
2672
+ supervisorTemporaryCopyObscured: runnable,
2673
+ supervisorPid1ExecutableExposure:
2674
+ supervisorBinding.pid1ExecutableExposure,
2675
+ supervisorExecutableIdentity: {
2676
+ path: supervisorBinding.path,
2677
+ fileId: supervisorBinding.fileId,
2678
+ sha256: supervisorBinding.sha256,
2679
+ bytes: supervisorBinding.bytes,
2680
+ mtimeMs: supervisorBinding.mtimeMs,
2681
+ mode: supervisorBinding.mode,
2682
+ uid: supervisorBinding.uid,
2683
+ gid: supervisorBinding.gid,
2684
+ },
2685
+ }
2686
+ : {}),
2597
2687
  handleAtomic: false,
2598
2688
  };
2599
2689
  }
@@ -2836,7 +2926,7 @@ function assignLinuxMountChildFds(mounts) {
2836
2926
  function linuxStdioWithPinnedMounts(
2837
2927
  stdio,
2838
2928
  descriptors,
2839
- { probe = false } = {},
2929
+ { probe = false, supervisorFd = null } = {},
2840
2930
  ) {
2841
2931
  let standard;
2842
2932
  if (probe) {
@@ -2849,8 +2939,10 @@ function linuxStdioWithPinnedMounts(
2849
2939
  standard = ["pipe", "pipe", "pipe"];
2850
2940
  }
2851
2941
  while (standard.length < 3) standard.push(undefined);
2942
+ const supervisor = Number.isInteger(supervisorFd) ? [supervisorFd] : [];
2852
2943
  return [
2853
2944
  ...standard.slice(0, 3),
2945
+ ...supervisor,
2854
2946
  ...descriptors.map((descriptor) => descriptor.fd),
2855
2947
  ];
2856
2948
  }
@@ -2862,10 +2954,13 @@ function validateLinuxPluginContract(
2862
2954
  contract,
2863
2955
  runtime,
2864
2956
  sync,
2957
+ { sealedEntry = false } = {},
2865
2958
  ) {
2866
2959
  const nodeContract = contract?.kind === "strict-plugin-node-bin";
2867
2960
  const nativeContract =
2868
2961
  contract?.kind === "strict-plugin-native-static-elf-bin";
2962
+ const entryIsSealed =
2963
+ (nodeContract || nativeContract) && sealedEntry === true;
2869
2964
  if (
2870
2965
  !contract ||
2871
2966
  contract.contractVersion !== 1 ||
@@ -2973,9 +3068,10 @@ function validateLinuxPluginContract(
2973
3068
  }
2974
3069
  if (
2975
3070
  !linuxDirectoryIdentityMatches(runtime, contract.rootIdentity) ||
2976
- !linuxIdentityMatches(runtime, contract.entryIdentity, {
2977
- executable: nativeContract,
2978
- }) ||
3071
+ (!entryIsSealed &&
3072
+ !linuxIdentityMatches(runtime, contract.entryIdentity, {
3073
+ executable: nativeContract,
3074
+ })) ||
2979
3075
  !linuxIdentityMatches(runtime, contract.runtimeIdentity, {
2980
3076
  executable: true,
2981
3077
  })
@@ -2983,7 +3079,7 @@ function validateLinuxPluginContract(
2983
3079
  return { ok: false, reason: "execution_identity_changed" };
2984
3080
  }
2985
3081
  let entryFormat = null;
2986
- if (nativeContract) {
3082
+ if (nativeContract && !entryIsSealed) {
2987
3083
  try {
2988
3084
  entryFormat = inspectLinuxStaticNativePath(
2989
3085
  runtime,
@@ -3073,6 +3169,500 @@ function hashLinuxOpenFile(runtime, fd, bytes) {
3073
3169
  return digest.digest("hex");
3074
3170
  }
3075
3171
 
3172
+ function linuxEntrySnapshotContract(targetRuntime) {
3173
+ if (targetRuntime === "native-static-elf") {
3174
+ return {
3175
+ scope: LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE,
3176
+ targetMode: LINUX_NATIVE_ENTRY_SNAPSHOT_TARGET_MODE,
3177
+ minimumBytes: LINUX_ELF64_HEADER_BYTES,
3178
+ errorPrefix: "native_entry_snapshot",
3179
+ };
3180
+ }
3181
+ if (targetRuntime === "node") {
3182
+ return {
3183
+ scope: LINUX_NODE_ENTRY_SNAPSHOT_SCOPE,
3184
+ targetMode: LINUX_NODE_ENTRY_SNAPSHOT_TARGET_MODE,
3185
+ minimumBytes: 0,
3186
+ errorPrefix: "node_entry_snapshot",
3187
+ };
3188
+ }
3189
+ throw new Error("entry_snapshot_runtime_unsupported");
3190
+ }
3191
+
3192
+ function linuxNodePluginTreeFileSnapshotContract(sourceMode) {
3193
+ if (!Number.isSafeInteger(sourceMode) || sourceMode < 0) {
3194
+ throw new Error("node_plugin_tree_snapshot_source_mode_invalid");
3195
+ }
3196
+ return {
3197
+ scope: LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE,
3198
+ targetMode: (sourceMode & 0o111) !== 0 ? "0500" : "0400",
3199
+ minimumBytes: 0,
3200
+ errorPrefix: "node_plugin_tree_snapshot",
3201
+ };
3202
+ }
3203
+
3204
+ function createLinuxRegularFileSnapshot(
3205
+ runtime,
3206
+ entryMount,
3207
+ entryIdentity,
3208
+ snapshotContract,
3209
+ ) {
3210
+ let writerFd;
3211
+ let probeFd;
3212
+ let finalFd;
3213
+ try {
3214
+ if (
3215
+ !snapshotContract ||
3216
+ typeof snapshotContract.scope !== "string" ||
3217
+ typeof snapshotContract.targetMode !== "string" ||
3218
+ !Number.isSafeInteger(snapshotContract.minimumBytes) ||
3219
+ typeof snapshotContract.errorPrefix !== "string"
3220
+ ) {
3221
+ throw new Error("file_snapshot_contract_invalid");
3222
+ }
3223
+ const snapshotError = (reason) =>
3224
+ new Error(`${snapshotContract.errorPrefix}_${reason}`);
3225
+ const expectedSha256 = entryIdentity?.sha256 ?? null;
3226
+ for (const method of [
3227
+ "openSync",
3228
+ "readSync",
3229
+ "writeSync",
3230
+ "fchmodSync",
3231
+ "fsyncSync",
3232
+ "fstatSync",
3233
+ "closeSync",
3234
+ ]) {
3235
+ if (typeof runtime.fs[method] !== "function") {
3236
+ throw snapshotError(`${method}_unavailable`);
3237
+ }
3238
+ }
3239
+ if (
3240
+ !Number.isInteger(entryMount?.fd) ||
3241
+ !entryIdentity ||
3242
+ (expectedSha256 !== null &&
3243
+ (typeof expectedSha256 !== "string" ||
3244
+ !/^[a-f0-9]{64}$/.test(expectedSha256))) ||
3245
+ !Number.isSafeInteger(entryIdentity.bytes) ||
3246
+ entryIdentity.bytes < snapshotContract.minimumBytes ||
3247
+ entryIdentity.bytes > LINUX_ATTESTED_FILE_MAX_BYTES
3248
+ ) {
3249
+ throw snapshotError("identity_invalid");
3250
+ }
3251
+
3252
+ const sourceBefore = runtime.fs.fstatSync(entryMount.fd);
3253
+ const sourceFileMode = Number(sourceBefore.mode) & 0o7777;
3254
+ if (
3255
+ !sourceBefore.isFile() ||
3256
+ Number(sourceBefore.nlink) !== 1 ||
3257
+ String(sourceBefore.dev) !== String(entryIdentity.fileId?.dev) ||
3258
+ String(sourceBefore.ino) !== String(entryIdentity.fileId?.ino) ||
3259
+ Number(sourceBefore.size) !== entryIdentity.bytes ||
3260
+ Number(sourceBefore.mtimeMs) !== Number(entryIdentity.mtimeMs)
3261
+ ) {
3262
+ throw snapshotError("source_changed");
3263
+ }
3264
+ if (
3265
+ snapshotContract.scope === LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE &&
3266
+ snapshotContract.targetMode !==
3267
+ linuxNodePluginTreeFileSnapshotContract(sourceFileMode).targetMode
3268
+ ) {
3269
+ throw snapshotError("source_mode_changed");
3270
+ }
3271
+ if (
3272
+ snapshotContract.scope === LINUX_NATIVE_ENTRY_SNAPSHOT_SCOPE &&
3273
+ (sourceFileMode & 0o111) === 0
3274
+ ) {
3275
+ throw snapshotError("source_mode_changed");
3276
+ }
3277
+
3278
+ const constants = runtime.fs.constants || fs.constants;
3279
+ const writerFlags =
3280
+ Number(constants.O_RDWR) |
3281
+ Number(constants.O_EXCL) |
3282
+ Number(constants.O_TMPFILE ?? LINUX_O_TMPFILE) |
3283
+ Number(constants.O_NOFOLLOW || 0) |
3284
+ Number(constants.O_NONBLOCK || 0);
3285
+ writerFd = runtime.fs.openSync(
3286
+ "/tmp",
3287
+ writerFlags,
3288
+ LINUX_ENTRY_SNAPSHOT_SOURCE_MODE,
3289
+ );
3290
+
3291
+ const sourceDigest = crypto.createHash("sha256");
3292
+ const chunk = Buffer.allocUnsafe(
3293
+ Math.max(
3294
+ 1,
3295
+ Math.min(LINUX_ATTESTATION_HASH_CHUNK_BYTES, entryIdentity.bytes),
3296
+ ),
3297
+ );
3298
+ let copied = 0;
3299
+ while (copied < entryIdentity.bytes) {
3300
+ const read = runtime.fs.readSync(
3301
+ entryMount.fd,
3302
+ chunk,
3303
+ 0,
3304
+ Math.min(chunk.length, entryIdentity.bytes - copied),
3305
+ copied,
3306
+ );
3307
+ if (read <= 0) {
3308
+ throw snapshotError("source_ended_early");
3309
+ }
3310
+ sourceDigest.update(chunk.subarray(0, read));
3311
+ let written = 0;
3312
+ while (written < read) {
3313
+ const count = runtime.fs.writeSync(
3314
+ writerFd,
3315
+ chunk,
3316
+ written,
3317
+ read - written,
3318
+ copied + written,
3319
+ );
3320
+ if (count <= 0) {
3321
+ throw snapshotError("write_failed");
3322
+ }
3323
+ written += count;
3324
+ }
3325
+ copied += read;
3326
+ }
3327
+ const sourceAfter = runtime.fs.fstatSync(entryMount.fd);
3328
+ const sourceSha256 = sourceDigest.digest("hex");
3329
+ if (
3330
+ !linuxOpenStatMatches(sourceBefore, sourceAfter) ||
3331
+ (expectedSha256 !== null && sourceSha256 !== expectedSha256)
3332
+ ) {
3333
+ throw snapshotError("source_changed");
3334
+ }
3335
+
3336
+ runtime.fs.fsyncSync(writerFd);
3337
+ const snapshotBeforeRead = runtime.fs.fstatSync(writerFd);
3338
+ if (
3339
+ !snapshotBeforeRead.isFile() ||
3340
+ Number(snapshotBeforeRead.nlink) !== 0 ||
3341
+ Number(snapshotBeforeRead.size) !== entryIdentity.bytes
3342
+ ) {
3343
+ throw snapshotError("identity_changed");
3344
+ }
3345
+ const snapshotSha256 = hashLinuxOpenFile(
3346
+ runtime,
3347
+ writerFd,
3348
+ entryIdentity.bytes,
3349
+ );
3350
+ const snapshotAfterRead = runtime.fs.fstatSync(writerFd);
3351
+ if (
3352
+ !linuxOpenStatMatches(snapshotBeforeRead, snapshotAfterRead) ||
3353
+ snapshotSha256 !== sourceSha256
3354
+ ) {
3355
+ throw snapshotError("identity_changed");
3356
+ }
3357
+
3358
+ runtime.fs.fchmodSync(writerFd, LINUX_ENTRY_SNAPSHOT_SOURCE_MODE);
3359
+ runtime.fs.fsyncSync(writerFd);
3360
+ const sealedBeforeRead = runtime.fs.fstatSync(writerFd);
3361
+ if (
3362
+ !sealedBeforeRead.isFile() ||
3363
+ Number(sealedBeforeRead.nlink) !== 0 ||
3364
+ Number(sealedBeforeRead.size) !== entryIdentity.bytes ||
3365
+ (Number(sealedBeforeRead.mode) & 0o777) !==
3366
+ LINUX_ENTRY_SNAPSHOT_SOURCE_MODE
3367
+ ) {
3368
+ throw snapshotError("readonly_mode_unattested");
3369
+ }
3370
+ const sealedSha256 = hashLinuxOpenFile(
3371
+ runtime,
3372
+ writerFd,
3373
+ entryIdentity.bytes,
3374
+ );
3375
+ const sealed = runtime.fs.fstatSync(writerFd);
3376
+ if (
3377
+ !linuxOpenStatMatches(sealedBeforeRead, sealed) ||
3378
+ sealedSha256 !== sourceSha256
3379
+ ) {
3380
+ throw snapshotError("identity_changed");
3381
+ }
3382
+
3383
+ const readerFlags =
3384
+ Number(constants.O_RDONLY) | Number(constants.O_NONBLOCK || 0);
3385
+ probeFd = runtime.fs.openSync(`/proc/self/fd/${writerFd}`, readerFlags);
3386
+ finalFd = runtime.fs.openSync(`/proc/self/fd/${writerFd}`, readerFlags);
3387
+ if (probeFd === finalFd) {
3388
+ throw snapshotError("readers_not_independent");
3389
+ }
3390
+ for (const fd of [probeFd, finalFd]) {
3391
+ const reader = runtime.fs.fstatSync(fd);
3392
+ if (!linuxOpenStatMatches(sealed, reader)) {
3393
+ throw snapshotError("reader_identity_changed");
3394
+ }
3395
+ }
3396
+ const ownedWriterFd = writerFd;
3397
+ writerFd = undefined;
3398
+ runtime.fs.closeSync(ownedWriterFd);
3399
+
3400
+ const verifyClosedWriterSnapshot = runtime.fs.fstatSync(finalFd);
3401
+ if (!linuxOpenStatMatches(sealed, verifyClosedWriterSnapshot)) {
3402
+ throw snapshotError("reader_identity_changed");
3403
+ }
3404
+
3405
+ const attestation = Object.freeze({
3406
+ scope: snapshotContract.scope,
3407
+ mechanism: LINUX_ENTRY_SNAPSHOT_MECHANISM,
3408
+ sha256: sourceSha256,
3409
+ bytes: entryIdentity.bytes,
3410
+ sourceFileId: Object.freeze({
3411
+ dev: String(entryIdentity.fileId.dev),
3412
+ ino: String(entryIdentity.fileId.ino),
3413
+ }),
3414
+ sourceMtimeMs: Number(entryIdentity.mtimeMs),
3415
+ sourceFileMode: sourceFileMode.toString(8).padStart(4, "0"),
3416
+ sourceMode: LINUX_ENTRY_SNAPSHOT_SOURCE_MODE.toString(8).padStart(4, "0"),
3417
+ targetMode: snapshotContract.targetMode,
3418
+ });
3419
+ const descriptor = (fd) => {
3420
+ const stat = runtime.fs.fstatSync(fd);
3421
+ return {
3422
+ ...entryMount,
3423
+ fd,
3424
+ fileId: {
3425
+ dev: String(stat.dev),
3426
+ ino: String(stat.ino),
3427
+ },
3428
+ bytes: Number(stat.size),
3429
+ mtimeMs: Number(stat.mtimeMs),
3430
+ mountMode: "ro-bind-data",
3431
+ mountPermissions: snapshotContract.targetMode,
3432
+ contentSnapshot: attestation,
3433
+ snapshotIdentity: Object.freeze({
3434
+ sha256: sourceSha256,
3435
+ bytes: entryIdentity.bytes,
3436
+ fileId: Object.freeze({
3437
+ dev: String(stat.dev),
3438
+ ino: String(stat.ino),
3439
+ }),
3440
+ mtimeMs: Number(stat.mtimeMs),
3441
+ }),
3442
+ };
3443
+ };
3444
+ const result = {
3445
+ attestation,
3446
+ probeMount: descriptor(probeFd),
3447
+ finalMount: descriptor(finalFd),
3448
+ };
3449
+ probeFd = undefined;
3450
+ finalFd = undefined;
3451
+ return result;
3452
+ } catch (error) {
3453
+ closeLinuxPinnedMounts(
3454
+ runtime,
3455
+ [probeFd, finalFd, writerFd]
3456
+ .filter(Number.isInteger)
3457
+ .map((fd) => ({ fd })),
3458
+ );
3459
+ throw error;
3460
+ }
3461
+ }
3462
+
3463
+ function createLinuxEntrySnapshot(
3464
+ runtime,
3465
+ entryMount,
3466
+ entryIdentity,
3467
+ targetRuntime,
3468
+ ) {
3469
+ return createLinuxRegularFileSnapshot(
3470
+ runtime,
3471
+ entryMount,
3472
+ entryIdentity,
3473
+ linuxEntrySnapshotContract(targetRuntime),
3474
+ );
3475
+ }
3476
+
3477
+ function attestLinuxRegularFileSnapshot(
3478
+ runtime,
3479
+ mount,
3480
+ attestation,
3481
+ snapshotContract,
3482
+ ) {
3483
+ const errorPrefix = snapshotContract.errorPrefix;
3484
+ const before = runtime.fs.fstatSync(mount?.fd);
3485
+ if (
3486
+ attestation?.scope !== snapshotContract.scope ||
3487
+ attestation?.mechanism !== LINUX_ENTRY_SNAPSHOT_MECHANISM ||
3488
+ attestation?.sourceMode !==
3489
+ LINUX_ENTRY_SNAPSHOT_SOURCE_MODE.toString(8).padStart(4, "0") ||
3490
+ typeof attestation?.sourceFileMode !== "string" ||
3491
+ !/^[0-7]{4}$/.test(attestation.sourceFileMode) ||
3492
+ attestation?.targetMode !== snapshotContract.targetMode ||
3493
+ mount?.mountMode !== "ro-bind-data" ||
3494
+ mount?.mountPermissions !== snapshotContract.targetMode ||
3495
+ mount?.contentSnapshot !== attestation ||
3496
+ !before.isFile() ||
3497
+ Number(before.nlink) !== 0 ||
3498
+ (Number(before.mode) & 0o777) !== LINUX_ENTRY_SNAPSHOT_SOURCE_MODE ||
3499
+ String(before.dev) !== String(mount?.snapshotIdentity?.fileId?.dev) ||
3500
+ String(before.ino) !== String(mount?.snapshotIdentity?.fileId?.ino) ||
3501
+ Number(before.size) !== Number(attestation?.bytes) ||
3502
+ Number(before.size) !== Number(mount?.snapshotIdentity?.bytes) ||
3503
+ Number(before.mtimeMs) !== Number(mount?.snapshotIdentity?.mtimeMs)
3504
+ ) {
3505
+ throw new Error(`${errorPrefix}_identity_changed`);
3506
+ }
3507
+ const sha256 = hashLinuxOpenFile(runtime, mount.fd, Number(before.size));
3508
+ const after = runtime.fs.fstatSync(mount.fd);
3509
+ if (
3510
+ !linuxOpenStatMatches(before, after) ||
3511
+ sha256 !== attestation?.sha256 ||
3512
+ sha256 !== mount.snapshotIdentity.sha256
3513
+ ) {
3514
+ throw new Error(`${errorPrefix}_identity_changed`);
3515
+ }
3516
+ }
3517
+
3518
+ function attestLinuxEntrySnapshot(runtime, mount, attestation, targetRuntime) {
3519
+ attestLinuxRegularFileSnapshot(
3520
+ runtime,
3521
+ mount,
3522
+ attestation,
3523
+ linuxEntrySnapshotContract(targetRuntime),
3524
+ );
3525
+ }
3526
+
3527
+ function buildLinuxNodePluginTreeSnapshotAttestation(mounts, entryDestination) {
3528
+ const members = [];
3529
+ const destinations = new Set();
3530
+ for (const mount of mounts || []) {
3531
+ if (!linuxPathWithin("/opt/chainless/plugin", mount?.destination)) continue;
3532
+ if (
3533
+ path.posix.normalize(mount.destination) !== mount.destination ||
3534
+ destinations.has(mount.destination)
3535
+ ) {
3536
+ throw new Error("node_plugin_tree_snapshot_destination_invalid");
3537
+ }
3538
+ destinations.add(mount.destination);
3539
+ const snapshot = mount.contentSnapshot;
3540
+ const entry = mount.destination === entryDestination;
3541
+ const expectedScope = entry
3542
+ ? LINUX_NODE_ENTRY_SNAPSHOT_SCOPE
3543
+ : LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE;
3544
+ const sourceMode = Number.parseInt(snapshot?.sourceFileMode, 8);
3545
+ const expectedTargetMode = entry
3546
+ ? LINUX_NODE_ENTRY_SNAPSHOT_TARGET_MODE
3547
+ : linuxNodePluginTreeFileSnapshotContract(sourceMode).targetMode;
3548
+ if (
3549
+ snapshot?.scope !== expectedScope ||
3550
+ snapshot?.mechanism !== LINUX_ENTRY_SNAPSHOT_MECHANISM ||
3551
+ typeof snapshot?.sha256 !== "string" ||
3552
+ !/^[a-f0-9]{64}$/.test(snapshot.sha256) ||
3553
+ typeof snapshot?.sourceFileId !== "object" ||
3554
+ typeof snapshot.sourceFileId?.dev !== "string" ||
3555
+ !/^\d+$/.test(snapshot.sourceFileId.dev) ||
3556
+ typeof snapshot.sourceFileId?.ino !== "string" ||
3557
+ !/^\d+$/.test(snapshot.sourceFileId.ino) ||
3558
+ !Number.isFinite(snapshot?.sourceMtimeMs) ||
3559
+ !Number.isSafeInteger(snapshot?.bytes) ||
3560
+ snapshot.bytes < 0 ||
3561
+ snapshot.bytes > LINUX_ATTESTED_FILE_MAX_BYTES ||
3562
+ typeof snapshot?.sourceFileMode !== "string" ||
3563
+ !/^[0-7]{4}$/.test(snapshot.sourceFileMode) ||
3564
+ snapshot.sourceMode !==
3565
+ LINUX_ENTRY_SNAPSHOT_SOURCE_MODE.toString(8).padStart(4, "0") ||
3566
+ snapshot.targetMode !== expectedTargetMode ||
3567
+ mount.mountMode !== "ro-bind-data" ||
3568
+ mount.mountPermissions !== expectedTargetMode
3569
+ ) {
3570
+ throw new Error("node_plugin_tree_snapshot_member_invalid");
3571
+ }
3572
+ members.push({
3573
+ destination: mount.destination,
3574
+ sourceFileId: snapshot.sourceFileId,
3575
+ sourceMtimeMs: snapshot.sourceMtimeMs,
3576
+ sourceFileMode: snapshot.sourceFileMode,
3577
+ sha256: snapshot.sha256,
3578
+ bytes: snapshot.bytes,
3579
+ sourceMode: snapshot.sourceMode,
3580
+ targetMode: snapshot.targetMode,
3581
+ scope: snapshot.scope,
3582
+ mechanism: snapshot.mechanism,
3583
+ });
3584
+ }
3585
+ members.sort((left, right) =>
3586
+ left.destination < right.destination
3587
+ ? -1
3588
+ : left.destination > right.destination
3589
+ ? 1
3590
+ : 0,
3591
+ );
3592
+ if (
3593
+ members.length < 1 ||
3594
+ members.length > LINUX_PLUGIN_TREE_SNAPSHOT_MAX_FILES ||
3595
+ !members.some((member) => member.destination === entryDestination)
3596
+ ) {
3597
+ throw new Error("node_plugin_tree_snapshot_file_count_invalid");
3598
+ }
3599
+ const bytes = members.reduce((total, member) => total + member.bytes, 0);
3600
+ if (
3601
+ !Number.isSafeInteger(bytes) ||
3602
+ bytes > LINUX_PLUGIN_TREE_SNAPSHOT_MAX_BYTES
3603
+ ) {
3604
+ throw new Error("node_plugin_tree_snapshot_bytes_exceeded");
3605
+ }
3606
+ const digest = crypto
3607
+ .createHash("sha256")
3608
+ .update(
3609
+ JSON.stringify({
3610
+ version: 1,
3611
+ scope: LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE,
3612
+ consistency: LINUX_NODE_PLUGIN_TREE_SNAPSHOT_CONSISTENCY,
3613
+ members,
3614
+ }),
3615
+ )
3616
+ .digest("hex");
3617
+ return Object.freeze({
3618
+ scope: LINUX_NODE_PLUGIN_TREE_SNAPSHOT_SCOPE,
3619
+ mechanism: LINUX_ENTRY_SNAPSHOT_MECHANISM,
3620
+ files: members.length,
3621
+ bytes,
3622
+ digest,
3623
+ consistency: LINUX_NODE_PLUGIN_TREE_SNAPSHOT_CONSISTENCY,
3624
+ contractBound: false,
3625
+ atomic: false,
3626
+ });
3627
+ }
3628
+
3629
+ function attestLinuxNodePluginTreeSnapshot(
3630
+ runtime,
3631
+ mounts,
3632
+ entryDestination,
3633
+ expected,
3634
+ ) {
3635
+ for (const mount of mounts || []) {
3636
+ if (!linuxPathWithin("/opt/chainless/plugin", mount?.destination)) continue;
3637
+ const snapshot = mount.contentSnapshot;
3638
+ const contract =
3639
+ mount.destination === entryDestination
3640
+ ? linuxEntrySnapshotContract("node")
3641
+ : linuxNodePluginTreeFileSnapshotContract(
3642
+ Number.parseInt(snapshot?.sourceFileMode, 8),
3643
+ );
3644
+ attestLinuxRegularFileSnapshot(runtime, mount, snapshot, contract);
3645
+ }
3646
+ const actual = buildLinuxNodePluginTreeSnapshotAttestation(
3647
+ mounts,
3648
+ entryDestination,
3649
+ );
3650
+ for (const field of [
3651
+ "scope",
3652
+ "mechanism",
3653
+ "files",
3654
+ "bytes",
3655
+ "digest",
3656
+ "consistency",
3657
+ "contractBound",
3658
+ "atomic",
3659
+ ]) {
3660
+ if (actual[field] !== expected?.[field]) {
3661
+ throw new Error("node_plugin_tree_snapshot_identity_changed");
3662
+ }
3663
+ }
3664
+ }
3665
+
3076
3666
  /**
3077
3667
  * Classify one already-pinned plugin entry without invoking it or any host
3078
3668
  * inspection utility. The native slice accepts conventional fully static
@@ -3355,8 +3945,12 @@ function linuxFdMountId(runtime, fd) {
3355
3945
  function closeLinuxPinnedMounts(runtime, mounts) {
3356
3946
  for (const mount of mounts || []) {
3357
3947
  if (!Number.isInteger(mount?.fd)) continue;
3948
+ const fd = mount.fd;
3949
+ // Clear ownership before close so repeated cleanup cannot close an
3950
+ // unrelated descriptor if the OS has already reused this number.
3951
+ mount.fd = null;
3358
3952
  try {
3359
- runtime.fs.closeSync(mount.fd);
3953
+ runtime.fs.closeSync(fd);
3360
3954
  } catch {
3361
3955
  // Best effort. The process is already fail-closed if a pin cannot be
3362
3956
  // created; cleanup must not replace the original boundary error.
@@ -3706,13 +4300,209 @@ function attestLinuxBubblewrapBinary(runtime) {
3706
4300
  );
3707
4301
  }
3708
4302
 
3709
- function attestLinuxBubblewrapCapabilities(runtime) {
4303
+ function linuxBubblewrapSupervisorStatValid(
4304
+ stat,
4305
+ { allowUnlinked = false } = {},
4306
+ ) {
4307
+ const linkCount = Number(stat?.nlink);
4308
+ return (
4309
+ stat?.isFile?.() === true &&
4310
+ Number(stat.uid) === 0 &&
4311
+ (linkCount === 1 || (allowUnlinked && linkCount === 0)) &&
4312
+ Number(stat.size) > 0 &&
4313
+ Number(stat.size) <= LINUX_ATTESTED_FILE_MAX_BYTES &&
4314
+ (Number(stat.mode) & 0o111) !== 0 &&
4315
+ (Number(stat.mode) & 0o022) === 0
4316
+ );
4317
+ }
4318
+
4319
+ function linuxBubblewrapPinnedStatMatches(expected, current) {
4320
+ return (
4321
+ linuxBubblewrapSupervisorStatValid(current, { allowUnlinked: true }) &&
4322
+ String(expected.dev) === String(current.dev) &&
4323
+ String(expected.ino) === String(current.ino) &&
4324
+ Number(expected.size) === Number(current.size) &&
4325
+ Number(expected.mtimeMs) === Number(current.mtimeMs) &&
4326
+ Number(expected.mode) === Number(current.mode) &&
4327
+ Number(expected.uid) === Number(current.uid) &&
4328
+ Number(expected.gid) === Number(current.gid)
4329
+ );
4330
+ }
4331
+
4332
+ function pinLinuxBubblewrapSupervisor(runtime) {
4333
+ let fd;
4334
+ try {
4335
+ for (const method of [
4336
+ "openSync",
4337
+ "readSync",
4338
+ "fstatSync",
4339
+ "statSync",
4340
+ "lstatSync",
4341
+ "closeSync",
4342
+ ]) {
4343
+ if (typeof runtime.fs?.[method] !== "function") {
4344
+ throw new Error(`supervisor_${method}_unavailable`);
4345
+ }
4346
+ }
4347
+ if (linuxRealpath(runtime, LINUX_BWRAP_PATH) !== LINUX_BWRAP_PATH) {
4348
+ throw new Error("supervisor_realpath_changed");
4349
+ }
4350
+ const pathBefore = runtime.fs.lstatSync(LINUX_BWRAP_PATH, {
4351
+ bigint: true,
4352
+ });
4353
+ if (
4354
+ pathBefore.isSymbolicLink?.() === true ||
4355
+ !linuxBubblewrapSupervisorStatValid(pathBefore)
4356
+ ) {
4357
+ throw new Error("supervisor_path_unattested");
4358
+ }
4359
+ const constants = runtime.fs.constants || fs.constants;
4360
+ const flags =
4361
+ Number(constants.O_RDONLY) |
4362
+ Number(constants.O_NOFOLLOW || 0) |
4363
+ Number(constants.O_NONBLOCK || 0);
4364
+ fd = runtime.fs.openSync(LINUX_BWRAP_PATH, flags);
4365
+ const openedBefore = runtime.fs.fstatSync(fd, { bigint: true });
4366
+ const currentPath = runtime.fs.statSync(LINUX_BWRAP_PATH, {
4367
+ bigint: true,
4368
+ });
4369
+ if (
4370
+ !linuxBubblewrapSupervisorStatValid(openedBefore) ||
4371
+ !linuxOpenStatMatches(pathBefore, openedBefore) ||
4372
+ !linuxOpenStatMatches(openedBefore, currentPath)
4373
+ ) {
4374
+ throw new Error("supervisor_identity_changed");
4375
+ }
4376
+ const sha256 = hashLinuxOpenFile(runtime, fd, Number(openedBefore.size));
4377
+ const openedAfter = runtime.fs.fstatSync(fd, { bigint: true });
4378
+ const pathAfter = runtime.fs.statSync(LINUX_BWRAP_PATH, {
4379
+ bigint: true,
4380
+ });
4381
+ if (
4382
+ !linuxOpenStatMatches(openedBefore, openedAfter) ||
4383
+ !linuxOpenStatMatches(openedAfter, pathAfter)
4384
+ ) {
4385
+ throw new Error("supervisor_identity_changed");
4386
+ }
4387
+ const attestation = Object.freeze({
4388
+ path: LINUX_BWRAP_PATH,
4389
+ fileId: Object.freeze({
4390
+ dev: String(openedAfter.dev),
4391
+ ino: String(openedAfter.ino),
4392
+ }),
4393
+ sha256,
4394
+ bytes: Number(openedAfter.size),
4395
+ mtimeMs: Number(openedAfter.mtimeMs),
4396
+ mode: Number(openedAfter.mode),
4397
+ uid: Number(openedAfter.uid),
4398
+ gid: Number(openedAfter.gid),
4399
+ mechanism: LINUX_BWRAP_SUPERVISOR_BINDING_MECHANISM,
4400
+ scope: "host-path-replacement",
4401
+ childFd: LINUX_BWRAP_SUPERVISOR_CHILD_FD,
4402
+ consumeOperation: "file-copy-close",
4403
+ consumePath: LINUX_BWRAP_SUPERVISOR_HIDDEN_PATH,
4404
+ consumeMode: "0000",
4405
+ obscuredBy: "tmpfs:/run",
4406
+ pid1ExecutableExposure: "procfs",
4407
+ });
4408
+ return {
4409
+ fd,
4410
+ openedStat: openedAfter,
4411
+ attestation,
4412
+ };
4413
+ } catch (error) {
4414
+ if (fd !== undefined) {
4415
+ try {
4416
+ runtime.fs.closeSync(fd);
4417
+ } catch {
4418
+ // Preserve the original attestation error.
4419
+ }
4420
+ }
4421
+ throw error;
4422
+ }
4423
+ }
4424
+
4425
+ function openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin) {
4426
+ let fd;
4427
+ try {
4428
+ if (
4429
+ !Number.isInteger(supervisorPin?.fd) ||
4430
+ !supervisorPin.openedStat ||
4431
+ !supervisorPin.attestation
4432
+ ) {
4433
+ throw new Error("supervisor_pin_missing");
4434
+ }
4435
+ const constants = runtime.fs.constants || fs.constants;
4436
+ const flags =
4437
+ Number(constants.O_RDONLY) | Number(constants.O_NONBLOCK || 0);
4438
+ fd = runtime.fs.openSync(`/proc/self/fd/${supervisorPin.fd}`, flags);
4439
+ const openedBefore = runtime.fs.fstatSync(fd, { bigint: true });
4440
+ if (
4441
+ !linuxBubblewrapPinnedStatMatches(supervisorPin.openedStat, openedBefore)
4442
+ ) {
4443
+ throw new Error("supervisor_launch_reader_identity_changed");
4444
+ }
4445
+ const sha256 = hashLinuxOpenFile(
4446
+ runtime,
4447
+ fd,
4448
+ supervisorPin.attestation.bytes,
4449
+ );
4450
+ const openedAfter = runtime.fs.fstatSync(fd, { bigint: true });
4451
+ if (
4452
+ !linuxOpenStatMatches(openedBefore, openedAfter) ||
4453
+ sha256 !== supervisorPin.attestation.sha256
4454
+ ) {
4455
+ throw new Error("supervisor_launch_reader_identity_changed");
4456
+ }
4457
+ return {
4458
+ fd,
4459
+ command: `/proc/self/fd/${LINUX_BWRAP_SUPERVISOR_CHILD_FD}`,
4460
+ };
4461
+ } catch (error) {
4462
+ if (fd !== undefined) {
4463
+ try {
4464
+ runtime.fs.closeSync(fd);
4465
+ } catch {
4466
+ // Preserve the original launch-reader error.
4467
+ }
4468
+ }
4469
+ throw error;
4470
+ }
4471
+ }
4472
+
4473
+ function attestLinuxBubblewrapSupervisorPin(runtime, supervisorPin) {
4474
+ if (
4475
+ !Number.isInteger(supervisorPin?.fd) ||
4476
+ !supervisorPin.openedStat ||
4477
+ !supervisorPin.attestation
4478
+ ) {
4479
+ throw new Error("supervisor_pin_missing");
4480
+ }
4481
+ const before = runtime.fs.fstatSync(supervisorPin.fd, { bigint: true });
4482
+ if (!linuxBubblewrapPinnedStatMatches(supervisorPin.openedStat, before)) {
4483
+ throw new Error("supervisor_identity_changed");
4484
+ }
4485
+ const sha256 = hashLinuxOpenFile(
4486
+ runtime,
4487
+ supervisorPin.fd,
4488
+ supervisorPin.attestation.bytes,
4489
+ );
4490
+ const after = runtime.fs.fstatSync(supervisorPin.fd, { bigint: true });
4491
+ if (
4492
+ !linuxOpenStatMatches(before, after) ||
4493
+ sha256 !== supervisorPin.attestation.sha256
4494
+ ) {
4495
+ throw new Error("supervisor_identity_changed");
4496
+ }
4497
+ }
4498
+
4499
+ function attestLinuxBubblewrapCapabilities(runtime, supervisorLaunch) {
3710
4500
  let result;
3711
4501
  try {
3712
- result = runtime.spawnSync(LINUX_BWRAP_PATH, ["--help"], {
4502
+ result = runtime.spawnSync(supervisorLaunch.command, ["--help"], {
3713
4503
  shell: false,
3714
4504
  encoding: "utf8",
3715
- stdio: ["ignore", "pipe", "pipe"],
4505
+ stdio: ["ignore", "pipe", "pipe", supervisorLaunch.fd],
3716
4506
  timeout: 10_000,
3717
4507
  env: {
3718
4508
  PATH: "/usr/bin:/bin",
@@ -3727,13 +4517,18 @@ function attestLinuxBubblewrapCapabilities(runtime) {
3727
4517
  return { ok: false, reason: "capability_probe_failed" };
3728
4518
  }
3729
4519
  const help = `${String(result.stdout || "")}\n${String(result.stderr || "")}`;
3730
- for (const option of [
4520
+ const supportedOptions = new Set(help.split(/\s+/).filter(Boolean));
4521
+ const requiredOptions = [
4522
+ "--file",
4523
+ "--perms",
3731
4524
  "--ro-bind-fd",
4525
+ "--ro-bind-data",
3732
4526
  "--disable-userns",
3733
4527
  "--assert-userns-disabled",
3734
4528
  "--seccomp",
3735
- ]) {
3736
- if (!help.includes(option)) {
4529
+ ];
4530
+ for (const option of requiredOptions) {
4531
+ if (!supportedOptions.has(option)) {
3737
4532
  return {
3738
4533
  ok: false,
3739
4534
  reason: `required_option_missing:${option.slice(2)}`,
@@ -3928,8 +4723,30 @@ function buildLinuxBubblewrapPolicyArgs(
3928
4723
  for (const directory of orderedDirectories) {
3929
4724
  args.push("--dir", directory);
3930
4725
  }
4726
+ // FD 3 is also the descriptor-backed executable used to start bubblewrap.
4727
+ // Consume and close it during setup, then obscure this staging copy with the
4728
+ // later /run tmpfs. The target can still observe bubblewrap itself as the
4729
+ // PID-namespace supervisor through procfs; this only contains the inherited
4730
+ // launch descriptor and its temporary setup path.
4731
+ args.push(
4732
+ "--perms",
4733
+ "0000",
4734
+ "--file",
4735
+ String(LINUX_BWRAP_SUPERVISOR_CHILD_FD),
4736
+ LINUX_BWRAP_SUPERVISOR_HIDDEN_PATH,
4737
+ );
3931
4738
  for (const mount of pinnedMounts) {
3932
- args.push("--ro-bind-fd", String(mount.childFd), mount.destination);
4739
+ if (mount.mountMode === "ro-bind-data") {
4740
+ args.push(
4741
+ "--perms",
4742
+ mount.mountPermissions,
4743
+ "--ro-bind-data",
4744
+ String(mount.childFd),
4745
+ mount.destination,
4746
+ );
4747
+ } else {
4748
+ args.push("--ro-bind-fd", String(mount.childFd), mount.destination);
4749
+ }
3933
4750
  }
3934
4751
  args.push(
3935
4752
  "--seccomp",
@@ -3974,29 +4791,50 @@ function buildLinuxBubblewrapPolicyArgs(
3974
4791
 
3975
4792
  function probeLinuxBubblewrapPolicy(
3976
4793
  runtime,
4794
+ supervisorLaunch,
4795
+ supervisorBinding,
3977
4796
  policyArgs,
3978
4797
  pinnedDescriptors,
3979
4798
  targetRuntime,
4799
+ contentSnapshot = null,
3980
4800
  ) {
4801
+ const supervisorIdentity = JSON.stringify({
4802
+ dev: supervisorBinding.fileId.dev,
4803
+ ino: supervisorBinding.fileId.ino,
4804
+ bytes: supervisorBinding.bytes,
4805
+ });
4806
+ const probeSource = [
4807
+ 'const fs = require("node:fs");',
4808
+ `const expected = ${supervisorIdentity};`,
4809
+ 'for (const name of fs.readdirSync("/proc/self/fd")) {',
4810
+ " let current;",
4811
+ " try { current = fs.fstatSync(Number(name), { bigint: true }); } catch { continue; }",
4812
+ " if (String(current.dev) === expected.dev &&",
4813
+ " String(current.ino) === expected.ino &&",
4814
+ " Number(current.size) === expected.bytes) {",
4815
+ ' throw new Error("bubblewrap supervisor descriptor leaked");',
4816
+ " }",
4817
+ "}",
4818
+ "try {",
4819
+ ` fs.lstatSync(${JSON.stringify(LINUX_BWRAP_SUPERVISOR_HIDDEN_PATH)});`,
4820
+ ' throw new Error("bubblewrap supervisor staging path visible");',
4821
+ "} catch (error) {",
4822
+ ' if (error?.code !== "ENOENT") throw error;',
4823
+ "}",
4824
+ `process.stdout.write(${JSON.stringify(LINUX_BWRAP_NODE_PROBE_SENTINEL)});`,
4825
+ ].join("\n");
3981
4826
  let result;
3982
4827
  try {
3983
4828
  result = runtime.spawnSync(
3984
- LINUX_BWRAP_PATH,
3985
- [
3986
- ...policyArgs,
3987
- "--",
3988
- "/opt/chainless/runtime/node",
3989
- "-e",
3990
- `process.stdout.write(${JSON.stringify(
3991
- LINUX_BWRAP_NODE_PROBE_SENTINEL,
3992
- )})`,
3993
- ],
4829
+ supervisorLaunch.command,
4830
+ [...policyArgs, "--", "/opt/chainless/runtime/node", "-e", probeSource],
3994
4831
  {
3995
4832
  cwd: "/",
3996
4833
  shell: false,
3997
4834
  encoding: "utf8",
3998
4835
  stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
3999
4836
  probe: true,
4837
+ supervisorFd: supervisorLaunch.fd,
4000
4838
  }),
4001
4839
  timeout: 15_000,
4002
4840
  env: {
@@ -4012,6 +4850,8 @@ function probeLinuxBubblewrapPolicy(
4012
4850
  false,
4013
4851
  "probe_spawn_failed",
4014
4852
  targetRuntime,
4853
+ contentSnapshot,
4854
+ supervisorBinding,
4015
4855
  );
4016
4856
  }
4017
4857
  if (result?.error || result?.status !== 0) {
@@ -4020,6 +4860,8 @@ function probeLinuxBubblewrapPolicy(
4020
4860
  false,
4021
4861
  result?.error ? "probe_spawn_failed" : "probe_failed",
4022
4862
  targetRuntime,
4863
+ contentSnapshot,
4864
+ supervisorBinding,
4023
4865
  );
4024
4866
  }
4025
4867
  if (String(result.stdout) !== LINUX_BWRAP_NODE_PROBE_SENTINEL) {
@@ -4028,9 +4870,18 @@ function probeLinuxBubblewrapPolicy(
4028
4870
  false,
4029
4871
  "node_runtime_probe_failed",
4030
4872
  targetRuntime,
4873
+ contentSnapshot,
4874
+ supervisorBinding,
4031
4875
  );
4032
4876
  }
4033
- return linuxBubblewrapProbe(true, true, null, targetRuntime);
4877
+ return linuxBubblewrapProbe(
4878
+ true,
4879
+ true,
4880
+ null,
4881
+ targetRuntime,
4882
+ contentSnapshot,
4883
+ supervisorBinding,
4884
+ );
4034
4885
  }
4035
4886
 
4036
4887
  /**
@@ -4129,24 +4980,6 @@ export function applyLinuxSandbox(
4129
4980
  guarantees: [],
4130
4981
  });
4131
4982
  }
4132
- const capabilities = attestLinuxBubblewrapCapabilities(runtime);
4133
- if (!capabilities.ok) {
4134
- return createSandboxPlan({
4135
- ...base,
4136
- backend: null,
4137
- candidateBackend: LINUX_BWRAP_BACKEND,
4138
- policyAttested: false,
4139
- runtimeProbe: linuxBubblewrapProbe(
4140
- true,
4141
- false,
4142
- capabilities.reason,
4143
- validation.entryRuntime,
4144
- ),
4145
- reason: "linux_bwrap_unavailable",
4146
- guarantees: [],
4147
- });
4148
- }
4149
-
4150
4983
  let runtimeMounts;
4151
4984
  let pinnedRuntimeMounts = [];
4152
4985
  try {
@@ -4181,6 +5014,14 @@ export function applyLinuxSandbox(
4181
5014
  let pluginTree;
4182
5015
  let entryFormat = null;
4183
5016
  let entryMount;
5017
+ const entryRelative = path.posix.relative(
5018
+ validation.contract.pluginRoot,
5019
+ validation.contract.entryIdentity.realPath,
5020
+ );
5021
+ const sandboxEntry = path.posix.join(
5022
+ "/opt/chainless/plugin",
5023
+ entryRelative,
5024
+ );
4184
5025
  try {
4185
5026
  pluginTree = pinLinuxPluginTree(
4186
5027
  runtime,
@@ -4240,8 +5081,124 @@ export function applyLinuxSandbox(
4240
5081
  });
4241
5082
  }
4242
5083
 
5084
+ let entrySnapshot = null;
5085
+ let pluginTreeSnapshot = null;
5086
+ const pluginTreeFileSnapshots = [];
5087
+ let probePinnedMounts = pinnedMounts;
5088
+ try {
5089
+ const entryIndex = pinnedMounts.indexOf(entryMount);
5090
+ if (entryIndex < 0) {
5091
+ throw new Error("plugin_entry_pin_missing");
5092
+ }
5093
+ const pluginMountIndexes = pinnedMounts
5094
+ .map((mount, index) =>
5095
+ linuxPathWithin("/opt/chainless/plugin", mount.destination)
5096
+ ? index
5097
+ : -1,
5098
+ )
5099
+ .filter((index) => index >= 0);
5100
+ const rawPluginMounts = pluginMountIndexes.map(
5101
+ (index) => pinnedMounts[index],
5102
+ );
5103
+ if (validation.entryRuntime === "node") {
5104
+ if (
5105
+ pluginMountIndexes.length < 1 ||
5106
+ pluginMountIndexes.length > LINUX_PLUGIN_TREE_SNAPSHOT_MAX_FILES
5107
+ ) {
5108
+ throw new Error("node_plugin_tree_snapshot_file_count_invalid");
5109
+ }
5110
+ const pluginBytes = pluginMountIndexes.reduce((total, index) => {
5111
+ const bytes = pinnedMounts[index]?.bytes;
5112
+ if (
5113
+ !Number.isSafeInteger(bytes) ||
5114
+ bytes < 0 ||
5115
+ bytes > LINUX_ATTESTED_FILE_MAX_BYTES
5116
+ ) {
5117
+ throw new Error("node_plugin_tree_snapshot_member_size_invalid");
5118
+ }
5119
+ return total + bytes;
5120
+ }, 0);
5121
+ if (
5122
+ !Number.isSafeInteger(pluginBytes) ||
5123
+ pluginBytes > LINUX_PLUGIN_TREE_SNAPSHOT_MAX_BYTES
5124
+ ) {
5125
+ throw new Error("node_plugin_tree_snapshot_bytes_exceeded");
5126
+ }
5127
+ }
5128
+ const rawEntryMount = entryMount;
5129
+ const snapshot = createLinuxEntrySnapshot(
5130
+ runtime,
5131
+ rawEntryMount,
5132
+ validation.contract.entryIdentity,
5133
+ validation.entryRuntime,
5134
+ );
5135
+ const finalMounts = [...pinnedMounts];
5136
+ const probeMounts = [...pinnedMounts];
5137
+ finalMounts[entryIndex] = snapshot.finalMount;
5138
+ probeMounts[entryIndex] = snapshot.probeMount;
5139
+ entrySnapshot = snapshot;
5140
+ if (validation.entryRuntime === "node") {
5141
+ for (const index of pluginMountIndexes) {
5142
+ if (index === entryIndex) continue;
5143
+ const rawMount = pinnedMounts[index];
5144
+ const source = runtime.fs.fstatSync(rawMount.fd);
5145
+ const fileSnapshot = createLinuxRegularFileSnapshot(
5146
+ runtime,
5147
+ rawMount,
5148
+ {
5149
+ fileId: rawMount.fileId,
5150
+ bytes: rawMount.bytes,
5151
+ mtimeMs: rawMount.mtimeMs,
5152
+ },
5153
+ linuxNodePluginTreeFileSnapshotContract(Number(source.mode)),
5154
+ );
5155
+ pluginTreeFileSnapshots.push(fileSnapshot);
5156
+ finalMounts[index] = fileSnapshot.finalMount;
5157
+ probeMounts[index] = fileSnapshot.probeMount;
5158
+ }
5159
+ pluginTreeSnapshot = buildLinuxNodePluginTreeSnapshotAttestation(
5160
+ finalMounts,
5161
+ sandboxEntry,
5162
+ );
5163
+ }
5164
+ pinnedMounts = finalMounts;
5165
+ probePinnedMounts = probeMounts;
5166
+ entryMount = entrySnapshot.finalMount;
5167
+ closeLinuxPinnedMounts(
5168
+ runtime,
5169
+ validation.entryRuntime === "node" ? rawPluginMounts : [rawEntryMount],
5170
+ );
5171
+ } catch (error) {
5172
+ closeLinuxPinnedMounts(runtime, pinnedMounts);
5173
+ closeLinuxPinnedMounts(runtime, [
5174
+ entrySnapshot?.probeMount,
5175
+ entrySnapshot?.finalMount,
5176
+ ...pluginTreeFileSnapshots.flatMap((snapshot) => [
5177
+ snapshot.probeMount,
5178
+ snapshot.finalMount,
5179
+ ]),
5180
+ ]);
5181
+ return createSandboxPlan({
5182
+ ...base,
5183
+ backend: null,
5184
+ candidateBackend: LINUX_BWRAP_BACKEND,
5185
+ policyAttested: false,
5186
+ runtimeProbe: linuxBubblewrapProbe(
5187
+ false,
5188
+ false,
5189
+ error.message || "entry_snapshot_unattested",
5190
+ validation.entryRuntime,
5191
+ ),
5192
+ reason: "linux_bwrap_plugin_snapshot_unattested",
5193
+ guarantees: [],
5194
+ });
5195
+ }
5196
+
4243
5197
  let probeSeccompFilter;
4244
5198
  let seccompFilter;
5199
+ const pluginTreeProbeMounts = pluginTreeFileSnapshots.map(
5200
+ (snapshot) => snapshot.probeMount,
5201
+ );
4245
5202
  try {
4246
5203
  probeSeccompFilter = pinLinuxNetworkSeccompFilter(runtime);
4247
5204
  seccompFilter = pinLinuxNetworkSeccompFilter(runtime);
@@ -4255,7 +5212,12 @@ export function applyLinuxSandbox(
4255
5212
  closeLinuxPinnedMounts(runtime, pinnedMounts);
4256
5213
  closeLinuxPinnedMounts(
4257
5214
  runtime,
4258
- [probeSeccompFilter, seccompFilter].filter(Boolean),
5215
+ [
5216
+ entrySnapshot?.probeMount,
5217
+ ...pluginTreeProbeMounts,
5218
+ probeSeccompFilter,
5219
+ seccompFilter,
5220
+ ].filter(Boolean),
4259
5221
  );
4260
5222
  return createSandboxPlan({
4261
5223
  ...base,
@@ -4273,7 +5235,89 @@ export function applyLinuxSandbox(
4273
5235
  });
4274
5236
  }
4275
5237
  const pinnedDescriptors = [...pinnedMounts, seccompFilter];
4276
- const probeDescriptors = [...pinnedMounts, probeSeccompFilter];
5238
+ const probeDescriptors = [...probePinnedMounts, probeSeccompFilter];
5239
+ const closeStrongLinuxResources = (...extra) => {
5240
+ closeLinuxPinnedMounts(runtime, [
5241
+ ...pinnedDescriptors,
5242
+ probeSeccompFilter,
5243
+ entrySnapshot?.probeMount,
5244
+ ...pluginTreeProbeMounts,
5245
+ ...extra,
5246
+ ]);
5247
+ };
5248
+ let supervisorPin;
5249
+ let capabilityLaunch;
5250
+ let probeLaunch;
5251
+ let finalLaunch;
5252
+ try {
5253
+ supervisorPin = pinLinuxBubblewrapSupervisor(runtime);
5254
+ capabilityLaunch = openLinuxBubblewrapSupervisorLaunch(
5255
+ runtime,
5256
+ supervisorPin,
5257
+ );
5258
+ } catch (error) {
5259
+ closeStrongLinuxResources(supervisorPin);
5260
+ return createSandboxPlan({
5261
+ ...base,
5262
+ backend: null,
5263
+ candidateBackend: LINUX_BWRAP_BACKEND,
5264
+ policyAttested: false,
5265
+ runtimeProbe: linuxBubblewrapProbe(
5266
+ false,
5267
+ false,
5268
+ error.message || "supervisor_descriptor_unattested",
5269
+ validation.entryRuntime,
5270
+ ),
5271
+ reason: "linux_bwrap_unavailable",
5272
+ guarantees: [],
5273
+ });
5274
+ }
5275
+ const capabilities = attestLinuxBubblewrapCapabilities(
5276
+ runtime,
5277
+ capabilityLaunch,
5278
+ );
5279
+ closeLinuxPinnedMounts(runtime, [capabilityLaunch]);
5280
+ capabilityLaunch = null;
5281
+ if (!capabilities.ok) {
5282
+ closeStrongLinuxResources(supervisorPin);
5283
+ return createSandboxPlan({
5284
+ ...base,
5285
+ backend: null,
5286
+ candidateBackend: LINUX_BWRAP_BACKEND,
5287
+ policyAttested: false,
5288
+ runtimeProbe: linuxBubblewrapProbe(
5289
+ true,
5290
+ false,
5291
+ capabilities.reason,
5292
+ validation.entryRuntime,
5293
+ null,
5294
+ supervisorPin.attestation,
5295
+ ),
5296
+ reason: "linux_bwrap_unavailable",
5297
+ guarantees: [],
5298
+ });
5299
+ }
5300
+ try {
5301
+ probeLaunch = openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin);
5302
+ } catch (error) {
5303
+ closeStrongLinuxResources(supervisorPin, probeLaunch);
5304
+ return createSandboxPlan({
5305
+ ...base,
5306
+ backend: null,
5307
+ candidateBackend: LINUX_BWRAP_BACKEND,
5308
+ policyAttested: false,
5309
+ runtimeProbe: linuxBubblewrapProbe(
5310
+ true,
5311
+ false,
5312
+ error.message || "supervisor_launch_reader_unattested",
5313
+ validation.entryRuntime,
5314
+ null,
5315
+ supervisorPin.attestation,
5316
+ ),
5317
+ reason: "linux_bwrap_unavailable",
5318
+ guarantees: [],
5319
+ });
5320
+ }
4277
5321
  const environment = linuxSandboxEnvironment();
4278
5322
  const policyArgs = buildLinuxBubblewrapPolicyArgs(
4279
5323
  pluginTree.directories,
@@ -4281,24 +5325,32 @@ export function applyLinuxSandbox(
4281
5325
  environment,
4282
5326
  seccompFilter,
4283
5327
  );
4284
- const entryRelative = path.posix.relative(
4285
- validation.contract.pluginRoot,
4286
- validation.contract.entryIdentity.realPath,
4287
- );
4288
- const sandboxEntry = path.posix.join(
4289
- "/opt/chainless/plugin",
4290
- entryRelative,
4291
- );
4292
5328
  const targetArgs =
4293
5329
  validation.entryRuntime === "native-static-elf"
4294
5330
  ? [sandboxEntry, ...args]
4295
5331
  : ["/opt/chainless/runtime/node", sandboxEntry, ...args.slice(1)];
5332
+ const snapshotPolicyBinding = entrySnapshot
5333
+ ? {
5334
+ scope: entrySnapshot.attestation.scope,
5335
+ mechanism: entrySnapshot.attestation.mechanism,
5336
+ sourceIdentity: validation.contract.entryIdentity,
5337
+ sha256: entrySnapshot.attestation.sha256,
5338
+ bytes: entrySnapshot.attestation.bytes,
5339
+ destination: sandboxEntry,
5340
+ ...(validation.entryRuntime === "node"
5341
+ ? { sourceFileMode: entrySnapshot.attestation.sourceFileMode }
5342
+ : {}),
5343
+ sourceMode: entrySnapshot.attestation.sourceMode,
5344
+ targetMode: entrySnapshot.attestation.targetMode,
5345
+ }
5346
+ : null;
4296
5347
  const policyDigest = crypto
4297
5348
  .createHash("sha256")
4298
5349
  .update(
4299
5350
  JSON.stringify({
4300
- version: 1,
5351
+ version: validation.entryRuntime === "node" ? 4 : 3,
4301
5352
  backend: LINUX_BWRAP_BACKEND,
5353
+ supervisor: supervisorPin.attestation,
4302
5354
  contract: {
4303
5355
  kind: validation.contract.kind,
4304
5356
  pluginRoot: validation.contract.pluginRoot,
@@ -4308,12 +5360,38 @@ export function applyLinuxSandbox(
4308
5360
  entryRuntime: validation.entryRuntime,
4309
5361
  entryFormat,
4310
5362
  },
4311
- mounts: pinnedMounts.map((mount) => ({
4312
- destination: mount.destination,
4313
- fileId: mount.fileId,
4314
- bytes: mount.bytes,
4315
- mtimeMs: mount.mtimeMs,
4316
- })),
5363
+ mounts: pinnedMounts.map((mount) =>
5364
+ mount.contentSnapshot
5365
+ ? {
5366
+ destination: mount.destination,
5367
+ fileId: mount.contentSnapshot.sourceFileId,
5368
+ bytes: mount.contentSnapshot.bytes,
5369
+ mtimeMs: mount.contentSnapshot.sourceMtimeMs,
5370
+ contentSnapshot: {
5371
+ scope: mount.contentSnapshot.scope,
5372
+ mechanism: mount.contentSnapshot.mechanism,
5373
+ sha256: mount.contentSnapshot.sha256,
5374
+ bytes: mount.contentSnapshot.bytes,
5375
+ ...(validation.entryRuntime === "node"
5376
+ ? {
5377
+ sourceFileMode: mount.contentSnapshot.sourceFileMode,
5378
+ }
5379
+ : {}),
5380
+ sourceMode: mount.contentSnapshot.sourceMode,
5381
+ targetMode: mount.contentSnapshot.targetMode,
5382
+ },
5383
+ }
5384
+ : {
5385
+ destination: mount.destination,
5386
+ fileId: mount.fileId,
5387
+ bytes: mount.bytes,
5388
+ mtimeMs: mount.mtimeMs,
5389
+ },
5390
+ ),
5391
+ contentSnapshot: snapshotPolicyBinding,
5392
+ ...(validation.entryRuntime === "node"
5393
+ ? { pluginTreeContentSnapshot: pluginTreeSnapshot }
5394
+ : {}),
4317
5395
  seccomp: {
4318
5396
  arch: seccompFilter.arch,
4319
5397
  policy: seccompFilter.policy,
@@ -4327,24 +5405,32 @@ export function applyLinuxSandbox(
4327
5405
  }),
4328
5406
  )
4329
5407
  .digest("hex");
4330
- const runtimeProbe = Object.freeze(
5408
+ const policyProbe = Object.freeze(
4331
5409
  probeLinuxBubblewrapPolicy(
4332
5410
  runtime,
5411
+ probeLaunch,
5412
+ supervisorPin.attestation,
4333
5413
  policyArgs,
4334
5414
  probeDescriptors,
4335
5415
  validation.entryRuntime,
4336
5416
  ),
4337
5417
  );
4338
- closeLinuxPinnedMounts(runtime, [probeSeccompFilter]);
4339
- if (!runtimeProbe.runnable) {
4340
- closeLinuxPinnedMounts(runtime, pinnedDescriptors);
5418
+ closeLinuxPinnedMounts(runtime, [
5419
+ probeLaunch,
5420
+ probeSeccompFilter,
5421
+ entrySnapshot?.probeMount,
5422
+ ...pluginTreeProbeMounts,
5423
+ ]);
5424
+ probeLaunch = null;
5425
+ if (!policyProbe.runnable) {
5426
+ closeStrongLinuxResources(supervisorPin, finalLaunch);
4341
5427
  return createSandboxPlan({
4342
5428
  ...base,
4343
5429
  backend: null,
4344
5430
  candidateBackend: LINUX_BWRAP_BACKEND,
4345
5431
  policyAttested: false,
4346
5432
  policyDigest,
4347
- runtimeProbe,
5433
+ runtimeProbe: policyProbe,
4348
5434
  reason: "linux_bwrap_policy_probe_failed",
4349
5435
  guarantees: [],
4350
5436
  });
@@ -4356,9 +5442,10 @@ export function applyLinuxSandbox(
4356
5442
  validation.contract,
4357
5443
  runtime,
4358
5444
  sandboxOpts.sync,
5445
+ { sealedEntry: entrySnapshot !== null },
4359
5446
  );
4360
5447
  if (!finalValidation.ok) {
4361
- closeLinuxPinnedMounts(runtime, pinnedDescriptors);
5448
+ closeStrongLinuxResources(supervisorPin, finalLaunch);
4362
5449
  return createSandboxPlan({
4363
5450
  ...base,
4364
5451
  backend: null,
@@ -4370,46 +5457,107 @@ export function applyLinuxSandbox(
4370
5457
  false,
4371
5458
  `post_probe_${finalValidation.reason}`,
4372
5459
  validation.entryRuntime,
5460
+ null,
5461
+ supervisorPin.attestation,
4373
5462
  ),
4374
5463
  reason: "linux_bwrap_execution_contract_changed",
4375
5464
  guarantees: [],
4376
5465
  });
4377
5466
  }
4378
- if (validation.entryRuntime === "native-static-elf") {
4379
- try {
4380
- const finalEntryFormat = inspectLinuxStaticNativeElf(
5467
+ try {
5468
+ if (validation.entryRuntime === "node") {
5469
+ attestLinuxNodePluginTreeSnapshot(
4381
5470
  runtime,
4382
- entryMount.fd,
4383
- validation.contract.entryIdentity,
5471
+ pinnedMounts,
5472
+ sandboxEntry,
5473
+ pluginTreeSnapshot,
5474
+ );
5475
+ } else {
5476
+ attestLinuxEntrySnapshot(
5477
+ runtime,
5478
+ entryMount,
5479
+ entrySnapshot.attestation,
5480
+ validation.entryRuntime,
4384
5481
  );
4385
- if (JSON.stringify(finalEntryFormat) !== JSON.stringify(entryFormat)) {
4386
- throw new Error("native_entry_format_changed");
4387
- }
4388
- if (
4389
- JSON.stringify(finalValidation.entryFormat) !==
4390
- JSON.stringify(entryFormat)
4391
- ) {
4392
- throw new Error("native_entry_path_format_changed");
4393
- }
4394
- } catch (error) {
4395
- closeLinuxPinnedMounts(runtime, pinnedDescriptors);
4396
- return createSandboxPlan({
4397
- ...base,
4398
- backend: null,
4399
- candidateBackend: LINUX_BWRAP_BACKEND,
4400
- policyAttested: false,
4401
- policyDigest,
4402
- runtimeProbe: linuxBubblewrapProbe(
4403
- true,
4404
- false,
4405
- `post_probe_${error.message || "native_entry_changed"}`,
4406
- validation.entryRuntime,
4407
- ),
4408
- reason: "linux_bwrap_execution_contract_changed",
4409
- guarantees: [],
4410
- });
4411
5482
  }
5483
+ } catch (error) {
5484
+ closeStrongLinuxResources(supervisorPin, finalLaunch);
5485
+ return createSandboxPlan({
5486
+ ...base,
5487
+ backend: null,
5488
+ candidateBackend: LINUX_BWRAP_BACKEND,
5489
+ policyAttested: false,
5490
+ policyDigest,
5491
+ runtimeProbe: linuxBubblewrapProbe(
5492
+ true,
5493
+ false,
5494
+ `post_probe_${error.message || "entry_snapshot_changed"}`,
5495
+ validation.entryRuntime,
5496
+ null,
5497
+ supervisorPin.attestation,
5498
+ ),
5499
+ reason: "linux_bwrap_execution_contract_changed",
5500
+ guarantees: [],
5501
+ });
5502
+ }
5503
+ try {
5504
+ attestLinuxBubblewrapSupervisorPin(runtime, supervisorPin);
5505
+ } catch (error) {
5506
+ closeStrongLinuxResources(supervisorPin, finalLaunch);
5507
+ return createSandboxPlan({
5508
+ ...base,
5509
+ backend: null,
5510
+ candidateBackend: LINUX_BWRAP_BACKEND,
5511
+ policyAttested: false,
5512
+ policyDigest,
5513
+ runtimeProbe: linuxBubblewrapProbe(
5514
+ true,
5515
+ false,
5516
+ `post_probe_${error.message || "supervisor_identity_changed"}`,
5517
+ validation.entryRuntime,
5518
+ null,
5519
+ supervisorPin.attestation,
5520
+ ),
5521
+ reason: "linux_bwrap_execution_contract_changed",
5522
+ guarantees: [],
5523
+ });
5524
+ }
5525
+ try {
5526
+ finalLaunch = openLinuxBubblewrapSupervisorLaunch(runtime, supervisorPin);
5527
+ } catch (error) {
5528
+ closeStrongLinuxResources(supervisorPin, finalLaunch);
5529
+ return createSandboxPlan({
5530
+ ...base,
5531
+ backend: null,
5532
+ candidateBackend: LINUX_BWRAP_BACKEND,
5533
+ policyAttested: false,
5534
+ policyDigest,
5535
+ runtimeProbe: linuxBubblewrapProbe(
5536
+ true,
5537
+ false,
5538
+ `post_probe_${error.message || "supervisor_launch_reader_unattested"}`,
5539
+ validation.entryRuntime,
5540
+ null,
5541
+ supervisorPin.attestation,
5542
+ ),
5543
+ reason: "linux_bwrap_execution_contract_changed",
5544
+ guarantees: [],
5545
+ });
4412
5546
  }
5547
+ const supervisorBinding = supervisorPin.attestation;
5548
+ closeLinuxPinnedMounts(runtime, [supervisorPin]);
5549
+ supervisorPin = null;
5550
+ const runtimeProbe = Object.freeze(
5551
+ linuxBubblewrapProbe(
5552
+ true,
5553
+ true,
5554
+ null,
5555
+ validation.entryRuntime,
5556
+ entrySnapshot?.attestation,
5557
+ supervisorBinding,
5558
+ pluginTreeSnapshot,
5559
+ ),
5560
+ );
4413
5561
  const options = {
4414
5562
  ...spawnOpts,
4415
5563
  cwd: "/",
@@ -4421,14 +5569,16 @@ export function applyLinuxSandbox(
4421
5569
  LC_ALL: "C",
4422
5570
  },
4423
5571
  stdio: Object.freeze(
4424
- linuxStdioWithPinnedMounts(spawnOpts?.stdio, pinnedDescriptors),
5572
+ linuxStdioWithPinnedMounts(spawnOpts?.stdio, pinnedDescriptors, {
5573
+ supervisorFd: finalLaunch.fd,
5574
+ }),
4425
5575
  ),
4426
5576
  };
4427
5577
  let pinsClosed = false;
4428
5578
  const cleanup = () => {
4429
5579
  if (pinsClosed) return;
4430
5580
  pinsClosed = true;
4431
- closeLinuxPinnedMounts(runtime, pinnedDescriptors);
5581
+ closeLinuxPinnedMounts(runtime, [...pinnedDescriptors, finalLaunch]);
4432
5582
  };
4433
5583
  return createSandboxPlan({
4434
5584
  ...base,
@@ -4441,7 +5591,7 @@ export function applyLinuxSandbox(
4441
5591
  runtimeProbe,
4442
5592
  reason: null,
4443
5593
  guarantees: [SANDBOX_BOUNDARIES.FILESYSTEM, SANDBOX_BOUNDARIES.NETWORK],
4444
- command: LINUX_BWRAP_PATH,
5594
+ command: finalLaunch.command,
4445
5595
  args: [...policyArgs, "--", ...targetArgs],
4446
5596
  options,
4447
5597
  cleanup,