@smthrs/platform-node 0.0.0-stage → 1.0.0-rc.3

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 (442) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/bin/darwin-arm64/smithers-jj-export +0 -0
  5. package/bin/darwin-x64/smithers-jj-export +0 -0
  6. package/bin/linux-arm64/smithers-jj-export +0 -0
  7. package/bin/linux-x64/smithers-jj-export +4 -0
  8. package/bin/win32-x64/smithers-jj-export.exe +0 -0
  9. package/dist/cjs/AtomicFileSystem.d.ts +147 -0
  10. package/dist/cjs/AtomicFileSystem.d.ts.map +1 -0
  11. package/dist/cjs/AtomicFileSystem.js +234 -0
  12. package/dist/cjs/AtomicFileSystem.js.map +7 -0
  13. package/dist/cjs/EgressHttpClient.d.ts +106 -0
  14. package/dist/cjs/EgressHttpClient.d.ts.map +1 -0
  15. package/dist/cjs/EgressHttpClient.js +202 -0
  16. package/dist/cjs/EgressHttpClient.js.map +7 -0
  17. package/dist/cjs/HostLiveness.d.ts +81 -0
  18. package/dist/cjs/HostLiveness.d.ts.map +1 -0
  19. package/dist/cjs/HostLiveness.js +49 -0
  20. package/dist/cjs/HostLiveness.js.map +7 -0
  21. package/dist/cjs/NodeHost.d.ts +145 -0
  22. package/dist/cjs/NodeHost.d.ts.map +1 -0
  23. package/dist/cjs/NodeHost.js +128 -0
  24. package/dist/cjs/NodeHost.js.map +7 -0
  25. package/dist/cjs/ProcessConfinement.d.ts +61 -0
  26. package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
  27. package/dist/cjs/ProcessConfinement.js +287 -0
  28. package/dist/cjs/ProcessConfinement.js.map +7 -0
  29. package/dist/cjs/ProcessReaper.d.ts +457 -0
  30. package/dist/cjs/ProcessReaper.d.ts.map +1 -0
  31. package/dist/cjs/ProcessReaper.js +349 -0
  32. package/dist/cjs/ProcessReaper.js.map +7 -0
  33. package/dist/cjs/ProcessSandbox.d.ts +377 -0
  34. package/dist/cjs/ProcessSandbox.d.ts.map +1 -0
  35. package/dist/cjs/ProcessSandbox.js +759 -0
  36. package/dist/cjs/ProcessSandbox.js.map +7 -0
  37. package/dist/cjs/ScopedProcess.d.ts +65 -0
  38. package/dist/cjs/ScopedProcess.d.ts.map +1 -0
  39. package/dist/cjs/ScopedProcess.js +87 -0
  40. package/dist/cjs/ScopedProcess.js.map +7 -0
  41. package/dist/cjs/index.d.ts +38 -0
  42. package/dist/cjs/index.d.ts.map +1 -0
  43. package/dist/cjs/index.js +47 -0
  44. package/dist/cjs/index.js.map +7 -0
  45. package/dist/cjs/internal/AtomicFileSystemExecutable.d.ts +61 -0
  46. package/dist/cjs/internal/AtomicFileSystemExecutable.d.ts.map +1 -0
  47. package/dist/cjs/internal/AtomicFileSystemExecutable.js +250 -0
  48. package/dist/cjs/internal/AtomicFileSystemExecutable.js.map +7 -0
  49. package/dist/cjs/internal/AtomicFileSystemProtocol.d.ts +78 -0
  50. package/dist/cjs/internal/AtomicFileSystemProtocol.d.ts.map +1 -0
  51. package/dist/cjs/internal/AtomicFileSystemProtocol.js +382 -0
  52. package/dist/cjs/internal/AtomicFileSystemProtocol.js.map +7 -0
  53. package/dist/cjs/internal/AtomicFileSystemTransport.d.ts +44 -0
  54. package/dist/cjs/internal/AtomicFileSystemTransport.d.ts.map +1 -0
  55. package/dist/cjs/internal/AtomicFileSystemTransport.js +197 -0
  56. package/dist/cjs/internal/AtomicFileSystemTransport.js.map +7 -0
  57. package/dist/cjs/internal/AtomicHelperError.d.ts +35 -0
  58. package/dist/cjs/internal/AtomicHelperError.d.ts.map +1 -0
  59. package/dist/cjs/internal/AtomicHelperError.js +45 -0
  60. package/dist/cjs/internal/AtomicHelperError.js.map +7 -0
  61. package/dist/cjs/internal/AtomicHelperFault.d.ts +59 -0
  62. package/dist/cjs/internal/AtomicHelperFault.d.ts.map +1 -0
  63. package/dist/cjs/internal/AtomicHelperFault.js +66 -0
  64. package/dist/cjs/internal/AtomicHelperFault.js.map +7 -0
  65. package/dist/cjs/internal/PipedProcess.d.ts +35 -0
  66. package/dist/cjs/internal/PipedProcess.d.ts.map +1 -0
  67. package/dist/cjs/internal/PipedProcess.js +309 -0
  68. package/dist/cjs/internal/PipedProcess.js.map +7 -0
  69. package/dist/cjs/internal/ProcSnapshot.d.ts +85 -0
  70. package/dist/cjs/internal/ProcSnapshot.d.ts.map +1 -0
  71. package/dist/cjs/internal/ProcSnapshot.js +123 -0
  72. package/dist/cjs/internal/ProcSnapshot.js.map +7 -0
  73. package/dist/cjs/internal/ProcessCleanup.d.ts +64 -0
  74. package/dist/cjs/internal/ProcessCleanup.d.ts.map +1 -0
  75. package/dist/cjs/internal/ProcessCleanup.js +62 -0
  76. package/dist/cjs/internal/ProcessCleanup.js.map +7 -0
  77. package/dist/cjs/internal/ProcessCleanupPolicyError.d.ts +30 -0
  78. package/dist/cjs/internal/ProcessCleanupPolicyError.d.ts.map +1 -0
  79. package/dist/cjs/internal/ProcessCleanupPolicyError.js +33 -0
  80. package/dist/cjs/internal/ProcessCleanupPolicyError.js.map +7 -0
  81. package/dist/cjs/internal/ProcessFault.d.ts +63 -0
  82. package/dist/cjs/internal/ProcessFault.d.ts.map +1 -0
  83. package/dist/cjs/internal/ProcessFault.js +63 -0
  84. package/dist/cjs/internal/ProcessFault.js.map +7 -0
  85. package/dist/cjs/internal/ProcessSupervisor.d.ts +141 -0
  86. package/dist/cjs/internal/ProcessSupervisor.d.ts.map +1 -0
  87. package/dist/cjs/internal/ProcessSupervisor.js +682 -0
  88. package/dist/cjs/internal/ProcessSupervisor.js.map +7 -0
  89. package/dist/cjs/internal/SupervisorProgram.d.ts +9 -0
  90. package/dist/cjs/internal/SupervisorProgram.d.ts.map +1 -0
  91. package/dist/cjs/internal/SupervisorProgram.js +306 -0
  92. package/dist/cjs/internal/SupervisorProgram.js.map +7 -0
  93. package/dist/cjs/internal/WindowsProcessJob.d.ts +39 -0
  94. package/dist/cjs/internal/WindowsProcessJob.d.ts.map +1 -0
  95. package/dist/cjs/internal/WindowsProcessJob.js +113 -0
  96. package/dist/cjs/internal/WindowsProcessJob.js.map +7 -0
  97. package/dist/cjs/package.json +1 -0
  98. package/dist/esm/AtomicFileSystem.d.ts +147 -0
  99. package/dist/esm/AtomicFileSystem.d.ts.map +1 -0
  100. package/dist/esm/AtomicFileSystem.js +290 -0
  101. package/dist/esm/AtomicFileSystem.js.map +1 -0
  102. package/dist/esm/EgressHttpClient.d.ts +106 -0
  103. package/dist/esm/EgressHttpClient.d.ts.map +1 -0
  104. package/dist/esm/EgressHttpClient.js +259 -0
  105. package/dist/esm/EgressHttpClient.js.map +1 -0
  106. package/dist/esm/HostLiveness.d.ts +81 -0
  107. package/dist/esm/HostLiveness.d.ts.map +1 -0
  108. package/dist/esm/HostLiveness.js +68 -0
  109. package/dist/esm/HostLiveness.js.map +1 -0
  110. package/dist/esm/NodeHost.d.ts +145 -0
  111. package/dist/esm/NodeHost.d.ts.map +1 -0
  112. package/dist/esm/NodeHost.js +162 -0
  113. package/dist/esm/NodeHost.js.map +1 -0
  114. package/dist/esm/ProcessConfinement.d.ts +61 -0
  115. package/dist/esm/ProcessConfinement.d.ts.map +1 -0
  116. package/dist/esm/ProcessConfinement.js +311 -0
  117. package/dist/esm/ProcessConfinement.js.map +1 -0
  118. package/dist/esm/ProcessReaper.d.ts +457 -0
  119. package/dist/esm/ProcessReaper.d.ts.map +1 -0
  120. package/dist/esm/ProcessReaper.js +711 -0
  121. package/dist/esm/ProcessReaper.js.map +1 -0
  122. package/dist/esm/ProcessSandbox.d.ts +377 -0
  123. package/dist/esm/ProcessSandbox.d.ts.map +1 -0
  124. package/dist/esm/ProcessSandbox.js +1014 -0
  125. package/dist/esm/ProcessSandbox.js.map +1 -0
  126. package/dist/esm/ScopedProcess.d.ts +65 -0
  127. package/dist/esm/ScopedProcess.d.ts.map +1 -0
  128. package/dist/esm/ScopedProcess.js +66 -0
  129. package/dist/esm/ScopedProcess.js.map +1 -0
  130. package/dist/esm/index.d.ts +38 -0
  131. package/dist/esm/index.d.ts.map +1 -0
  132. package/dist/esm/index.js +38 -0
  133. package/dist/esm/index.js.map +1 -0
  134. package/dist/esm/internal/AtomicFileSystemExecutable.d.ts +61 -0
  135. package/dist/esm/internal/AtomicFileSystemExecutable.d.ts.map +1 -0
  136. package/dist/esm/internal/AtomicFileSystemExecutable.js +350 -0
  137. package/dist/esm/internal/AtomicFileSystemExecutable.js.map +1 -0
  138. package/dist/esm/internal/AtomicFileSystemProtocol.d.ts +78 -0
  139. package/dist/esm/internal/AtomicFileSystemProtocol.d.ts.map +1 -0
  140. package/dist/esm/internal/AtomicFileSystemProtocol.js +412 -0
  141. package/dist/esm/internal/AtomicFileSystemProtocol.js.map +1 -0
  142. package/dist/esm/internal/AtomicFileSystemTransport.d.ts +44 -0
  143. package/dist/esm/internal/AtomicFileSystemTransport.d.ts.map +1 -0
  144. package/dist/esm/internal/AtomicFileSystemTransport.js +242 -0
  145. package/dist/esm/internal/AtomicFileSystemTransport.js.map +1 -0
  146. package/dist/esm/internal/AtomicHelperError.d.ts +35 -0
  147. package/dist/esm/internal/AtomicHelperError.d.ts.map +1 -0
  148. package/dist/esm/internal/AtomicHelperError.js +39 -0
  149. package/dist/esm/internal/AtomicHelperError.js.map +1 -0
  150. package/dist/esm/internal/AtomicHelperFault.d.ts +59 -0
  151. package/dist/esm/internal/AtomicHelperFault.d.ts.map +1 -0
  152. package/dist/esm/internal/AtomicHelperFault.js +66 -0
  153. package/dist/esm/internal/AtomicHelperFault.js.map +1 -0
  154. package/dist/esm/internal/PipedProcess.d.ts +35 -0
  155. package/dist/esm/internal/PipedProcess.d.ts.map +1 -0
  156. package/dist/esm/internal/PipedProcess.js +321 -0
  157. package/dist/esm/internal/PipedProcess.js.map +1 -0
  158. package/dist/esm/internal/ProcSnapshot.d.ts +85 -0
  159. package/dist/esm/internal/ProcSnapshot.d.ts.map +1 -0
  160. package/dist/esm/internal/ProcSnapshot.js +188 -0
  161. package/dist/esm/internal/ProcSnapshot.js.map +1 -0
  162. package/dist/esm/internal/ProcessCleanup.d.ts +64 -0
  163. package/dist/esm/internal/ProcessCleanup.d.ts.map +1 -0
  164. package/dist/esm/internal/ProcessCleanup.js +45 -0
  165. package/dist/esm/internal/ProcessCleanup.js.map +1 -0
  166. package/dist/esm/internal/ProcessCleanupPolicyError.d.ts +30 -0
  167. package/dist/esm/internal/ProcessCleanupPolicyError.d.ts.map +1 -0
  168. package/dist/esm/internal/ProcessCleanupPolicyError.js +25 -0
  169. package/dist/esm/internal/ProcessCleanupPolicyError.js.map +1 -0
  170. package/dist/esm/internal/ProcessFault.d.ts +63 -0
  171. package/dist/esm/internal/ProcessFault.d.ts.map +1 -0
  172. package/dist/esm/internal/ProcessFault.js +64 -0
  173. package/dist/esm/internal/ProcessFault.js.map +1 -0
  174. package/dist/esm/internal/ProcessSupervisor.d.ts +141 -0
  175. package/dist/esm/internal/ProcessSupervisor.d.ts.map +1 -0
  176. package/dist/esm/internal/ProcessSupervisor.js +682 -0
  177. package/dist/esm/internal/ProcessSupervisor.js.map +1 -0
  178. package/dist/esm/internal/SupervisorProgram.d.ts +9 -0
  179. package/dist/esm/internal/SupervisorProgram.d.ts.map +1 -0
  180. package/dist/esm/internal/SupervisorProgram.js +290 -0
  181. package/dist/esm/internal/SupervisorProgram.js.map +1 -0
  182. package/dist/esm/internal/WindowsProcessJob.d.ts +39 -0
  183. package/dist/esm/internal/WindowsProcessJob.d.ts.map +1 -0
  184. package/dist/esm/internal/WindowsProcessJob.js +115 -0
  185. package/dist/esm/internal/WindowsProcessJob.js.map +1 -0
  186. package/dist/vendor/adapters.json +1757 -0
  187. package/dist/vendor/chunk-2Y7WC4KU.js +109 -0
  188. package/dist/vendor/chunk-3GR5VZ2M.js +118 -0
  189. package/dist/vendor/chunk-42QKY6T6.js +40 -0
  190. package/dist/vendor/chunk-54RLBMTZ.js +61 -0
  191. package/dist/vendor/chunk-5ZMHBXIK.js +55 -0
  192. package/dist/vendor/chunk-6ME2W7TF.js +28 -0
  193. package/dist/vendor/chunk-AMQKOQNT.js +34 -0
  194. package/dist/vendor/chunk-B3FZ67QU.js +59 -0
  195. package/dist/vendor/chunk-BKTBUBQY.js +437 -0
  196. package/dist/vendor/chunk-DS7GF46S.js +34 -0
  197. package/dist/vendor/chunk-DVOAHXPG.js +104 -0
  198. package/dist/vendor/chunk-DWSHTSVX.js +229 -0
  199. package/dist/vendor/chunk-EGBNVEEW.js +14 -0
  200. package/dist/vendor/chunk-EZWY45HP.js +18 -0
  201. package/dist/vendor/chunk-FNZOJHSV.js +71 -0
  202. package/dist/vendor/chunk-GXCHI7BM.js +131 -0
  203. package/dist/vendor/chunk-IIJ52ZU6.js +20 -0
  204. package/dist/vendor/chunk-J7A4CZDO.js +92 -0
  205. package/dist/vendor/chunk-JG5TTWPT.js +85 -0
  206. package/dist/vendor/chunk-JJ24ARBQ.js +394 -0
  207. package/dist/vendor/chunk-KOZMM5PZ.js +426 -0
  208. package/dist/vendor/chunk-KUUXSID7.js +18 -0
  209. package/dist/vendor/chunk-LEUPYT3Q.js +36 -0
  210. package/dist/vendor/chunk-M32LDTW7.js +18 -0
  211. package/dist/vendor/chunk-M7CLUILA.js +0 -0
  212. package/dist/vendor/chunk-MBSKBQ5D.js +21 -0
  213. package/dist/vendor/chunk-MLKGABMK.js +9 -0
  214. package/dist/vendor/chunk-NBVRTDTV.js +129 -0
  215. package/dist/vendor/chunk-OXNGN5H7.js +81 -0
  216. package/dist/vendor/chunk-QPLDT6A6.js +564 -0
  217. package/dist/vendor/chunk-QYBKAIA6.js +18 -0
  218. package/dist/vendor/chunk-RO2R4MJH.js +62 -0
  219. package/dist/vendor/chunk-SIR62W3Y.js +398 -0
  220. package/dist/vendor/chunk-TFIKFFSC.js +26 -0
  221. package/dist/vendor/chunk-TXKLEBBL.js +143 -0
  222. package/dist/vendor/chunk-UFH5UUZK.js +48 -0
  223. package/dist/vendor/chunk-VANKHF46.js +96 -0
  224. package/dist/vendor/chunk-VZBDV3TV.js +18 -0
  225. package/dist/vendor/chunk-XDKNKFBW.js +22 -0
  226. package/dist/vendor/chunk-XH4U3JSV.js +249 -0
  227. package/dist/vendor/chunk-Y35ZAO5U.js +53 -0
  228. package/dist/vendor/chunk-ZJMRMY57.js +0 -0
  229. package/dist/vendor/cjs/node/NodeChildProcessSpawner.d.ts +7 -0
  230. package/dist/vendor/cjs/node/NodeChildProcessSpawner.js +1 -0
  231. package/dist/vendor/cjs/node/NodeClusterHttp.d.ts +63 -0
  232. package/dist/vendor/cjs/node/NodeClusterHttp.js +1 -0
  233. package/dist/vendor/cjs/node/NodeClusterSocket.d.ts +79 -0
  234. package/dist/vendor/cjs/node/NodeClusterSocket.js +1 -0
  235. package/dist/vendor/cjs/node/NodeCrypto.d.ts +10 -0
  236. package/dist/vendor/cjs/node/NodeCrypto.js +1 -0
  237. package/dist/vendor/cjs/node/NodeFileSystem.d.ts +10 -0
  238. package/dist/vendor/cjs/node/NodeFileSystem.js +1 -0
  239. package/dist/vendor/cjs/node/NodeHttpClient.d.ts +179 -0
  240. package/dist/vendor/cjs/node/NodeHttpClient.js +1 -0
  241. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.d.ts +62 -0
  242. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.js +1 -0
  243. package/dist/vendor/cjs/node/NodeHttpPlatform.d.ts +36 -0
  244. package/dist/vendor/cjs/node/NodeHttpPlatform.js +1 -0
  245. package/dist/vendor/cjs/node/NodeHttpServer.d.ts +120 -0
  246. package/dist/vendor/cjs/node/NodeHttpServer.js +1 -0
  247. package/dist/vendor/cjs/node/NodeHttpServerRequest.d.ts +29 -0
  248. package/dist/vendor/cjs/node/NodeHttpServerRequest.js +1 -0
  249. package/dist/vendor/cjs/node/NodeMultipart.d.ts +46 -0
  250. package/dist/vendor/cjs/node/NodeMultipart.js +1 -0
  251. package/dist/vendor/cjs/node/NodeMultipartParser.d.ts +97 -0
  252. package/dist/vendor/cjs/node/NodeMultipartParser.js +1 -0
  253. package/dist/vendor/cjs/node/NodePath.d.ts +27 -0
  254. package/dist/vendor/cjs/node/NodePath.js +1 -0
  255. package/dist/vendor/cjs/node/NodeRedis.d.ts +74 -0
  256. package/dist/vendor/cjs/node/NodeRedis.js +1 -0
  257. package/dist/vendor/cjs/node/NodeRuntime.d.ts +85 -0
  258. package/dist/vendor/cjs/node/NodeRuntime.js +1 -0
  259. package/dist/vendor/cjs/node/NodeServices.d.ts +34 -0
  260. package/dist/vendor/cjs/node/NodeServices.js +1 -0
  261. package/dist/vendor/cjs/node/NodeSink.d.ts +8 -0
  262. package/dist/vendor/cjs/node/NodeSink.js +1 -0
  263. package/dist/vendor/cjs/node/NodeSocket.d.ts +38 -0
  264. package/dist/vendor/cjs/node/NodeSocket.js +1 -0
  265. package/dist/vendor/cjs/node/NodeSocketServer.d.ts +8 -0
  266. package/dist/vendor/cjs/node/NodeSocketServer.js +1 -0
  267. package/dist/vendor/cjs/node/NodeStdio.d.ts +11 -0
  268. package/dist/vendor/cjs/node/NodeStdio.js +1 -0
  269. package/dist/vendor/cjs/node/NodeStream.d.ts +8 -0
  270. package/dist/vendor/cjs/node/NodeStream.js +1 -0
  271. package/dist/vendor/cjs/node/NodeTerminal.d.ts +21 -0
  272. package/dist/vendor/cjs/node/NodeTerminal.js +1 -0
  273. package/dist/vendor/cjs/node/NodeWorker.d.ts +22 -0
  274. package/dist/vendor/cjs/node/NodeWorker.js +1 -0
  275. package/dist/vendor/cjs/node/NodeWorkerRunner.d.ts +12 -0
  276. package/dist/vendor/cjs/node/NodeWorkerRunner.js +1 -0
  277. package/dist/vendor/cjs/node/Undici.d.ts +28 -0
  278. package/dist/vendor/cjs/node/Undici.js +1 -0
  279. package/dist/vendor/cjs/node/index.d.ts +100 -0
  280. package/dist/vendor/cjs/node/index.js +1 -0
  281. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  282. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.js +1 -0
  283. package/dist/vendor/cjs/node-shared/NodeClusterSocket.d.ts +22 -0
  284. package/dist/vendor/cjs/node-shared/NodeClusterSocket.js +1 -0
  285. package/dist/vendor/cjs/node-shared/NodeCrypto.d.ts +27 -0
  286. package/dist/vendor/cjs/node-shared/NodeCrypto.js +1 -0
  287. package/dist/vendor/cjs/node-shared/NodeFileSystem.d.ts +11 -0
  288. package/dist/vendor/cjs/node-shared/NodeFileSystem.js +1 -0
  289. package/dist/vendor/cjs/node-shared/NodeHttpCompression.d.ts +36 -0
  290. package/dist/vendor/cjs/node-shared/NodeHttpCompression.js +1 -0
  291. package/dist/vendor/cjs/node-shared/NodePath.d.ts +27 -0
  292. package/dist/vendor/cjs/node-shared/NodePath.js +1 -0
  293. package/dist/vendor/cjs/node-shared/NodeRuntime.d.ts +47 -0
  294. package/dist/vendor/cjs/node-shared/NodeRuntime.js +1 -0
  295. package/dist/vendor/cjs/node-shared/NodeSink.d.ts +69 -0
  296. package/dist/vendor/cjs/node-shared/NodeSink.js +1 -0
  297. package/dist/vendor/cjs/node-shared/NodeSocket.d.ts +134 -0
  298. package/dist/vendor/cjs/node-shared/NodeSocket.js +1 -0
  299. package/dist/vendor/cjs/node-shared/NodeSocketServer.d.ts +80 -0
  300. package/dist/vendor/cjs/node-shared/NodeSocketServer.js +1 -0
  301. package/dist/vendor/cjs/node-shared/NodeStdio.d.ts +12 -0
  302. package/dist/vendor/cjs/node-shared/NodeStdio.js +1 -0
  303. package/dist/vendor/cjs/node-shared/NodeStream.d.ts +179 -0
  304. package/dist/vendor/cjs/node-shared/NodeStream.js +1 -0
  305. package/dist/vendor/cjs/node-shared/NodeTerminal.d.ts +22 -0
  306. package/dist/vendor/cjs/node-shared/NodeTerminal.js +1 -0
  307. package/dist/vendor/cjs/node-shared/index.d.ts +56 -0
  308. package/dist/vendor/cjs/node-shared/index.js +1 -0
  309. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  310. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.js +1 -0
  311. package/dist/vendor/cjs/node-shared/internal/utils.d.ts +2 -0
  312. package/dist/vendor/cjs/node-shared/internal/utils.js +1 -0
  313. package/dist/vendor/cjs/package.json +1 -0
  314. package/dist/vendor/cjs/platform-node-shared.d.ts +56 -0
  315. package/dist/vendor/cjs/platform-node-shared.js +1 -0
  316. package/dist/vendor/cjs/platform-node.d.ts +100 -0
  317. package/dist/vendor/cjs/platform-node.js +1 -0
  318. package/dist/vendor/node/LICENSE +21 -0
  319. package/dist/vendor/node/NodeChildProcessSpawner.d.ts +7 -0
  320. package/dist/vendor/node/NodeChildProcessSpawner.js +14 -0
  321. package/dist/vendor/node/NodeClusterHttp.d.ts +63 -0
  322. package/dist/vendor/node/NodeClusterHttp.js +43 -0
  323. package/dist/vendor/node/NodeClusterSocket.d.ts +79 -0
  324. package/dist/vendor/node/NodeClusterSocket.js +30 -0
  325. package/dist/vendor/node/NodeCrypto.d.ts +10 -0
  326. package/dist/vendor/node/NodeCrypto.js +8 -0
  327. package/dist/vendor/node/NodeFileSystem.d.ts +10 -0
  328. package/dist/vendor/node/NodeFileSystem.js +9 -0
  329. package/dist/vendor/node/NodeHttpClient.d.ts +179 -0
  330. package/dist/vendor/node/NodeHttpClient.js +46 -0
  331. package/dist/vendor/node/NodeHttpIncomingMessage.d.ts +62 -0
  332. package/dist/vendor/node/NodeHttpIncomingMessage.js +10 -0
  333. package/dist/vendor/node/NodeHttpPlatform.d.ts +36 -0
  334. package/dist/vendor/node/NodeHttpPlatform.js +16 -0
  335. package/dist/vendor/node/NodeHttpServer.d.ts +120 -0
  336. package/dist/vendor/node/NodeHttpServer.js +46 -0
  337. package/dist/vendor/node/NodeHttpServerRequest.d.ts +29 -0
  338. package/dist/vendor/node/NodeHttpServerRequest.js +9 -0
  339. package/dist/vendor/node/NodeMultipart.d.ts +46 -0
  340. package/dist/vendor/node/NodeMultipart.js +15 -0
  341. package/dist/vendor/node/NodeMultipartParser.d.ts +97 -0
  342. package/dist/vendor/node/NodeMultipartParser.js +11 -0
  343. package/dist/vendor/node/NodePath.d.ts +27 -0
  344. package/dist/vendor/node/NodePath.js +12 -0
  345. package/dist/vendor/node/NodeRedis.d.ts +74 -0
  346. package/dist/vendor/node/NodeRedis.js +11 -0
  347. package/dist/vendor/node/NodeRuntime.d.ts +85 -0
  348. package/dist/vendor/node/NodeRuntime.js +8 -0
  349. package/dist/vendor/node/NodeServices.d.ts +34 -0
  350. package/dist/vendor/node/NodeServices.js +23 -0
  351. package/dist/vendor/node/NodeSink.d.ts +8 -0
  352. package/dist/vendor/node/NodeSink.js +12 -0
  353. package/dist/vendor/node/NodeSocket.d.ts +38 -0
  354. package/dist/vendor/node/NodeSocket.js +31 -0
  355. package/dist/vendor/node/NodeSocketServer.d.ts +8 -0
  356. package/dist/vendor/node/NodeSocketServer.js +21 -0
  357. package/dist/vendor/node/NodeStdio.d.ts +11 -0
  358. package/dist/vendor/node/NodeStdio.js +10 -0
  359. package/dist/vendor/node/NodeStream.d.ts +8 -0
  360. package/dist/vendor/node/NodeStream.js +27 -0
  361. package/dist/vendor/node/NodeTerminal.d.ts +21 -0
  362. package/dist/vendor/node/NodeTerminal.js +10 -0
  363. package/dist/vendor/node/NodeWorker.d.ts +22 -0
  364. package/dist/vendor/node/NodeWorker.js +9 -0
  365. package/dist/vendor/node/NodeWorkerRunner.d.ts +12 -0
  366. package/dist/vendor/node/NodeWorkerRunner.js +7 -0
  367. package/dist/vendor/node/Undici.d.ts +28 -0
  368. package/dist/vendor/node/Undici.js +9 -0
  369. package/dist/vendor/node/index.d.ts +100 -0
  370. package/dist/vendor/node/index.js +115 -0
  371. package/dist/vendor/node-shared/LICENSE +21 -0
  372. package/dist/vendor/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  373. package/dist/vendor/node-shared/NodeChildProcessSpawner.js +13 -0
  374. package/dist/vendor/node-shared/NodeClusterSocket.d.ts +22 -0
  375. package/dist/vendor/node-shared/NodeClusterSocket.js +11 -0
  376. package/dist/vendor/node-shared/NodeCrypto.d.ts +27 -0
  377. package/dist/vendor/node-shared/NodeCrypto.js +9 -0
  378. package/dist/vendor/node-shared/NodeFileSystem.d.ts +11 -0
  379. package/dist/vendor/node-shared/NodeFileSystem.js +8 -0
  380. package/dist/vendor/node-shared/NodeHttpCompression.d.ts +36 -0
  381. package/dist/vendor/node-shared/NodeHttpCompression.js +13 -0
  382. package/dist/vendor/node-shared/NodePath.d.ts +27 -0
  383. package/dist/vendor/node-shared/NodePath.js +11 -0
  384. package/dist/vendor/node-shared/NodeRuntime.d.ts +47 -0
  385. package/dist/vendor/node-shared/NodeRuntime.js +7 -0
  386. package/dist/vendor/node-shared/NodeSink.d.ts +69 -0
  387. package/dist/vendor/node-shared/NodeSink.js +11 -0
  388. package/dist/vendor/node-shared/NodeSocket.d.ts +134 -0
  389. package/dist/vendor/node-shared/NodeSocket.js +23 -0
  390. package/dist/vendor/node-shared/NodeSocketServer.d.ts +80 -0
  391. package/dist/vendor/node-shared/NodeSocketServer.js +20 -0
  392. package/dist/vendor/node-shared/NodeStdio.d.ts +12 -0
  393. package/dist/vendor/node-shared/NodeStdio.js +9 -0
  394. package/dist/vendor/node-shared/NodeStream.d.ts +179 -0
  395. package/dist/vendor/node-shared/NodeStream.js +26 -0
  396. package/dist/vendor/node-shared/NodeTerminal.d.ts +22 -0
  397. package/dist/vendor/node-shared/NodeTerminal.js +9 -0
  398. package/dist/vendor/node-shared/index.d.ts +56 -0
  399. package/dist/vendor/node-shared/index.js +58 -0
  400. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  401. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.js +7 -0
  402. package/dist/vendor/node-shared/internal/utils.d.ts +2 -0
  403. package/dist/vendor/node-shared/internal/utils.js +7 -0
  404. package/dist/vendor/platform-node-shared.d.ts +56 -0
  405. package/dist/vendor/platform-node-shared.js +58 -0
  406. package/dist/vendor/platform-node.d.ts +100 -0
  407. package/dist/vendor/platform-node.js +115 -0
  408. package/docs/README.md +129 -0
  409. package/docs/api.md +552 -0
  410. package/docs/concepts/descriptor-relative-filesystem.md +181 -0
  411. package/docs/concepts/host-bundle.md +125 -0
  412. package/docs/concepts/process-containment.md +182 -0
  413. package/docs/guides/answer-run-ownership.md +87 -0
  414. package/docs/guides/configure-the-filesystem-helper.md +152 -0
  415. package/docs/guides/contain-child-processes.md +139 -0
  416. package/docs/guides/match-files-with-glob.md +151 -0
  417. package/docs/installation.md +204 -0
  418. package/docs/quickstart.md +147 -0
  419. package/docs/troubleshooting.md +130 -0
  420. package/package.json +202 -3
  421. package/src/AtomicFileSystem.ts +415 -0
  422. package/src/EgressHttpClient.ts +327 -0
  423. package/src/HostLiveness.ts +103 -0
  424. package/src/NodeHost.ts +253 -0
  425. package/src/ProcessConfinement.ts +356 -0
  426. package/src/ProcessReaper.ts +951 -0
  427. package/src/ProcessSandbox.ts +1273 -0
  428. package/src/ScopedProcess.ts +123 -0
  429. package/src/index.ts +44 -0
  430. package/src/internal/AtomicFileSystemExecutable.ts +379 -0
  431. package/src/internal/AtomicFileSystemProtocol.ts +502 -0
  432. package/src/internal/AtomicFileSystemTransport.ts +272 -0
  433. package/src/internal/AtomicHelperError.ts +50 -0
  434. package/src/internal/AtomicHelperFault.ts +84 -0
  435. package/src/internal/PipedProcess.ts +357 -0
  436. package/src/internal/ProcSnapshot.ts +193 -0
  437. package/src/internal/ProcessCleanup.ts +99 -0
  438. package/src/internal/ProcessCleanupPolicyError.ts +30 -0
  439. package/src/internal/ProcessFault.ts +82 -0
  440. package/src/internal/ProcessSupervisor.ts +801 -0
  441. package/src/internal/SupervisorProgram.ts +289 -0
  442. package/src/internal/WindowsProcessJob.ts +114 -0
