reeval 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +115 -259
  2. package/assets/package.json +1 -1
  3. package/assets/pnpm-lock.yaml +41 -30
  4. package/dist/{adapter-DigeVFWC.js → adapter-IKKo4sZj.js} +3 -3
  5. package/dist/{adapter-DigeVFWC.js.map → adapter-IKKo4sZj.js.map} +1 -1
  6. package/dist/{agent-CatUEo5h.js → agent-ClE-g9VL.js} +5 -5
  7. package/dist/{agent-CatUEo5h.js.map → agent-ClE-g9VL.js.map} +1 -1
  8. package/dist/{analysis-Bxub9LMT.js → analysis-B0nOfAzA.js} +3 -3
  9. package/dist/{analysis-Bxub9LMT.js.map → analysis-B0nOfAzA.js.map} +1 -1
  10. package/dist/{assemble-CF063ms6.js → assemble-BO93VAk1.js} +12 -12
  11. package/dist/{assemble-CF063ms6.js.map → assemble-BO93VAk1.js.map} +1 -1
  12. package/dist/assemble-VQxyoDxK.js +1 -0
  13. package/dist/{attempt-CGPV3BZY.js → attempt-jI0OWxlz.js} +2 -2
  14. package/dist/{attempt-CGPV3BZY.js.map → attempt-jI0OWxlz.js.map} +1 -1
  15. package/dist/{capture-BiD7lVaM.js → capture-dn2hjSLj.js} +10 -10
  16. package/dist/{capture-BiD7lVaM.js.map → capture-dn2hjSLj.js.map} +1 -1
  17. package/dist/capture-nIJ-7kUE.js +1 -0
  18. package/dist/{cli-B5zQCR2r.js → cli-BCE1xlm4.js} +65 -54
  19. package/dist/{cli-B5zQCR2r.js.map → cli-BCE1xlm4.js.map} +1 -1
  20. package/dist/cli.js +2 -2
  21. package/dist/{command-DTgBZR0k.js → command-Bh3yhN_b.js} +8 -8
  22. package/dist/{command-DTgBZR0k.js.map → command-Bh3yhN_b.js.map} +1 -1
  23. package/dist/{command-C94CQNh8.js → command-C6vwmjS7.js} +10 -10
  24. package/dist/{command-C94CQNh8.js.map → command-C6vwmjS7.js.map} +1 -1
  25. package/dist/{command-CaZgCTIw.js → command-CANun0m0.js} +13 -13
  26. package/dist/command-CANun0m0.js.map +1 -0
  27. package/dist/{command-CVtgyqlM.js → command-CBaHukyl.js} +11 -11
  28. package/dist/{command-CVtgyqlM.js.map → command-CBaHukyl.js.map} +1 -1
  29. package/dist/{command-BMHXHqMT.js → command-D_kEnYcX.js} +14 -14
  30. package/dist/{command-BMHXHqMT.js.map → command-D_kEnYcX.js.map} +1 -1
  31. package/dist/{commands-CjpGBUWr.js → commands-7tH3EsVu.js} +9 -9
  32. package/dist/{commands-CjpGBUWr.js.map → commands-7tH3EsVu.js.map} +1 -1
  33. package/dist/{commands-BXzIDB3X.js → commands-BKGzBMgb.js} +1 -1
  34. package/dist/{config-Dv5MG7Rp.js → config-D7TFvUpL.js} +23 -17
  35. package/dist/config-D7TFvUpL.js.map +1 -0
  36. package/dist/{contrasts-CcoMLC_y.js → contrasts-B4nOhn0Z.js} +2 -2
  37. package/dist/{contrasts-CcoMLC_y.js.map → contrasts-B4nOhn0Z.js.map} +1 -1
  38. package/dist/{db-BeqRiWLl.js → db-DJUuFoWQ.js} +4 -4
  39. package/dist/{db-BeqRiWLl.js.map → db-DJUuFoWQ.js.map} +1 -1
  40. package/dist/db-W9EFlXnn.js +2 -0
  41. package/dist/{dist-CMjp3qqF.js → dist-Dp_AJgYN.js} +2 -2
  42. package/dist/{dist-CMjp3qqF.js.map → dist-Dp_AJgYN.js.map} +1 -1
  43. package/dist/{docker-CCXdbREk.js → docker-CQxucRPe.js} +7 -22
  44. package/dist/docker-CQxucRPe.js.map +1 -0
  45. package/dist/{docker-DC69dKxB.js → docker-Dac6Zalz.js} +1 -1
  46. package/dist/{docker-BbTlMmYi.js → docker-DuHcQidD.js} +3 -3
  47. package/dist/{docker-BbTlMmYi.js.map → docker-DuHcQidD.js.map} +1 -1
  48. package/dist/{doctor-CqttrZcs.js → doctor-De_QlOTM.js} +34 -35
  49. package/dist/doctor-De_QlOTM.js.map +1 -0
  50. package/dist/{egress-BfK_O9ce.js → egress-DEAvq8Fl.js} +32 -15
  51. package/dist/egress-DEAvq8Fl.js.map +1 -0
  52. package/dist/{environment-BBz1Yes9.js → environment-B4IuURK1.js} +26 -7
  53. package/dist/environment-B4IuURK1.js.map +1 -0
  54. package/dist/{evaluate-yjZKUexW.d.ts → evaluate-C9-ritA5.d.ts} +2 -2
  55. package/dist/{evaluate-CXQ1o-pd.js → evaluate-Djk1YPFL.js} +18 -18
  56. package/dist/{evaluate-CXQ1o-pd.js.map → evaluate-Djk1YPFL.js.map} +1 -1
  57. package/dist/{facts-DtENlf9s.js → facts-Cfx1H70y.js} +4 -4
  58. package/dist/{facts-DtENlf9s.js.map → facts-Cfx1H70y.js.map} +1 -1
  59. package/dist/{files-BsQbYz2i.js → files-Bs6H3HiG.js} +3 -3
  60. package/dist/{files-BsQbYz2i.js.map → files-Bs6H3HiG.js.map} +1 -1
  61. package/dist/{files-B-eUpCZ1.js → files-CN1QfenC.js} +7 -7
  62. package/dist/{files-B-eUpCZ1.js.map → files-CN1QfenC.js.map} +1 -1
  63. package/dist/files-CtjEXWXm.js +1 -0
  64. package/dist/{files-COHQLEHE.js → files-D27nUk0K.js} +3 -3
  65. package/dist/{files-COHQLEHE.js.map → files-D27nUk0K.js.map} +1 -1
  66. package/dist/files-D3gJa91B.js +1 -0
  67. package/dist/{generate-Cs2TMH_k.js → generate-BnrvBAzF.js} +10 -11
  68. package/dist/{generate-Cs2TMH_k.js.map → generate-BnrvBAzF.js.map} +1 -1
  69. package/dist/{git-DhykwgKF.js → git-rPfl7AD1.js} +3 -3
  70. package/dist/{git-DhykwgKF.js.map → git-rPfl7AD1.js.map} +1 -1
  71. package/dist/{harness-BAjw1fcr.js → harness-1OkumOAm.js} +2 -2
  72. package/dist/{harness-BAjw1fcr.js.map → harness-1OkumOAm.js.map} +1 -1
  73. package/dist/harness.d.ts +1 -1
  74. package/dist/harness.js +1 -1
  75. package/dist/{header-kSmWatcO.js → header-BVzbRnNv.js} +2 -2
  76. package/dist/{header-kSmWatcO.js.map → header-BVzbRnNv.js.map} +1 -1
  77. package/dist/{images-BmJRuTpm.js → images-m784ueGK.js} +8 -8
  78. package/dist/{images-BmJRuTpm.js.map → images-m784ueGK.js.map} +1 -1
  79. package/dist/index.d.ts +2 -2
  80. package/dist/index.js +4 -4
  81. package/dist/{init-Bwc9byQr.js → init-hiTkiM4L.js} +7 -7
  82. package/dist/{init-Bwc9byQr.js.map → init-hiTkiM4L.js.map} +1 -1
  83. package/dist/init-kMkTgTZc.js +1 -0
  84. package/dist/{invocations-CQTbP092.js → invocations-D_ePcwyL.js} +2 -2
  85. package/dist/{invocations-CQTbP092.js.map → invocations-D_ePcwyL.js.map} +1 -1
  86. package/dist/{judge-Dt6KW6_O.js → judge-CEDIfRiU.js} +48 -15
  87. package/dist/judge-CEDIfRiU.js.map +1 -0
  88. package/dist/judge.d.ts +1 -1
  89. package/dist/judge.js +1 -1
  90. package/dist/model-B8H3gL69.js +1 -0
  91. package/dist/{model-C7tSwT7f.js → model-CTiuzkWw.js} +9 -7
  92. package/dist/model-CTiuzkWw.js.map +1 -0
  93. package/dist/model-DcIbmggK.js +1 -0
  94. package/dist/{model-B-mel-Gv.js → model-DpkclE4r.js} +26 -16
  95. package/dist/model-DpkclE4r.js.map +1 -0
  96. package/dist/{model-CWlAqmbU.js → model-UgHDMhI1.js} +11 -8
  97. package/dist/model-UgHDMhI1.js.map +1 -0
  98. package/dist/{node-D0mtjRAO.js → node-CGZYh4Fj.js} +2 -2
  99. package/dist/{node-D0mtjRAO.js.map → node-CGZYh4Fj.js.map} +1 -1
  100. package/dist/{opencode-providers-J9colP0-.js → opencode-providers-snfeWd6Z.js} +86 -47
  101. package/dist/opencode-providers-snfeWd6Z.js.map +1 -0
  102. package/dist/{options-CPUW97_7.js → options-CyAyLjgJ.js} +20 -7
  103. package/dist/options-CyAyLjgJ.js.map +1 -0
  104. package/dist/{patch-D4oluWzH.js → patch-uMq9rCl_.js} +2 -2
  105. package/dist/{patch-D4oluWzH.js.map → patch-uMq9rCl_.js.map} +1 -1
  106. package/dist/{plan-OkwUcJQ9.js → plan-CZYgXnZh.js} +5 -5
  107. package/dist/{plan-OkwUcJQ9.js.map → plan-CZYgXnZh.js.map} +1 -1
  108. package/dist/{plugins-CUWMnSNb.js → plugins-P-uh54VF.js} +3 -3
  109. package/dist/{plugins-CUWMnSNb.js.map → plugins-P-uh54VF.js.map} +1 -1
  110. package/dist/{preflight-B6pY89bF.js → preflight-BQlPTHVu.js} +1 -1
  111. package/dist/{preflight-Bjm7_ylE.js → preflight-D3HvKeAx.js} +3 -3
  112. package/dist/{preflight-Bjm7_ylE.js.map → preflight-D3HvKeAx.js.map} +1 -1
  113. package/dist/{prepare-DbdgtZKe.js → prepare-DCwM2KKD.js} +2 -2
  114. package/dist/{prepare-CQmK3SBn.js → prepare-Dx7dOnJB.js} +7 -7
  115. package/dist/{prepare-CQmK3SBn.js.map → prepare-Dx7dOnJB.js.map} +1 -1
  116. package/dist/{provenance-B3Y17oCC.js → provenance-C_-8y-x3.js} +3 -3
  117. package/dist/{provenance-B3Y17oCC.js.map → provenance-C_-8y-x3.js.map} +1 -1
  118. package/dist/{providers-eoniqK6w.js → providers-CF5UEaP8.js} +5 -5
  119. package/dist/{providers-eoniqK6w.js.map → providers-CF5UEaP8.js.map} +1 -1
  120. package/dist/{providers-CXN2RFz-.js → providers-CTBYvMxu.js} +1 -1
  121. package/dist/{query-CLRZmWBT.js → query-DXthKmq0.js} +2 -2
  122. package/dist/{query-CLRZmWBT.js.map → query-DXthKmq0.js.map} +1 -1
  123. package/dist/query-D_1ARx5q.js +1 -0
  124. package/dist/{questions-1XOulevV.js → questions-KENpWnfV.js} +6 -7
  125. package/dist/questions-KENpWnfV.js.map +1 -0
  126. package/dist/{redact-DJ9JCShu.js → redact-CRGdfV2s.js} +4 -2
  127. package/dist/redact-CRGdfV2s.js.map +1 -0
  128. package/dist/{remarks-DlJCqWRs.js → remarks-Ml6bcSdV.js} +8 -8
  129. package/dist/{remarks-DlJCqWRs.js.map → remarks-Ml6bcSdV.js.map} +1 -1
  130. package/dist/{remote-cE2VWfkJ.js → remote-Dkeghk8k.js} +3 -3
  131. package/dist/{remote-cE2VWfkJ.js.map → remote-Dkeghk8k.js.map} +1 -1
  132. package/dist/resolve-CMmhACr6.js +1 -0
  133. package/dist/{resolve-C4zeccCH.js → resolve-DzdWcIL6.js} +7 -7
  134. package/dist/{resolve-C4zeccCH.js.map → resolve-DzdWcIL6.js.map} +1 -1
  135. package/dist/{results-Drtt0uFl.js → results-D-naai_Q.js} +6 -6
  136. package/dist/{results-Drtt0uFl.js.map → results-D-naai_Q.js.map} +1 -1
  137. package/dist/{resume-BEFazUfy.js → resume-8-nnW8iO.js} +3 -3
  138. package/dist/{resume-BEFazUfy.js.map → resume-8-nnW8iO.js.map} +1 -1
  139. package/dist/{root-DICDHCtp.js → root-DNuLhaA5.js} +2 -2
  140. package/dist/root-DNuLhaA5.js.map +1 -0
  141. package/dist/{rules-BFzQqPIm.js → rules-CcV3zLDR.js} +4 -3
  142. package/dist/rules-CcV3zLDR.js.map +1 -0
  143. package/dist/{run-BeMy92Us.js → run-Bel7Vde4.js} +10 -10
  144. package/dist/run-Bel7Vde4.js.map +1 -0
  145. package/dist/{sandbox-CQaJj7n9.js → sandbox-YLbk9mO7.js} +2 -2
  146. package/dist/{sandbox-CQaJj7n9.js.map → sandbox-YLbk9mO7.js.map} +1 -1
  147. package/dist/sandbox.d.ts +1 -1
  148. package/dist/sandbox.js +1 -1
  149. package/dist/server-C-ysqkLv.js +1 -0
  150. package/dist/{server-xK0les4f.js → server-De8oH3bt.js} +8 -8
  151. package/dist/{server-xK0les4f.js.map → server-De8oH3bt.js.map} +1 -1
  152. package/dist/{source-Q1hEKbCs.js → source-C686eZpY.js} +2 -2
  153. package/dist/{source-Q1hEKbCs.js.map → source-C686eZpY.js.map} +1 -1
  154. package/dist/source-DjgypUHW.js +1 -0
  155. package/dist/src-3mP821Pt.js +2 -0
  156. package/dist/src-B3KCLZVk.js +3 -0
  157. package/dist/src-BAyg6CtQ.js +2 -0
  158. package/dist/{src-C0CeKiLU.js → src-BYLM3hEz.js} +4 -3
  159. package/dist/src-BYLM3hEz.js.map +1 -0
  160. package/dist/src-BezgGX4X.js +2 -0
  161. package/dist/{src-BhmaorOU.js → src-C7W6kg81.js} +13 -5
  162. package/dist/{src-BhmaorOU.js.map → src-C7W6kg81.js.map} +1 -1
  163. package/dist/{src-LekQrr_S.js → src-CIpKL45U.js} +593 -160
  164. package/dist/src-CIpKL45U.js.map +1 -0
  165. package/dist/src-ChBhk06Q.js +2 -0
  166. package/dist/{src-DXNh5mTr.js → src-CoqZYspK.js} +1 -1
  167. package/dist/{src-CFp86GtV.js → src-CzQrAcF8.js} +49 -49
  168. package/dist/src-CzQrAcF8.js.map +1 -0
  169. package/dist/src-D9fSTxlc.js +1 -0
  170. package/dist/{src-DiLMkIr9.js → src-DeY52Gyf.js} +47 -17
  171. package/dist/src-DeY52Gyf.js.map +1 -0
  172. package/dist/src-DrJSnoMR.js +2 -0
  173. package/dist/{src-qGoFkuJF.js → src-Xj_BZKbq.js} +58 -8
  174. package/dist/src-Xj_BZKbq.js.map +1 -0
  175. package/dist/{src-DPIjmReZ.js → src-dhSTRN50.js} +1 -1
  176. package/dist/{src-C7_1SqDP.js → src-djNNYIdP.js} +2 -2
  177. package/dist/{src-C7_1SqDP.js.map → src-djNNYIdP.js.map} +1 -1
  178. package/dist/state-BGWGAwdo.js +1 -0
  179. package/dist/{state-BmZ8QVZ8.js → state-C0i_ujQZ.js} +17 -15
  180. package/dist/{state-BmZ8QVZ8.js.map → state-C0i_ujQZ.js.map} +1 -1
  181. package/dist/studio-app/assets/{index-CnYJijyI.js → index-Bb_cid9k.js} +2 -2
  182. package/dist/studio-app/index.html +1 -1
  183. package/dist/{suite-C8sw2pox.js → suite-DmuF_ApO.js} +3 -5
  184. package/dist/suite-DmuF_ApO.js.map +1 -0
  185. package/dist/suite-mn9g4veR.js +2 -0
  186. package/dist/{summary-0oQNGskv.js → summary-BFMHMTz2.js} +4 -4
  187. package/dist/{summary-0oQNGskv.js.map → summary-BFMHMTz2.js.map} +1 -1
  188. package/dist/testing.d.ts +2 -2
  189. package/dist/testing.js +2 -2
  190. package/dist/{types-GraZtfHN.d.ts → types-B-9b1MDV.d.ts} +6 -4
  191. package/dist/types-C5vI3g1C.js.map +1 -1
  192. package/dist/{validate-BdoXUuL_.js → validate-DWjkshl1.js} +12 -12
  193. package/dist/{validate-BdoXUuL_.js.map → validate-DWjkshl1.js.map} +1 -1
  194. package/dist/view/index.d.ts +13 -8
  195. package/dist/view/index.js +5 -5
  196. package/dist/views-DE2A1jEX.js +2 -0
  197. package/dist/{views-BoUoX4_A.js → views-Dk8MCQKr.js} +5 -5
  198. package/dist/{views-BoUoX4_A.js.map → views-Dk8MCQKr.js.map} +1 -1
  199. package/dist/{worker-entry-D0-GErmU.js → worker-entry-CqrzGPF4.js} +2 -2
  200. package/dist/{worker-entry-D0-GErmU.js.map → worker-entry-CqrzGPF4.js.map} +1 -1
  201. package/dist/workspace-B1eGO46M.js +1 -0
  202. package/dist/{workspace-tjZFkjxC.js → workspace-CLeOybkr.js} +45 -11
  203. package/dist/workspace-CLeOybkr.js.map +1 -0
  204. package/package.json +3 -1
  205. package/dist/assemble-CJoD_Rzr.js +0 -1
  206. package/dist/capture-Dp3q7K7Y.js +0 -1
  207. package/dist/command-CaZgCTIw.js.map +0 -1
  208. package/dist/config-Dv5MG7Rp.js.map +0 -1
  209. package/dist/db-CJDRVQal.js +0 -2
  210. package/dist/docker-CCXdbREk.js.map +0 -1
  211. package/dist/doctor-CqttrZcs.js.map +0 -1
  212. package/dist/egress-BfK_O9ce.js.map +0 -1
  213. package/dist/environment-BBz1Yes9.js.map +0 -1
  214. package/dist/files-DqCBW7i8.js +0 -1
  215. package/dist/files-Ha-jsQXf.js +0 -1
  216. package/dist/init-D7Y1ny1z.js +0 -1
  217. package/dist/judge-Dt6KW6_O.js.map +0 -1
  218. package/dist/model-B-mel-Gv.js.map +0 -1
  219. package/dist/model-BGKYVpoj.js +0 -1
  220. package/dist/model-C7tSwT7f.js.map +0 -1
  221. package/dist/model-CWlAqmbU.js.map +0 -1
  222. package/dist/model-DHY-dfoO.js +0 -1
  223. package/dist/opencode-providers-J9colP0-.js.map +0 -1
  224. package/dist/options-CPUW97_7.js.map +0 -1
  225. package/dist/query-CmBDYt0_.js +0 -1
  226. package/dist/questions-1XOulevV.js.map +0 -1
  227. package/dist/redact-DJ9JCShu.js.map +0 -1
  228. package/dist/resolve-D30yZYmB.js +0 -1
  229. package/dist/root-DICDHCtp.js.map +0 -1
  230. package/dist/rules-BFzQqPIm.js.map +0 -1
  231. package/dist/run-BeMy92Us.js.map +0 -1
  232. package/dist/server-V4-2w0l8.js +0 -1
  233. package/dist/source-BDow_oW8.js +0 -1
  234. package/dist/src-B9L9yGKI.js +0 -1
  235. package/dist/src-Bcah6ZkT.js +0 -3
  236. package/dist/src-C0CeKiLU.js.map +0 -1
  237. package/dist/src-C0xowZ3C.js +0 -2
  238. package/dist/src-CFp86GtV.js.map +0 -1
  239. package/dist/src-CHIfIKUu.js +0 -2
  240. package/dist/src-DLtyz4w5.js +0 -2
  241. package/dist/src-DiLMkIr9.js.map +0 -1
  242. package/dist/src-LekQrr_S.js.map +0 -1
  243. package/dist/src-bCnsA6Vp.js +0 -2
  244. package/dist/src-qGoFkuJF.js.map +0 -1
  245. package/dist/src-ynBVosfX.js +0 -2
  246. package/dist/state-BQR_BCm4.js +0 -1
  247. package/dist/suite-BaHPmAGy.js +0 -1
  248. package/dist/suite-C8sw2pox.js.map +0 -1
  249. package/dist/views-noVQNCUP.js +0 -2
  250. package/dist/workspace-B90YDU_a.js +0 -1
  251. package/dist/workspace-tjZFkjxC.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { u as ownFileURL } from "./root-DICDHCtp.js";
