reeval 0.11.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 (312) hide show
  1. package/README.md +115 -257
  2. package/assets/package.json +1 -1
  3. package/assets/pnpm-lock.yaml +41 -30
  4. package/bin/mcp-timer.cjs +224 -0
  5. package/dist/{adapter-Bwy3QxAK.js → adapter-IKKo4sZj.js} +4 -4
  6. package/dist/{adapter-Bwy3QxAK.js.map → adapter-IKKo4sZj.js.map} +1 -1
  7. package/dist/{agent-DpE80WPz.js → agent-ClE-g9VL.js} +22 -7
  8. package/dist/{agent-DpE80WPz.js.map → agent-ClE-g9VL.js.map} +1 -1
  9. package/dist/{analysis-Bxub9LMT.js → analysis-B0nOfAzA.js} +3 -3
  10. package/dist/{analysis-Bxub9LMT.js.map → analysis-B0nOfAzA.js.map} +1 -1
  11. package/dist/{assemble-D9GWr-HS.js → assemble-BO93VAk1.js} +22 -20
  12. package/dist/assemble-BO93VAk1.js.map +1 -0
  13. package/dist/assemble-VQxyoDxK.js +1 -0
  14. package/dist/{attempt-CGPV3BZY.js → attempt-jI0OWxlz.js} +2 -2
  15. package/dist/{attempt-CGPV3BZY.js.map → attempt-jI0OWxlz.js.map} +1 -1
  16. package/dist/bridge/index.mjs +2 -1
  17. package/dist/{capture-BQlGHiMC.js → capture-dn2hjSLj.js} +29 -21
  18. package/dist/{capture-BQlGHiMC.js.map → capture-dn2hjSLj.js.map} +1 -1
  19. package/dist/capture-nIJ-7kUE.js +1 -0
  20. package/dist/{cli-ClpPT8kn.js → cli-BCE1xlm4.js} +176 -120
  21. package/dist/cli-BCE1xlm4.js.map +1 -0
  22. package/dist/{cli-spec-qmEnQ25_.js → cli-spec-BXIBUlaK.js} +34 -15
  23. package/dist/cli-spec-BXIBUlaK.js.map +1 -0
  24. package/dist/cli.js +2 -2
  25. package/dist/{command-BlM0y8jN.js → command-Bh3yhN_b.js} +8 -8
  26. package/dist/{command-BlM0y8jN.js.map → command-Bh3yhN_b.js.map} +1 -1
  27. package/dist/command-C6vwmjS7.js +91 -0
  28. package/dist/command-C6vwmjS7.js.map +1 -0
  29. package/dist/{command-DbAe1Rzb.js → command-CANun0m0.js} +16 -19
  30. package/dist/command-CANun0m0.js.map +1 -0
  31. package/dist/command-CBaHukyl.js +335 -0
  32. package/dist/command-CBaHukyl.js.map +1 -0
  33. package/dist/{command-TgbzPizb.js → command-D_kEnYcX.js} +17 -16
  34. package/dist/command-D_kEnYcX.js.map +1 -0
  35. package/dist/commands-7tH3EsVu.js +666 -0
  36. package/dist/commands-7tH3EsVu.js.map +1 -0
  37. package/dist/{commands-CthqwkAn.js → commands-BKGzBMgb.js} +1 -1
  38. package/dist/{config-DrVSEVN0.js → config-D7TFvUpL.js} +40 -55
  39. package/dist/config-D7TFvUpL.js.map +1 -0
  40. package/dist/{context-C1mxXh80.js → context-Dgn48BqT.js} +13 -2
  41. package/dist/context-Dgn48BqT.js.map +1 -0
  42. package/dist/{contrasts-5zsj32gk.js → contrasts-B4nOhn0Z.js} +3 -3
  43. package/dist/{contrasts-5zsj32gk.js.map → contrasts-B4nOhn0Z.js.map} +1 -1
  44. package/dist/db-DJUuFoWQ.js +602 -0
  45. package/dist/db-DJUuFoWQ.js.map +1 -0
  46. package/dist/db-W9EFlXnn.js +2 -0
  47. package/dist/{dist-CuY4ehEk.js → dist-Dp_AJgYN.js} +3 -3
  48. package/dist/{dist-CuY4ehEk.js.map → dist-Dp_AJgYN.js.map} +1 -1
  49. package/dist/{docker-D1HZDS6q.js → docker-CQxucRPe.js} +16 -15
  50. package/dist/docker-CQxucRPe.js.map +1 -0
  51. package/dist/{docker-DNmMPOBb.js → docker-Dac6Zalz.js} +1 -1
  52. package/dist/{docker-DOvjji9I.js → docker-DuHcQidD.js} +3 -3
  53. package/dist/{docker-DOvjji9I.js.map → docker-DuHcQidD.js.map} +1 -1
  54. package/dist/{doctor-D2MbNO8L.js → doctor-De_QlOTM.js} +72 -38
  55. package/dist/doctor-De_QlOTM.js.map +1 -0
  56. package/dist/{egress-2-qzPvGi.js → egress-DEAvq8Fl.js} +453 -31
  57. package/dist/egress-DEAvq8Fl.js.map +1 -0
  58. package/dist/env-BQqj93gu.js +21 -0
  59. package/dist/env-BQqj93gu.js.map +1 -0
  60. package/dist/{environment-Cca6a4wB.js → environment-B4IuURK1.js} +247 -14
  61. package/dist/environment-B4IuURK1.js.map +1 -0
  62. package/dist/{evaluate-DMyaGQEj.d.ts → evaluate-C9-ritA5.d.ts} +2 -2
  63. package/dist/{evaluate-DKIl54Hb.js → evaluate-Djk1YPFL.js} +540 -560
  64. package/dist/evaluate-Djk1YPFL.js.map +1 -0
  65. package/dist/facts-Cfx1H70y.js +898 -0
  66. package/dist/facts-Cfx1H70y.js.map +1 -0
  67. package/dist/files-Bs6H3HiG.js +65 -0
  68. package/dist/files-Bs6H3HiG.js.map +1 -0
  69. package/dist/{files-BpUYBwGu.js → files-CN1QfenC.js} +31 -8
  70. package/dist/files-CN1QfenC.js.map +1 -0
  71. package/dist/files-CtjEXWXm.js +1 -0
  72. package/dist/{files-BXHZFWmx.js → files-D27nUk0K.js} +6 -17
  73. package/dist/files-D27nUk0K.js.map +1 -0
  74. package/dist/files-D3gJa91B.js +1 -0
  75. package/dist/generate-BnrvBAzF.js +1646 -0
  76. package/dist/generate-BnrvBAzF.js.map +1 -0
  77. package/dist/{git-Crl3b87o.js → git-rPfl7AD1.js} +3 -3
  78. package/dist/{git-Crl3b87o.js.map → git-rPfl7AD1.js.map} +1 -1
  79. package/dist/{harness-CEuntrla.js → harness-1OkumOAm.js} +3 -3
  80. package/dist/{harness-CEuntrla.js.map → harness-1OkumOAm.js.map} +1 -1
  81. package/dist/harness.d.ts +1 -1
  82. package/dist/harness.js +1 -1
  83. package/dist/{header-DHQv_ZrF.js → header-BVzbRnNv.js} +10 -3
  84. package/dist/{header-DHQv_ZrF.js.map → header-BVzbRnNv.js.map} +1 -1
  85. package/dist/{images-Bi3aGxb9.js → images-m784ueGK.js} +61 -13
  86. package/dist/images-m784ueGK.js.map +1 -0
  87. package/dist/index.d.ts +3 -3
  88. package/dist/index.js +4 -4
  89. package/dist/init-hiTkiM4L.js +543 -0
  90. package/dist/init-hiTkiM4L.js.map +1 -0
  91. package/dist/init-kMkTgTZc.js +1 -0
  92. package/dist/insights-B2Xmhz9o.js +64 -0
  93. package/dist/insights-B2Xmhz9o.js.map +1 -0
  94. package/dist/{invocations-BYDPe3fV.js → invocations-D_ePcwyL.js} +4 -4
  95. package/dist/{invocations-BYDPe3fV.js.map → invocations-D_ePcwyL.js.map} +1 -1
  96. package/dist/{judge-DW_GDOmB.js → judge-CEDIfRiU.js} +50 -15
  97. package/dist/judge-CEDIfRiU.js.map +1 -0
  98. package/dist/judge.d.ts +7 -6
  99. package/dist/judge.js +8 -7
  100. package/dist/judge.js.map +1 -1
  101. package/dist/{live-jlfszAbb.js → live-DYd2QEyR.js} +2 -2
  102. package/dist/{live-jlfszAbb.js.map → live-DYd2QEyR.js.map} +1 -1
  103. package/dist/model-B8H3gL69.js +1 -0
  104. package/dist/{model-DaVCY3rb.js → model-CTiuzkWw.js} +9 -7
  105. package/dist/model-CTiuzkWw.js.map +1 -0
  106. package/dist/model-DcIbmggK.js +1 -0
  107. package/dist/{model-D0IcOQao.js → model-DpkclE4r.js} +60 -25
  108. package/dist/model-DpkclE4r.js.map +1 -0
  109. package/dist/{model-DaX4bNLZ.js → model-UgHDMhI1.js} +144 -46
  110. package/dist/model-UgHDMhI1.js.map +1 -0
  111. package/dist/{node-B3ydHo6t.js → node-CGZYh4Fj.js} +4 -4
  112. package/dist/{node-B3ydHo6t.js.map → node-CGZYh4Fj.js.map} +1 -1
  113. package/dist/{opencode-providers-J9colP0-.js → opencode-providers-snfeWd6Z.js} +86 -47
  114. package/dist/opencode-providers-snfeWd6Z.js.map +1 -0
  115. package/dist/{options-C0d0AzeR.js → options-CyAyLjgJ.js} +22 -11
  116. package/dist/options-CyAyLjgJ.js.map +1 -0
  117. package/dist/{pairs-CnInRu-G.js → pairs-DP6XOJby.js} +2 -2
  118. package/dist/{pairs-CnInRu-G.js.map → pairs-DP6XOJby.js.map} +1 -1
  119. package/dist/{patch-B1bnfhN-.js → patch-uMq9rCl_.js} +2 -2
  120. package/dist/{patch-B1bnfhN-.js.map → patch-uMq9rCl_.js.map} +1 -1
  121. package/dist/{plan-DgQxp9yy.js → plan-CZYgXnZh.js} +21 -8
  122. package/dist/plan-CZYgXnZh.js.map +1 -0
  123. package/dist/plugins-P-uh54VF.js +654 -0
  124. package/dist/plugins-P-uh54VF.js.map +1 -0
  125. package/dist/{preflight-DlQvbySb.js → preflight-BQlPTHVu.js} +1 -1
  126. package/dist/{preflight-BRzD30L5.js → preflight-D3HvKeAx.js} +14 -10
  127. package/dist/preflight-D3HvKeAx.js.map +1 -0
  128. package/dist/prepare-DCwM2KKD.js +2 -0
  129. package/dist/{prepare-B_y-AFMv.js → prepare-Dx7dOnJB.js} +70 -23
  130. package/dist/prepare-Dx7dOnJB.js.map +1 -0
  131. package/dist/{provenance-DQ0AXn6c.js → provenance-C_-8y-x3.js} +4 -4
  132. package/dist/{provenance-DQ0AXn6c.js.map → provenance-C_-8y-x3.js.map} +1 -1
  133. package/dist/{providers-DoTq1jHW.js → providers-CF5UEaP8.js} +24 -10
  134. package/dist/providers-CF5UEaP8.js.map +1 -0
  135. package/dist/providers-CTBYvMxu.js +1 -0
  136. package/dist/{query-BO-ux7RB.js → query-DXthKmq0.js} +3 -3
  137. package/dist/{query-BO-ux7RB.js.map → query-DXthKmq0.js.map} +1 -1
  138. package/dist/query-D_1ARx5q.js +1 -0
  139. package/dist/{questions-C2dym2ce.js → questions-KENpWnfV.js} +8 -9
  140. package/dist/questions-KENpWnfV.js.map +1 -0
  141. package/dist/{record-BKmMvZ6O.js → record-kk9M1hj-.js} +8 -4
  142. package/dist/record-kk9M1hj-.js.map +1 -0
  143. package/dist/{redact-DJ9JCShu.js → redact-CRGdfV2s.js} +4 -2
  144. package/dist/redact-CRGdfV2s.js.map +1 -0
  145. package/dist/remarks-1521zwfW.js +38 -0
  146. package/dist/remarks-1521zwfW.js.map +1 -0
  147. package/dist/remarks-BhAV1H4V.js +1 -0
  148. package/dist/remarks-Ml6bcSdV.js +278 -0
  149. package/dist/remarks-Ml6bcSdV.js.map +1 -0
  150. package/dist/{remote-CPPW86Q9.js → remote-Dkeghk8k.js} +3 -3
  151. package/dist/{remote-CPPW86Q9.js.map → remote-Dkeghk8k.js.map} +1 -1
  152. package/dist/resolve-CMmhACr6.js +1 -0
  153. package/dist/resolve-DzdWcIL6.js +724 -0
  154. package/dist/resolve-DzdWcIL6.js.map +1 -0
  155. package/dist/{results-BE2f-1XS.js → results-D-naai_Q.js} +33 -8
  156. package/dist/results-D-naai_Q.js.map +1 -0
  157. package/dist/{resume-Ca1c8vix.js → resume-8-nnW8iO.js} +4 -4
  158. package/dist/{resume-Ca1c8vix.js.map → resume-8-nnW8iO.js.map} +1 -1
  159. package/dist/{root-DICDHCtp.js → root-DNuLhaA5.js} +2 -2
  160. package/dist/root-DNuLhaA5.js.map +1 -0
  161. package/dist/rules-CcV3zLDR.js +318 -0
  162. package/dist/rules-CcV3zLDR.js.map +1 -0
  163. package/dist/run-Bel7Vde4.js +88 -0
  164. package/dist/run-Bel7Vde4.js.map +1 -0
  165. package/dist/{sandbox-DLrsvu7U.js → sandbox-YLbk9mO7.js} +2 -2
  166. package/dist/{sandbox-DLrsvu7U.js.map → sandbox-YLbk9mO7.js.map} +1 -1
  167. package/dist/sandbox.d.ts +1 -1
  168. package/dist/sandbox.js +1 -1
  169. package/dist/server-C-ysqkLv.js +1 -0
  170. package/dist/{server-TUY5xA_A.js → server-De8oH3bt.js} +10 -10
  171. package/dist/{server-TUY5xA_A.js.map → server-De8oH3bt.js.map} +1 -1
  172. package/dist/{source-DJGS0a0u.js → source-C686eZpY.js} +2 -2
  173. package/dist/{source-DJGS0a0u.js.map → source-C686eZpY.js.map} +1 -1
  174. package/dist/source-DjgypUHW.js +1 -0
  175. package/dist/src-3mP821Pt.js +2 -0
  176. package/dist/src-B3KCLZVk.js +3 -0
  177. package/dist/src-BAyg6CtQ.js +2 -0
  178. package/dist/{src-CF17Ug08.js → src-BYLM3hEz.js} +22 -6
  179. package/dist/src-BYLM3hEz.js.map +1 -0
  180. package/dist/src-BezgGX4X.js +2 -0
  181. package/dist/{src-DvCg0NBs.js → src-C7W6kg81.js} +40 -10
  182. package/dist/{src-DvCg0NBs.js.map → src-C7W6kg81.js.map} +1 -1
  183. package/dist/{src-ftu5wPSt.js → src-CIpKL45U.js} +782 -169
  184. package/dist/src-CIpKL45U.js.map +1 -0
  185. package/dist/src-ChBhk06Q.js +2 -0
  186. package/dist/{src-BxfCKA3_.js → src-CoqZYspK.js} +1 -1
  187. package/dist/{src-CFp86GtV.js → src-CzQrAcF8.js} +49 -49
  188. package/dist/src-CzQrAcF8.js.map +1 -0
  189. package/dist/src-D9fSTxlc.js +1 -0
  190. package/dist/{src-FGXhkEkH.js → src-DeY52Gyf.js} +182 -23
  191. package/dist/src-DeY52Gyf.js.map +1 -0
  192. package/dist/src-DrJSnoMR.js +2 -0
  193. package/dist/{src-DvK0EIZx.js → src-Xj_BZKbq.js} +112 -13
  194. package/dist/src-Xj_BZKbq.js.map +1 -0
  195. package/dist/{src-u8_ixn_F.js → src-cbCb-MPy.js} +20 -4
  196. package/dist/src-cbCb-MPy.js.map +1 -0
  197. package/dist/src-dhSTRN50.js +2 -0
  198. package/dist/{src-C7_1SqDP.js → src-djNNYIdP.js} +2 -2
  199. package/dist/{src-C7_1SqDP.js.map → src-djNNYIdP.js.map} +1 -1
  200. package/dist/state-BGWGAwdo.js +1 -0
  201. package/dist/{state-DfpHox6b.js → state-C0i_ujQZ.js} +19 -17
  202. package/dist/{state-DfpHox6b.js.map → state-C0i_ujQZ.js.map} +1 -1
  203. package/dist/studio-app/assets/{index-CBkhNAv8.js → index-Bb_cid9k.js} +9 -9
  204. package/dist/studio-app/index.html +1 -1
  205. package/dist/{suite-Cw_7G2RM.js → suite-DmuF_ApO.js} +80 -30
  206. package/dist/suite-DmuF_ApO.js.map +1 -0
  207. package/dist/suite-mn9g4veR.js +2 -0
  208. package/dist/{summary-BUKfU-9l.js → summary-BFMHMTz2.js} +273 -47
  209. package/dist/summary-BFMHMTz2.js.map +1 -0
  210. package/dist/testing.d.ts +2 -2
  211. package/dist/testing.js +2 -2
  212. package/dist/{text-gEcKjAyp.js → text-CrVy5ioL.js} +18 -2
  213. package/dist/text-CrVy5ioL.js.map +1 -0
  214. package/dist/{transcript-BNNANU8A.js → transcript-BTxJpW4R.js} +2 -2
  215. package/dist/{transcript-BNNANU8A.js.map → transcript-BTxJpW4R.js.map} +1 -1
  216. package/dist/{types-BYdlwMwo.d.ts → types-B-9b1MDV.d.ts} +850 -34
  217. package/dist/types-C5vI3g1C.js.map +1 -1
  218. package/dist/usage-BDo2q_yu.js +1 -0
  219. package/dist/{command-Bj39y-Wm.js → validate-DWjkshl1.js} +20 -377
  220. package/dist/validate-DWjkshl1.js.map +1 -0
  221. package/dist/view/index.d.ts +31 -263
  222. package/dist/view/index.js +8 -7
  223. package/dist/views-DE2A1jEX.js +2 -0
  224. package/dist/{views-B8w2J-gD.js → views-Dk8MCQKr.js} +8 -98
  225. package/dist/views-Dk8MCQKr.js.map +1 -0
  226. package/dist/{worker-entry-DNnveLqx.js → worker-entry-CqrzGPF4.js} +2 -2
  227. package/dist/{worker-entry-DNnveLqx.js.map → worker-entry-CqrzGPF4.js.map} +1 -1
  228. package/dist/workspace-B1eGO46M.js +1 -0
  229. package/dist/{workspace-C4RA3srV.js → workspace-CLeOybkr.js} +45 -11
  230. package/dist/workspace-CLeOybkr.js.map +1 -0
  231. package/package.json +5 -1
  232. package/skills/eval-rubric/SKILL.md +130 -0
  233. package/dist/agent-D483ru7D.js +0 -334
  234. package/dist/agent-D483ru7D.js.map +0 -1
  235. package/dist/assemble-D5KGrwnJ.js +0 -1
  236. package/dist/assemble-D9GWr-HS.js.map +0 -1
  237. package/dist/capture-CYGLpWxR.js +0 -1
  238. package/dist/cli-ClpPT8kn.js.map +0 -1
  239. package/dist/cli-spec-qmEnQ25_.js.map +0 -1
  240. package/dist/command-Bj39y-Wm.js.map +0 -1
  241. package/dist/command-C-cVjlny.js +0 -1
  242. package/dist/command-DbAe1Rzb.js.map +0 -1
  243. package/dist/command-TgbzPizb.js.map +0 -1
  244. package/dist/commands-YUwlyTbr.js +0 -228
  245. package/dist/commands-YUwlyTbr.js.map +0 -1
  246. package/dist/config-DrVSEVN0.js.map +0 -1
  247. package/dist/context-C1mxXh80.js.map +0 -1
  248. package/dist/db-CKg5NtiW.js +0 -2
  249. package/dist/db-DfhipHEF.js +0 -1416
  250. package/dist/db-DfhipHEF.js.map +0 -1
  251. package/dist/docker-D1HZDS6q.js.map +0 -1
  252. package/dist/doctor-D2MbNO8L.js.map +0 -1
  253. package/dist/egress-2-qzPvGi.js.map +0 -1
  254. package/dist/environment-Cca6a4wB.js.map +0 -1
  255. package/dist/evaluate-DKIl54Hb.js.map +0 -1
  256. package/dist/files-BXHZFWmx.js.map +0 -1
  257. package/dist/files-BpUYBwGu.js.map +0 -1
  258. package/dist/files-Bw8lWEjg.js +0 -1
  259. package/dist/files-D6J0iSdx.js +0 -1
  260. package/dist/images-Bi3aGxb9.js.map +0 -1
  261. package/dist/init-Cl3f3NBT.js +0 -360
  262. package/dist/init-Cl3f3NBT.js.map +0 -1
  263. package/dist/init-fhOXED8F.js +0 -1
  264. package/dist/judge-DW_GDOmB.js.map +0 -1
  265. package/dist/model-CXhrziBe.js +0 -1
  266. package/dist/model-D0IcOQao.js.map +0 -1
  267. package/dist/model-DaVCY3rb.js.map +0 -1
  268. package/dist/model-DaX4bNLZ.js.map +0 -1
  269. package/dist/model-_bxNrk2h.js +0 -1
  270. package/dist/opencode-providers-J9colP0-.js.map +0 -1
  271. package/dist/options-C0d0AzeR.js.map +0 -1
  272. package/dist/plan-DgQxp9yy.js.map +0 -1
  273. package/dist/preflight-BRzD30L5.js.map +0 -1
  274. package/dist/prepare-B_y-AFMv.js.map +0 -1
  275. package/dist/prepare-DCKTLHN3.js +0 -2
  276. package/dist/providers-B9yTdIqF.js +0 -1
  277. package/dist/providers-DoTq1jHW.js.map +0 -1
  278. package/dist/query-CRQfsw0e.js +0 -1
  279. package/dist/questions-C2dym2ce.js.map +0 -1
  280. package/dist/record-BKmMvZ6O.js.map +0 -1
  281. package/dist/redact-DJ9JCShu.js.map +0 -1
  282. package/dist/results-BE2f-1XS.js.map +0 -1
  283. package/dist/root-DICDHCtp.js.map +0 -1
  284. package/dist/server-Dp8eOeK5.js +0 -1
  285. package/dist/source-DdbLGR1M.js +0 -1
  286. package/dist/src-CF17Ug08.js.map +0 -1
  287. package/dist/src-CFp86GtV.js.map +0 -1
  288. package/dist/src-CLoOMVWI.js +0 -3
  289. package/dist/src-CcrQbpG8.js +0 -2
  290. package/dist/src-DCFtJRp0.js +0 -2
  291. package/dist/src-DSYYPEOj.js +0 -2
  292. package/dist/src-DYO3DdnQ.js +0 -1
  293. package/dist/src-DvK0EIZx.js.map +0 -1
  294. package/dist/src-FGXhkEkH.js.map +0 -1
  295. package/dist/src-WD-zgQYZ.js +0 -2
  296. package/dist/src-_oI9AePb.js +0 -2
  297. package/dist/src-ftu5wPSt.js.map +0 -1
  298. package/dist/src-qsKCqv7a.js +0 -2
  299. package/dist/src-u8_ixn_F.js.map +0 -1
  300. package/dist/state-DpAxYyED.js +0 -1
  301. package/dist/suite-Cw_7G2RM.js.map +0 -1
  302. package/dist/suite-DamAEnNF.js +0 -1
  303. package/dist/summary-BUKfU-9l.js.map +0 -1
  304. package/dist/summary-Baa4rTHS.js +0 -192
  305. package/dist/summary-Baa4rTHS.js.map +0 -1
  306. package/dist/text-gEcKjAyp.js.map +0 -1
  307. package/dist/tools-DLHgetpx.js +0 -249
  308. package/dist/tools-DLHgetpx.js.map +0 -1
  309. package/dist/views-B8w2J-gD.js.map +0 -1
  310. package/dist/views-DblnI3kA.js +0 -2
  311. package/dist/workspace-C4RA3srV.js.map +0 -1
  312. package/dist/workspace-C_X0tdv6.js +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 = {
@@ -1508,12 +1509,22 @@ for (let i = 0; i < Number(seconds); i++) {
1508
1509
  child.on("error", () => {});
1509
1510
  child.unref();
1510
1511
  }
1511
- async function dockerState(engine) {
1512
+ async function dockerState(engine, signal) {
1513
+ let missing = false;
1514
+ const format = engineSpec(engine).stateFormat;
1512
1515
  const r = await docker([
1513
1516
  "info",
1514
1517
  "--format",
1515
- engineSpec(engine).stateFormat
1516
- ], { engine }).catch(() => null);
1518
+ format
1519
+ ], {
1520
+ engine,
1521
+ signal
1522
+ }).catch((error) => {
1523
+ if (signal?.aborted) throw signal.reason;
1524
+ missing = error?.code === "ENOENT";
1525
+ return null;
1526
+ });
1527
+ if (missing) return "missing";
1517
1528
  const [version, os] = r?.exitCode === 0 ? r.stdout.toString("utf8").trim().split(/\s+/) : [];
1518
1529
  if (!version) return "down";
1519
1530
  return os === "windows" ? "windows-containers" : "running";
@@ -1601,20 +1612,20 @@ async function imageInfo(image, engine) {
1601
1612
  const HARNESS_PACKAGES = {
1602
1613
  cursor: {
1603
1614
  name: "@reeval/harness-cursor-sdk",
1604
- version: "0.1.5"
1615
+ version: "0.1.6"
1605
1616
  },
1606
1617
  "claude-code": {
1607
1618
  name: "@ai-sdk/harness-claude-code",
1608
1619
  version: "1.0.160",
1609
- patch: "907f9184"
1620
+ patch: "b17cde50"
1610
1621
  },
1611
1622
  antigravity: {
1612
1623
  name: "@reeval/harness-antigravity",
1613
- version: "0.1.8"
1624
+ version: "0.1.11"
1614
1625
  },
1615
1626
  opencode: {
1616
1627
  name: "@reeval/harness-opencode",
1617
- version: "0.2.8"
1628
+ version: "0.2.9"
1618
1629
  },
1619
1630
  goose: {
1620
1631
  name: "@reeval/harness-goose",
@@ -1622,7 +1633,7 @@ const HARNESS_PACKAGES = {
1622
1633
  },
1623
1634
  pi: {
1624
1635
  name: "@reeval/harness-pi",
1625
- version: "0.2.7"
1636
+ version: "0.2.8"
1626
1637
  },
1627
1638
  codex: {
1628
1639
  name: "@reeval/harness-codex",
@@ -1634,7 +1645,7 @@ const HARNESS_PACKAGES = {
1634
1645
  },
1635
1646
  "grok-build": {
1636
1647
  name: "@reeval/harness-grok-build",
1637
- version: "0.1.0"
1648
+ version: "0.1.2"
1638
1649
  }
1639
1650
  };
1640
1651
  /** Every harness with an image, in {@link HARNESS_PACKAGES} order. */
@@ -1675,6 +1686,11 @@ const SANDBOX_HOME = `/home/${SANDBOX_USER}`;
1675
1686
  const SANDBOX_ROOT = "/reeval";
1676
1687
  /** The agent's working directory, relative to {@link SANDBOX_ROOT}. */
1677
1688
  const WORKSPACE_DIR = "workspace";
1689
+ /**
1690
+ * reeval's own state in the container, root's and mode 0700: outside {@link SANDBOX_ROOT}, which
1691
+ * {@link SANDBOX_USER} owns and so could rename it away and put its own in its place.
1692
+ */
1693
+ const SANDBOX_STATE = "/var/lib/reeval";
1678
1694
  const BRIDGE_PORT = 43210;
1679
1695
  //#endregion
1680
1696
  //#region ../sandbox-docker/src/egress/cert.ts
@@ -1812,7 +1828,8 @@ function ipv6Bytes(address) {
1812
1828
  //#region ../sandbox-docker/src/egress/mux.ts
1813
1829
  /**
1814
1830
  * Many TCP streams over one byte pipe (a `docker exec` stdio), with per-stream credit so a slow
1815
- * 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.
1816
1833
  *
1817
1834
  * Must stay self-contained, with no imports or references to module scope: the forwarder runs
1818
1835
  * this function's source inside the container.
@@ -1826,9 +1843,43 @@ function createMux(options) {
1826
1843
  const READY = 6;
1827
1844
  const windowSize = options.window ?? 262144;
1828
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
+ };
1829
1879
  const streams = /* @__PURE__ */ new Map();
1830
1880
  let nextId = options.parity === 0 ? 2 : 1;
1831
1881
  let pending = Buffer.alloc(0);
1882
+ let broken = false;
1832
1883
  const frame = (type, id, payload = Buffer.alloc(0)) => {
1833
1884
  const header = Buffer.alloc(9);
1834
1885
  header.writeUInt8(type, 0);
@@ -1881,6 +1932,25 @@ function createMux(options) {
1881
1932
  streams.set(id, state);
1882
1933
  return stream;
1883
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
+ };
1884
1954
  const handle = (type, id, payload) => {
1885
1955
  if (type === READY) return options.onReady?.();
1886
1956
  if (type === OPEN) return options.onOpen(create(id), payload.readUInt16BE(0));
@@ -1909,15 +1979,22 @@ function createMux(options) {
1909
1979
  };
1910
1980
  return {
1911
1981
  receive(chunk) {
1982
+ if (broken) return;
1912
1983
  pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
1913
- while (pending.length >= 9) {
1914
- const length = pending.readUInt32BE(5);
1915
- if (pending.length < 9 + length) break;
1916
- const type = pending.readUInt8(0);
1917
- const id = pending.readUInt32BE(1);
1918
- const payload = pending.subarray(9, 9 + length);
1919
- pending = pending.subarray(9 + length);
1920
- 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}`);
1921
1998
  }
1922
1999
  },
1923
2000
  open(port) {
@@ -1971,6 +2048,10 @@ function forwarderMain(io, makeMux, linkStreams, proxyPort) {
1971
2048
  host: "127.0.0.1",
1972
2049
  allowHalfOpen: true
1973
2050
  }), stream);
2051
+ },
2052
+ onError(message) {
2053
+ io.error(`reeval egress forwarder: ${message}`);
2054
+ io.exit(1);
1974
2055
  }
1975
2056
  });
1976
2057
  io.input.on("data", (chunk) => mux.receive(chunk));
@@ -2051,13 +2132,45 @@ const normalise = (host) => host.toLowerCase().replace(/\.$/, "").replace(/^\[|\
2051
2132
  * mustn't resolve into them, or anyone who can add a DNS record under the pattern's domain
2052
2133
  * reaches the worker's LAN.
2053
2134
  */
2054
- const PRIVATE = blockList([
2135
+ const PRIVATE_RANGES = [
2055
2136
  "10.0.0.0/8",
2056
2137
  "100.64.0.0/10",
2057
2138
  "172.16.0.0/12",
2058
2139
  "192.168.0.0/16",
2059
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"
2060
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];
2061
2174
  /** How `host` matches: listed as it is, only by a `*.` pattern (never an IP address), or not. */
2062
2175
  function hostMatch(patterns, host) {
2063
2176
  const h = normalise(host);
@@ -2090,7 +2203,7 @@ function createPolicyCheck(policy, options = {}) {
2090
2203
  reason: "the policy denies all egress"
2091
2204
  };
2092
2205
  const listed = hostMatch(custom?.allowedHosts ?? [], host);
2093
- if (custom && !listed && !custom.allowedCIDRs?.length) return {
2206
+ if (custom && !listed && !(custom.allowedCIDRs?.length && isIP(host))) return {
2094
2207
  allowed: false,
2095
2208
  status: 403,
2096
2209
  reason: `${host} isn't in the allowlist`
@@ -2111,16 +2224,26 @@ function createPolicyCheck(policy, options = {}) {
2111
2224
  status: 502,
2112
2225
  reason: `${host} has no addresses`
2113
2226
  };
2114
- if (addresses.some((a) => inList(always, a))) return {
2227
+ const reached = addresses.flatMap(reaches);
2228
+ if (reached.some((a) => inList(always, a))) return {
2115
2229
  allowed: false,
2116
2230
  status: 403,
2117
2231
  reason: `${host} is a loopback or link-local address`
2118
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
+ }
2119
2242
  if (!custom) return {
2120
2243
  allowed: true,
2121
2244
  address: first
2122
2245
  };
2123
- if (addresses.some((a) => inList(deniedCidrs, a))) return {
2246
+ if (reached.some((a) => inList(deniedCidrs, a))) return {
2124
2247
  allowed: false,
2125
2248
  status: 403,
2126
2249
  reason: `${host} is in a denied range`
@@ -2175,6 +2298,35 @@ const onEndpoint = (endpoints, request) => endpoints.some((match) => matches({
2175
2298
  }, request));
2176
2299
  /** Whether the gateway must decrypt connections to `host`: some rule could apply there. */
2177
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
+ }
2178
2330
  /**
2179
2331
  * The request's headers after every matching rule, in order, so later rules win. Returns the
2180
2332
  * (lower-case) names that were replaced, for logging without values. With `endpoints`, no rule
@@ -2283,59 +2435,75 @@ function scrubHeaders(subs, headers) {
2283
2435
  const one = (v) => scrubValue(subs, v);
2284
2436
  return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, v === void 0 ? v : Array.isArray(v) ? v.map(one) : one(String(v))]));
2285
2437
  }
2438
+ const bufferPairs = (pairs) => pairs.filter(([from]) => from.length > 0).map(([from, to]) => [Buffer.from(from), Buffer.from(to)]);
2286
2439
  /**
2287
- * Replaces byte strings in a stream, including across chunk boundaries: it holds back the
2288
- * longest `from` less one byte, so a match split between chunks is still found.
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;
2483
+ /**
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.
2289
2486
  */
2290
2487
  var ReplaceStream = class extends Transform {
2291
2488
  #pairs;
2292
- #hold;
2293
2489
  #tail = Buffer.alloc(0);
2294
2490
  /** How many replacements were made. */
2295
2491
  count = 0;
2296
2492
  constructor(pairs) {
2297
2493
  super();
2298
- this.#pairs = pairs.filter(([from]) => from.length > 0).map(([from, to]) => [Buffer.from(from), Buffer.from(to)]);
2299
- this.#hold = Math.max(0, ...this.#pairs.map(([from]) => from.length - 1));
2300
- }
2301
- /**
2302
- * Replaces every whole match in `data` (original bytes only: output is never re-scanned), and
2303
- * returns where the unscanned tail starts, which is `data.length` when `final`.
2304
- */
2305
- #scan(data, final) {
2306
- const parts = [];
2307
- let pos = 0;
2308
- for (;;) {
2309
- let best = -1;
2310
- let pair;
2311
- for (const p of this.#pairs) {
2312
- const i = data.indexOf(p[0], pos);
2313
- if (i !== -1 && (best === -1 || i < best)) {
2314
- best = i;
2315
- pair = p;
2316
- }
2317
- }
2318
- if (best === -1 || !pair) break;
2319
- parts.push(data.subarray(pos, best), pair[1]);
2320
- pos = best + pair[0].length;
2321
- this.count++;
2322
- }
2323
- const rest = final ? data.length : Math.max(pos, data.length - this.#hold);
2324
- parts.push(data.subarray(pos, rest));
2325
- return {
2326
- out: Buffer.concat(parts),
2327
- rest
2328
- };
2494
+ this.#pairs = bufferPairs(pairs);
2329
2495
  }
2330
2496
  _transform(chunk, _enc, done) {
2331
2497
  const data = Buffer.concat([this.#tail, chunk]);
2332
- const { out, rest } = this.#scan(data, false);
2498
+ const { out, rest, count } = scan(this.#pairs, data, false);
2499
+ this.count += count;
2333
2500
  this.#tail = Buffer.from(data.subarray(rest));
2334
2501
  if (out.length) this.push(out);
2335
2502
  done();
2336
2503
  }
2337
2504
  _flush(done) {
2338
- const { out } = this.#scan(this.#tail, true);
2505
+ const { out, count } = scan(this.#pairs, this.#tail, true);
2506
+ this.count += count;
2339
2507
  if (out.length) this.push(out);
2340
2508
  done();
2341
2509
  }
@@ -2391,6 +2559,85 @@ function tokeniseResponse(derived, body, known) {
2391
2559
  };
2392
2560
  }
2393
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
2394
2641
  //#region ../sandbox-docker/src/egress/proxy.ts
2395
2642
  const HOP_BY_HOP = /* @__PURE__ */ new Set([
2396
2643
  "connection",
@@ -2414,6 +2661,11 @@ function parseAuthority(authority, defaultPort) {
2414
2661
  port: Number(m?.[2] ?? defaultPort)
2415
2662
  };
2416
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
+ };
2417
2669
  const pathOnly = (path) => path.split("?")[0];
2418
2670
  function reply(socket, status, reason) {
2419
2671
  const text = http.STATUS_CODES[status] ?? "";
@@ -2424,6 +2676,15 @@ const counter = (count) => new Transform({ transform(chunk, _encoding, done) {
2424
2676
  count(chunk.length);
2425
2677
  done(null, chunk);
2426
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);
2427
2688
  const plainHeaders = (headers) => Object.fromEntries(Object.entries(headers).flatMap(([k, v]) => v === void 0 ? [] : [[k, Array.isArray(v) ? v.map(String) : String(v)]]));
2428
2689
  function createEgressProxy(options) {
2429
2690
  const log = options.log ?? (() => {});
@@ -2670,25 +2931,39 @@ function createEgressProxy(options) {
2670
2931
  } catch {
2671
2932
  return res.writeHead(400).end("reeval egress gateway: expected an absolute URL\n");
2672
2933
  }
2673
- if (url.protocol !== "http:") return res.writeHead(400).end("reeval egress gateway: http only\n");
2674
- 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}`;
2675
2938
  const done = requestEvent(res, {
2676
2939
  method: req.method ?? "GET",
2677
- url: `http://${host}:${port}${url.pathname}`,
2940
+ url: `${shown}${url.pathname}`,
2678
2941
  host,
2679
2942
  port,
2680
- via: "http",
2943
+ via: secure ? "decrypted" : "http",
2681
2944
  requestHeaders: plainHeaders(req.headers)
2682
2945
  });
2683
2946
  check(host).then((verdict) => {
2684
2947
  if (!verdict.allowed) {
2685
- log(`reeval:egress ${req.method} http://${host}:${port} denied (${verdict.reason})`);
2948
+ log(`reeval:egress ${req.method} ${shown} denied (${verdict.reason})`);
2686
2949
  block(req.socket, host, port, verdict.reason);
2687
2950
  done.refuse(verdict.reason);
2688
2951
  return res.writeHead(verdict.status).end(`blocked by the reeval egress policy: ${verdict.reason}\n`);
2689
2952
  }
2690
- log(`reeval:egress ${req.method} http://${host}:${port}${url.pathname}`);
2691
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}`);
2692
2967
  const upstream = http.request({
2693
2968
  host: verdict.address,
2694
2969
  port,
@@ -2699,7 +2974,12 @@ function createEgressProxy(options) {
2699
2974
  host: url.host
2700
2975
  }
2701
2976
  });
2702
- 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
+ });
2703
2983
  });
2704
2984
  });
2705
2985
  front.on("upgrade", (_req, socket) => {
@@ -2724,6 +3004,21 @@ function createEgressProxy(options) {
2724
3004
  * is refused (already logged): a Host that isn't the CONNECT target, or an unbrokered endpoint.
2725
3005
  */
2726
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
+ }
2727
3022
  const named = String(headers[":authority"] ?? headers.host ?? "");
2728
3023
  const asked = parseAuthority(named, target.port).host;
2729
3024
  if (named && asked !== target.host) {
@@ -2791,7 +3086,7 @@ function createEgressProxy(options) {
2791
3086
  path,
2792
3087
  headers: {}
2793
3088
  });
2794
- 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"];
2795
3090
  const notes = [
2796
3091
  replaced.length ? `set ${replaced.join(", ")}` : "",
2797
3092
  substituted.names.length ? `substituted ${substituted.names.join(", ")}` : "",
@@ -2818,19 +3113,72 @@ function createEgressProxy(options) {
2818
3113
  delete headers["content-length"];
2819
3114
  return counted.pipe(bodySubstituter(own));
2820
3115
  }
2821
- /** The response body as sent to the sandbox: real values put back as placeholders. */
2822
3116
  const scrubs = options.scrubResponses !== false;
2823
- 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) {
2824
3148
  if (readsModel && t) readModel(up, t);
2825
- const body = scrubbedInBodies(own).length && scrubs ? up.pipe(scrubber(own)) : up;
2826
- 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;
2827
3158
  }
2828
- const responseHeaders = (headers, own) => {
3159
+ /** Header (and trailer) values with every real value put back as its placeholder. */
3160
+ const scrubbedHeaders = (headers, all) => {
2829
3161
  const out = endToEnd(headers);
2830
- if (!own.length || !scrubs) return out;
2831
- if (scrubbedInBodies(own).length) delete out["content-length"];
2832
- return scrubHeaders(own, out);
3162
+ if (!all.length || !scrubs) return out;
3163
+ return scrubHeaders(all, out);
2833
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
+ }
2834
3182
  /**
2835
3183
  * How far into a response the gateway looks for the model that served it: OpenAI's first event
2836
3184
  * echoes the request's instructions and tools before it.
@@ -2859,7 +3207,7 @@ function createEgressProxy(options) {
2859
3207
  * A token exchange's response, read whole, with each token replaced by a placeholder. A success
2860
3208
  * that can't be read (compressed, too big, not JSON) is a 502: the real tokens never go through.
2861
3209
  */
2862
- function tokenised(exchange, url, status, headers, up, res, own, t) {
3210
+ function tokenised(exchange, url, status, headers, up, res, own, creds, t) {
2863
3211
  const chunks = [];
2864
3212
  let size = 0;
2865
3213
  const refuse = (why) => {
@@ -2868,15 +3216,20 @@ function createEgressProxy(options) {
2868
3216
  `);
2869
3217
  else res.destroy();
2870
3218
  };
2871
- 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);
2872
3225
  scrubbed.on("data", (chunk) => {
2873
3226
  size += chunk.length;
2874
3227
  if (size <= EXCHANGE_LIMIT) chunks.push(chunk);
2875
3228
  });
2876
- up.once("error", (error) => refuse(error.message));
3229
+ scrubbed.once("error", (error) => refuse(error.message));
2877
3230
  scrubbed.once("end", () => {
2878
3231
  const body = Buffer.concat(chunks);
2879
- const out = responseHeaders(headers, own);
3232
+ const out = responseHeaders(headers, own, creds, plan);
2880
3233
  t?.set({ responseHeaders: plainHeaders(out) });
2881
3234
  if (status < 200 || status >= 300) return res.writeHead(status, out).end(body);
2882
3235
  const encoding = String(headers["content-encoding"] ?? "identity");
@@ -2898,10 +3251,10 @@ function createEgressProxy(options) {
2898
3251
  }).end(text);
2899
3252
  });
2900
3253
  }
2901
- async function onDecrypted(target, req, res) {
3254
+ async function onDecrypted(target, req, res, asked = req.url ?? "/", sent = req.headers, tracker) {
2902
3255
  const method = req.method ?? "GET";
2903
- const url = `https://${target.host}${pathOnly(req.url ?? "/")}`;
2904
- const t = requestEvent(res, {
3256
+ const url = `https://${target.host}${pathOnly(asked)}`;
3257
+ const t = tracker ?? requestEvent(res, {
2905
3258
  method,
2906
3259
  url,
2907
3260
  host: target.host,
@@ -2909,7 +3262,7 @@ function createEgressProxy(options) {
2909
3262
  via: "decrypted",
2910
3263
  requestHeaders: plainHeaders(req.headers)
2911
3264
  });
2912
- 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);
2913
3266
  t.set({
2914
3267
  substituted: names,
2915
3268
  credentials
@@ -2922,7 +3275,7 @@ function createEgressProxy(options) {
2922
3275
  delete headers.upgrade;
2923
3276
  delete headers.connection;
2924
3277
  const body = requestBody(req, headers, own, t);
2925
- const authority = target.port === 443 ? target.host : `${target.host}:${target.port}`;
3278
+ const authority = authorityOf(target.host, target.port);
2926
3279
  const session = req.httpVersionMajor === 2 ? await upstreamSession(target) : null;
2927
3280
  if (!session) return forwardHttp1(body, res, https.request({
2928
3281
  host: target.address,
@@ -2936,10 +3289,14 @@ function createEgressProxy(options) {
2936
3289
  },
2937
3290
  agent,
2938
3291
  ca
2939
- }), own, exchange && {
2940
- exchange,
2941
- url
2942
- }, t, readsModel);
3292
+ }), {
3293
+ own,
3294
+ creds: credentialScrubs(target.host),
3295
+ url,
3296
+ ...exchange ? { exchange } : {},
3297
+ ...t ? { t } : {},
3298
+ readsModel: !!readsModel
3299
+ });
2943
3300
  const stream = session.request({
2944
3301
  ...headers,
2945
3302
  ":method": method,
@@ -2952,30 +3309,41 @@ function createEgressProxy(options) {
2952
3309
  else res.destroy();
2953
3310
  };
2954
3311
  stream.once("error", fail);
3312
+ const creds = credentialScrubs(target.host);
2955
3313
  stream.once("response", (h) => {
2956
- if (exchange) return tokenised(exchange, url, Number(h[":status"]), h, stream, res, own, t);
2957
- const out = responseHeaders(h, own);
2958
- t.set({ responseHeaders: plainHeaders(out) });
2959
- res.writeHead(Number(h[":status"]), out);
2960
- stream.once("trailers", (trailers) => res.addTrailers(responseHeaders(trailers, own)));
2961
- 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])));
2962
3318
  });
2963
3319
  body.pipe(stream);
2964
3320
  req.once("aborted", () => stream.close(http2.constants.NGHTTP2_CANCEL));
2965
3321
  }
2966
- 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;
2967
3340
  upstream.once("response", (up) => {
2968
- if (tokenise) {
2969
- const { exchange, url } = tokenise;
2970
- return tokenised(exchange, url, up.statusCode ?? 502, up.headers, up, res, own, t);
2971
- }
2972
- const out = responseHeaders(up.headers, own);
2973
- t?.set({ responseHeaders: plainHeaders(out) });
2974
- 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;
2975
3344
  up.once("end", () => {
2976
- 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]));
2977
3346
  });
2978
- responseBody(up, own, t, readsModel).pipe(res);
2979
3347
  });
2980
3348
  upstream.once("error", (error) => {
2981
3349
  t?.set({
@@ -2991,7 +3359,9 @@ function createEgressProxy(options) {
2991
3359
  const method = req.method ?? "GET";
2992
3360
  const startedAt = Date.now();
2993
3361
  const { headers, path, own, names, credentials, refused } = transformed(target, method, req.url ?? "/", req.headers, req.httpVersion);
2994
- 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"];
2995
3365
  const upgraded = (fields) => emit({
2996
3366
  type: "request",
2997
3367
  method,
@@ -3029,7 +3399,7 @@ function createEgressProxy(options) {
3029
3399
  ca
3030
3400
  });
3031
3401
  upstream.once("upgrade", (up, upSocket, upHead) => {
3032
- const out = scrubs ? scrubHeaders(own, up.headers) : up.headers;
3402
+ const out = scrubHeaders(all, up.headers);
3033
3403
  upgraded({
3034
3404
  status: up.statusCode,
3035
3405
  responseHeaders: plainHeaders(out)
@@ -3037,9 +3407,20 @@ function createEgressProxy(options) {
3037
3407
  const lines = [`HTTP/1.1 ${up.statusCode} ${up.statusMessage}`];
3038
3408
  for (let i = 0; i < up.rawHeaders.length; i += 2) lines.push(`${up.rawHeaders[i]}: ${scrubbed(up.rawHeaders[i + 1] ?? "")}`);
3039
3409
  socket.write(`${lines.join("\r\n")}\r\n\r\n`);
3040
- if (upHead.length) socket.write(upHead);
3041
3410
  if (head.length) upSocket.write(head);
3042
- 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
+ }));
3043
3424
  });
3044
3425
  upstream.once("response", (up) => {
3045
3426
  reply(socket, up.statusCode ?? 502, `upstream refused the upgrade (${up.statusCode})`);
@@ -3067,7 +3448,7 @@ function createEgressProxy(options) {
3067
3448
  socket.destroy();
3068
3449
  return resolve(null);
3069
3450
  }
3070
- const session = http2.connect(`https://${key}`, { createConnection: () => socket });
3451
+ const session = http2.connect(`https://${authorityOf(target.host, target.port)}`, { createConnection: () => socket });
3071
3452
  session.on("error", () => {});
3072
3453
  session.once("close", () => sessions.get(key) === session && sessions.delete(key));
3073
3454
  session.once("goaway", () => sessions.get(key) === session && sessions.delete(key));
@@ -3177,6 +3558,7 @@ async function startGateway(containerId, options, signal, engine) {
3177
3558
  });
3178
3559
  child.stdin.on("error", () => {});
3179
3560
  let closed = false;
3561
+ let started = false;
3180
3562
  const mux = createMux({
3181
3563
  Duplex,
3182
3564
  parity: 0,
@@ -3187,7 +3569,15 @@ async function startGateway(containerId, options, signal, engine) {
3187
3569
  if (port === 43128) proxy.handle(stream);
3188
3570
  else stream.destroy();
3189
3571
  },
3190
- 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
+ }
3191
3581
  });
3192
3582
  const ready = {};
3193
3583
  ready.promise = new Promise((resolve, reject) => Object.assign(ready, {
@@ -3199,17 +3589,18 @@ async function startGateway(containerId, options, signal, engine) {
3199
3589
  const listeners = /* @__PURE__ */ new Map();
3200
3590
  const tunnels = /* @__PURE__ */ new Set();
3201
3591
  let stopped;
3202
- child.once("close", (code) => {
3592
+ /** Stops on its own: nothing reaches the container any more, so connections are refused. */
3593
+ const stop = (reason) => {
3203
3594
  const asked = closed;
3204
3595
  closed = true;
3205
3596
  mux.destroy();
3206
- const reason = `the forwarder exited (${code}): ${stderr.trim().slice(-500)}`.replace(/: $/, "");
3207
3597
  ready.reject(new Error(reason));
3208
3598
  if (asked) return;
3209
3599
  stopped = reason;
3210
3600
  for (const socket of tunnels) socket.destroy();
3211
3601
  for (const l of listeners.values()) l.then((s) => s.close());
3212
- });
3602
+ };
3603
+ child.once("close", (code) => stop(`the forwarder exited (${code}): ${stderr.trim().slice(-500)}`.replace(/: $/, "")));
3213
3604
  const timer = setTimeout(() => ready.reject(/* @__PURE__ */ new Error(`the forwarder didn't start within ${READY_TIMEOUT_MS / 1e3}s`)), READY_TIMEOUT_MS);
3214
3605
  const onAbort = () => ready.reject(signal?.reason);
3215
3606
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -3321,6 +3712,21 @@ const ROOT_CAPABILITIES = [
3321
3712
  "SETUID",
3322
3713
  "SETGID"
3323
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;
3324
3730
  const hostCpus = /* @__PURE__ */ new Map();
3325
3731
  /** Docker refuses `--cpus` above the host's count, so cap it there. */
3326
3732
  async function cpuLimit(cpus, engine = "docker") {
@@ -3333,18 +3739,23 @@ async function cpuLimit(cpus, engine = "docker") {
3333
3739
  return String(available ? Math.min(cpus, available) : cpus);
3334
3740
  }
3335
3741
  /**
3336
- * The `docker run` flags that confine the container: its user, capabilities and limits, and on
3337
- * 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.
3338
3745
  */
3339
3746
  async function confinementArgs(options) {
3340
3747
  const limits = {
3341
3748
  ...DEFAULT_LIMITS,
3342
3749
  ...options.limits
3343
3750
  };
3344
- const caps = [...options.root ? ROOT_CAPABILITIES : [], ...options.capabilities ?? []];
3751
+ const caps = [
3752
+ ...PRIVILEGED_CAPABILITIES,
3753
+ ...options.root ? ROOT_CAPABILITIES : [],
3754
+ ...options.capabilities ?? []
3755
+ ];
3345
3756
  return [
3346
3757
  "--user",
3347
- options.root ? "0:0" : SANDBOX_USER,
3758
+ "0:0",
3348
3759
  "--cap-drop",
3349
3760
  "ALL",
3350
3761
  ...[...new Set(caps)].flatMap((c) => ["--cap-add", c]),
@@ -3363,16 +3774,27 @@ async function confinementArgs(options) {
3363
3774
  }
3364
3775
  /** Runtime `npm i -g` goes to the sandbox user's own prefix, whose `bin` the images put on PATH. */
3365
3776
  const userEnv = (options) => options.root ? {} : { NPM_CONFIG_PREFIX: `${SANDBOX_HOME}/.local` };
3777
+ /** Root's PATH in a privileged exec: none of the sandbox user's own `bin` directories. */
3778
+ const SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
3366
3779
  /** Set on every exec, so a kill can find the command's whole process tree in the container. */
3367
3780
  const EXEC_MARKER = "REEVAL_EXEC_ID";
3368
3781
  /**
3782
+ * Every exec that can create a file starts with this. An exec'd process inherits the umask of the
3783
+ * engine's shim, not the container's: since Docker 29.6 that's 0000 wherever dockerd starts
3784
+ * containerd itself (Docker-in-Docker, rootless, OrbStack), which makes homes, the workspace and
3785
+ * everything an agent writes world-writable, and tools such as fx and OpenSSH refuse them.
3786
+ */
3787
+ const EXEC_UMASK = "umask 022";
3788
+ /**
3369
3789
  * The container's cgroup counts every process the kernel killed at its memory limit: cgroup v2's
3370
3790
  * `memory.events` and v1's `memory.oom_control` both have an `oom_kill` line. Without either,
3371
3791
  * Docker's `OOMKilled` says only whether there was one.
3372
3792
  */
3373
- async function oomKills(id, engine) {
3793
+ async function oomKills(id, engine, user = SANDBOX_USER) {
3374
3794
  const events = await docker([
3375
3795
  "exec",
3796
+ "--user",
3797
+ user,
3376
3798
  id,
3377
3799
  "sh",
3378
3800
  "-c",
@@ -3450,12 +3872,7 @@ async function createDockerSession(options) {
3450
3872
  env,
3451
3873
  engine
3452
3874
  }).catch((error) => {
3453
- const sweep = () => sweepLabelDetached(`${LABEL.session}=${nonce}`, void 0, engine);
3454
- if (signal?.aborted) sweep();
3455
- if (/unable to find user|no matching entries in passwd/i.test(error.message)) {
3456
- sweep();
3457
- 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 }).`);
3458
- }
3875
+ if (signal?.aborted) sweepLabelDetached(`${LABEL.session}=${nonce}`, void 0, engine);
3459
3876
  throw error;
3460
3877
  });
3461
3878
  let gateway;
@@ -3469,27 +3886,33 @@ async function createDockerSession(options) {
3469
3886
  await remove();
3470
3887
  throw signal.reason;
3471
3888
  }
3472
- if (!spec.createsWorkdir) {
3473
- const made = await docker([
3474
- "exec",
3475
- "--user",
3476
- "0:0",
3477
- id,
3478
- "mkdir",
3479
- "-p",
3480
- SANDBOX_ROOT
3481
- ], {
3482
- engine,
3483
- signal
3484
- }).catch((error) => ({
3485
- exitCode: 1,
3486
- stderr: error.message
3487
- }));
3488
- if (made.exitCode !== 0) {
3489
- await remove();
3490
- if (signal?.aborted) throw signal.reason;
3491
- throw new Error(`Couldn't create ${SANDBOX_ROOT} in the container: ${made.stderr.trim()}`);
3492
- }
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()}`);
3493
3916
  }
3494
3917
  const egressOptions = options.egress === true ? {} : options.egress || void 0;
3495
3918
  let egress;
@@ -3541,10 +3964,12 @@ async function createDockerSession(options) {
3541
3964
  ...gateway.env,
3542
3965
  ...env
3543
3966
  } : env;
3967
+ const user = ["--user", execUser(options)];
3544
3968
  let execs = 0;
3545
3969
  const execArgs = (command, marker, cwd, env) => [
3546
3970
  "exec",
3547
3971
  "--interactive",
3972
+ ...user,
3548
3973
  "--workdir",
3549
3974
  cwd ? posix.resolve(SANDBOX_ROOT, cwd) : SANDBOX_ROOT,
3550
3975
  "--env",
@@ -3553,17 +3978,23 @@ async function createDockerSession(options) {
3553
3978
  id,
3554
3979
  "bash",
3555
3980
  "-c",
3556
- command
3981
+ `${EXEC_UMASK}; ${command}`
3557
3982
  ];
3558
3983
  const nextMarker = () => `${id.slice(0, 12)}-${++execs}`;
3559
3984
  /**
3560
3985
  * Killing the host's `docker exec` client doesn't stop the command in the container, so kill
3561
3986
  * every process there that inherited this exec's marker: the command and all it started.
3562
3987
  */
3563
- const killExec = (marker) => docker([
3988
+ const killExec = (marker, asRoot = false) => docker([
3564
3989
  "exec",
3990
+ ...asRoot ? [
3991
+ "--user",
3992
+ "0:0",
3993
+ "--env",
3994
+ `PATH=${SYSTEM_PATH}`
3995
+ ] : user,
3565
3996
  id,
3566
- "sh",
3997
+ "/bin/sh",
3567
3998
  "-c",
3568
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`
3569
4000
  ], { engine }).then(() => {}, () => {});
@@ -3586,9 +4017,42 @@ async function createDockerSession(options) {
3586
4017
  throw error;
3587
4018
  }
3588
4019
  };
4020
+ const runPrivileged = async (o) => {
4021
+ const marker = nextMarker();
4022
+ const args = [
4023
+ "exec",
4024
+ "--user",
4025
+ "0:0",
4026
+ "--workdir",
4027
+ SANDBOX_STATE,
4028
+ "--env",
4029
+ `${EXEC_MARKER}=${marker}`,
4030
+ "--env",
4031
+ `PATH=${SYSTEM_PATH}`,
4032
+ id,
4033
+ "/bin/sh",
4034
+ "-c",
4035
+ `${EXEC_UMASK}; ${o.command}`
4036
+ ];
4037
+ try {
4038
+ const r = await docker(args, {
4039
+ signal: o.abortSignal,
4040
+ engine
4041
+ });
4042
+ return {
4043
+ exitCode: r.exitCode,
4044
+ stdout: r.stdout.toString("utf8"),
4045
+ stderr: r.stderr
4046
+ };
4047
+ } catch (error) {
4048
+ if (o.abortSignal?.aborted) await killExec(marker, true);
4049
+ throw error;
4050
+ }
4051
+ };
3589
4052
  const readBinaryFile = async (o) => {
3590
4053
  const r = await docker([
3591
4054
  "exec",
4055
+ ...user,
3592
4056
  id,
3593
4057
  "cat",
3594
4058
  "--",
@@ -3601,13 +4065,29 @@ async function createDockerSession(options) {
3601
4065
  };
3602
4066
  const writeBinaryFile = async (o) => {
3603
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
+ }
3604
4083
  await dockerOk([
3605
4084
  "exec",
3606
4085
  "--interactive",
4086
+ ...user,
3607
4087
  id,
3608
4088
  "sh",
3609
4089
  "-c",
3610
- `mkdir -p ${shellQuote(posix.dirname(path))} && cat > ${shellQuote(path)}`
4090
+ `${make} && cat > ${shellQuote(path)}`
3611
4091
  ], {
3612
4092
  input: Buffer.from(o.content),
3613
4093
  signal: o.abortSignal,
@@ -3646,8 +4126,9 @@ async function createDockerSession(options) {
3646
4126
  },
3647
4127
  ...brokering,
3648
4128
  ...substituting,
3649
- oomKills: () => oomKills(id, engine),
4129
+ oomKills: () => oomKills(id, engine, execUser(options)),
3650
4130
  run,
4131
+ runPrivileged,
3651
4132
  async spawn(o) {
3652
4133
  const env = envFor(o.env);
3653
4134
  const marker = nextMarker();
@@ -3773,10 +4254,16 @@ const slugify = (text, max) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").re
3773
4254
  /** The build context of reeval's base image, shipped in the package. */
3774
4255
  const DOCKER_CONTEXT = fileURLToPath(ownFileURL("docker"));
3775
4256
  /**
4257
+ * How reeval bakes a harness into an image, beyond the build context and the adapter: bump it when
4258
+ * that changes what a baked image holds, so every image baked before rebuilds on its next use.
4259
+ */
4260
+ const BAKE_REVISION = 2;
4261
+ /**
3776
4262
  * The image `docker({ from })` evals run in: `from` plus Node, git and the harness's CLI. The hash
3777
- * covers the toolchain layer too, so a change to the sandbox user or paths builds anew.
4263
+ * covers the toolchain layer and {@link BAKE_REVISION} too, so a change to the sandbox user, paths
4264
+ * or how the harness is baked builds anew.
3778
4265
  */
3779
- const harnessImageFor = (harness, from) => `${imageFor(harness)}-${slugify(from, 40) || "image"}-${sha256(toolchainDockerfile(from)).slice(0, 8)}`;
4266
+ const harnessImageFor = (harness, from) => `${imageFor(harness)}-${slugify(from, 40) || "image"}-${sha256(`${BAKE_REVISION}\0${toolchainDockerfile(from)}`).slice(0, 8)}`;
3780
4267
  /**
3781
4268
  * How each harness's CLI gets into an image: its adapter's bootstrap, and the binary it leaves
3782
4269
  * (or, for an adapter without one, a command that prints what it installed).
@@ -3786,22 +4273,22 @@ const BOOTSTRAPS = {
3786
4273
  name: "Cursor",
3787
4274
  cli: "@cursor/sdk",
3788
4275
  version: `node -p "'@cursor/sdk ' + require(process.env.HOME + '/.ai-sdk-harness/.harness-bootstrap/cursor-sdk/node_modules/@cursor/sdk/package.json').version"`,
3789
- adapter: async () => (await import("./src-WD-zgQYZ.js")).createCursorSdk()
4276
+ adapter: async () => (await import("./src-DrJSnoMR.js")).createCursorSdk()
3790
4277
  },
3791
4278
  "claude-code": {
3792
4279
  name: "Claude Code",
3793
4280
  cli: "claude",
3794
- adapter: async () => (await import("./dist-CuY4ehEk.js")).createClaudeCode()
4281
+ adapter: async () => (await import("./dist-Dp_AJgYN.js")).createClaudeCode()
3795
4282
  },
3796
4283
  antigravity: {
3797
4284
  name: "Antigravity",
3798
4285
  cli: "agy",
3799
- adapter: async () => (await import("./src-_oI9AePb.js")).createAntigravity()
4286
+ adapter: async () => (await import("./src-BAyg6CtQ.js")).createAntigravity()
3800
4287
  },
3801
4288
  opencode: {
3802
4289
  name: "OpenCode",
3803
4290
  cli: "opencode",
3804
- adapter: async () => (await import("./src-CLoOMVWI.js")).createOpenCode({
4291
+ adapter: async () => (await import("./src-B3KCLZVk.js")).createOpenCode({
3805
4292
  auth: { GOOGLE_GENERATIVE_AI_API_KEY: "unused" },
3806
4293
  model: "google/unused"
3807
4294
  })
@@ -3809,7 +4296,7 @@ const BOOTSTRAPS = {
3809
4296
  goose: {
3810
4297
  name: "Goose",
3811
4298
  cli: "goose",
3812
- adapter: async () => (await import("./src-qsKCqv7a.js")).createGoose({
4299
+ adapter: async () => (await import("./src-dhSTRN50.js")).createGoose({
3813
4300
  auth: { GOOGLE_API_KEY: "unused" },
3814
4301
  model: "unused"
3815
4302
  })
@@ -3817,17 +4304,17 @@ const BOOTSTRAPS = {
3817
4304
  pi: {
3818
4305
  name: "pi",
3819
4306
  cli: "pi",
3820
- adapter: async () => (await import("./src-DSYYPEOj.js")).createPi({ auth: { GEMINI_API_KEY: "unused" } })
4307
+ adapter: async () => (await import("./src-BezgGX4X.js")).createPi({ auth: { GEMINI_API_KEY: "unused" } })
3821
4308
  },
3822
4309
  codex: {
3823
4310
  name: "Codex",
3824
4311
  cli: "codex",
3825
- adapter: async () => (await import("./src-DCFtJRp0.js")).createCodex({ auth: { OPENAI_API_KEY: "unused" } })
4312
+ adapter: async () => (await import("./src-ChBhk06Q.js")).createCodex({ auth: { OPENAI_API_KEY: "unused" } })
3826
4313
  },
3827
4314
  fx: {
3828
4315
  name: "fx",
3829
4316
  cli: "fx",
3830
- adapter: async () => (await import("./src-BxfCKA3_.js")).createFx({
4317
+ adapter: async () => (await import("./src-CoqZYspK.js")).createFx({
3831
4318
  auth: { AI_GATEWAY_API_KEY: "unused" },
3832
4319
  model: "unused/unused"
3833
4320
  })
@@ -3835,7 +4322,7 @@ const BOOTSTRAPS = {
3835
4322
  "grok-build": {
3836
4323
  name: "Grok Build",
3837
4324
  cli: "grok",
3838
- adapter: async () => (await import("./src-CcrQbpG8.js")).createGrokBuild({
4325
+ adapter: async () => (await import("./src-3mP821Pt.js")).createGrokBuild({
3839
4326
  auth: { XAI_API_KEY: "unused" },
3840
4327
  model: "unused"
3841
4328
  })
@@ -3843,11 +4330,21 @@ const BOOTSTRAPS = {
3843
4330
  };
3844
4331
  /** Keeps the image's `PATH` in login shells, which Debian's `/etc/profile` resets. */
3845
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";
3846
4343
  /**
3847
4344
  * Adds what the harness bridge needs to a Debian or Ubuntu based toolchain image: Node (copied
3848
4345
  * from the official image, so the base's own toolchain is untouched), pnpm, git, curl and bash.
3849
4346
  */
3850
- const toolchainDockerfile = (from) => `FROM ${from}
4347
+ const toolchainDockerfile = (from) => `FROM ${imageReference(from)}
3851
4348
  SHELL ["/bin/sh", "-c"]
3852
4349
  RUN command -v apt-get >/dev/null || { echo "reeval needs a Debian or Ubuntu based image (glibc and apt-get)" >&2; exit 1; } \\
3853
4350
  && apt-get update \\
@@ -4023,9 +4520,13 @@ const removeQuietly = async (ref, engine) => {
4023
4520
  ref
4024
4521
  ], { engine }).catch(() => {});
4025
4522
  };
4026
- /** The hash of reeval's base image build context, which its images carry as a label. */
4523
+ /**
4524
+ * The hash of reeval's base image build context and {@link BAKE_REVISION}, which its images carry
4525
+ * as a label.
4526
+ */
4027
4527
  function contextHash() {
4028
4528
  const hash = createHash("sha256");
4529
+ hash.update(`bake ${BAKE_REVISION}\0`);
4029
4530
  for (const path of contextFiles(DOCKER_CONTEXT, join(DOCKER_CONTEXT, "Dockerfile"))) {
4030
4531
  hash.update(`${path}\0`);
4031
4532
  hash.update(readFileSync(join(DOCKER_CONTEXT, path)));
@@ -4157,7 +4658,7 @@ async function bake(session, bootstrap) {
4157
4658
  }
4158
4659
  const version = await session.run({ command: probe });
4159
4660
  if (bootstrap.version && version.exitCode !== 0) throw new Error(`The harness bootstrap did not install ${bootstrap.cli}:\n${tail(version.stderr)}`);
4160
- return version.stdout.trim() || "unknown";
4661
+ return versionLabel(version.stdout.trim());
4161
4662
  }
4162
4663
  /**
4163
4664
  * The image `docker({ install })` runs in: the commands on top of `parent`, tagged by the parent's
@@ -4231,6 +4732,108 @@ async function buildInstallLayer(parent, steps, options = {}) {
4231
4732
  }
4232
4733
  return tag;
4233
4734
  }
4735
+ /** Where a plugin layer leaves each built plugin, by its build's hash. */
4736
+ const PLUGIN_BUILDS_DIR = `${SANDBOX_ROOT}/plugin-builds`;
4737
+ /**
4738
+ * The sha256 of a plugin build's source as the layer gets it: every file `docker build` sends from
4739
+ * `context` (less `.dockerignore` and `.git`), with its executable bit or symlink target.
4740
+ */
4741
+ function sourceTreeHash(context) {
4742
+ const hash = createHash("sha256");
4743
+ for (const path of contextFiles(context, null)) {
4744
+ if (path === ".git" || path.startsWith(".git/")) continue;
4745
+ const full = join(context, path);
4746
+ const stat = lstatSync(full);
4747
+ hash.update(`${path}\0${stat.mode & 73 ? "x" : "-"}\0`);
4748
+ hash.update(stat.isSymbolicLink() ? readlinkSync(full) : readFileSync(full));
4749
+ hash.update("\0");
4750
+ }
4751
+ return hash.digest("hex");
4752
+ }
4753
+ /** The hash a plugin layer's tag and directory carry: commit, tree and build. */
4754
+ const pluginBuildKey = (b) => sha256(JSON.stringify({
4755
+ commit: b.commit,
4756
+ tree: b.tree,
4757
+ path: b.path,
4758
+ cwd: b.cwd,
4759
+ run: b.steps
4760
+ }));
4761
+ /** The image a plugin build makes on `parent`: the parent's id, then the build's hash. */
4762
+ const pluginLayerTag = (parent, b) => `reeval-plugin:${parent.id.replace(/^sha256:/, "").slice(0, 12)}-${pluginBuildKey(b).slice(0, 16)}`;
4763
+ /** Where a plugin layer leaves the plugin's directory. */
4764
+ const pluginBuildDir = (b) => `${PLUGIN_BUILDS_DIR}/${pluginBuildKey(b).slice(0, 16)}`;
4765
+ /** The line a plugin build's script fails with when its `cwd` isn't in the source. */
4766
+ const noCwd = (cwd) => `reeval: build cwd ${cwd} isn't a directory in the source`;
4767
+ /**
4768
+ * A plugin build's commands as one script, run from `cwd` under `src` (the source's root), stopping
4769
+ * at the first failure as an install script does, or before any when `cwd` isn't there.
4770
+ */
4771
+ const pluginBuildScript = (b, src) => installScript(b.steps).replaceAll("reeval: install ", "reeval: build ").replace(/^cd \/\n/, `cd ${shellQuote(`${src}/${b.cwd}`)} || { echo ${shellQuote(noCwd(b.cwd))} >&2; exit 1; }\n`);
4772
+ /**
4773
+ * Builds a plugin's layer on `parent` if it's missing: the whole source, then its commands from
4774
+ * `cwd` as root with the sandbox user's home as HOME (as an install layer runs), keeping only the
4775
+ * plugin's directory at {@link pluginBuildDir}. Secrets and the workspace never exist here.
4776
+ */
4777
+ async function buildPluginLayer(parent, b, options) {
4778
+ const { signal, engine } = options;
4779
+ const tag = pluginLayerTag(parent, b);
4780
+ if ((await docker([
4781
+ "image",
4782
+ "inspect",
4783
+ "--format",
4784
+ "{{.Created}}",
4785
+ tag
4786
+ ], {
4787
+ signal,
4788
+ engine
4789
+ })).exitCode === 0) return tag;
4790
+ const src = "/tmp/reeval-plugin-src";
4791
+ const script = pluginBuildScript(b, src);
4792
+ const encoded = Buffer.from(script).toString("base64");
4793
+ const home = `$(getent passwd ${SANDBOX_USER} | cut -d: -f6)`;
4794
+ const dir = pluginBuildDir(b);
4795
+ const dockerfile = [
4796
+ `FROM ${parent.ref}`,
4797
+ "USER root",
4798
+ `COPY . ${src}`,
4799
+ `RUN rm -rf ${src}/.git && echo ${encoded} | base64 -d > /tmp/reeval-plugin-build.sh \\`,
4800
+ ` && if id -u ${SANDBOX_USER} >/dev/null 2>&1; then export HOME="${home}"; fi \\`,
4801
+ " && bash /tmp/reeval-plugin-build.sh && rm /tmp/reeval-plugin-build.sh \\",
4802
+ ` && mkdir -p ${dir} && cp -a ${shellQuote(`${src}/${b.path}`)}/. ${dir}/ && rm -rf ${src} ${dir}/.git \\`,
4803
+ ` && chmod -R a+rX ${dir} \\`,
4804
+ ` && { ! id -u ${SANDBOX_USER} >/dev/null 2>&1 || find "$HOME" -xdev ! -user ${SANDBOX_USER} -exec chown -h ${SANDBOX_USER}: {} +; }`,
4805
+ `WORKDIR ${SANDBOX_ROOT}`
4806
+ ].join("\n");
4807
+ const build = await docker([
4808
+ "build",
4809
+ "--progress",
4810
+ "plain",
4811
+ ...BUILT_BY,
4812
+ "--tag",
4813
+ tag,
4814
+ "-f",
4815
+ "-",
4816
+ b.context
4817
+ ], {
4818
+ input: dockerfile,
4819
+ signal,
4820
+ engine
4821
+ });
4822
+ const output = `${build.stdout.toString("utf8")}${build.stderr}`;
4823
+ options.output?.write(`${script}\n${dockerfile}\n\n${output}`);
4824
+ if (build.exitCode !== 0) {
4825
+ const failed = /reeval: build (\d+\/\d+) exited (\d+): (.*)/.exec(output);
4826
+ const missing = output.includes(noCwd(b.cwd));
4827
+ throw buildError(output, {
4828
+ stage: "plugin",
4829
+ ref: tag,
4830
+ headline: `Building ${options.name}`,
4831
+ log: options.output?.path ?? null,
4832
+ engine
4833
+ }, () => missing ? noCwd(b.cwd).slice(8) : failed ? `build ${failed[1]} exited ${failed[2]}: ${clipLine(failed[3] ?? "")}` : null, (line) => !line.startsWith("reeval: build "));
4834
+ }
4835
+ return tag;
4836
+ }
4234
4837
  /** The build arg a Dockerfile can start `FROM` to layer on reeval's sandbox image. */
4235
4838
  const REEVAL_BASE_ARG = "REEVAL_BASE";
4236
4839
  /**
@@ -4268,11 +4871,14 @@ function dockerignoreMatcher(text) {
4268
4871
  return excluded;
4269
4872
  };
4270
4873
  }
4271
- /** Files in the build context that `docker build` would send, sorted, relative to `context`. */
4874
+ /**
4875
+ * Files in the build context that `docker build` would send, sorted, relative to `context`. A
4876
+ * Dockerfile read from stdin (`dockerfile` null) has no `<Dockerfile>.dockerignore` of its own.
4877
+ */
4272
4878
  function contextFiles(context, dockerfile) {
4273
- const own = [`${dockerfile}.dockerignore`, join(context, ".dockerignore")].find(existsSync);
4879
+ const own = [...dockerfile ? [`${dockerfile}.dockerignore`] : [], join(context, ".dockerignore")].find(existsSync);
4274
4880
  const ignored = dockerignoreMatcher(own ? readFileSync(own, "utf8") : "");
4275
- const always = /* @__PURE__ */ new Set([relative(context, dockerfile), ".dockerignore"]);
4881
+ const always = /* @__PURE__ */ new Set([...dockerfile ? [relative(context, dockerfile)] : [], ".dockerignore"]);
4276
4882
  const files = [];
4277
4883
  const walk = (rel) => {
4278
4884
  for (const entry of readdirSync(join(context, rel), { withFileTypes: true })) {
@@ -4352,6 +4958,13 @@ async function buildDockerfile(dockerfile, options) {
4352
4958
  return tag;
4353
4959
  }
4354
4960
  //#endregion
4355
- export { removeContainerDetached as $, hostAllowed as A, WORKSPACE_DIR as B, applyTransformations as C, HARNESS_HOSTS as D, DOCKER_EGRESS as E, HARNESS_PACKAGES as F, docker as G, harnessLabel as H, LABEL as I, dockerOk as J, dockerAvailable as K, SANDBOX_HOME as L, BASE_IMAGE as M, BRIDGE_PORT as N, PACKAGE_HOSTS as O, DEFAULT_IMAGE as P, isRegistryDigest as Q, SANDBOX_ROOT as R, placeholder as S, onEndpoint as T, imageFor as U, adapterVersion as V, isBakedHarness as W, imageInfo as X, dockerState as Y, imageTags as Z, ROOT_CAPABILITIES as _, buildInstallLayer as a, causeLine as at, CA_PATH as b, dockerfileTag as c, parseBuildOutput as ct, installLayerTag as d, dockerSeccompFile as dt, onDockerCommand as et, installScript as f, engineSpec as ft, DEFAULT_LIMITS as g, usesReevalBase as h, buildImage as i, RETRY_DELAYS as it, BAKED_HARNESSES as j, defaultAllowedHosts as k, dockerignoreMatcher as l, retryTransient as lt, resolveFrom as m, REEVAL_BASE_ARG as n, shellQuote as nt, contextFiles as o, failureCause as ot, pruneImages as p, normalizeImageId as pt, dockerCapacity as q, buildDockerfile as r, ImageBuildError as rt, contextHash as s, isTransient as st, DOCKER_CONTEXT as t, run as tt, harnessImageFor as u, ENGINES as ut, createDockerSession as v, matches as w, PROXY_PORT as x, dockerSandboxProvider as y, SANDBOX_USER as z };
4961
+ //#region ../sandbox-docker/src/mcp-timer.ts
4962
+ /**
4963
+ * reeval's MCP start-up timer, shipped in the package: a Node script that runs a stdio MCP server
4964
+ * and logs when it answers `initialize` (see its header). It runs in any sandbox with `node`.
4965
+ */
4966
+ const MCP_TIMER = fileURLToPath(ownFileURL("bin/mcp-timer.cjs"));
4967
+ //#endregion
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 };
4356
4969
 
4357
- //# sourceMappingURL=src-ftu5wPSt.js.map
4970
+ //# sourceMappingURL=src-CIpKL45U.js.map