@@ -0,0 +1,1273 @@
1
+ /**
2
+ * The one operating-system sandbox every confined process goes through.
3
+ *
4
+ * A caller resolves what a run may touch, a {@link Request}: the network
5
+ * posture, the read and write sets, and optionally which mechanism it wants.
6
+ * This module turns the request into an argv the host enforces, or into an
7
+ * {@link Unenforceable} refusal naming what the host lacks. Two callers share
8
+ * it: the build graph confines every target's tool run under its declared
9
+ * inputs and outputs (`@smthrs/targets/ExecSandbox`), and the Node
10
+ * `ProcessConfinement` confines every approved agent command under the
11
+ * grants in force.
12
+ *
13
+ * Three mechanisms, one contract:
14
+ *
15
+ * - Linux: bubblewrap. An empty root contains only enumerated system/runtime
16
+ * paths and declared reads and writes. The workspace and root are remounted
17
+ * read-only after mounting grants. A private tmp supplies HOME. The network
18
+ * namespace is unshared unless explicitly opened; loopback-only is refused.
19
+ * - macOS: seatbelt (`sandbox-exec`). Host reads are denied except enumerated
20
+ * system/runtime paths, declared inputs, external reads, writes and private
21
+ * tmp. Host credential locations are re-closed under broad grants. Writes
22
+ * and network access are denied except what the request opens.
23
+ * - Docker: `docker run` with a read-only root, declared reads mounted
24
+ * read-only at their host paths, declared writes read-write, `--network
25
+ * none` unless the request opens it. The image supplies the toolchain. It is
26
+ * the path on Windows and on any host without the native mechanism.
27
+ *
28
+ * Every declared confinement is enforced or the caller fails closed. Nothing
29
+ * here logs "unenforced" and carries on; the one caller that may run
30
+ * unconfined when no mechanism exists, `ProcessConfinement`, says so in its
31
+ * own options and never on this module's behalf.
32
+ *
33
+ * @since 1.0.0-rc.1
34
+ */
35
+
36
+ import { execFileSync } from "node:child_process"
37
+ import { randomUUID } from "node:crypto"
38
+ import * as NodeFs from "node:fs"
39
+ import * as NodeOs from "node:os"
40
+ import * as NodePath from "node:path"
41
+
42
+ /**
43
+ * The network posture a run is confined to. `"none"` closes IP networking.
44
+ * Build tools retain host Unix sockets by default. Native agent confinement
45
+ * additionally closes host Unix sockets regardless of the IP posture, except
46
+ * macOS's system DNS resolver endpoint when IP networking is open, and
47
+ * refuses all Linux socket creation when IP networking is closed;
48
+ * socketpair remains available for communication inside the process family.
49
+ *
50
+ * @category models
51
+ * @since 1.0.0-rc.1
52
+ */
53
+ export type Network = "none" | "loopback" | "open"
54
+
55
+ /**
56
+ * Which mechanism a request asks for. Absent means the platform's native
57
+ * mechanism: bubblewrap on Linux, seatbelt on macOS.
58
+ *
59
+ * @category models
60
+ * @since 1.0.0-rc.1
61
+ */
62
+ export type Selection =
63
+ | { readonly _tag: "bubblewrap" }
64
+ | { readonly _tag: "docker"; readonly image: string }
65
+
66
+ /**
67
+ * What a caller asks for: the network posture, the mechanism choice, and the
68
+ * read and write sets. Every path is workspace-relative and posix-shaped;
69
+ * a read may name a file or a directory, a write names a directory. A
70
+ * `readOnly` entry re-closes a subtree under a declared write.
71
+ *
72
+ * @category models
73
+ * @since 1.0.0-rc.1
74
+ */
75
+ export interface Request {
76
+ readonly network: Network
77
+ /** Whether host Unix sockets are permitted; defaults to true for build tools. */
78
+ readonly unixSockets?: boolean | undefined
79
+ /** Closed-by-default process and IPC policy for untrusted native commands. */
80
+ readonly strict?: boolean | undefined
81
+ readonly mechanism?: Selection | undefined
82
+ readonly reads: ReadonlyArray<string>
83
+ readonly writes: ReadonlyArray<string>
84
+ /**
85
+ * Declared outputs whose leaf is a file rather than a directory. A file
86
+ * cannot be bound writable on its own and does not exist before the tool
87
+ * writes it, so its parent directory becomes the writable bind. Kept apart
88
+ * from `writes` because only the caller knows which of the two a
89
+ * declaration meant: inferring it from the path would hand a not-yet-created
90
+ * `.cargo-home` or `dist.new` its parent, and a parent that is the
91
+ * workspace root opens the whole workspace for the run.
92
+ */
93
+ readonly writeFiles?: ReadonlyArray<string> | undefined
94
+ readonly readOnly?: ReadonlyArray<string> | undefined
95
+ /**
96
+ * Absolute host paths outside the workspace the tool reads: a git
97
+ * submodule's local source repository, for one. Bound read-only where the
98
+ * mechanism would otherwise hide them; a path that does not exist, or that
99
+ * sits inside the workspace, is dropped.
100
+ */
101
+ readonly externalReads?: ReadonlyArray<string> | undefined
102
+ }
103
+
104
+ /**
105
+ * The host facts mechanism selection reads. Injectable so every platform's
106
+ * argv can be tested on every other platform.
107
+ *
108
+ * @category models
109
+ * @since 1.0.0-rc.1
110
+ */
111
+ export interface Host {
112
+ readonly platform: NodeJS.Platform
113
+ /** Selected Xcode developer tree on a macOS runner. */
114
+ readonly developerDirectory?: string | undefined
115
+ /** The real host home, used to mask credentials and locate runtime caches. */
116
+ readonly home?: string | undefined
117
+ /** Resolves an executable name on `PATH` to an absolute path, or nothing. */
118
+ readonly executable: (name: string) => string | undefined
119
+ /** Whether a host path exists. */
120
+ readonly exists: (path: string) => boolean
121
+ /** Whether a host path is a directory. */
122
+ readonly isDirectory: (path: string) => boolean
123
+ /** Tests the entry itself, including dangling links; defaults to the real host. */
124
+ readonly isSymbolicLink?: ((path: string) => boolean) | undefined
125
+ /**
126
+ * The names of a directory's direct children, or undefined when the host
127
+ * cannot list it. Optional: a host without it keeps every declared file as
128
+ * its own entry, which is correct and merely long.
129
+ */
130
+ readonly entries?: ((directory: string) => ReadonlyArray<string> | undefined) | undefined
131
+ /**
132
+ * Where a path's bytes actually live once every symbolic link on it is
133
+ * followed, or `undefined` when the host cannot say. Write validation uses
134
+ * the real host when this probe is omitted and refuses unresolved existing
135
+ * paths. Read aliases are only collected when this probe is supplied.
136
+ */
137
+ readonly realpath?: ((path: string) => string | undefined) | undefined
138
+ readonly uid: number | undefined
139
+ readonly gid: number | undefined
140
+ }
141
+
142
+ /**
143
+ * One selected enforcement mechanism.
144
+ *
145
+ * @category models
146
+ * @since 1.0.0-rc.1
147
+ */
148
+ export type Mechanism =
149
+ | { readonly _tag: "bubblewrap"; readonly executable: string }
150
+ | { readonly _tag: "seatbelt"; readonly executable: string }
151
+ | { readonly _tag: "docker"; readonly executable: string; readonly image: string }
152
+ | { readonly _tag: "none" }
153
+
154
+ /**
155
+ * The refusal a host that cannot enforce a declared confinement produces.
156
+ * `mechanism` names what was selected, `missing` what the host lacks.
157
+ *
158
+ * @category models
159
+ * @since 1.0.0-rc.1
160
+ */
161
+ export interface Unenforceable {
162
+ readonly _tag: "@smthrs/platform-node/SandboxUnenforceable"
163
+ readonly platform: NodeJS.Platform
164
+ readonly mechanism: string
165
+ readonly missing: string
166
+ readonly message: string
167
+ }
168
+
169
+ /**
170
+ * Checks for an {@link Unenforceable} refusal.
171
+ *
172
+ * @category guards
173
+ * @since 1.0.0-rc.1
174
+ */
175
+ export const isUnenforceable = (value: unknown): value is Unenforceable =>
176
+ typeof value === "object" && value !== null &&
177
+ (value as { readonly _tag?: unknown })._tag === "@smthrs/platform-node/SandboxUnenforceable"
178
+
179
+ /**
180
+ * Builds an {@link Unenforceable} refusal.
181
+ *
182
+ * @category constructors
183
+ * @since 1.0.0-rc.1
184
+ */
185
+ export const unenforceable = (
186
+ platform: NodeJS.Platform,
187
+ mechanism: string,
188
+ missing: string,
189
+ detail: string
190
+ ): Unenforceable => ({
191
+ _tag: "@smthrs/platform-node/SandboxUnenforceable",
192
+ platform,
193
+ mechanism,
194
+ missing,
195
+ message: `sandbox: the declared confinement cannot be enforced on this ${platform} host: ${detail}.`
196
+ })
197
+
198
+ /**
199
+ * The resolved confinement one spawn runs under. Every path is absolute and
200
+ * canonical.
201
+ *
202
+ * @category models
203
+ * @since 1.0.0-rc.1
204
+ */
205
+ export interface Plan {
206
+ readonly mechanism: Exclude<Mechanism, { readonly _tag: "none" }>
207
+ readonly network: Network
208
+ /** Whether host Unix sockets are permitted; defaults to true for build tools. */
209
+ readonly unixSockets?: boolean | undefined
210
+ /** Closed-by-default process and IPC policy for untrusted native commands. */
211
+ readonly strict?: boolean | undefined
212
+ readonly workspaceRoot: string
213
+ readonly cwd: string
214
+ readonly reads: ReadonlyArray<string>
215
+ /**
216
+ * Paths inside a granted directory that the declaration did not cover,
217
+ * re-closed after the grant. Only a mechanism with deny rules (seatbelt)
218
+ * folds; the others keep every declared file as its own entry.
219
+ */
220
+ readonly readDenies: ReadonlyArray<string>
221
+ readonly writes: ReadonlyArray<string>
222
+ readonly readOnly: ReadonlyArray<string>
223
+ /**
224
+ * Where a declared read's bytes actually live when the path reaches them
225
+ * through a symbolic link that leaves the workspace.
226
+ *
227
+ * Bubblewrap needs them because it replaces `/tmp` with a private tmpfs, so
228
+ * a link into the host's `/tmp` (the scratch tree's `node_modules`, which
229
+ * points back at the real workspace) resolves to nothing inside the sandbox.
230
+ * Binding the real location read-only makes the link resolve to the same
231
+ * bytes the host sees, which is what declaring the read asked for. Docker
232
+ * mounts them for the same reason: the image supplies everything outside
233
+ * the workspace, and these paths are on the host. Seatbelt leaves the host
234
+ * filesystem in place but must grant the resolved spelling too. The
235
+ * request's own `externalReads` land here too.
236
+ */
237
+ readonly externalReads: ReadonlyArray<string>
238
+ /** A host directory the run may scribble in; created by the caller, removed after. */
239
+ readonly tmp: string
240
+ readonly uid: number | undefined
241
+ readonly gid: number | undefined
242
+ }
243
+
244
+ const pathSeparator = (platform: NodeJS.Platform): string => platform === "win32" ? ";" : ":"
245
+
246
+ /** Ambient host environment for runtime lookup and cache locations. */
247
+ const ambientEnvironment = (): Readonly<Record<string, string | undefined>> => process.env
248
+
249
+ /**
250
+ * The real host: platform, `PATH` lookup, and filesystem probes.
251
+ *
252
+ * @category resolution
253
+ * @since 0.1.0
254
+ */
255
+ export const host = (env: Readonly<Record<string, string | undefined>> = ambientEnvironment()): Host => {
256
+ const platform = process.platform
257
+ let developerDirectory = env["DEVELOPER_DIR"]
258
+ if (platform === "darwin" && developerDirectory === undefined) {
259
+ try {
260
+ developerDirectory = execFileSync("/usr/bin/xcode-select", ["-p"], { encoding: "utf8", timeout: 2_000 }).trim()
261
+ } catch {
262
+ // A host without Xcode needs no developer-tree grant.
263
+ }
264
+ }
265
+ const entries = (env["PATH"] ?? "").split(pathSeparator(platform)).filter((entry) => entry !== "")
266
+ const extensions = platform === "win32" ? (env["PATHEXT"] ?? ".EXE;.CMD;.BAT").split(";") : [""]
267
+ return {
268
+ platform,
269
+ developerDirectory,
270
+ home: NodeOs.homedir(),
271
+ executable: (name) => {
272
+ if (NodePath.isAbsolute(name)) return isExecutable(name) ? name : undefined
273
+ for (const entry of entries) {
274
+ for (const extension of extensions) {
275
+ const candidate = NodePath.join(entry, name + extension)
276
+ if (isExecutable(candidate)) return candidate
277
+ }
278
+ }
279
+ return undefined
280
+ },
281
+ exists: (path) => NodeFs.existsSync(path),
282
+ isSymbolicLink,
283
+ isDirectory: (path) => {
284
+ try {
285
+ return NodeFs.statSync(path).isDirectory()
286
+ } catch {
287
+ return false
288
+ }
289
+ },
290
+ entries: (directory) => {
291
+ try {
292
+ return NodeFs.readdirSync(directory)
293
+ } catch {
294
+ return undefined
295
+ }
296
+ },
297
+ realpath: (path) => {
298
+ try {
299
+ return NodeFs.realpathSync(path)
300
+ } catch {
301
+ return undefined
302
+ }
303
+ },
304
+ uid: typeof process.getuid === "function" ? process.getuid() : undefined,
305
+ gid: typeof process.getgid === "function" ? process.getgid() : undefined
306
+ }
307
+ }
308
+
309
+ const isExecutable = (path: string): boolean => {
310
+ try {
311
+ NodeFs.accessSync(path, NodeFs.constants.X_OK)
312
+ return NodeFs.statSync(path).isFile()
313
+ } catch {
314
+ return false
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Selects the mechanism a request runs under on a host.
320
+ *
321
+ * A docker selection is honored when `docker` is on `PATH` and refused
322
+ * otherwise; a bubblewrap selection is refused off Linux. With no selection
323
+ * the platform decides: bubblewrap on Linux, seatbelt on macOS, and a refusal
324
+ * elsewhere, because Windows has no user-level process sandbox and Docker
325
+ * needs an image only the caller can name.
326
+ *
327
+ * @category resolution
328
+ * @since 1.0.0-rc.1
329
+ */
330
+ export const select = (
331
+ request: Pick<Request, "network" | "mechanism">,
332
+ hostFacts: Host
333
+ ): Exclude<Mechanism, { readonly _tag: "none" }> | Unenforceable => {
334
+ const declared = request.mechanism
335
+ const platform = hostFacts.platform
336
+ if (declared !== undefined && declared._tag === "docker") {
337
+ const executable = hostFacts.executable("docker")
338
+ if (executable === undefined) {
339
+ return unenforceable(platform, "docker", "docker", "a docker mechanism is selected but docker is not on PATH")
340
+ }
341
+ return { _tag: "docker", executable, image: declared.image }
342
+ }
343
+ if (declared !== undefined && platform !== "linux") {
344
+ return unenforceable(
345
+ platform,
346
+ "bubblewrap",
347
+ "linux",
348
+ "bubblewrap is selected but runs only on Linux; on macOS the seatbelt mechanism is selected when no mechanism is named"
349
+ )
350
+ }
351
+ if (platform === "linux") {
352
+ const executable = hostFacts.executable("bwrap")
353
+ if (executable === undefined) {
354
+ return unenforceable(platform, "bubblewrap", "bwrap", "bwrap is not on PATH (install the bubblewrap package)")
355
+ }
356
+ if (request.network === "loopback") {
357
+ return unenforceable(
358
+ platform,
359
+ "bubblewrap",
360
+ "network: true",
361
+ "bubblewrap cannot expose only the host loopback interface: sharing that interface also grants full host networking; " +
362
+ "open the whole network to opt into that access"
363
+ )
364
+ }
365
+ return { _tag: "bubblewrap", executable }
366
+ }
367
+ if (platform === "darwin") {
368
+ const executable = hostFacts.executable("/usr/bin/sandbox-exec")
369
+ if (executable === undefined) {
370
+ return unenforceable(platform, "seatbelt", "/usr/bin/sandbox-exec", "/usr/bin/sandbox-exec is missing")
371
+ }
372
+ return { _tag: "seatbelt", executable }
373
+ }
374
+ return unenforceable(
375
+ platform,
376
+ "docker",
377
+ "docker image",
378
+ `${platform} has no process sandbox of its own; select a docker mechanism with an image`
379
+ )
380
+ }
381
+
382
+ const toPosix = (path: string): string => path.split(NodePath.sep).join("/")
383
+
384
+ /**
385
+ * The path with the symlinks of its existing ancestors resolved. A path that
386
+ * does not exist yet keeps its unresolved tail below the deepest ancestor
387
+ * that does; a path with no existing ancestor is returned as given.
388
+ */
389
+ const realized = (path: string): string => {
390
+ let head = path
391
+ const tail: Array<string> = []
392
+ while (head !== NodePath.dirname(head)) {
393
+ try {
394
+ const real = NodeFs.realpathSync(head)
395
+ return tail.length === 0 ? real : NodePath.join(real, ...tail)
396
+ } catch {
397
+ tail.unshift(NodePath.basename(head))
398
+ head = NodePath.dirname(head)
399
+ }
400
+ }
401
+ return path
402
+ }
403
+
404
+ const insideRoot = (root: string, candidate: string): boolean => {
405
+ const relative = NodePath.relative(root, candidate)
406
+ return relative === "" ||
407
+ (relative !== ".." && !relative.startsWith(`..${NodePath.sep}`) && !NodePath.isAbsolute(relative))
408
+ }
409
+
410
+ /** Unlike exists/stat, lstat sees dangling links and does not follow them. */
411
+ const isSymbolicLink = (path: string): boolean => {
412
+ try {
413
+ return NodeFs.lstatSync(path).isSymbolicLink()
414
+ } catch (cause) {
415
+ if ((cause as NodeJS.ErrnoException).code === "ENOENT") return false
416
+ throw cause
417
+ }
418
+ }
419
+
420
+ /** Check each component, including existing ancestors of a missing output. */
421
+ const safeWrite = (root: string, path: string, hostFacts: Host): boolean => {
422
+ if (!insideRoot(root, path)) return false
423
+ try {
424
+ const realpath = hostFacts.realpath ?? NodeFs.realpathSync
425
+ const link = hostFacts.isSymbolicLink ?? isSymbolicLink
426
+ if (realpath(root) !== root) return false
427
+ let current = root
428
+ for (const component of NodePath.relative(root, path).split(NodePath.sep).filter(Boolean)) {
429
+ current = NodePath.join(current, component)
430
+ if (link(current)) return false
431
+ if (hostFacts.exists(current) && realpath(current) !== current) return false
432
+ }
433
+ return true
434
+ } catch {
435
+ return false
436
+ }
437
+ }
438
+
439
+ const unsafeWrite = (platform: NodeJS.Platform, mechanism: string, path: string): Unenforceable =>
440
+ unenforceable(
441
+ platform,
442
+ mechanism,
443
+ "canonical workspace write path",
444
+ `write path ${path} has a symbolic link, an unresolved ancestor, or leaves the canonical workspace`
445
+ )
446
+
447
+ /**
448
+ * Rechecks write grants before directory creation and before rendering. Throws
449
+ * an {@link Unenforceable} refusal if a path changed since planning. Callers
450
+ * must keep the workspace stable until the operating system consumes mounts.
451
+ *
452
+ * @category resolution
453
+ * @since 0.1.0
454
+ */
455
+ export const validateWrites = (confinement: Plan, hostFacts: Host = host()): void => {
456
+ for (const write of confinement.writes) {
457
+ if (!safeWrite(confinement.workspaceRoot, write, hostFacts)) {
458
+ throw unsafeWrite(hostFacts.platform, confinement.mechanism._tag, write)
459
+ }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Folds a declared file set into directories plus the entries to re-close.
465
+ *
466
+ * The seatbelt compiler caps one rule's data at 64 KiB, so a workspace with a
467
+ * few thousand declared sources cannot be granted file by file. Deepest
468
+ * first, a directory in which at least as many entries are covered as not is
469
+ * granted whole, and its uncovered entries are listed for a deny rule that
470
+ * follows the grant. The result admits exactly the declared set plus the
471
+ * granted directories' future entries. A directory whose uncovered entry
472
+ * still holds a covered path deeper down is never folded over that entry,
473
+ * the workspace root is never folded, and a host that cannot list a
474
+ * directory leaves its files as they were. Covered means a declared read, a
475
+ * declared write, or the private tmp.
476
+ */
477
+ const fold = (
478
+ root: string,
479
+ declared: ReadonlyArray<string>,
480
+ alsoCovered: ReadonlyArray<string>,
481
+ hostFacts: Host
482
+ ): { readonly reads: ReadonlyArray<string>; readonly denies: ReadonlyArray<string> } => {
483
+ const entries = hostFacts.entries
484
+ if (entries === undefined) return { reads: declared, denies: [] }
485
+ const covered = new Set([...declared, ...alsoCovered])
486
+ const candidates = new Set<string>()
487
+ for (const path of declared) {
488
+ let current = NodePath.dirname(path)
489
+ while (insideRoot(root, current) && current !== root && !candidates.has(current)) {
490
+ candidates.add(current)
491
+ current = NodePath.dirname(current)
492
+ }
493
+ }
494
+ // An undeclared directory on the way down to a covered write or read-only
495
+ // path is not a candidate, so nothing else would stop its parent being
496
+ // folded over it. The deny that follows the grant names the directory, and
497
+ // because later rules win in SBPL it closes reads on the very subtree the
498
+ // plan grants a write to.
499
+ const holdsCovered = new Set<string>()
500
+ for (const path of alsoCovered) {
501
+ let current = NodePath.dirname(path)
502
+ while (insideRoot(root, current) && current !== root && !holdsCovered.has(current)) {
503
+ holdsCovered.add(current)
504
+ current = NodePath.dirname(current)
505
+ }
506
+ }
507
+ const deepestFirst = [...candidates].sort((left, right) => right.length - left.length || (left < right ? -1 : 1))
508
+ const promoted: Array<string> = []
509
+ const denies: Array<string> = []
510
+ for (const directory of deepestFirst) {
511
+ if (covered.has(directory)) continue
512
+ const children = entries(directory)
513
+ if (children === undefined || children.length === 0) continue
514
+ const uncovered: Array<string> = []
515
+ let coveredCount = 0
516
+ let partial = false
517
+ for (const child of children) {
518
+ const path = NodePath.join(directory, child)
519
+ if (covered.has(path)) coveredCount += 1
520
+ else if (candidates.has(path) || holdsCovered.has(path)) partial = true
521
+ else uncovered.push(path)
522
+ }
523
+ if (partial || coveredCount === 0 || coveredCount < uncovered.length) continue
524
+ covered.add(directory)
525
+ promoted.push(directory)
526
+ denies.push(...uncovered)
527
+ }
528
+ return { reads: [...declared, ...promoted], denies }
529
+ }
530
+
531
+ /**
532
+ * Orders paths so that every subtree is contiguous and its root comes first.
533
+ *
534
+ * Comparing on the path plus its separator is what makes that hold: `a/b` and
535
+ * `a/b/c` sort around `a/b.ts`, because `.` precedes `/`, and a plain
536
+ * comparison would then put a sibling between an ancestor and its child.
537
+ */
538
+ const bySubtree = (left: string, right: string): number => {
539
+ const leftKey = left + NodePath.sep
540
+ const rightKey = right + NodePath.sep
541
+ return leftKey < rightKey ? -1 : 1
542
+ }
543
+
544
+ /**
545
+ * Drops a path covered by another entry of the same set, keeping the set ordered.
546
+ *
547
+ * One sweep in subtree order carries the shallowest kept ancestor forward,
548
+ * because searching the kept set per path is quadratic in the sibling files a
549
+ * bubblewrap or Docker plan declares one by one.
550
+ */
551
+ const collapse = (paths: ReadonlyArray<string>): ReadonlyArray<string> => {
552
+ const unique = [...new Set(paths)]
553
+ const kept = new Set<string>()
554
+ let ancestor: string | undefined
555
+ for (const path of [...unique].sort(bySubtree)) {
556
+ if (ancestor !== undefined && path.startsWith(ancestor + NodePath.sep)) continue
557
+ kept.add(path)
558
+ ancestor = path
559
+ }
560
+ return unique.filter((path) => kept.has(path)).sort((left, right) =>
561
+ left.length - right.length || (left < right ? -1 : 1)
562
+ )
563
+ }
564
+
565
+ /**
566
+ * Resolves a request against a workspace into a {@link Plan}, or refuses.
567
+ *
568
+ * Every relative path is anchored at the canonical workspace root; one that
569
+ * escapes the workspace is dropped, because the sandbox never grants anything
570
+ * outside the tree the key covers. Reads that do not exist are dropped too:
571
+ * bubblewrap cannot bind a missing path, and a declared input that vanished is
572
+ * the executor's measurement failure, not this module's. A write containing
573
+ * any symbolic link below the canonical root is refused, including a missing
574
+ * output below a linked ancestor.
575
+ *
576
+ * @category resolution
577
+ * @since 0.1.0
578
+ */
579
+ export const plan = (
580
+ request: Request,
581
+ location: { readonly workspaceRoot: string; readonly cwd: string; readonly tmp: string },
582
+ hostFacts: Host
583
+ ): Plan | Unenforceable => {
584
+ const selected = select(request, hostFacts)
585
+ if (isUnenforceable(selected)) return selected
586
+ let root: string
587
+ try {
588
+ const canonical = (hostFacts.realpath ?? NodeFs.realpathSync)(location.workspaceRoot)
589
+ if (canonical === undefined) return unsafeWrite(hostFacts.platform, selected._tag, location.workspaceRoot)
590
+ root = canonical
591
+ } catch {
592
+ return unsafeWrite(hostFacts.platform, selected._tag, location.workspaceRoot)
593
+ }
594
+ const anchor = (relative: string): string | undefined => {
595
+ const absolute = NodePath.resolve(root, ...relative.split("/"))
596
+ return insideRoot(root, absolute) ? absolute : undefined
597
+ }
598
+ const declared: Array<string> = []
599
+ // A declared link grants its target as well: native read allowlists and
600
+ // Docker's image filesystem otherwise hide bytes outside the workspace.
601
+ const externalReads: Array<string> = []
602
+ for (const relative of request.reads) {
603
+ const absolute = anchor(relative)
604
+ if (absolute === undefined || !hostFacts.exists(absolute)) continue
605
+ declared.push(absolute)
606
+ const real = hostFacts.realpath?.(absolute)
607
+ if (real !== undefined && real !== absolute && !insideRoot(root, real)) externalReads.push(real)
608
+ }
609
+ for (const path of request.externalReads ?? []) {
610
+ if (!NodePath.isAbsolute(path) || insideRoot(root, path) || !hostFacts.exists(path)) continue
611
+ externalReads.push(hostFacts.realpath?.(path) ?? path)
612
+ }
613
+ // A write names a directory the tool may fill, and is bound as declared
614
+ // whether or not it exists yet: a name is never read as a file, because a
615
+ // dot in `.cargo-home` or `dist.new` would otherwise widen the bind to the
616
+ // parent, and a parent that is the workspace root opens every file in it.
617
+ // A path that is already a file on the host is the one exception, since a
618
+ // file cannot be bound as a writable directory.
619
+ const writes: Array<string> = []
620
+ const add = (absolute: string): Unenforceable | undefined => {
621
+ if (!safeWrite(root, absolute, hostFacts)) return unsafeWrite(hostFacts.platform, selected._tag, absolute)
622
+ writes.push(absolute)
623
+ return undefined
624
+ }
625
+ for (const relative of request.writes) {
626
+ const absolute = anchor(relative)
627
+ if (absolute === undefined) continue
628
+ if (!safeWrite(root, absolute, hostFacts)) return unsafeWrite(hostFacts.platform, selected._tag, absolute)
629
+ const refusal = add(
630
+ hostFacts.exists(absolute) && !hostFacts.isDirectory(absolute) ? NodePath.dirname(absolute) : absolute
631
+ )
632
+ if (refusal !== undefined) return refusal
633
+ }
634
+ // A declared output file opens its parent: a file cannot be bound before it
635
+ // exists, and a tool that writes by rename needs the directory anyway.
636
+ for (const relative of request.writeFiles ?? []) {
637
+ const absolute = anchor(relative)
638
+ if (absolute === undefined) continue
639
+ if (!safeWrite(root, absolute, hostFacts)) return unsafeWrite(hostFacts.platform, selected._tag, absolute)
640
+ const refusal = add(
641
+ hostFacts.exists(absolute) && hostFacts.isDirectory(absolute) ? absolute : NodePath.dirname(absolute)
642
+ )
643
+ if (refusal !== undefined) return refusal
644
+ }
645
+ const readOnly: Array<string> = []
646
+ for (const relative of request.readOnly ?? []) {
647
+ const absolute = anchor(relative)
648
+ if (absolute !== undefined) readOnly.push(absolute)
649
+ }
650
+ const folded = selected._tag === "seatbelt"
651
+ ? fold(root, declared, [...writes, ...readOnly, location.tmp], hostFacts)
652
+ : { reads: declared, denies: [] }
653
+ return {
654
+ mechanism: selected,
655
+ network: request.network,
656
+ unixSockets: request.unixSockets,
657
+ strict: request.strict,
658
+ workspaceRoot: root,
659
+ cwd: location.cwd,
660
+ reads: collapse(folded.reads),
661
+ readDenies: collapse(folded.denies),
662
+ writes: collapse(writes),
663
+ readOnly: collapse(readOnly),
664
+ externalReads: collapse(externalReads),
665
+ tmp: location.tmp,
666
+ uid: hostFacts.uid,
667
+ gid: hostFacts.gid
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Where corepack keeps the package-manager binaries it manages, resolved the
673
+ * way corepack itself resolves it from a host environment.
674
+ *
675
+ * A `pnpm`, `yarn`, or `npm` on `PATH` is often corepack's shim, and the
676
+ * program it execs lives in this directory, not on `PATH`. Redirecting `HOME`
677
+ * and `XDG_CACHE_HOME` to the private tmp moves that directory out from under
678
+ * the shim, which then tries to re-download the package manager from the
679
+ * registry — and a confined run has no network, so every rule that drives the
680
+ * package manager fails with a connect error that names the registry rather
681
+ * than the missing cache.
682
+ */
683
+ const corepackHome = (env: Readonly<Record<string, string | undefined>>, home: string): string => {
684
+ const declared = env["COREPACK_HOME"]
685
+ if (declared !== undefined && declared !== "") return declared
686
+ const base = env["XDG_CACHE_HOME"] ?? env["LOCALAPPDATA"] ??
687
+ NodePath.join(home, process.platform === "win32" ? "AppData/Local" : ".cache")
688
+ return NodePath.join(base, "node", "corepack")
689
+ }
690
+
691
+ /**
692
+ * Environment the confined process receives on top of the tool environment:
693
+ * a private temporary directory and home, so nothing a tool caches lands in
694
+ * the real home or the shared temp directory.
695
+ *
696
+ * `COREPACK_HOME` is the one host cache kept: it holds the package-manager
697
+ * program a corepack shim execs, the run may only read it (the mechanisms
698
+ * deny writes outside the workspace), and without it a confined `pnpm` is a
699
+ * shim with nothing to run. A non-default cache location requires an explicit
700
+ * `externalReads` grant; preserving the variable does not grant access.
701
+ *
702
+ * @category rendering
703
+ * @since 0.1.0
704
+ */
705
+ export const environment = (
706
+ confinement: Plan,
707
+ hostEnv: Readonly<Record<string, string | undefined>> = ambientEnvironment(),
708
+ hostHome: string = NodeOs.homedir()
709
+ ): Readonly<Record<string, string>> => {
710
+ const tmp = confinement.mechanism._tag === "seatbelt" ? confinement.tmp : "/tmp"
711
+ const home = NodePath.posix.join(toPosix(tmp), "home")
712
+ return {
713
+ COREPACK_HOME: corepackHome(hostEnv, hostHome),
714
+ HOME: home,
715
+ TMPDIR: tmp,
716
+ TMP: tmp,
717
+ TEMP: tmp,
718
+ XDG_CACHE_HOME: NodePath.posix.join(toPosix(tmp), "cache"),
719
+ XDG_CONFIG_HOME: NodePath.posix.join(toPosix(tmp), "config"),
720
+ XDG_DATA_HOME: NodePath.posix.join(toPosix(tmp), "data"),
721
+ XDG_STATE_HOME: NodePath.posix.join(toPosix(tmp), "state")
722
+ }
723
+ }
724
+
725
+ /** The fixed runtime surface, never the host root, home, PATH, or arbitrary env paths. */
726
+ const runtimeReads = (hostFacts: Host): ReadonlyArray<string> => {
727
+ const paths = [
728
+ "/usr",
729
+ "/bin",
730
+ "/sbin",
731
+ "/lib",
732
+ "/lib64",
733
+ "/nix/store",
734
+ "/opt/homebrew",
735
+ // A Smithers machine's toolchain layer: the repository's Node, pnpm, Go,
736
+ // Rust and Python, which the machine's environment puts first on PATH
737
+ // (packages/backend/microsandbox layers.go toolchainRoot). Never all of
738
+ // /opt/smithers, which holds the machine's environment and helpers.
739
+ "/opt/smithers/toolchain",
740
+ "/opt/smithers/rust",
741
+ "/System/Library",
742
+ "/System/Volumes/Preboot/Cryptexes/OS",
743
+ "/private/var/db/dyld",
744
+ // macOS shell and developer-tool dispatch read these system selectors.
745
+ "/var/select/sh",
746
+ "/private/var/select/sh",
747
+ "/var/select/developer_dir",
748
+ "/private/var/select/developer_dir",
749
+ "/Library/Apple",
750
+ "/Library/Developer",
751
+ "/Applications/Xcode.app/Contents/Developer",
752
+ "/Applications/Xcode.app/Contents/Info.plist",
753
+ "/Applications/Xcode.app/Contents/Frameworks",
754
+ "/Applications/Xcode.app/Contents/SharedFrameworks",
755
+ "/Applications/Xcode.app/Contents/PlugIns",
756
+ "/etc/ld.so.cache",
757
+ "/etc/ld.so.conf",
758
+ "/etc/ld.so.conf.d",
759
+ "/etc/localtime",
760
+ "/etc/ssl",
761
+ "/etc/hosts",
762
+ "/etc/resolv.conf",
763
+ "/etc/nsswitch.conf"
764
+ ]
765
+ const developerDirectory = hostFacts.developerDirectory
766
+ if (
767
+ hostFacts.platform === "darwin" && developerDirectory !== undefined &&
768
+ /^\/Applications\/Xcode[^/]*\.app\/Contents\/Developer$/.test(developerDirectory)
769
+ ) {
770
+ paths.push(developerDirectory)
771
+ }
772
+ if (hostFacts.home !== undefined) paths.push(NodePath.join(hostFacts.home, ".cache/node/corepack"))
773
+ for (const name of ["node", "bun"]) {
774
+ const executable = hostFacts.executable(name)
775
+ if (executable !== undefined) {
776
+ paths.push(executable)
777
+ const real = hostFacts.realpath?.(executable) ?? executable
778
+ paths.push(real)
779
+ // Node distributions keep npm/corepack here; never grant the install
780
+ // prefix itself, which can be the real home for a custom installation.
781
+ paths.push(NodePath.resolve(NodePath.dirname(real), "../lib/node_modules"))
782
+ }
783
+ }
784
+ const present = paths.filter((path) => hostFacts.exists(path))
785
+ return collapse(present.flatMap((path) => [path, hostFacts.realpath?.(path) ?? path]))
786
+ }
787
+
788
+ /** Credentials remain closed even when a workspace or runtime grant contains the home. */
789
+ const credentialPaths = (hostFacts: Host): ReadonlyArray<string> => {
790
+ const home = hostFacts.home
791
+ if (home === undefined) return []
792
+ return collapse([
793
+ ".aws",
794
+ ".ssh",
795
+ ".gnupg",
796
+ ".azure",
797
+ ".kube",
798
+ ".docker",
799
+ ".config/gcloud",
800
+ ".config/gh",
801
+ ".npmrc",
802
+ ".netrc",
803
+ ".git-credentials"
804
+ ].flatMap((relative) => {
805
+ const path = NodePath.join(home, relative)
806
+ return [path, hostFacts.realpath?.(path) ?? path]
807
+ }))
808
+ }
809
+
810
+ /**
811
+ * The program a confined run execs, made reachable inside its empty root.
812
+ *
813
+ * @internal
814
+ *
815
+ * Bubblewrap resolves a bare program name with `execvp` against `PATH` inside
816
+ * the new root, and that root holds only the runtime paths and the declared
817
+ * sets. A package manager installed outside them — pnpm under the
818
+ * `pnpm/action-setup` prefix on a CI runner, a `.bin` link into a
819
+ * `node_modules` tree — is on the host `PATH` and absent from the sandbox, so
820
+ * the run died with `bwrap: execvp pnpm` before the tool started (#3140).
821
+ *
822
+ * A bare name is resolved on the tool's `PATH` (the host's when none is
823
+ * given). A program already inside a granted path is left exactly as spelled,
824
+ * so a multi-call binary keeps the name it dispatches on, and so is one inside
825
+ * the workspace, whose reads only the declaration may open. Otherwise the run
826
+ * execs the program's real file, and the nearest package directory holding it
827
+ * (its `package.json`, at most three levels up, never the home or the root)
828
+ * is bound read-only, because a script like `pnpm.cjs` loads its siblings.
829
+ * With no package directory only the real file is bound. A name that resolves
830
+ * nowhere is left for `execvp` to report.
831
+ */
832
+ export const launcher = (
833
+ program: string,
834
+ searchPath: string | undefined,
835
+ granted: ReadonlyArray<string>,
836
+ workspaceRoot: string,
837
+ hostFacts: Host
838
+ ): { readonly program: string; readonly reads: ReadonlyArray<string> } => {
839
+ const unchanged = { program, reads: [] }
840
+ if (program.includes("/") && !NodePath.isAbsolute(program)) return unchanged
841
+ const found = NodePath.isAbsolute(program)
842
+ ? hostFacts.executable(program)
843
+ : searchPath === undefined
844
+ ? hostFacts.executable(program)
845
+ : searchPath.split(":").filter((entry) => NodePath.isAbsolute(entry))
846
+ .map((entry) => hostFacts.executable(NodePath.join(entry, program)))
847
+ .find((candidate) => candidate !== undefined)
848
+ if (found === undefined || granted.some((parent) => insideRoot(parent, found))) return unchanged
849
+ const real = hostFacts.realpath?.(found) ?? found
850
+ if (insideRoot(workspaceRoot, found) || insideRoot(workspaceRoot, real)) return unchanged
851
+ if (granted.some((parent) => insideRoot(parent, real))) return { program: real, reads: [] }
852
+ let directory = NodePath.dirname(real)
853
+ for (let level = 0; level < 3; level++) {
854
+ if (directory === NodePath.dirname(directory) || directory === hostFacts.home) break
855
+ if (hostFacts.exists(NodePath.join(directory, "package.json"))) return { program: real, reads: [directory] }
856
+ directory = NodePath.dirname(directory)
857
+ }
858
+ return { program: real, reads: [real] }
859
+ }
860
+
861
+ /**
862
+ * The bubblewrap argv for a plan.
863
+ *
864
+ * Order matters: bubblewrap applies operations in argument order, so the
865
+ * empty root goes first, runtime paths are bound, the workspace tmpfs shadows
866
+ * any broad runtime grant, declared paths are bound on top, and re-closed
867
+ * subtrees are bound read-only over their writable parents. The tmpfs is
868
+ * remounted read-only last so an undeclared write
869
+ * under the workspace fails with `EROFS` instead of vanishing into the tmpfs.
870
+ *
871
+ * @category rendering
872
+ * @since 0.1.0
873
+ */
874
+ export const bubblewrap = (
875
+ confinement: Plan,
876
+ argv: ReadonlyArray<string>,
877
+ hostFacts: Host = host(),
878
+ searchPath?: string | undefined
879
+ ): ReadonlyArray<string> => {
880
+ if (confinement.mechanism._tag !== "bubblewrap") throw new Error("bubblewrap argv needs a bubblewrap plan")
881
+ validateWrites(confinement, hostFacts)
882
+ const home = "/tmp/home"
883
+ const out: Array<string> = [
884
+ confinement.mechanism.executable,
885
+ "--tmpfs",
886
+ "/"
887
+ ]
888
+ const runtime = runtimeReads(hostFacts)
889
+ for (const read of runtime) out.push("--ro-bind", read, read)
890
+ out.push(
891
+ "--dev",
892
+ "/dev",
893
+ "--proc",
894
+ "/proc",
895
+ "--tmpfs",
896
+ "/tmp",
897
+ "--dir",
898
+ home,
899
+ "--dir",
900
+ "/tmp/cache",
901
+ "--tmpfs",
902
+ confinement.workspaceRoot,
903
+ "--dir",
904
+ confinement.cwd
905
+ )
906
+ // The target of a link that leaves the workspace is bound first, so the
907
+ // link a declared read then binds resolves to the same bytes the host sees
908
+ // even when `/tmp` above the target has just become a private tmpfs.
909
+ for (const real of confinement.externalReads) out.push("--ro-bind", real, real)
910
+ const [program, ...args] = argv
911
+ const launch = program === undefined ? undefined : launcher(
912
+ program,
913
+ searchPath,
914
+ [...runtime, ...confinement.reads, ...confinement.writes, ...confinement.externalReads],
915
+ confinement.workspaceRoot,
916
+ hostFacts
917
+ )
918
+ for (const read of launch?.reads ?? []) out.push("--ro-bind", read, read)
919
+ for (const read of confinement.reads) out.push("--ro-bind", read, read)
920
+ for (const write of confinement.writes) out.push("--bind", write, write)
921
+ for (const closed of confinement.readOnly) {
922
+ if (confinement.writes.some((write) => closed === write || closed.startsWith(write + NodePath.sep))) {
923
+ out.push("--ro-bind-try", closed, closed)
924
+ }
925
+ }
926
+ const granted = [
927
+ ...runtime,
928
+ ...(launch?.reads ?? []),
929
+ ...confinement.reads,
930
+ ...confinement.writes,
931
+ ...confinement.externalReads
932
+ ]
933
+ for (const path of credentialPaths(hostFacts)) {
934
+ if (!hostFacts.exists(path) || !granted.some((parent) => insideRoot(parent, path))) continue
935
+ // An explicit external read can name a credential or a file inside it.
936
+ // Mask first, then restore only those explicit paths below the mask.
937
+ if (confinement.externalReads.some((parent) => insideRoot(parent, path))) continue
938
+ const directory = hostFacts.isDirectory(path)
939
+ if (directory) out.push("--tmpfs", path)
940
+ else out.push("--ro-bind", "/dev/null", path)
941
+ for (const read of confinement.externalReads) {
942
+ if (insideRoot(path, read)) out.push("--ro-bind", read, read)
943
+ }
944
+ if (directory) out.push("--remount-ro", path)
945
+ }
946
+ // `--remount-ro` acts on the mount at that exact path. When the root itself
947
+ // is a declared write (a declared output file at the top level opens its
948
+ // parent), that mount is the writable bind, and remounting it would deny
949
+ // every write the declaration admitted; the tmpfs it replaced needs no
950
+ // re-closing.
951
+ if (!confinement.writes.includes(confinement.workspaceRoot)) out.push("--remount-ro", confinement.workspaceRoot)
952
+ out.push("--remount-ro", "/")
953
+ out.push("--chdir", confinement.cwd, "--unshare-all", "--new-session", "--die-with-parent")
954
+ // Bubblewrap cannot expose only the host loopback interface. Planning
955
+ // refuses that posture on Linux, so only an explicit full-network opening
956
+ // reaches `--share-net` here.
957
+ if (confinement.network === "loopback") {
958
+ throw new Error("bubblewrap cannot render loopback-only networking")
959
+ }
960
+ if (confinement.network === "open") out.push("--share-net")
961
+ out.push("--", ...(launch === undefined ? argv : [launch.program, ...args]))
962
+ return out
963
+ }
964
+
965
+ /** Escapes one string for a seatbelt profile literal. */
966
+ const sbpl = (value: string): string => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`
967
+
968
+ /** Every directory from the workspace root down to the parent of each path. */
969
+ const ancestors = (root: string, paths: ReadonlyArray<string>): ReadonlyArray<string> => {
970
+ const found = new Set<string>([root])
971
+ for (const path of paths) {
972
+ let current = NodePath.dirname(path)
973
+ while (current.length >= root.length && current !== NodePath.dirname(current)) {
974
+ found.add(current)
975
+ if (current === root) break
976
+ current = NodePath.dirname(current)
977
+ }
978
+ }
979
+ return [...found].sort()
980
+ }
981
+
982
+ /**
983
+ * The seatbelt profile for a plan.
984
+ *
985
+ * File reads start closed, then enumerated runtime paths and declared sets
986
+ * are opened. The workspace is re-closed before its grants so a workspace
987
+ * under a runtime root does not inherit the broad runtime grant. Credentials
988
+ * are re-closed before explicit external reads, which can opt into them.
989
+ *
990
+ * @category rendering
991
+ * @since 0.1.0
992
+ */
993
+ export const seatbelt = (confinement: Plan, hostFacts: Host = host()): string => {
994
+ if (confinement.mechanism._tag !== "seatbelt") throw new Error("a seatbelt profile needs a seatbelt plan")
995
+ validateWrites(confinement, hostFacts)
996
+ const lines: Array<string> = ["(version 1)", confinement.strict === true ? "(deny default)" : "(allow default)"]
997
+ if (confinement.strict === true) {
998
+ // No Mach lookup, Apple Events or LaunchServices grants: work must stay
999
+ // in descendants inheriting this sandbox, never delegated to a host daemon.
1000
+ lines.push(
1001
+ "(allow process-exec)",
1002
+ "(allow process-fork)",
1003
+ "(allow signal (target same-sandbox))",
1004
+ "(allow process-info* (target same-sandbox))",
1005
+ "(allow sysctl-read (sysctl-name-prefix \"hw.\") (sysctl-name-prefix \"machdep.cpu.\") (sysctl-name \"kern.argmax\") (sysctl-name \"kern.hostname\") (sysctl-name \"kern.maxfilesperproc\") (sysctl-name \"kern.maxproc\") (sysctl-name \"kern.osproductversion\") (sysctl-name \"kern.osrelease\") (sysctl-name \"kern.ostype\") (sysctl-name \"kern.osversion\") (sysctl-name \"kern.secure_kernel\") (sysctl-name \"kern.usrstack64\") (sysctl-name \"kern.version\") (sysctl-name \"sysctl.proc_cputype\") (sysctl-name \"vm.loadavg\"))",
1006
+ "(allow mach-host*)"
1007
+ )
1008
+ if (confinement.network === "open") lines.push("(allow network*)")
1009
+ }
1010
+ if (confinement.network !== "open") {
1011
+ lines.push("(deny network*)")
1012
+ if (confinement.unixSockets !== false) lines.push("(allow network* (local unix-socket))")
1013
+ if (confinement.network === "loopback") {
1014
+ lines.push(
1015
+ "(allow network-bind (local ip \"localhost:*\"))",
1016
+ "(allow network-inbound (local ip \"localhost:*\"))",
1017
+ "(allow network-outbound (remote ip \"localhost:*\"))"
1018
+ )
1019
+ }
1020
+ }
1021
+ if (confinement.network === "open" && confinement.unixSockets === false) {
1022
+ lines.push("(deny network* (local unix-socket))")
1023
+ if (confinement.strict === true) {
1024
+ // Darwin resolves DNS through this trusted endpoint. Full IP authority
1025
+ // permits DNS only; other host daemons remain closed, including Mach IPC.
1026
+ lines.push("(allow network-outbound (literal \"/private/var/run/mDNSResponder\"))")
1027
+ }
1028
+ }
1029
+ const writable = [...confinement.writes, confinement.tmp]
1030
+ lines.push(
1031
+ "(deny file-write*)",
1032
+ `(allow file-write* ${confinement.strict === true ? "(literal \"/dev/null\")" : "(subpath \"/dev\")"} ${
1033
+ writable.map((path) => `(subpath ${sbpl(path)})`).join(" ")
1034
+ })`
1035
+ )
1036
+ if (confinement.readOnly.length > 0) {
1037
+ lines.push(`(deny file-write* ${confinement.readOnly.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1038
+ }
1039
+ lines.push(
1040
+ "(deny file-read*)",
1041
+ confinement.strict === true
1042
+ ? "(allow file-read* (literal \"/\") (literal \"/dev/null\") (literal \"/dev/zero\") (literal \"/dev/random\") (literal \"/dev/urandom\"))"
1043
+ : "(allow file-read* (literal \"/\") (subpath \"/dev\"))",
1044
+ // macOS's system aliases need readlink access before a shell can execute
1045
+ // an admitted file through them. Literal grants expose the links only;
1046
+ // their target directories still cannot be listed or read wholesale.
1047
+ "(allow file-read* (literal \"/tmp\") (literal \"/var\") (literal \"/etc\"))"
1048
+ )
1049
+ const runtime = runtimeReads(hostFacts)
1050
+ if (runtime.length > 0) lines.push(`(allow file-read* ${runtime.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1051
+ const readable = [...confinement.reads, ...confinement.writes, confinement.tmp]
1052
+ // Node realpath walks every ancestor, including parents outside the
1053
+ // workspace. Grant only their metadata, without listing or reading bytes.
1054
+ const traversable = ancestors("/", [...runtime, ...readable, ...confinement.externalReads])
1055
+ .filter((path) => !insideRoot(confinement.workspaceRoot, path))
1056
+ lines.push(`(allow file-read-metadata ${traversable.map((path) => `(literal ${sbpl(path)})`).join(" ")})`)
1057
+ const listable = ancestors(
1058
+ confinement.workspaceRoot,
1059
+ readable.filter((path) => insideRoot(confinement.workspaceRoot, path))
1060
+ )
1061
+ lines.push(
1062
+ `(deny file-read* (subpath ${sbpl(confinement.workspaceRoot)}))`
1063
+ )
1064
+ if (confinement.strict === true) {
1065
+ const metadata = [
1066
+ ...new Set([
1067
+ ...listable,
1068
+ ...ancestors(confinement.workspaceRoot, [confinement.cwd]),
1069
+ confinement.cwd
1070
+ ])
1071
+ ].sort()
1072
+ lines.push(`(allow file-read-metadata ${metadata.map((path) => `(literal ${sbpl(path)})`).join(" ")})`)
1073
+ } else {
1074
+ lines.push(`(allow file-read-metadata (subpath ${sbpl(confinement.workspaceRoot)}))`)
1075
+ }
1076
+ lines.push(
1077
+ `(allow file-read* ${
1078
+ [
1079
+ ...(confinement.strict === true ? [] : listable.map((path) => `(literal ${sbpl(path)})`)),
1080
+ ...readable.map((path) => `(subpath ${sbpl(path)})`)
1081
+ ].join(" ")
1082
+ })`
1083
+ )
1084
+ if (confinement.readDenies.length > 0) {
1085
+ lines.push(`(deny file-read* ${confinement.readDenies.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1086
+ }
1087
+ const credentials = credentialPaths(hostFacts)
1088
+ if (credentials.length > 0) {
1089
+ lines.push(`(deny file-read* ${credentials.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1090
+ if (confinement.strict === true) {
1091
+ lines.push(`(deny file-write* ${credentials.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1092
+ }
1093
+ }
1094
+ if (confinement.externalReads.length > 0) {
1095
+ lines.push(`(allow file-read* ${confinement.externalReads.map((path) => `(subpath ${sbpl(path)})`).join(" ")})`)
1096
+ }
1097
+ return lines.join("")
1098
+ }
1099
+
1100
+ /**
1101
+ * One `key=value` field of a `docker run --mount` value. Docker parses the
1102
+ * value as one CSV record, so a field containing a comma, a quote, or a line
1103
+ * break is wrapped in quotes with inner quotes doubled; any other field keeps
1104
+ * its plain spelling.
1105
+ */
1106
+ const mountField = (key: string, value: string): string => {
1107
+ const field = `${key}=${value}`
1108
+ return /[",\r\n]/.test(field) ? `"${field.replaceAll("\"", "\"\"")}"` : field
1109
+ }
1110
+
1111
+ const bindMount = (path: string, readonly: boolean): string =>
1112
+ ["type=bind", mountField("src", path), mountField("dst", toPosix(path)), ...(readonly ? ["readonly"] : [])].join(",")
1113
+
1114
+ /**
1115
+ * The `docker run` argv for a plan. Paths keep their host spelling inside the
1116
+ * container so an argv that names workspace paths works unchanged; the image
1117
+ * supplies everything outside the workspace.
1118
+ *
1119
+ * @category rendering
1120
+ * @since 0.1.0
1121
+ */
1122
+ export const docker = (
1123
+ confinement: Plan,
1124
+ argv: ReadonlyArray<string>,
1125
+ env: Readonly<Record<string, string>>,
1126
+ hostFacts: Host = host(),
1127
+ containerName: string = `smithers-${randomUUID()}`
1128
+ ): ReadonlyArray<string> => {
1129
+ if (confinement.mechanism._tag !== "docker") throw new Error("docker argv needs a docker plan")
1130
+ validateWrites(confinement, hostFacts)
1131
+ const out: Array<string> = [
1132
+ confinement.mechanism.executable,
1133
+ "run",
1134
+ "--rm",
1135
+ "--init",
1136
+ "--name",
1137
+ containerName,
1138
+ "--read-only",
1139
+ "--tmpfs",
1140
+ "/tmp:rw,exec",
1141
+ "--network",
1142
+ confinement.network === "open" ? "bridge" : "none",
1143
+ "--workdir",
1144
+ toPosix(confinement.cwd)
1145
+ ]
1146
+ if (confinement.uid !== undefined && confinement.gid !== undefined) {
1147
+ out.push("--user", `${confinement.uid}:${confinement.gid}`)
1148
+ }
1149
+ for (const real of confinement.externalReads) out.push("--mount", bindMount(real, true))
1150
+ for (const read of confinement.reads) out.push("--mount", bindMount(read, true))
1151
+ for (const write of confinement.writes) out.push("--mount", bindMount(write, false))
1152
+ for (const closed of confinement.readOnly) {
1153
+ if (confinement.writes.some((write) => closed === write || closed.startsWith(write + NodePath.sep))) {
1154
+ out.push("--mount", bindMount(closed, true))
1155
+ }
1156
+ }
1157
+ for (const [name, value] of Object.entries(env).sort(([left], [right]) => (left < right ? -1 : 1))) {
1158
+ if (name === "PATH" || name === "HOME") continue
1159
+ out.push("--env", `${name}=${value}`)
1160
+ }
1161
+ out.push("--env", "HOME=/tmp/home", confinement.mechanism.image, ...argv)
1162
+ return out
1163
+ }
1164
+
1165
+ /**
1166
+ * Wraps a tool argv in the plan's mechanism and reports the environment the
1167
+ * wrapper needs the child to carry. Docker runs also return the unique
1168
+ * container name the caller must forcibly remove when the client scope closes.
1169
+ *
1170
+ * @category rendering
1171
+ * @since 0.1.0
1172
+ */
1173
+ export const wrap = (
1174
+ confinement: Plan,
1175
+ argv: ReadonlyArray<string>,
1176
+ env: Readonly<Record<string, string>>,
1177
+ hostFacts: Host = host()
1178
+ ): {
1179
+ readonly argv: ReadonlyArray<string>
1180
+ readonly env: Readonly<Record<string, string>>
1181
+ readonly containerName?: string
1182
+ } => {
1183
+ const extra = environment(confinement)
1184
+ switch (confinement.mechanism._tag) {
1185
+ case "bubblewrap":
1186
+ return { argv: bubblewrap(confinement, argv, hostFacts, env["PATH"]), env: extra }
1187
+ case "seatbelt": {
1188
+ const profile = seatbelt(confinement, hostFacts)
1189
+ return { argv: [confinement.mechanism.executable, "-p", profile, ...argv], env: extra }
1190
+ }
1191
+ case "docker": {
1192
+ const containerName = `smithers-${randomUUID()}`
1193
+ return { argv: docker(confinement, argv, { ...env, ...extra }, hostFacts, containerName), env: {}, containerName }
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * Each pattern names the path a tool complained about and whether the text
1200
+ * proves the denied operation was a write. A confined process never sees an
1201
+ * undeclared path: seatbelt answers EPERM, bubblewrap answers ENOENT because
1202
+ * the path was never mounted, and dash spells that "Directory nonexistent".
1203
+ */
1204
+ const denialPatterns: ReadonlyArray<{ readonly pattern: RegExp; readonly write: boolean }> = [
1205
+ { pattern: /\b(?:EACCES|EPERM|ENOENT)\b[^'"\n]*['"]([^'"\n]+)['"]/g, write: false },
1206
+ { pattern: /\bEROFS\b[^'"\n]*['"]([^'"\n]+)['"]/g, write: true },
1207
+ {
1208
+ pattern:
1209
+ /(?:^|[\s:])((?:\/|\.{0,2}\/?)[^\s:'"]+): (?:Operation not permitted|Permission denied|No such file or directory|Directory nonexistent|Not a directory)/gm,
1210
+ write: false
1211
+ },
1212
+ { pattern: /(?:^|[\s:])((?:\/|\.{0,2}\/?)[^\s:'"]+): Read-only file system/gm, write: true },
1213
+ {
1214
+ pattern:
1215
+ /(?:Operation not permitted|Permission denied|No such file or directory|Directory nonexistent)[^\n]*?[:\s]['"]?((?:\/|[A-Za-z]:\\)[^'"\s:]+)/g,
1216
+ write: false
1217
+ },
1218
+ { pattern: /Read-only file system[^\n]*?[:\s]['"]?((?:\/|[A-Za-z]:\\)[^'"\s:]+)/g, write: true },
1219
+ {
1220
+ pattern: /cannot (?:create|write|touch|mkdir|remove) (?:directory )?['"]?((?:\/|[A-Za-z]:\\|\.{0,2}\/?)[^'":\s]+)/g,
1221
+ write: true
1222
+ },
1223
+ { pattern: /cannot (?:open|access|stat|read) ['"]?((?:\/|[A-Za-z]:\\|\.{0,2}\/?)[^'":\s]+)/g, write: false }
1224
+ ]
1225
+
1226
+ /**
1227
+ * Names, from a failed tool's own output, the paths it touched outside the
1228
+ * declared set. The sandbox denies at the kernel, so the only witness is the
1229
+ * tool's error text; this reads it back and says which side of the boundary
1230
+ * each path fell on.
1231
+ *
1232
+ * @category diagnostics
1233
+ * @since 0.1.0
1234
+ */
1235
+ export const diagnose = (confinement: Plan, text: string): string | undefined => {
1236
+ // Path → whether the text proved the denied operation was a write.
1237
+ const named = new Map<string, boolean>()
1238
+ for (const { pattern, write } of denialPatterns) {
1239
+ for (const match of text.matchAll(pattern)) {
1240
+ const raw = match[1]!
1241
+ const absolute = NodePath.isAbsolute(raw) ? raw : NodePath.resolve(confinement.cwd, raw)
1242
+ if (insideRoot(confinement.workspaceRoot, absolute)) named.set(absolute, (named.get(absolute) ?? false) || write)
1243
+ if (named.size >= 5) break
1244
+ }
1245
+ if (named.size >= 5) break
1246
+ }
1247
+ if (named.size === 0) return undefined
1248
+ const covered = (path: string, set: ReadonlyArray<string>): boolean =>
1249
+ set.some((entry) => entry === path || path.startsWith(entry + NodePath.sep))
1250
+ const lines = [...named.entries()].sort(([left], [right]) => (left < right ? -1 : 1)).map(([path, write]) => {
1251
+ const relative = toPosix(NodePath.relative(confinement.workspaceRoot, path))
1252
+ // A path that lexically sits in the workspace can still leave it through
1253
+ // a symlink in an existing ancestor; the declaration covers the tree, not
1254
+ // wherever a link points.
1255
+ const real = realized(path)
1256
+ const escapes = !insideRoot(confinement.workspaceRoot, real)
1257
+ const readable = !escapes && (covered(path, confinement.reads) || covered(path, confinement.writes))
1258
+ const writable = !escapes && covered(path, confinement.writes) && !covered(path, confinement.readOnly)
1259
+ if (escapes) {
1260
+ return `sandbox: ${relative} resolves to ${real}, outside the declared ${write ? "write" : "read"} set`
1261
+ }
1262
+ if (write) {
1263
+ return writable
1264
+ ? `sandbox: ${relative} was denied inside the declared set`
1265
+ : `sandbox: ${relative} is outside the declared write set`
1266
+ }
1267
+ if (!readable) return `sandbox: ${relative} is outside the declared read set`
1268
+ if (!writable) return `sandbox: ${relative} is outside the declared write set`
1269
+ return `sandbox: ${relative} was denied inside the declared set`
1270
+ })
1271
+ return `${lines.join("\n")}\nsandbox: ${confinement.mechanism._tag}, network ${confinement.network}, ` +
1272
+ `${confinement.reads.length} read path(s), ${confinement.writes.length} write path(s)`
1273
+ }