1
+ import { u as ownFileURL } from "./root-DNuLhaA5.js";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, dirname, join, posix, relative, win32 } from "node:path";
@@ -13,6 +13,7 @@ import http from "node:http";
13
13
  import http2 from "node:http2";
14
14
  import https from "node:https";
15
15
  import tls from "node:tls";
16
+ import zlib from "node:zlib";
16
17
  import { lookup } from "node:dns/promises";
17
18
  //#region ../sandbox-docker/src/seccomp-profile.ts
18
19
  const DOCKER_SECCOMP_PROFILE = {
@@ -1620,11 +1621,11 @@ const HARNESS_PACKAGES = {
1620
1621
  },
1621
1622
  antigravity: {
1622
1623
  name: "@reeval/harness-antigravity",
1623
- version: "0.1.9"
1624
+ version: "0.1.11"
1624
1625
  },
1625
1626
  opencode: {
1626
1627
  name: "@reeval/harness-opencode",
1627
- version: "0.2.8"
1628
+ version: "0.2.9"
1628
1629
  },
1629
1630
  goose: {
1630
1631
  name: "@reeval/harness-goose",
@@ -1632,7 +1633,7 @@ const HARNESS_PACKAGES = {
1632
1633
  },
1633
1634
  pi: {
1634
1635
  name: "@reeval/harness-pi",
1635
- version: "0.2.7"
1636
+ version: "0.2.8"
1636
1637
  },
1637
1638
  codex: {
1638
1639
  name: "@reeval/harness-codex",
@@ -1644,7 +1645,7 @@ const HARNESS_PACKAGES = {
1644
1645
  },
1645
1646
  "grok-build": {
1646
1647
  name: "@reeval/harness-grok-build",
1647
- version: "0.1.1"
1648
+ version: "0.1.2"
1648
1649
  }
1649
1650
  };
1650
1651
  /** Every harness with an image, in {@link HARNESS_PACKAGES} order. */
@@ -1827,7 +1828,8 @@ function ipv6Bytes(address) {
1827
1828
  //#region ../sandbox-docker/src/egress/mux.ts
1828
1829
  /**
1829
1830
  * Many TCP streams over one byte pipe (a `docker exec` stdio), with per-stream credit so a slow
1830
- * reader only stalls its own stream. Frames are `type u8, id u32, length u32, payload`.
1831
+ * reader only stalls its own stream. Frames are `type u8, id u32, length u32, payload`, and a
1832
+ * payload is at most 64 KiB.
1831
1833
  *
1832
1834
  * Must stay self-contained, with no imports or references to module scope: the forwarder runs
1833
1835
  * this function's source inside the container.
@@ -1841,9 +1843,43 @@ function createMux(options) {
1841
1843
  const READY = 6;
1842
1844
  const windowSize = options.window ?? 262144;
1843
1845
  const maxPayload = 65536;
1846
+ /** The payload length each type must have, or its range. */
1847
+ const LENGTHS = {
1848
+ [OPEN]: [
1849
+ "OPEN",
1850
+ 2,
1851
+ 2
1852
+ ],
1853
+ [DATA]: [
1854
+ "DATA",
1855
+ 0,
1856
+ maxPayload
1857
+ ],
1858
+ [FIN]: [
1859
+ "FIN",
1860
+ 0,
1861
+ 0
1862
+ ],
1863
+ [RST]: [
1864
+ "RST",
1865
+ 0,
1866
+ 0
1867
+ ],
1868
+ [WINDOW]: [
1869
+ "WINDOW",
1870
+ 4,
1871
+ 4
1872
+ ],
1873
+ [READY]: [
1874
+ "READY",
1875
+ 0,
1876
+ 0
1877
+ ]
1878
+ };
1844
1879
  const streams = /* @__PURE__ */ new Map();
1845
1880
  let nextId = options.parity === 0 ? 2 : 1;
1846
1881
  let pending = Buffer.alloc(0);
1882
+ let broken = false;
1847
1883
  const frame = (type, id, payload = Buffer.alloc(0)) => {
1848
1884
  const header = Buffer.alloc(9);
1849
1885
  header.writeUInt8(type, 0);
@@ -1896,6 +1932,25 @@ function createMux(options) {
1896
1932
  streams.set(id, state);
1897
1933
  return stream;
1898
1934
  };
1935
+ /** Why a frame's header breaks the protocol, or null. */
1936
+ const malformed = (type, id, length) => {
1937
+ const expected = LENGTHS[type];
1938
+ if (!expected) return `a frame of unknown type ${type}`;
1939
+ const [name, min, max] = expected;
1940
+ if (length > maxPayload) return `a frame too large (${length} bytes)`;
1941
+ if (length < min || length > max) return `a malformed ${name} frame (${length} bytes, not ${min === max ? min : `0 to ${max}`} bytes)`;
1942
+ if (type === OPEN && (id % 2 === options.parity || streams.has(id))) return `an OPEN frame for stream ${id}, which this side allocates or has open`;
1943
+ return null;
1944
+ };
1945
+ const fail = (message) => {
1946
+ broken = true;
1947
+ pending = Buffer.alloc(0);
1948
+ for (const state of [...streams.values()]) {
1949
+ state.reset = true;
1950
+ state.stream.destroy();
1951
+ }
1952
+ options.onError?.(`the peer sent ${message}`);
1953
+ };
1899
1954
  const handle = (type, id, payload) => {
1900
1955
  if (type === READY) return options.onReady?.();
1901
1956
  if (type === OPEN) return options.onOpen(create(id), payload.readUInt16BE(0));
@@ -1924,15 +1979,22 @@ function createMux(options) {
1924
1979
  };
1925
1980
  return {
1926
1981
  receive(chunk) {
1982
+ if (broken) return;
1927
1983
  pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
1928
- while (pending.length >= 9) {
1929
- const length = pending.readUInt32BE(5);
1930
- if (pending.length < 9 + length) break;
1931
- const type = pending.readUInt8(0);
1932
- const id = pending.readUInt32BE(1);
1933
- const payload = pending.subarray(9, 9 + length);
1934
- pending = pending.subarray(9 + length);
1935
- handle(type, id, payload);
1984
+ try {
1985
+ while (pending.length >= 9) {
1986
+ const type = pending.readUInt8(0);
1987
+ const id = pending.readUInt32BE(1);
1988
+ const length = pending.readUInt32BE(5);
1989
+ const problem = malformed(type, id, length);
1990
+ if (problem) return fail(problem);
1991
+ if (pending.length < 9 + length) break;
1992
+ const payload = pending.subarray(9, 9 + length);
1993
+ pending = pending.subarray(9 + length);
1994
+ handle(type, id, payload);
1995
+ }
1996
+ } catch (error) {
1997
+ fail(`a frame it couldn't handle: ${error.message}`);
1936
1998
  }
1937
1999
  },
1938
2000
  open(port) {
@@ -1986,6 +2048,10 @@ function forwarderMain(io, makeMux, linkStreams, proxyPort) {
1986
2048
  host: "127.0.0.1",
1987
2049
  allowHalfOpen: true
1988
2050
  }), stream);
2051
+ },
2052
+ onError(message) {
2053
+ io.error(`reeval egress forwarder: ${message}`);
2054
+ io.exit(1);
1989
2055
  }
1990
2056
  });
1991
2057
  io.input.on("data", (chunk) => mux.receive(chunk));
@@ -2066,13 +2132,45 @@ const normalise = (host) => host.toLowerCase().replace(/\.$/, "").replace(/^\[|\
2066
2132
  * mustn't resolve into them, or anyone who can add a DNS record under the pattern's domain
2067
2133
  * reaches the worker's LAN.
2068
2134
  */
2069
- const PRIVATE = blockList([
2135
+ const PRIVATE_RANGES = [
2070
2136
  "10.0.0.0/8",
2071
2137
  "100.64.0.0/10",
2072
2138
  "172.16.0.0/12",
2073
2139
  "192.168.0.0/16",
2074
2140
  "fc00::/7"
2141
+ ];
2142
+ const PRIVATE = blockList(PRIVATE_RANGES);
2143
+ /**
2144
+ * Reachable only with an `allowedCIDRs` entry that covers them, so never under allow-all: the
2145
+ * gateway connects from the worker, so these would reach its LAN, the Docker host's bridge, cloud
2146
+ * metadata on a unique-local or CGNAT address (AWS's `fd00:ec2::254`, Alibaba's
2147
+ * `100.100.100.200`), multicast and broadcast. NAT64 addresses are checked by their embedded
2148
+ * IPv4 address as well.
2149
+ */
2150
+ const RESERVED = blockList([
2151
+ ...PRIVATE_RANGES,
2152
+ "224.0.0.0/4",
2153
+ "255.255.255.255/32",
2154
+ "ff00::/8",
2155
+ "64:ff9b::/96"
2075
2156
  ]);
2157
+ const NAT64 = blockList(["64:ff9b::/96"]);
2158
+ /** The IPv4 address in the last 32 bits of an IPv6 address, as `a.b.c.d`. */
2159
+ function embeddedIPv4(address) {
2160
+ const tail = address.slice(address.lastIndexOf(":") + 1);
2161
+ if (isIP(tail) === 4) return tail;
2162
+ const groups = address.split(":");
2163
+ const low = Number.parseInt(groups.at(-1) || "0", 16);
2164
+ const high = groups.length > 1 ? Number.parseInt(groups.at(-2) || "0", 16) : 0;
2165
+ return [
2166
+ high >> 8,
2167
+ high & 255,
2168
+ low >> 8,
2169
+ low & 255
2170
+ ].join(".");
2171
+ }
2172
+ /** The address, and for a NAT64 address the IPv4 address it reaches too. */
2173
+ const reaches = (address) => isIP(address) === 6 && inList(NAT64, address) ? [address, embeddedIPv4(address)] : [address];
2076
2174
  /** How `host` matches: listed as it is, only by a `*.` pattern (never an IP address), or not. */
2077
2175
  function hostMatch(patterns, host) {
2078
2176
  const h = normalise(host);
@@ -2105,7 +2203,7 @@ function createPolicyCheck(policy, options = {}) {
2105
2203
  reason: "the policy denies all egress"
2106
2204
  };
2107
2205
  const listed = hostMatch(custom?.allowedHosts ?? [], host);
2108
- if (custom && !listed && !custom.allowedCIDRs?.length) return {
2206
+ if (custom && !listed && !(custom.allowedCIDRs?.length && isIP(host))) return {
2109
2207
  allowed: false,
2110
2208
  status: 403,
2111
2209
  reason: `${host} isn't in the allowlist`
@@ -2126,16 +2224,26 @@ function createPolicyCheck(policy, options = {}) {
2126
2224
  status: 502,
2127
2225
  reason: `${host} has no addresses`
2128
2226
  };
2129
- if (addresses.some((a) => inList(always, a))) return {
2227
+ const reached = addresses.flatMap(reaches);
2228
+ if (reached.some((a) => inList(always, a))) return {
2130
2229
  allowed: false,
2131
2230
  status: 403,
2132
2231
  reason: `${host} is a loopback or link-local address`
2133
2232
  };
2233
+ const reserved = reached.find((a) => inList(RESERVED, a) && !inList(allowedCidrs, a));
2234
+ if (reserved) {
2235
+ const which = reserved === host ? host : `${host} (${reserved})`;
2236
+ return {
2237
+ allowed: false,
2238
+ status: 403,
2239
+ reason: custom ? `${which} is a private or reserved address; list the address itself in the allowlist to reach it` : `${which} is a private or reserved address, which allow-all doesn't reach; list the address in an allowlist to reach it`
2240
+ };
2241
+ }
2134
2242
  if (!custom) return {
2135
2243
  allowed: true,
2136
2244
  address: first
2137
2245
  };
2138
- if (addresses.some((a) => inList(deniedCidrs, a))) return {
2246
+ if (reached.some((a) => inList(deniedCidrs, a))) return {
2139
2247
  allowed: false,
2140
2248
  status: 403,
2141
2249
  reason: `${host} is in a denied range`
@@ -2190,6 +2298,35 @@ const onEndpoint = (endpoints, request) => endpoints.some((match) => matches({
2190
2298
  }, request));
2191
2299
  /** Whether the gateway must decrypt connections to `host`: some rule could apply there. */
2192
2300
  const decrypts = (rules, host) => rules.some((r) => normaliseHost(r.match.host) === normaliseHost(host));
2301
+ /** What a credential the sandbox never sent comes back as. */
2302
+ const REDACTED = "reeval_redacted_credential";
2303
+ /** Shorter values aren't credentials, and scrubbing them would mangle ordinary text. */
2304
+ const MIN_CREDENTIAL = 16;
2305
+ const schemeAndToken = (value) => /^([A-Za-z][\w-]*) +(\S+)$/.exec(value);
2306
+ /**
2307
+ * The values credential rules for `host` set (rules keyed on a header the sandbox sends, such as
2308
+ * a placeholder key), each paired with what the sandbox sent in its place, for scrubbing from
2309
+ * responses. A value with a scheme (`Bearer <key>`) is paired as a whole and by its token too.
2310
+ */
2311
+ function credentialValues(rules, host) {
2312
+ const pairs = /* @__PURE__ */ new Map();
2313
+ const add = (value, shown) => {
2314
+ if (value.length >= MIN_CREDENTIAL && value !== shown && !pairs.has(value)) pairs.set(value, shown);
2315
+ };
2316
+ for (const rule of rules) {
2317
+ const keyed = rule.match.headers ?? [];
2318
+ if (!keyed.length || normaliseHost(rule.match.host) !== normaliseHost(host)) continue;
2319
+ for (const [name, value] of Object.entries(rule.transform.headers)) {
2320
+ const sent = keyed.find((m) => m.key && "exact" in m.key && m.key.exact.toLowerCase() === name.toLowerCase())?.value;
2321
+ const shown = sent && "exact" in sent ? sent.exact : REDACTED;
2322
+ add(value, shown);
2323
+ const token = schemeAndToken(value);
2324
+ const shownToken = schemeAndToken(shown);
2325
+ if (token?.[2]) add(token[2], shownToken?.[2] ?? REDACTED);
2326
+ }
2327
+ }
2328
+ return [...pairs];
2329
+ }
2193
2330
  /**
2194
2331
  * The request's headers after every matching rule, in order, so later rules win. Returns the
2195
2332
  * (lower-case) names that were replaced, for logging without values. With `endpoints`, no rule
@@ -2298,59 +2435,75 @@ function scrubHeaders(subs, headers) {
2298
2435
  const one = (v) => scrubValue(subs, v);
2299
2436
  return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, v === void 0 ? v : Array.isArray(v) ? v.map(one) : one(String(v))]));
2300
2437
  }
2438
+ const bufferPairs = (pairs) => pairs.filter(([from]) => from.length > 0).map(([from, to]) => [Buffer.from(from), Buffer.from(to)]);
2439
+ /**
2440
+ * Replaces every whole match in `data` (original bytes only: output is never re-scanned), and
2441
+ * returns where the unscanned tail starts: `data.length` when `final`, else the start of the
2442
+ * shortest tail that could begin a match, so only bytes a later chunk might complete are held.
2443
+ */
2444
+ function scan(pairs, data, final) {
2445
+ const parts = [];
2446
+ let pos = 0;
2447
+ let count = 0;
2448
+ for (;;) {
2449
+ let best = -1;
2450
+ let pair;
2451
+ for (const p of pairs) {
2452
+ const i = data.indexOf(p[0], pos);
2453
+ if (i !== -1 && (best === -1 || i < best)) {
2454
+ best = i;
2455
+ pair = p;
2456
+ }
2457
+ }
2458
+ if (best === -1 || !pair) break;
2459
+ parts.push(data.subarray(pos, best), pair[1]);
2460
+ pos = best + pair[0].length;
2461
+ count++;
2462
+ }
2463
+ let rest = data.length;
2464
+ if (!final) {
2465
+ const hold = Math.max(0, ...pairs.map(([from]) => from.length - 1));
2466
+ for (let i = Math.max(pos, data.length - hold); i < data.length; i++) {
2467
+ const tail = data.subarray(i);
2468
+ if (pairs.some(([from]) => from.subarray(0, tail.length).equals(tail))) {
2469
+ rest = i;
2470
+ break;
2471
+ }
2472
+ }
2473
+ }
2474
+ parts.push(data.subarray(pos, rest));
2475
+ return {
2476
+ out: Buffer.concat(parts),
2477
+ rest,
2478
+ count
2479
+ };
2480
+ }
2481
+ /** `data` with every `from` replaced by its `to`, in one pass. */
2482
+ const replaceAll = (data, pairs) => scan(bufferPairs(pairs), data, true).out;
2301
2483
  /**
2302
- * Replaces byte strings in a stream, including across chunk boundaries: it holds back the
2303
- * longest `from` less one byte, so a match split between chunks is still found.
2484
+ * Replaces byte strings in a stream, including across chunk boundaries: it holds back a chunk's
2485
+ * tail while it could be the start of a match, so a match split between chunks is still found.
2304
2486
  */
2305
2487
  var ReplaceStream = class extends Transform {
2306
2488
  #pairs;
2307
- #hold;
2308
2489
  #tail = Buffer.alloc(0);
2309
2490
  /** How many replacements were made. */
2310
2491
  count = 0;
2311
2492
  constructor(pairs) {
2312
2493
  super();
2313
- this.#pairs = pairs.filter(([from]) => from.length > 0).map(([from, to]) => [Buffer.from(from), Buffer.from(to)]);
2314
- this.#hold = Math.max(0, ...this.#pairs.map(([from]) => from.length - 1));
2315
- }
2316
- /**
2317
- * Replaces every whole match in `data` (original bytes only: output is never re-scanned), and
2318
- * returns where the unscanned tail starts, which is `data.length` when `final`.
2319
- */
2320
- #scan(data, final) {
2321
- const parts = [];
2322
- let pos = 0;
2323
- for (;;) {
2324
- let best = -1;
2325
- let pair;
2326
- for (const p of this.#pairs) {
2327
- const i = data.indexOf(p[0], pos);
2328
- if (i !== -1 && (best === -1 || i < best)) {
2329
- best = i;
2330
- pair = p;
2331
- }
2332
- }
2333
- if (best === -1 || !pair) break;
2334
- parts.push(data.subarray(pos, best), pair[1]);
2335
- pos = best + pair[0].length;
2336
- this.count++;
2337
- }
2338
- const rest = final ? data.length : Math.max(pos, data.length - this.#hold);
2339
- parts.push(data.subarray(pos, rest));
2340
- return {
2341
- out: Buffer.concat(parts),
2342
- rest
2343
- };
2494
+ this.#pairs = bufferPairs(pairs);
2344
2495
  }
2345
2496
  _transform(chunk, _enc, done) {
2346
2497
  const data = Buffer.concat([this.#tail, chunk]);
2347
- const { out, rest } = this.#scan(data, false);
2498
+ const { out, rest, count } = scan(this.#pairs, data, false);
2499
+ this.count += count;
2348
2500
  this.#tail = Buffer.from(data.subarray(rest));
2349
2501
  if (out.length) this.push(out);
2350
2502
  done();
2351
2503
  }
2352
2504
  _flush(done) {
2353
- const { out } = this.#scan(this.#tail, true);
2505
+ const { out, count } = scan(this.#pairs, this.#tail, true);
2506
+ this.count += count;
2354
2507
  if (out.length) this.push(out);
2355
2508
  done();
2356
2509
  }
@@ -2406,6 +2559,85 @@ function tokeniseResponse(derived, body, known) {
2406
2559
  };
2407
2560
  }
2408
2561
  //#endregion
2562
+ //#region ../sandbox-docker/src/egress/websocket.ts
2563
+ /** The most a message may hold while its frames are gathered to be scrubbed. */
2564
+ const MAX_MESSAGE = 16777216;
2565
+ /** One unmasked frame, as a server sends it. */
2566
+ function frame(first, payload) {
2567
+ const n = payload.length;
2568
+ if (n < 126) return Buffer.concat([Buffer.from([first, n]), payload]);
2569
+ const head = Buffer.alloc(n < 65536 ? 4 : 10);
2570
+ head[0] = first;
2571
+ if (n < 65536) {
2572
+ head[1] = 126;
2573
+ head.writeUInt16BE(n, 2);
2574
+ } else {
2575
+ head[1] = 127;
2576
+ head.writeBigUInt64BE(BigInt(n), 2);
2577
+ }
2578
+ return Buffer.concat([head, payload]);
2579
+ }
2580
+ /**
2581
+ * Server-to-client WebSocket frames with every real value put back as its placeholder. Each data
2582
+ * message is gathered whole, so a value split across its frames is still found, and sent on as
2583
+ * one frame; control frames pass straight through, even between a message's frames. A
2584
+ * compressed frame (the upgrade request goes up without extensions, so a server shouldn't send
2585
+ * one), or a message over 16 MiB, errors the stream rather than pass unread.
2586
+ */
2587
+ function webSocketScrubber(subs) {
2588
+ const pairs = subs.map((s) => [s.value, s.placeholder]);
2589
+ let pending = Buffer.alloc(0);
2590
+ let message = null;
2591
+ return new Transform({ transform(chunk, _encoding, done) {
2592
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
2593
+ const out = [];
2594
+ while (pending.length >= 2) {
2595
+ const first = pending[0] ?? 0;
2596
+ const second = pending[1] ?? 0;
2597
+ let length = second & 127;
2598
+ let at = 2;
2599
+ if (length === 126) {
2600
+ if (pending.length < 4) break;
2601
+ length = pending.readUInt16BE(2);
2602
+ at = 4;
2603
+ } else if (length === 127) {
2604
+ if (pending.length < 10) break;
2605
+ const big = pending.readBigUInt64BE(2);
2606
+ if (big > BigInt(MAX_MESSAGE)) return done(/* @__PURE__ */ new Error("a WebSocket frame over 16 MiB"));
2607
+ length = Number(big);
2608
+ at = 10;
2609
+ }
2610
+ const masked = (second & 128) !== 0;
2611
+ const maskAt = at;
2612
+ if (masked) at += 4;
2613
+ if (pending.length < at + length) break;
2614
+ const payload = Buffer.from(pending.subarray(at, at + length));
2615
+ if (masked) for (let i = 0; i < payload.length; i++) payload[i] = (payload[i] ?? 0) ^ (pending[maskAt + i % 4] ?? 0);
2616
+ pending = pending.subarray(at + length);
2617
+ const opcode = first & 15;
2618
+ if (first & 112) return done(/* @__PURE__ */ new Error("a compressed or extended WebSocket frame, which can't be scrubbed"));
2619
+ if (opcode >= 8) {
2620
+ out.push(frame(first, payload));
2621
+ continue;
2622
+ }
2623
+ if (opcode === 0 ? !message : message) return done(/* @__PURE__ */ new Error("WebSocket frames out of order"));
2624
+ message ??= {
2625
+ opcode,
2626
+ parts: [],
2627
+ size: 0
2628
+ };
2629
+ message.parts.push(payload);
2630
+ message.size += payload.length;
2631
+ if (message.size > MAX_MESSAGE) return done(/* @__PURE__ */ new Error("a WebSocket message over 16 MiB"));
2632
+ if (first & 128) {
2633
+ out.push(frame(128 | message.opcode, replaceAll(Buffer.concat(message.parts), pairs)));
2634
+ message = null;
2635
+ }
2636
+ }
2637
+ done(null, out.length ? Buffer.concat(out) : void 0);
2638
+ } });
2639
+ }
2640
+ //#endregion
2409
2641
  //#region ../sandbox-docker/src/egress/proxy.ts
2410
2642
  const HOP_BY_HOP = /* @__PURE__ */ new Set([
2411
2643
  "connection",
@@ -2429,6 +2661,11 @@ function parseAuthority(authority, defaultPort) {
2429
2661
  port: Number(m?.[2] ?? defaultPort)
2430
2662
  };
2431
2663
  }
2664
+ /** `host` or `host:port` for a Host header or URL, IPv6 in brackets. */
2665
+ const authorityOf = (host, port) => {
2666
+ const name = net.isIPv6(host) ? `[${host}]` : host;
2667
+ return port === 443 ? name : `${name}:${port}`;
2668
+ };
2432
2669
  const pathOnly = (path) => path.split("?")[0];
2433
2670
  function reply(socket, status, reason) {
2434
2671
  const text = http.STATUS_CODES[status] ?? "";
@@ -2439,6 +2676,15 @@ const counter = (count) => new Transform({ transform(chunk, _encoding, done) {
2439
2676
  count(chunk.length);
2440
2677
  done(null, chunk);
2441
2678
  } });
2679
+ /** The response encodings the gateway can decode to scrub a body. */
2680
+ const DECODERS = {
2681
+ gzip: () => zlib.createGunzip(),
2682
+ "x-gzip": () => zlib.createGunzip(),
2683
+ deflate: () => zlib.createInflate(),
2684
+ br: () => zlib.createBrotliDecompress()
2685
+ };
2686
+ /** Text a credential could be echoed into, or no type at all. */
2687
+ const isText = (type) => !type.trim() || /^\s*(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|x-ndjson|[\w.-]+\+(json|xml))\b)/i.test(type);
2442
2688
  const plainHeaders = (headers) => Object.fromEntries(Object.entries(headers).flatMap(([k, v]) => v === void 0 ? [] : [[k, Array.isArray(v) ? v.map(String) : String(v)]]));
2443
2689
  function createEgressProxy(options) {
2444
2690
  const log = options.log ?? (() => {});
@@ -2685,25 +2931,39 @@ function createEgressProxy(options) {
2685
2931
  } catch {
2686
2932
  return res.writeHead(400).end("reeval egress gateway: expected an absolute URL\n");
2687
2933
  }
2688
- if (url.protocol !== "http:") return res.writeHead(400).end("reeval egress gateway: http only\n");
2689
- const { host, port } = parseAuthority(url.host, 80);
2934
+ if (url.protocol !== "http:" && url.protocol !== "https:") return res.writeHead(400).end(`reeval egress gateway: expected an http:// or https:// URL, not ${url.protocol}\n`);
2935
+ const secure = url.protocol === "https:";
2936
+ const { host, port } = parseAuthority(url.host, secure ? 443 : 80);
2937
+ const shown = secure ? `https://${host}` : `http://${host}:${port}`;
2690
2938
  const done = requestEvent(res, {
2691
2939
  method: req.method ?? "GET",
2692
- url: `http://${host}:${port}${url.pathname}`,
2940
+ url: `${shown}${url.pathname}`,
2693
2941
  host,
2694
2942
  port,
2695
- via: "http",
2943
+ via: secure ? "decrypted" : "http",
2696
2944
  requestHeaders: plainHeaders(req.headers)
2697
2945
  });
2698
2946
  check(host).then((verdict) => {
2699
2947
  if (!verdict.allowed) {
2700
- log(`reeval:egress ${req.method} http://${host}:${port} denied (${verdict.reason})`);
2948
+ log(`reeval:egress ${req.method} ${shown} denied (${verdict.reason})`);
2701
2949
  block(req.socket, host, port, verdict.reason);
2702
2950
  done.refuse(verdict.reason);
2703
2951
  return res.writeHead(verdict.status).end(`blocked by the reeval egress policy: ${verdict.reason}\n`);
2704
2952
  }
2705
- log(`reeval:egress ${req.method} http://${host}:${port}${url.pathname}`);
2706
2953
  contact(req.socket, host, port);
2954
+ if (secure) {
2955
+ const headers = {
2956
+ ...req.headers,
2957
+ host: url.host
2958
+ };
2959
+ onDecrypted({
2960
+ host,
2961
+ port,
2962
+ address: verdict.address
2963
+ }, req, res, url.pathname + url.search, headers, done);
2964
+ return;
2965
+ }
2966
+ log(`reeval:egress ${req.method} http://${host}:${port}${url.pathname}`);
2707
2967
  const upstream = http.request({
2708
2968
  host: verdict.address,
2709
2969
  port,
@@ -2714,7 +2974,12 @@ function createEgressProxy(options) {
2714
2974
  host: url.host
2715
2975
  }
2716
2976
  });
2717
- forwardHttp1(req.pipe(counter(done.sent)), res, upstream, [], void 0, done);
2977
+ forwardHttp1(req.pipe(counter(done.sent)), res, upstream, {
2978
+ own: [],
2979
+ creds: [],
2980
+ url: `${shown}${url.pathname}`,
2981
+ t: done
2982
+ });
2718
2983
  });
2719
2984
  });
2720
2985
  front.on("upgrade", (_req, socket) => {
@@ -2739,6 +3004,21 @@ function createEgressProxy(options) {
2739
3004
  * is refused (already logged): a Host that isn't the CONNECT target, or an unbrokered endpoint.
2740
3005
  */
2741
3006
  function transformed(target, method, path, headers, version) {
3007
+ if (!path.startsWith("/") && !(method === "OPTIONS" && path === "*")) {
3008
+ const reason = `reeval egress gateway: expected a path in the request line on a connection to ${target.host}, not an absolute URL.`;
3009
+ log(`reeval:egress ${method} https://${target.host} refused (absolute-form target)`);
3010
+ return {
3011
+ headers: {},
3012
+ path,
3013
+ own: [],
3014
+ names: [],
3015
+ credentials: [],
3016
+ refused: {
3017
+ status: 400,
3018
+ reason
3019
+ }
3020
+ };
3021
+ }
2742
3022
  const named = String(headers[":authority"] ?? headers.host ?? "");
2743
3023
  const asked = parseAuthority(named, target.port).host;
2744
3024
  if (named && asked !== target.host) {
@@ -2806,7 +3086,7 @@ function createEgressProxy(options) {
2806
3086
  path,
2807
3087
  headers: {}
2808
3088
  });
2809
- if (scrubbedInBodies(own).length || exchange || readsModel) delete substituted.headers["accept-encoding"];
3089
+ if (scrubbedInBodies(own).length || credentialValues(rules, target.host).length || exchange || readsModel) delete substituted.headers["accept-encoding"];
2810
3090
  const notes = [
2811
3091
  replaced.length ? `set ${replaced.join(", ")}` : "",
2812
3092
  substituted.names.length ? `substituted ${substituted.names.join(", ")}` : "",
@@ -2833,19 +3113,72 @@ function createEgressProxy(options) {
2833
3113
  delete headers["content-length"];
2834
3114
  return counted.pipe(bodySubstituter(own));
2835
3115
  }
2836
- /** The response body as sent to the sandbox: real values put back as placeholders. */
2837
3116
  const scrubs = options.scrubResponses !== false;
2838
- function responseBody(up, own, t, readsModel = false) {
3117
+ /**
3118
+ * The harness credentials the rules set on `host`, shaped as substitutions that are never
3119
+ * swapped in, only scrubbed from responses.
3120
+ */
3121
+ const credentialScrubs = (host) => credentialValues(rules, host).map(([value, shown]) => ({
3122
+ name: "credential",
3123
+ placeholder: shown,
3124
+ value,
3125
+ hosts: [host],
3126
+ in: []
3127
+ }));
3128
+ function scrubPlan(status, headers, own, creds) {
3129
+ if (!scrubs || status === 204 || status === 304) return { body: [] };
3130
+ const type = String(headers["content-type"] ?? "");
3131
+ const body = [...scrubbedInBodies(own), ...status >= 300 || isText(type) ? creds : []];
3132
+ const encoding = String(headers["content-encoding"] ?? "identity").toLowerCase();
3133
+ if (!body.length || encoding === "identity") return { body };
3134
+ if (encoding in DECODERS) return {
3135
+ body,
3136
+ decode: encoding
3137
+ };
3138
+ return {
3139
+ body,
3140
+ refuse: `the response is ${encoding}-encoded, so it can't be scrubbed`
3141
+ };
3142
+ }
3143
+ /**
3144
+ * The response body as sent to the sandbox: decoded if the plan says so, with real values put
3145
+ * back as placeholders. An error anywhere upstream of it (a corrupt encoding) errors it.
3146
+ */
3147
+ function responseBody(up, plan, t, readsModel = false) {
2839
3148
  if (readsModel && t) readModel(up, t);
2840
- const body = scrubbedInBodies(own).length && scrubs ? up.pipe(scrubber(own)) : up;
2841
- return t ? body.pipe(counter(t.received)) : body;
3149
+ const into = (from, to) => {
3150
+ from.once("error", (error) => to.destroy(error));
3151
+ return from.pipe(to);
3152
+ };
3153
+ let body = up;
3154
+ const decoder = plan.decode ? DECODERS[plan.decode] : void 0;
3155
+ if (decoder) body = into(body, decoder());
3156
+ if (plan.body.length) body = into(body, scrubber(plan.body));
3157
+ return t ? into(body, counter(t.received)) : body;
2842
3158
  }
2843
- const responseHeaders = (headers, own) => {
3159
+ /** Header (and trailer) values with every real value put back as its placeholder. */
3160
+ const scrubbedHeaders = (headers, all) => {
2844
3161
  const out = endToEnd(headers);
2845
- if (!own.length || !scrubs) return out;
2846
- if (scrubbedInBodies(own).length) delete out["content-length"];
2847
- return scrubHeaders(own, out);
3162
+ if (!all.length || !scrubs) return out;
3163
+ return scrubHeaders(all, out);
2848
3164
  };
3165
+ const responseHeaders = (headers, own, creds, plan) => {
3166
+ const out = scrubbedHeaders(headers, [...own, ...creds]);
3167
+ if (plan.body.length) delete out["content-length"];
3168
+ if (plan.decode) delete out["content-encoding"];
3169
+ return out;
3170
+ };
3171
+ /** Answers a response the gateway won't pass on with a 502 saying why. */
3172
+ function refuseResponse(res, up, url, why, t) {
3173
+ log(`reeval:egress ${url} response refused (${why})`);
3174
+ up.resume();
3175
+ t?.set({
3176
+ status: 502,
3177
+ reason: why
3178
+ });
3179
+ if (res.headersSent) return res.destroy();
3180
+ res.writeHead(502, { "content-type": "text/plain" }).end(`reeval egress gateway: ${why}\n`);
3181
+ }
2849
3182
  /**
2850
3183
  * How far into a response the gateway looks for the model that served it: OpenAI's first event
2851
3184
  * echoes the request's instructions and tools before it.
@@ -2874,7 +3207,7 @@ function createEgressProxy(options) {
2874
3207
  * A token exchange's response, read whole, with each token replaced by a placeholder. A success
2875
3208
  * that can't be read (compressed, too big, not JSON) is a 502: the real tokens never go through.
2876
3209
  */
2877
- function tokenised(exchange, url, status, headers, up, res, own, t) {
3210
+ function tokenised(exchange, url, status, headers, up, res, own, creds, t) {
2878
3211
  const chunks = [];
2879
3212
  let size = 0;
2880
3213
  const refuse = (why) => {
@@ -2883,15 +3216,20 @@ function createEgressProxy(options) {
2883
3216
  `);
2884
3217
  else res.destroy();
2885
3218
  };
2886
- const scrubbed = responseBody(up, own, t);
3219
+ const plan = scrubPlan(status, headers, own, creds);
3220
+ if (plan.refuse) {
3221
+ up.resume();
3222
+ return refuse(plan.refuse);
3223
+ }
3224
+ const scrubbed = responseBody(up, plan, t);
2887
3225
  scrubbed.on("data", (chunk) => {
2888
3226
  size += chunk.length;
2889
3227
  if (size <= EXCHANGE_LIMIT) chunks.push(chunk);
2890
3228
  });
2891
- up.once("error", (error) => refuse(error.message));
3229
+ scrubbed.once("error", (error) => refuse(error.message));
2892
3230
  scrubbed.once("end", () => {
2893
3231
  const body = Buffer.concat(chunks);
2894
- const out = responseHeaders(headers, own);
3232
+ const out = responseHeaders(headers, own, creds, plan);
2895
3233
  t?.set({ responseHeaders: plainHeaders(out) });
2896
3234
  if (status < 200 || status >= 300) return res.writeHead(status, out).end(body);
2897
3235
  const encoding = String(headers["content-encoding"] ?? "identity");
@@ -2913,10 +3251,10 @@ function createEgressProxy(options) {
2913
3251
  }).end(text);
2914
3252
  });
2915
3253
  }
2916
- async function onDecrypted(target, req, res) {
3254
+ async function onDecrypted(target, req, res, asked = req.url ?? "/", sent = req.headers, tracker) {
2917
3255
  const method = req.method ?? "GET";
2918
- const url = `https://${target.host}${pathOnly(req.url ?? "/")}`;
2919
- const t = requestEvent(res, {
3256
+ const url = `https://${target.host}${pathOnly(asked)}`;
3257
+ const t = tracker ?? requestEvent(res, {
2920
3258
  method,
2921
3259
  url,
2922
3260
  host: target.host,
@@ -2924,7 +3262,7 @@ function createEgressProxy(options) {
2924
3262
  via: "decrypted",
2925
3263
  requestHeaders: plainHeaders(req.headers)
2926
3264
  });
2927
- const { headers, path, own, names, credentials, refused, exchange, readsModel } = transformed(target, method, req.url ?? "/", req.headers, req.httpVersion);
3265
+ const { headers, path, own, names, credentials, refused, exchange, readsModel } = transformed(target, method, asked, sent, req.httpVersion);
2928
3266
  t.set({
2929
3267
  substituted: names,
2930
3268
  credentials
@@ -2937,7 +3275,7 @@ function createEgressProxy(options) {
2937
3275
  delete headers.upgrade;
2938
3276
  delete headers.connection;
2939
3277
  const body = requestBody(req, headers, own, t);
2940
- const authority = target.port === 443 ? target.host : `${target.host}:${target.port}`;
3278
+ const authority = authorityOf(target.host, target.port);
2941
3279
  const session = req.httpVersionMajor === 2 ? await upstreamSession(target) : null;
2942
3280
  if (!session) return forwardHttp1(body, res, https.request({
2943
3281
  host: target.address,
@@ -2951,10 +3289,14 @@ function createEgressProxy(options) {
2951
3289
  },
2952
3290
  agent,
2953
3291
  ca
2954
- }), own, exchange && {
2955
- exchange,
2956
- url
2957
- }, t, readsModel);
3292
+ }), {
3293
+ own,
3294
+ creds: credentialScrubs(target.host),
3295
+ url,
3296
+ ...exchange ? { exchange } : {},
3297
+ ...t ? { t } : {},
3298
+ readsModel: !!readsModel
3299
+ });
2958
3300
  const stream = session.request({
2959
3301
  ...headers,
2960
3302
  ":method": method,
@@ -2967,30 +3309,41 @@ function createEgressProxy(options) {
2967
3309
  else res.destroy();
2968
3310
  };
2969
3311
  stream.once("error", fail);
3312
+ const creds = credentialScrubs(target.host);
2970
3313
  stream.once("response", (h) => {
2971
- if (exchange) return tokenised(exchange, url, Number(h[":status"]), h, stream, res, own, t);
2972
- const out = responseHeaders(h, own);
2973
- t.set({ responseHeaders: plainHeaders(out) });
2974
- res.writeHead(Number(h[":status"]), out);
2975
- stream.once("trailers", (trailers) => res.addTrailers(responseHeaders(trailers, own)));
2976
- responseBody(stream, own, t, readsModel).pipe(res);
3314
+ const status = Number(h[":status"]);
3315
+ if (exchange) return tokenised(exchange, url, status, h, stream, res, own, creds, t);
3316
+ if (!respond(status, h, stream, res, own, creds, url, t, readsModel)) return;
3317
+ stream.once("trailers", (trailers) => res.addTrailers(scrubbedHeaders(trailers, [...own, ...creds])));
2977
3318
  });
2978
3319
  body.pipe(stream);
2979
3320
  req.once("aborted", () => stream.close(http2.constants.NGHTTP2_CANCEL));
2980
3321
  }
2981
- function forwardHttp1(req, res, upstream, own = [], tokenise, t, readsModel = false) {
3322
+ /**
3323
+ * Sends an upstream response on to the sandbox, scrubbed. False when it was refused instead,
3324
+ * already answered with a 502.
3325
+ */
3326
+ function respond(status, headers, up, res, own, creds, url, t, readsModel = false) {
3327
+ const plan = scrubPlan(status, headers, own, creds);
3328
+ if (plan.refuse) {
3329
+ refuseResponse(res, up, url, plan.refuse, t);
3330
+ return false;
3331
+ }
3332
+ const out = responseHeaders(headers, own, creds, plan);
3333
+ t?.set({ responseHeaders: plainHeaders(out) });
3334
+ res.writeHead(status, out);
3335
+ responseBody(up, plan, t, readsModel).once("error", () => res.destroy()).pipe(res);
3336
+ return true;
3337
+ }
3338
+ function forwardHttp1(req, res, upstream, to) {
3339
+ const { own, creds, url, exchange, t } = to;
2982
3340
  upstream.once("response", (up) => {
2983
- if (tokenise) {
2984
- const { exchange, url } = tokenise;
2985
- return tokenised(exchange, url, up.statusCode ?? 502, up.headers, up, res, own, t);
2986
- }
2987
- const out = responseHeaders(up.headers, own);
2988
- t?.set({ responseHeaders: plainHeaders(out) });
2989
- res.writeHead(up.statusCode ?? 502, out);
3341
+ const status = up.statusCode ?? 502;
3342
+ if (exchange) return tokenised(exchange, url, status, up.headers, up, res, own, creds, t);
3343
+ if (!respond(status, up.headers, up, res, own, creds, url, t, to.readsModel)) return;
2990
3344
  up.once("end", () => {
2991
- if (Object.keys(up.trailers).length) res.addTrailers(responseHeaders(up.trailers, own));
3345
+ if (Object.keys(up.trailers).length) res.addTrailers(scrubbedHeaders(up.trailers, [...own, ...creds]));
2992
3346
  });
2993
- responseBody(up, own, t, readsModel).pipe(res);
2994
3347
  });
2995
3348
  upstream.once("error", (error) => {
2996
3349
  t?.set({
@@ -3006,7 +3359,9 @@ function createEgressProxy(options) {
3006
3359
  const method = req.method ?? "GET";
3007
3360
  const startedAt = Date.now();
3008
3361
  const { headers, path, own, names, credentials, refused } = transformed(target, method, req.url ?? "/", req.headers, req.httpVersion);
3009
- const scrubbed = (value) => scrubs ? scrubValue(own, value) : value;
3362
+ const all = scrubs ? [...own, ...credentialScrubs(target.host)] : [];
3363
+ const scrubbed = (value) => scrubValue(all, value);
3364
+ if (all.length) delete headers["sec-websocket-extensions"];
3010
3365
  const upgraded = (fields) => emit({
3011
3366
  type: "request",
3012
3367
  method,
@@ -3044,7 +3399,7 @@ function createEgressProxy(options) {
3044
3399
  ca
3045
3400
  });
3046
3401
  upstream.once("upgrade", (up, upSocket, upHead) => {
3047
- const out = scrubs ? scrubHeaders(own, up.headers) : up.headers;
3402
+ const out = scrubHeaders(all, up.headers);
3048
3403
  upgraded({
3049
3404
  status: up.statusCode,
3050
3405
  responseHeaders: plainHeaders(out)
@@ -3052,9 +3407,20 @@ function createEgressProxy(options) {
3052
3407
  const lines = [`HTTP/1.1 ${up.statusCode} ${up.statusMessage}`];
3053
3408
  for (let i = 0; i < up.rawHeaders.length; i += 2) lines.push(`${up.rawHeaders[i]}: ${scrubbed(up.rawHeaders[i + 1] ?? "")}`);
3054
3409
  socket.write(`${lines.join("\r\n")}\r\n\r\n`);
3055
- if (upHead.length) socket.write(upHead);
3056
3410
  if (head.length) upSocket.write(head);
3057
- link(socket, track(upSocket));
3411
+ track(upSocket);
3412
+ if (!all.length) {
3413
+ if (upHead.length) socket.write(upHead);
3414
+ return link(socket, upSocket);
3415
+ }
3416
+ const frames = webSocketScrubber(all);
3417
+ if (upHead.length) frames.write(upHead);
3418
+ upSocket.pipe(frames);
3419
+ upSocket.once("error", (error) => frames.destroy(error));
3420
+ link(socket, Duplex.from({
3421
+ readable: frames,
3422
+ writable: upSocket
3423
+ }));
3058
3424
  });
3059
3425
  upstream.once("response", (up) => {
3060
3426
  reply(socket, up.statusCode ?? 502, `upstream refused the upgrade (${up.statusCode})`);
@@ -3082,7 +3448,7 @@ function createEgressProxy(options) {
3082
3448
  socket.destroy();
3083
3449
  return resolve(null);
3084
3450
  }
3085
- const session = http2.connect(`https://${key}`, { createConnection: () => socket });
3451
+ const session = http2.connect(`https://${authorityOf(target.host, target.port)}`, { createConnection: () => socket });
3086
3452
  session.on("error", () => {});
3087
3453
  session.once("close", () => sessions.get(key) === session && sessions.delete(key));
3088
3454
  session.once("goaway", () => sessions.get(key) === session && sessions.delete(key));
@@ -3192,6 +3558,7 @@ async function startGateway(containerId, options, signal, engine) {
3192
3558
  });
3193
3559
  child.stdin.on("error", () => {});
3194
3560
  let closed = false;
3561
+ let started = false;
3195
3562
  const mux = createMux({
3196
3563
  Duplex,
3197
3564
  parity: 0,
@@ -3202,7 +3569,15 @@ async function startGateway(containerId, options, signal, engine) {
3202
3569
  if (port === 43128) proxy.handle(stream);
3203
3570
  else stream.destroy();
3204
3571
  },
3205
- onReady: () => ready.resolve()
3572
+ onReady: () => {
3573
+ started = true;
3574
+ ready.resolve();
3575
+ },
3576
+ onError: (message) => {
3577
+ if (!started) return;
3578
+ stop(`the forwarder's output broke the protocol: ${message}`);
3579
+ child.kill("SIGKILL");
3580
+ }
3206
3581
  });
3207
3582
  const ready = {};
3208
3583
  ready.promise = new Promise((resolve, reject) => Object.assign(ready, {
@@ -3214,17 +3589,18 @@ async function startGateway(containerId, options, signal, engine) {
3214
3589
  const listeners = /* @__PURE__ */ new Map();
3215
3590
  const tunnels = /* @__PURE__ */ new Set();
3216
3591
  let stopped;
3217
- child.once("close", (code) => {
3592
+ /** Stops on its own: nothing reaches the container any more, so connections are refused. */
3593
+ const stop = (reason) => {
3218
3594
  const asked = closed;
3219
3595
  closed = true;
3220
3596
  mux.destroy();
3221
- const reason = `the forwarder exited (${code}): ${stderr.trim().slice(-500)}`.replace(/: $/, "");
3222
3597
  ready.reject(new Error(reason));
3223
3598
  if (asked) return;
3224
3599
  stopped = reason;
3225
3600
  for (const socket of tunnels) socket.destroy();
3226
3601
  for (const l of listeners.values()) l.then((s) => s.close());
3227
- });
3602
+ };
3603
+ child.once("close", (code) => stop(`the forwarder exited (${code}): ${stderr.trim().slice(-500)}`.replace(/: $/, "")));
3228
3604
  const timer = setTimeout(() => ready.reject(/* @__PURE__ */ new Error(`the forwarder didn't start within ${READY_TIMEOUT_MS / 1e3}s`)), READY_TIMEOUT_MS);
3229
3605
  const onAbort = () => ready.reject(signal?.reason);
3230
3606
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -3336,6 +3712,21 @@ const ROOT_CAPABILITIES = [
3336
3712
  "SETUID",
3337
3713
  "SETGID"
3338
3714
  ];
3715
+ /**
3716
+ * Every container's capabilities, for {@link DockerSession.runPrivileged}'s root execs: reading
3717
+ * and owning any file. The container's own process is root, and each exec names its user: an
3718
+ * exec as {@link SANDBOX_USER} holds none of them, and `no-new-privileges` keeps it from gaining
3719
+ * them. Podman gives a container's non-root user its added capabilities as ambient ones, which is
3720
+ * why the container itself isn't {@link SANDBOX_USER}.
3721
+ */
3722
+ const PRIVILEGED_CAPABILITIES = [
3723
+ "CHOWN",
3724
+ "DAC_OVERRIDE",
3725
+ "DAC_READ_SEARCH",
3726
+ "FOWNER"
3727
+ ];
3728
+ /** The user each of the sandbox's own execs runs as. */
3729
+ const execUser = (options) => options.root ? "0:0" : SANDBOX_USER;
3339
3730
  const hostCpus = /* @__PURE__ */ new Map();
3340
3731
  /** Docker refuses `--cpus` above the host's count, so cap it there. */
3341
3732
  async function cpuLimit(cpus, engine = "docker") {
@@ -3348,18 +3739,23 @@ async function cpuLimit(cpus, engine = "docker") {
3348
3739
  return String(available ? Math.min(cpus, available) : cpus);
3349
3740
  }
3350
3741
  /**
3351
- * The `docker run` flags that confine the container: its user, capabilities and limits, and on
3352
- * Podman the bridge network and Docker's seccomp profile.
3742
+ * The `docker run` flags that confine the container: its capabilities and limits, and on Podman
3743
+ * the bridge network and Docker's seccomp profile. Its own process is root's `sleep`; every exec
3744
+ * names its user.
3353
3745
  */
3354
3746
  async function confinementArgs(options) {
3355
3747
  const limits = {
3356
3748
  ...DEFAULT_LIMITS,
3357
3749
  ...options.limits
3358
3750
  };
3359
- const caps = [...options.root ? ROOT_CAPABILITIES : [], ...options.capabilities ?? []];
3751
+ const caps = [
3752
+ ...PRIVILEGED_CAPABILITIES,
3753
+ ...options.root ? ROOT_CAPABILITIES : [],
3754
+ ...options.capabilities ?? []
3755
+ ];
3360
3756
  return [
3361
3757
  "--user",
3362
- options.root ? "0:0" : SANDBOX_USER,
3758
+ "0:0",
3363
3759
  "--cap-drop",
3364
3760
  "ALL",
3365
3761
  ...[...new Set(caps)].flatMap((c) => ["--cap-add", c]),
@@ -3394,9 +3790,11 @@ const EXEC_UMASK = "umask 022";
3394
3790
  * `memory.events` and v1's `memory.oom_control` both have an `oom_kill` line. Without either,
3395
3791
  * Docker's `OOMKilled` says only whether there was one.
3396
3792
  */
3397
- async function oomKills(id, engine) {
3793
+ async function oomKills(id, engine, user = SANDBOX_USER) {
3398
3794
  const events = await docker([
3399
3795
  "exec",
3796
+ "--user",
3797
+ user,
3400
3798
  id,
3401
3799
  "sh",
3402
3800
  "-c",
@@ -3474,12 +3872,7 @@ async function createDockerSession(options) {
3474
3872
  env,
3475
3873
  engine
3476
3874
  }).catch((error) => {
3477
- const sweep = () => sweepLabelDetached(`${LABEL.session}=${nonce}`, void 0, engine);
3478
- if (signal?.aborted) sweep();
3479
- if (/unable to find user|no matching entries in passwd/i.test(error.message)) {
3480
- sweep();
3481
- throw new Error(`${options.image} has no ${SANDBOX_USER} user to run the sandbox as. Images reeval builds have one: rebuild it (reeval image build), or start from reeval's image or ${spec.fn}({ from }). An eval that needs root at run time can opt in with ${spec.fn}({ root: true }).`);
3482
- }
3875
+ if (signal?.aborted) sweepLabelDetached(`${LABEL.session}=${nonce}`, void 0, engine);
3483
3876
  throw error;
3484
3877
  });
3485
3878
  let gateway;
@@ -3493,27 +3886,33 @@ async function createDockerSession(options) {
3493
3886
  await remove();
3494
3887
  throw signal.reason;
3495
3888
  }
3496
- if (!spec.createsWorkdir) {
3497
- const made = await docker([
3498
- "exec",
3499
- "--user",
3500
- "0:0",
3501
- id,
3502
- "sh",
3503
- "-c",
3504
- `${EXEC_UMASK} && mkdir -p ${SANDBOX_ROOT}`
3505
- ], {
3506
- engine,
3507
- signal
3508
- }).catch((error) => ({
3509
- exitCode: 1,
3510
- stderr: error.message
3511
- }));
3512
- if (made.exitCode !== 0) {
3513
- await remove();
3514
- if (signal?.aborted) throw signal.reason;
3515
- throw new Error(`Couldn't create ${SANDBOX_ROOT} in the container: ${made.stderr.trim()}`);
3516
- }
3889
+ const NO_USER = "reeval:no-sandbox-user";
3890
+ const made = await docker([
3891
+ "exec",
3892
+ "--user",
3893
+ "0:0",
3894
+ id,
3895
+ "/bin/sh",
3896
+ "-c",
3897
+ [
3898
+ EXEC_UMASK,
3899
+ ...options.root ? [] : [`{ id -u ${SANDBOX_USER} >/dev/null 2>&1 || { echo ${NO_USER}; exit 1; }; }`],
3900
+ ...spec.createsWorkdir ? [] : [`mkdir -p ${SANDBOX_ROOT}`],
3901
+ `mkdir -p ${SANDBOX_STATE} && chown 0:0 ${SANDBOX_STATE} && chmod 700 ${SANDBOX_STATE}`
3902
+ ].join(" && ")
3903
+ ], {
3904
+ engine,
3905
+ signal
3906
+ }).catch((error) => ({
3907
+ exitCode: 1,
3908
+ stdout: Buffer.alloc(0),
3909
+ stderr: error.message
3910
+ }));
3911
+ if (made.exitCode !== 0) {
3912
+ await remove();
3913
+ if (signal?.aborted) throw signal.reason;
3914
+ if (made.stdout.toString("utf8").includes(NO_USER)) throw new Error(`${options.image} has no ${SANDBOX_USER} user to run the sandbox as. Images reeval builds have one: rebuild it (reeval image build), or start from reeval's image or ${spec.fn}({ from }). An eval that needs root at run time can opt in with ${spec.fn}({ root: true }).`);
3915
+ throw new Error(`Couldn't set up the container: ${made.stderr.trim()}`);
3517
3916
  }
3518
3917
  const egressOptions = options.egress === true ? {} : options.egress || void 0;
3519
3918
  let egress;
@@ -3565,10 +3964,12 @@ async function createDockerSession(options) {
3565
3964
  ...gateway.env,
3566
3965
  ...env
3567
3966
  } : env;
3967
+ const user = ["--user", execUser(options)];
3568
3968
  let execs = 0;
3569
3969
  const execArgs = (command, marker, cwd, env) => [
3570
3970
  "exec",
3571
3971
  "--interactive",
3972
+ ...user,
3572
3973
  "--workdir",
3573
3974
  cwd ? posix.resolve(SANDBOX_ROOT, cwd) : SANDBOX_ROOT,
3574
3975
  "--env",
@@ -3584,10 +3985,16 @@ async function createDockerSession(options) {
3584
3985
  * Killing the host's `docker exec` client doesn't stop the command in the container, so kill
3585
3986
  * every process there that inherited this exec's marker: the command and all it started.
3586
3987
  */
3587
- const killExec = (marker) => docker([
3988
+ const killExec = (marker, asRoot = false) => docker([
3588
3989
  "exec",
3990
+ ...asRoot ? [
3991
+ "--user",
3992
+ "0:0",
3993
+ "--env",
3994
+ `PATH=${SYSTEM_PATH}`
3995
+ ] : user,
3589
3996
  id,
3590
- "sh",
3997
+ "/bin/sh",
3591
3998
  "-c",
3592
3999
  `for e in /proc/[0-9]*/environ; do grep -qsxzF ${shellQuote(`${EXEC_MARKER}=${marker}`)} "$e" && kill -9 "$(basename "$(dirname "$e")")" 2>/dev/null; done; true`
3593
4000
  ], { engine }).then(() => {}, () => {});
@@ -3614,11 +4021,10 @@ async function createDockerSession(options) {
3614
4021
  const marker = nextMarker();
3615
4022
  const args = [
3616
4023
  "exec",
3617
- "--privileged",
3618
4024
  "--user",
3619
4025
  "0:0",
3620
4026
  "--workdir",
3621
- o.workingDirectory ? posix.resolve(SANDBOX_ROOT, o.workingDirectory) : SANDBOX_ROOT,
4027
+ SANDBOX_STATE,
3622
4028
  "--env",
3623
4029
  `${EXEC_MARKER}=${marker}`,
3624
4030
  "--env",
@@ -3639,13 +4045,14 @@ async function createDockerSession(options) {
3639
4045
  stderr: r.stderr
3640
4046
  };
3641
4047
  } catch (error) {
3642
- if (o.abortSignal?.aborted) await killExec(marker);
4048
+ if (o.abortSignal?.aborted) await killExec(marker, true);
3643
4049
  throw error;
3644
4050
  }
3645
4051
  };
3646
4052
  const readBinaryFile = async (o) => {
3647
4053
  const r = await docker([
3648
4054
  "exec",
4055
+ ...user,
3649
4056
  id,
3650
4057
  "cat",
3651
4058
  "--",
@@ -3658,13 +4065,29 @@ async function createDockerSession(options) {
3658
4065
  };
3659
4066
  const writeBinaryFile = async (o) => {
3660
4067
  const path = posix.resolve(SANDBOX_ROOT, o.path);
4068
+ const make = `${EXEC_UMASK} && mkdir -p ${shellQuote(posix.dirname(path))}`;
4069
+ if (o.content.byteLength === 0) {
4070
+ await dockerOk([
4071
+ "exec",
4072
+ ...user,
4073
+ id,
4074
+ "sh",
4075
+ "-c",
4076
+ `${make} && : > ${shellQuote(path)}`
4077
+ ], {
4078
+ signal: o.abortSignal,
4079
+ engine
4080
+ });
4081
+ return;
4082
+ }
3661
4083
  await dockerOk([
3662
4084
  "exec",
3663
4085
  "--interactive",
4086
+ ...user,
3664
4087
  id,
3665
4088
  "sh",
3666
4089
  "-c",
3667
- `${EXEC_UMASK} && mkdir -p ${shellQuote(posix.dirname(path))} && cat > ${shellQuote(path)}`
4090
+ `${make} && cat > ${shellQuote(path)}`
3668
4091
  ], {
3669
4092
  input: Buffer.from(o.content),
3670
4093
  signal: o.abortSignal,
@@ -3703,7 +4126,7 @@ async function createDockerSession(options) {
3703
4126
  },
3704
4127
  ...brokering,
3705
4128
  ...substituting,
3706
- oomKills: () => oomKills(id, engine),
4129
+ oomKills: () => oomKills(id, engine, execUser(options)),
3707
4130
  run,
3708
4131
  runPrivileged,
3709
4132
  async spawn(o) {
@@ -3850,22 +4273,22 @@ const BOOTSTRAPS = {
3850
4273
  name: "Cursor",
3851
4274
  cli: "@cursor/sdk",
3852
4275
  version: `node -p "'@cursor/sdk ' + require(process.env.HOME + '/.ai-sdk-harness/.harness-bootstrap/cursor-sdk/node_modules/@cursor/sdk/package.json').version"`,
3853
- adapter: async () => (await import("./src-C0xowZ3C.js")).createCursorSdk()
4276
+ adapter: async () => (await import("./src-DrJSnoMR.js")).createCursorSdk()
3854
4277
  },
3855
4278
  "claude-code": {
3856
4279
  name: "Claude Code",
3857
4280
  cli: "claude",
3858
- adapter: async () => (await import("./dist-CMjp3qqF.js")).createClaudeCode()
4281
+ adapter: async () => (await import("./dist-Dp_AJgYN.js")).createClaudeCode()
3859
4282
  },
3860
4283
  antigravity: {
3861
4284
  name: "Antigravity",
3862
4285
  cli: "agy",
3863
- adapter: async () => (await import("./src-bCnsA6Vp.js")).createAntigravity()
4286
+ adapter: async () => (await import("./src-BAyg6CtQ.js")).createAntigravity()
3864
4287
  },
3865
4288
  opencode: {
3866
4289
  name: "OpenCode",
3867
4290
  cli: "opencode",
3868
- adapter: async () => (await import("./src-Bcah6ZkT.js")).createOpenCode({
4291
+ adapter: async () => (await import("./src-B3KCLZVk.js")).createOpenCode({
3869
4292
  auth: { GOOGLE_GENERATIVE_AI_API_KEY: "unused" },
3870
4293
  model: "google/unused"
3871
4294
  })
@@ -3873,7 +4296,7 @@ const BOOTSTRAPS = {
3873
4296
  goose: {
3874
4297
  name: "Goose",
3875
4298
  cli: "goose",
3876
- adapter: async () => (await import("./src-DPIjmReZ.js")).createGoose({
4299
+ adapter: async () => (await import("./src-dhSTRN50.js")).createGoose({
3877
4300
  auth: { GOOGLE_API_KEY: "unused" },
3878
4301
  model: "unused"
3879
4302
  })
@@ -3881,17 +4304,17 @@ const BOOTSTRAPS = {
3881
4304
  pi: {
3882
4305
  name: "pi",
3883
4306
  cli: "pi",
3884
- adapter: async () => (await import("./src-ynBVosfX.js")).createPi({ auth: { GEMINI_API_KEY: "unused" } })
4307
+ adapter: async () => (await import("./src-BezgGX4X.js")).createPi({ auth: { GEMINI_API_KEY: "unused" } })
3885
4308
  },
3886
4309
  codex: {
3887
4310
  name: "Codex",
3888
4311
  cli: "codex",
3889
- adapter: async () => (await import("./src-DLtyz4w5.js")).createCodex({ auth: { OPENAI_API_KEY: "unused" } })
4312
+ adapter: async () => (await import("./src-ChBhk06Q.js")).createCodex({ auth: { OPENAI_API_KEY: "unused" } })
3890
4313
  },
3891
4314
  fx: {
3892
4315
  name: "fx",
3893
4316
  cli: "fx",
3894
- adapter: async () => (await import("./src-DXNh5mTr.js")).createFx({
4317
+ adapter: async () => (await import("./src-CoqZYspK.js")).createFx({
3895
4318
  auth: { AI_GATEWAY_API_KEY: "unused" },
3896
4319
  model: "unused/unused"
3897
4320
  })
@@ -3899,7 +4322,7 @@ const BOOTSTRAPS = {
3899
4322
  "grok-build": {
3900
4323
  name: "Grok Build",
3901
4324
  cli: "grok",
3902
- adapter: async () => (await import("./src-CHIfIKUu.js")).createGrokBuild({
4325
+ adapter: async () => (await import("./src-3mP821Pt.js")).createGrokBuild({
3903
4326
  auth: { XAI_API_KEY: "unused" },
3904
4327
  model: "unused"
3905
4328
  })
@@ -3907,11 +4330,21 @@ const BOOTSTRAPS = {
3907
4330
  };
3908
4331
  /** Keeps the image's `PATH` in login shells, which Debian's `/etc/profile` resets. */
3909
4332
  const profilePathScript = () => readFileSync(join(DOCKER_CONTEXT, "reeval-path.sh")).toString("base64");
4333
+ /** `from` as it goes after `FROM`: one word, so it can't add a stage name or another instruction. */
4334
+ function imageReference(from) {
4335
+ if (!/^\S+$/.test(from)) throw new Error(`docker({ from }) is an image reference without spaces or line breaks (got ${JSON.stringify(from)})`);
4336
+ return from;
4337
+ }
4338
+ /**
4339
+ * A CLI's `--version` output as a label value in `docker commit --change`: its first line, without
4340
+ * quotes, backslashes or anything else that could end the value and add an instruction.
4341
+ */
4342
+ const versionLabel = (output) => (output.split(/\r?\n/)[0] ?? "").replace(/[^\w .+:/()@,~-]/g, "").trim().slice(0, 100) || "unknown";
3910
4343
  /**
3911
4344
  * Adds what the harness bridge needs to a Debian or Ubuntu based toolchain image: Node (copied
3912
4345
  * from the official image, so the base's own toolchain is untouched), pnpm, git, curl and bash.
3913
4346
  */
3914
- const toolchainDockerfile = (from) => `FROM ${from}
4347
+ const toolchainDockerfile = (from) => `FROM ${imageReference(from)}
3915
4348
  SHELL ["/bin/sh", "-c"]
3916
4349
  RUN command -v apt-get >/dev/null || { echo "reeval needs a Debian or Ubuntu based image (glibc and apt-get)" >&2; exit 1; } \\
3917
4350
  && apt-get update \\
@@ -4225,7 +4658,7 @@ async function bake(session, bootstrap) {
4225
4658
  }
4226
4659
  const version = await session.run({ command: probe });
4227
4660
  if (bootstrap.version && version.exitCode !== 0) throw new Error(`The harness bootstrap did not install ${bootstrap.cli}:\n${tail(version.stderr)}`);
4228
- return version.stdout.trim() || "unknown";
4661
+ return versionLabel(version.stdout.trim());
4229
4662
  }
4230
4663
  /**
4231
4664
  * The image `docker({ install })` runs in: the commands on top of `parent`, tagged by the parent's
@@ -4532,6 +4965,6 @@ async function buildDockerfile(dockerfile, options) {
4532
4965
  */
4533
4966
  const MCP_TIMER = fileURLToPath(ownFileURL("bin/mcp-timer.cjs"));
4534
4967
  //#endregion
4535
- export { docker as $, applyTransformations as A, BRIDGE_PORT as B, DEFAULT_LIMITS as C, CA_PATH as D, dockerSandboxProvider as E, PACKAGE_HOSTS as F, SANDBOX_ROOT as G, HARNESS_PACKAGES as H, defaultAllowedHosts as I, WORKSPACE_DIR as J, SANDBOX_STATE as K, hostAllowed as L, onEndpoint as M, DOCKER_EGRESS as N, PROXY_PORT as O, HARNESS_HOSTS as P, isBakedHarness as Q, BAKED_HARNESSES as R, usesReevalBase as S, createDockerSession as T, LABEL as U, DEFAULT_IMAGE as V, SANDBOX_HOME as W, harnessLabel as X, adapterVersion as Y, imageFor as Z, pluginBuildKey as _, retryTransient as _t, buildDockerfile as a, imageTags as at, resolveFrom as b, engineSpec as bt, buildPluginLayer as c, onDockerCommand as ct, dockerfileTag as d, ImageBuildError as dt, dockerAvailable as et, dockerignoreMatcher as f, RETRY_DELAYS as ft, pluginBuildDir as g, parseBuildOutput as gt, installScript as h, isTransient as ht, REEVAL_BASE_ARG as i, imageInfo as it, matches as j, placeholder as k, contextFiles as l, run as lt, installLayerTag as m, failureCause as mt, DOCKER_CONTEXT as n, dockerOk as nt, buildImage as o, isRegistryDigest as ot, harnessImageFor as p, causeLine as pt, SANDBOX_USER as q, PLUGIN_BUILDS_DIR as r, dockerState as rt, buildInstallLayer as s, removeContainerDetached as st, MCP_TIMER as t, dockerCapacity as tt, contextHash as u, shellQuote as ut, pluginLayerTag as v, ENGINES as vt, ROOT_CAPABILITIES as w, sourceTreeHash as x, normalizeImageId as xt, pruneImages as y, dockerSeccompFile as yt, BASE_IMAGE as z };
4968
+ export { isBakedHarness as $, placeholder as A, BASE_IMAGE as B, DEFAULT_LIMITS as C, dockerSandboxProvider as D, createDockerSession as E, HARNESS_HOSTS as F, SANDBOX_HOME as G, DEFAULT_IMAGE as H, PACKAGE_HOSTS as I, SANDBOX_USER as J, SANDBOX_ROOT as K, defaultAllowedHosts as L, matches as M, onEndpoint as N, CA_PATH as O, DOCKER_EGRESS as P, imageFor as Q, hostAllowed as R, usesReevalBase as S, normalizeImageId as St, ROOT_CAPABILITIES as T, HARNESS_PACKAGES as U, BRIDGE_PORT as V, LABEL as W, adapterVersion as X, WORKSPACE_DIR as Y, harnessLabel as Z, pluginBuildKey as _, parseBuildOutput as _t, buildDockerfile as a, imageInfo as at, resolveFrom as b, dockerSeccompFile as bt, buildPluginLayer as c, removeContainerDetached as ct, dockerfileTag as d, shellQuote as dt, docker as et, dockerignoreMatcher as f, ImageBuildError as ft, pluginBuildDir as g, isTransient as gt, installScript as h, failureCause as ht, REEVAL_BASE_ARG as i, dockerState as it, applyTransformations as j, PROXY_PORT as k, contextFiles as l, onDockerCommand as lt, installLayerTag as m, causeLine as mt, DOCKER_CONTEXT as n, dockerCapacity as nt, buildImage as o, imageTags as ot, harnessImageFor as p, RETRY_DELAYS as pt, SANDBOX_STATE as q, PLUGIN_BUILDS_DIR as r, dockerOk as rt, buildInstallLayer as s, isRegistryDigest as st, MCP_TIMER as t, dockerAvailable as tt, contextHash as u, run as ut, pluginLayerTag as v, retryTransient as vt, PRIVILEGED_CAPABILITIES as w, sourceTreeHash as x, engineSpec as xt, pruneImages as y, ENGINES as yt, BAKED_HARNESSES as z };
4536
4969
 
4537
- //# sourceMappingURL=src-LekQrr_S.js.map
4970
+ //# sourceMappingURL=src-CIpKL45U.js.map