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.
- package/README.md +115 -257
- package/assets/package.json +1 -1
- package/assets/pnpm-lock.yaml +41 -30
- package/bin/mcp-timer.cjs +224 -0
- package/dist/{adapter-Bwy3QxAK.js → adapter-IKKo4sZj.js} +4 -4
- package/dist/{adapter-Bwy3QxAK.js.map → adapter-IKKo4sZj.js.map} +1 -1
- package/dist/{agent-DpE80WPz.js → agent-ClE-g9VL.js} +22 -7
- package/dist/{agent-DpE80WPz.js.map → agent-ClE-g9VL.js.map} +1 -1
- package/dist/{analysis-Bxub9LMT.js → analysis-B0nOfAzA.js} +3 -3
- package/dist/{analysis-Bxub9LMT.js.map → analysis-B0nOfAzA.js.map} +1 -1
- package/dist/{assemble-D9GWr-HS.js → assemble-BO93VAk1.js} +22 -20
- package/dist/assemble-BO93VAk1.js.map +1 -0
- package/dist/assemble-VQxyoDxK.js +1 -0
- package/dist/{attempt-CGPV3BZY.js → attempt-jI0OWxlz.js} +2 -2
- package/dist/{attempt-CGPV3BZY.js.map → attempt-jI0OWxlz.js.map} +1 -1
- package/dist/bridge/index.mjs +2 -1
- package/dist/{capture-BQlGHiMC.js → capture-dn2hjSLj.js} +29 -21
- package/dist/{capture-BQlGHiMC.js.map → capture-dn2hjSLj.js.map} +1 -1
- package/dist/capture-nIJ-7kUE.js +1 -0
- package/dist/{cli-ClpPT8kn.js → cli-BCE1xlm4.js} +176 -120
- package/dist/cli-BCE1xlm4.js.map +1 -0
- package/dist/{cli-spec-qmEnQ25_.js → cli-spec-BXIBUlaK.js} +34 -15
- package/dist/cli-spec-BXIBUlaK.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{command-BlM0y8jN.js → command-Bh3yhN_b.js} +8 -8
- package/dist/{command-BlM0y8jN.js.map → command-Bh3yhN_b.js.map} +1 -1
- package/dist/command-C6vwmjS7.js +91 -0
- package/dist/command-C6vwmjS7.js.map +1 -0
- package/dist/{command-DbAe1Rzb.js → command-CANun0m0.js} +16 -19
- package/dist/command-CANun0m0.js.map +1 -0
- package/dist/command-CBaHukyl.js +335 -0
- package/dist/command-CBaHukyl.js.map +1 -0
- package/dist/{command-TgbzPizb.js → command-D_kEnYcX.js} +17 -16
- package/dist/command-D_kEnYcX.js.map +1 -0
- package/dist/commands-7tH3EsVu.js +666 -0
- package/dist/commands-7tH3EsVu.js.map +1 -0
- package/dist/{commands-CthqwkAn.js → commands-BKGzBMgb.js} +1 -1
- package/dist/{config-DrVSEVN0.js → config-D7TFvUpL.js} +40 -55
- package/dist/config-D7TFvUpL.js.map +1 -0
- package/dist/{context-C1mxXh80.js → context-Dgn48BqT.js} +13 -2
- package/dist/context-Dgn48BqT.js.map +1 -0
- package/dist/{contrasts-5zsj32gk.js → contrasts-B4nOhn0Z.js} +3 -3
- package/dist/{contrasts-5zsj32gk.js.map → contrasts-B4nOhn0Z.js.map} +1 -1
- package/dist/db-DJUuFoWQ.js +602 -0
- package/dist/db-DJUuFoWQ.js.map +1 -0
- package/dist/db-W9EFlXnn.js +2 -0
- package/dist/{dist-CuY4ehEk.js → dist-Dp_AJgYN.js} +3 -3
- package/dist/{dist-CuY4ehEk.js.map → dist-Dp_AJgYN.js.map} +1 -1
- package/dist/{docker-D1HZDS6q.js → docker-CQxucRPe.js} +16 -15
- package/dist/docker-CQxucRPe.js.map +1 -0
- package/dist/{docker-DNmMPOBb.js → docker-Dac6Zalz.js} +1 -1
- package/dist/{docker-DOvjji9I.js → docker-DuHcQidD.js} +3 -3
- package/dist/{docker-DOvjji9I.js.map → docker-DuHcQidD.js.map} +1 -1
- package/dist/{doctor-D2MbNO8L.js → doctor-De_QlOTM.js} +72 -38
- package/dist/doctor-De_QlOTM.js.map +1 -0
- package/dist/{egress-2-qzPvGi.js → egress-DEAvq8Fl.js} +453 -31
- package/dist/egress-DEAvq8Fl.js.map +1 -0
- package/dist/env-BQqj93gu.js +21 -0
- package/dist/env-BQqj93gu.js.map +1 -0
- package/dist/{environment-Cca6a4wB.js → environment-B4IuURK1.js} +247 -14
- package/dist/environment-B4IuURK1.js.map +1 -0
- package/dist/{evaluate-DMyaGQEj.d.ts → evaluate-C9-ritA5.d.ts} +2 -2
- package/dist/{evaluate-DKIl54Hb.js → evaluate-Djk1YPFL.js} +540 -560
- package/dist/evaluate-Djk1YPFL.js.map +1 -0
- package/dist/facts-Cfx1H70y.js +898 -0
- package/dist/facts-Cfx1H70y.js.map +1 -0
- package/dist/files-Bs6H3HiG.js +65 -0
- package/dist/files-Bs6H3HiG.js.map +1 -0
- package/dist/{files-BpUYBwGu.js → files-CN1QfenC.js} +31 -8
- package/dist/files-CN1QfenC.js.map +1 -0
- package/dist/files-CtjEXWXm.js +1 -0
- package/dist/{files-BXHZFWmx.js → files-D27nUk0K.js} +6 -17
- package/dist/files-D27nUk0K.js.map +1 -0
- package/dist/files-D3gJa91B.js +1 -0
- package/dist/generate-BnrvBAzF.js +1646 -0
- package/dist/generate-BnrvBAzF.js.map +1 -0
- package/dist/{git-Crl3b87o.js → git-rPfl7AD1.js} +3 -3
- package/dist/{git-Crl3b87o.js.map → git-rPfl7AD1.js.map} +1 -1
- package/dist/{harness-CEuntrla.js → harness-1OkumOAm.js} +3 -3
- package/dist/{harness-CEuntrla.js.map → harness-1OkumOAm.js.map} +1 -1
- package/dist/harness.d.ts +1 -1
- package/dist/harness.js +1 -1
- package/dist/{header-DHQv_ZrF.js → header-BVzbRnNv.js} +10 -3
- package/dist/{header-DHQv_ZrF.js.map → header-BVzbRnNv.js.map} +1 -1
- package/dist/{images-Bi3aGxb9.js → images-m784ueGK.js} +61 -13
- package/dist/images-m784ueGK.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/init-hiTkiM4L.js +543 -0
- package/dist/init-hiTkiM4L.js.map +1 -0
- package/dist/init-kMkTgTZc.js +1 -0
- package/dist/insights-B2Xmhz9o.js +64 -0
- package/dist/insights-B2Xmhz9o.js.map +1 -0
- package/dist/{invocations-BYDPe3fV.js → invocations-D_ePcwyL.js} +4 -4
- package/dist/{invocations-BYDPe3fV.js.map → invocations-D_ePcwyL.js.map} +1 -1
- package/dist/{judge-DW_GDOmB.js → judge-CEDIfRiU.js} +50 -15
- package/dist/judge-CEDIfRiU.js.map +1 -0
- package/dist/judge.d.ts +7 -6
- package/dist/judge.js +8 -7
- package/dist/judge.js.map +1 -1
- package/dist/{live-jlfszAbb.js → live-DYd2QEyR.js} +2 -2
- package/dist/{live-jlfszAbb.js.map → live-DYd2QEyR.js.map} +1 -1
- package/dist/model-B8H3gL69.js +1 -0
- package/dist/{model-DaVCY3rb.js → model-CTiuzkWw.js} +9 -7
- package/dist/model-CTiuzkWw.js.map +1 -0
- package/dist/model-DcIbmggK.js +1 -0
- package/dist/{model-D0IcOQao.js → model-DpkclE4r.js} +60 -25
- package/dist/model-DpkclE4r.js.map +1 -0
- package/dist/{model-DaX4bNLZ.js → model-UgHDMhI1.js} +144 -46
- package/dist/model-UgHDMhI1.js.map +1 -0
- package/dist/{node-B3ydHo6t.js → node-CGZYh4Fj.js} +4 -4
- package/dist/{node-B3ydHo6t.js.map → node-CGZYh4Fj.js.map} +1 -1
- package/dist/{opencode-providers-J9colP0-.js → opencode-providers-snfeWd6Z.js} +86 -47
- package/dist/opencode-providers-snfeWd6Z.js.map +1 -0
- package/dist/{options-C0d0AzeR.js → options-CyAyLjgJ.js} +22 -11
- package/dist/options-CyAyLjgJ.js.map +1 -0
- package/dist/{pairs-CnInRu-G.js → pairs-DP6XOJby.js} +2 -2
- package/dist/{pairs-CnInRu-G.js.map → pairs-DP6XOJby.js.map} +1 -1
- package/dist/{patch-B1bnfhN-.js → patch-uMq9rCl_.js} +2 -2
- package/dist/{patch-B1bnfhN-.js.map → patch-uMq9rCl_.js.map} +1 -1
- package/dist/{plan-DgQxp9yy.js → plan-CZYgXnZh.js} +21 -8
- package/dist/plan-CZYgXnZh.js.map +1 -0
- package/dist/plugins-P-uh54VF.js +654 -0
- package/dist/plugins-P-uh54VF.js.map +1 -0
- package/dist/{preflight-DlQvbySb.js → preflight-BQlPTHVu.js} +1 -1
- package/dist/{preflight-BRzD30L5.js → preflight-D3HvKeAx.js} +14 -10
- package/dist/preflight-D3HvKeAx.js.map +1 -0
- package/dist/prepare-DCwM2KKD.js +2 -0
- package/dist/{prepare-B_y-AFMv.js → prepare-Dx7dOnJB.js} +70 -23
- package/dist/prepare-Dx7dOnJB.js.map +1 -0
- package/dist/{provenance-DQ0AXn6c.js → provenance-C_-8y-x3.js} +4 -4
- package/dist/{provenance-DQ0AXn6c.js.map → provenance-C_-8y-x3.js.map} +1 -1
- package/dist/{providers-DoTq1jHW.js → providers-CF5UEaP8.js} +24 -10
- package/dist/providers-CF5UEaP8.js.map +1 -0
- package/dist/providers-CTBYvMxu.js +1 -0
- package/dist/{query-BO-ux7RB.js → query-DXthKmq0.js} +3 -3
- package/dist/{query-BO-ux7RB.js.map → query-DXthKmq0.js.map} +1 -1
- package/dist/query-D_1ARx5q.js +1 -0
- package/dist/{questions-C2dym2ce.js → questions-KENpWnfV.js} +8 -9
- package/dist/questions-KENpWnfV.js.map +1 -0
- package/dist/{record-BKmMvZ6O.js → record-kk9M1hj-.js} +8 -4
- package/dist/record-kk9M1hj-.js.map +1 -0
- package/dist/{redact-DJ9JCShu.js → redact-CRGdfV2s.js} +4 -2
- package/dist/redact-CRGdfV2s.js.map +1 -0
- package/dist/remarks-1521zwfW.js +38 -0
- package/dist/remarks-1521zwfW.js.map +1 -0
- package/dist/remarks-BhAV1H4V.js +1 -0
- package/dist/remarks-Ml6bcSdV.js +278 -0
- package/dist/remarks-Ml6bcSdV.js.map +1 -0
- package/dist/{remote-CPPW86Q9.js → remote-Dkeghk8k.js} +3 -3
- package/dist/{remote-CPPW86Q9.js.map → remote-Dkeghk8k.js.map} +1 -1
- package/dist/resolve-CMmhACr6.js +1 -0
- package/dist/resolve-DzdWcIL6.js +724 -0
- package/dist/resolve-DzdWcIL6.js.map +1 -0
- package/dist/{results-BE2f-1XS.js → results-D-naai_Q.js} +33 -8
- package/dist/results-D-naai_Q.js.map +1 -0
- package/dist/{resume-Ca1c8vix.js → resume-8-nnW8iO.js} +4 -4
- package/dist/{resume-Ca1c8vix.js.map → resume-8-nnW8iO.js.map} +1 -1
- package/dist/{root-DICDHCtp.js → root-DNuLhaA5.js} +2 -2
- package/dist/root-DNuLhaA5.js.map +1 -0
- package/dist/rules-CcV3zLDR.js +318 -0
- package/dist/rules-CcV3zLDR.js.map +1 -0
- package/dist/run-Bel7Vde4.js +88 -0
- package/dist/run-Bel7Vde4.js.map +1 -0
- package/dist/{sandbox-DLrsvu7U.js → sandbox-YLbk9mO7.js} +2 -2
- package/dist/{sandbox-DLrsvu7U.js.map → sandbox-YLbk9mO7.js.map} +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +1 -1
- package/dist/server-C-ysqkLv.js +1 -0
- package/dist/{server-TUY5xA_A.js → server-De8oH3bt.js} +10 -10
- package/dist/{server-TUY5xA_A.js.map → server-De8oH3bt.js.map} +1 -1
- package/dist/{source-DJGS0a0u.js → source-C686eZpY.js} +2 -2
- package/dist/{source-DJGS0a0u.js.map → source-C686eZpY.js.map} +1 -1
- package/dist/source-DjgypUHW.js +1 -0
- package/dist/src-3mP821Pt.js +2 -0
- package/dist/src-B3KCLZVk.js +3 -0
- package/dist/src-BAyg6CtQ.js +2 -0
- package/dist/{src-CF17Ug08.js → src-BYLM3hEz.js} +22 -6
- package/dist/src-BYLM3hEz.js.map +1 -0
- package/dist/src-BezgGX4X.js +2 -0
- package/dist/{src-DvCg0NBs.js → src-C7W6kg81.js} +40 -10
- package/dist/{src-DvCg0NBs.js.map → src-C7W6kg81.js.map} +1 -1
- package/dist/{src-ftu5wPSt.js → src-CIpKL45U.js} +782 -169
- package/dist/src-CIpKL45U.js.map +1 -0
- package/dist/src-ChBhk06Q.js +2 -0
- package/dist/{src-BxfCKA3_.js → src-CoqZYspK.js} +1 -1
- package/dist/{src-CFp86GtV.js → src-CzQrAcF8.js} +49 -49
- package/dist/src-CzQrAcF8.js.map +1 -0
- package/dist/src-D9fSTxlc.js +1 -0
- package/dist/{src-FGXhkEkH.js → src-DeY52Gyf.js} +182 -23
- package/dist/src-DeY52Gyf.js.map +1 -0
- package/dist/src-DrJSnoMR.js +2 -0
- package/dist/{src-DvK0EIZx.js → src-Xj_BZKbq.js} +112 -13
- package/dist/src-Xj_BZKbq.js.map +1 -0
- package/dist/{src-u8_ixn_F.js → src-cbCb-MPy.js} +20 -4
- package/dist/src-cbCb-MPy.js.map +1 -0
- package/dist/src-dhSTRN50.js +2 -0
- package/dist/{src-C7_1SqDP.js → src-djNNYIdP.js} +2 -2
- package/dist/{src-C7_1SqDP.js.map → src-djNNYIdP.js.map} +1 -1
- package/dist/state-BGWGAwdo.js +1 -0
- package/dist/{state-DfpHox6b.js → state-C0i_ujQZ.js} +19 -17
- package/dist/{state-DfpHox6b.js.map → state-C0i_ujQZ.js.map} +1 -1
- package/dist/studio-app/assets/{index-CBkhNAv8.js → index-Bb_cid9k.js} +9 -9
- package/dist/studio-app/index.html +1 -1
- package/dist/{suite-Cw_7G2RM.js → suite-DmuF_ApO.js} +80 -30
- package/dist/suite-DmuF_ApO.js.map +1 -0
- package/dist/suite-mn9g4veR.js +2 -0
- package/dist/{summary-BUKfU-9l.js → summary-BFMHMTz2.js} +273 -47
- package/dist/summary-BFMHMTz2.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +2 -2
- package/dist/{text-gEcKjAyp.js → text-CrVy5ioL.js} +18 -2
- package/dist/text-CrVy5ioL.js.map +1 -0
- package/dist/{transcript-BNNANU8A.js → transcript-BTxJpW4R.js} +2 -2
- package/dist/{transcript-BNNANU8A.js.map → transcript-BTxJpW4R.js.map} +1 -1
- package/dist/{types-BYdlwMwo.d.ts → types-B-9b1MDV.d.ts} +850 -34
- package/dist/types-C5vI3g1C.js.map +1 -1
- package/dist/usage-BDo2q_yu.js +1 -0
- package/dist/{command-Bj39y-Wm.js → validate-DWjkshl1.js} +20 -377
- package/dist/validate-DWjkshl1.js.map +1 -0
- package/dist/view/index.d.ts +31 -263
- package/dist/view/index.js +8 -7
- package/dist/views-DE2A1jEX.js +2 -0
- package/dist/{views-B8w2J-gD.js → views-Dk8MCQKr.js} +8 -98
- package/dist/views-Dk8MCQKr.js.map +1 -0
- package/dist/{worker-entry-DNnveLqx.js → worker-entry-CqrzGPF4.js} +2 -2
- package/dist/{worker-entry-DNnveLqx.js.map → worker-entry-CqrzGPF4.js.map} +1 -1
- package/dist/workspace-B1eGO46M.js +1 -0
- package/dist/{workspace-C4RA3srV.js → workspace-CLeOybkr.js} +45 -11
- package/dist/workspace-CLeOybkr.js.map +1 -0
- package/package.json +5 -1
- package/skills/eval-rubric/SKILL.md +130 -0
- package/dist/agent-D483ru7D.js +0 -334
- package/dist/agent-D483ru7D.js.map +0 -1
- package/dist/assemble-D5KGrwnJ.js +0 -1
- package/dist/assemble-D9GWr-HS.js.map +0 -1
- package/dist/capture-CYGLpWxR.js +0 -1
- package/dist/cli-ClpPT8kn.js.map +0 -1
- package/dist/cli-spec-qmEnQ25_.js.map +0 -1
- package/dist/command-Bj39y-Wm.js.map +0 -1
- package/dist/command-C-cVjlny.js +0 -1
- package/dist/command-DbAe1Rzb.js.map +0 -1
- package/dist/command-TgbzPizb.js.map +0 -1
- package/dist/commands-YUwlyTbr.js +0 -228
- package/dist/commands-YUwlyTbr.js.map +0 -1
- package/dist/config-DrVSEVN0.js.map +0 -1
- package/dist/context-C1mxXh80.js.map +0 -1
- package/dist/db-CKg5NtiW.js +0 -2
- package/dist/db-DfhipHEF.js +0 -1416
- package/dist/db-DfhipHEF.js.map +0 -1
- package/dist/docker-D1HZDS6q.js.map +0 -1
- package/dist/doctor-D2MbNO8L.js.map +0 -1
- package/dist/egress-2-qzPvGi.js.map +0 -1
- package/dist/environment-Cca6a4wB.js.map +0 -1
- package/dist/evaluate-DKIl54Hb.js.map +0 -1
- package/dist/files-BXHZFWmx.js.map +0 -1
- package/dist/files-BpUYBwGu.js.map +0 -1
- package/dist/files-Bw8lWEjg.js +0 -1
- package/dist/files-D6J0iSdx.js +0 -1
- package/dist/images-Bi3aGxb9.js.map +0 -1
- package/dist/init-Cl3f3NBT.js +0 -360
- package/dist/init-Cl3f3NBT.js.map +0 -1
- package/dist/init-fhOXED8F.js +0 -1
- package/dist/judge-DW_GDOmB.js.map +0 -1
- package/dist/model-CXhrziBe.js +0 -1
- package/dist/model-D0IcOQao.js.map +0 -1
- package/dist/model-DaVCY3rb.js.map +0 -1
- package/dist/model-DaX4bNLZ.js.map +0 -1
- package/dist/model-_bxNrk2h.js +0 -1
- package/dist/opencode-providers-J9colP0-.js.map +0 -1
- package/dist/options-C0d0AzeR.js.map +0 -1
- package/dist/plan-DgQxp9yy.js.map +0 -1
- package/dist/preflight-BRzD30L5.js.map +0 -1
- package/dist/prepare-B_y-AFMv.js.map +0 -1
- package/dist/prepare-DCKTLHN3.js +0 -2
- package/dist/providers-B9yTdIqF.js +0 -1
- package/dist/providers-DoTq1jHW.js.map +0 -1
- package/dist/query-CRQfsw0e.js +0 -1
- package/dist/questions-C2dym2ce.js.map +0 -1
- package/dist/record-BKmMvZ6O.js.map +0 -1
- package/dist/redact-DJ9JCShu.js.map +0 -1
- package/dist/results-BE2f-1XS.js.map +0 -1
- package/dist/root-DICDHCtp.js.map +0 -1
- package/dist/server-Dp8eOeK5.js +0 -1
- package/dist/source-DdbLGR1M.js +0 -1
- package/dist/src-CF17Ug08.js.map +0 -1
- package/dist/src-CFp86GtV.js.map +0 -1
- package/dist/src-CLoOMVWI.js +0 -3
- package/dist/src-CcrQbpG8.js +0 -2
- package/dist/src-DCFtJRp0.js +0 -2
- package/dist/src-DSYYPEOj.js +0 -2
- package/dist/src-DYO3DdnQ.js +0 -1
- package/dist/src-DvK0EIZx.js.map +0 -1
- package/dist/src-FGXhkEkH.js.map +0 -1
- package/dist/src-WD-zgQYZ.js +0 -2
- package/dist/src-_oI9AePb.js +0 -2
- package/dist/src-ftu5wPSt.js.map +0 -1
- package/dist/src-qsKCqv7a.js +0 -2
- package/dist/src-u8_ixn_F.js.map +0 -1
- package/dist/state-DpAxYyED.js +0 -1
- package/dist/suite-Cw_7G2RM.js.map +0 -1
- package/dist/suite-DamAEnNF.js +0 -1
- package/dist/summary-BUKfU-9l.js.map +0 -1
- package/dist/summary-Baa4rTHS.js +0 -192
- package/dist/summary-Baa4rTHS.js.map +0 -1
- package/dist/text-gEcKjAyp.js.map +0 -1
- package/dist/tools-DLHgetpx.js +0 -249
- package/dist/tools-DLHgetpx.js.map +0 -1
- package/dist/views-B8w2J-gD.js.map +0 -1
- package/dist/views-DblnI3kA.js +0 -2
- package/dist/workspace-C4RA3srV.js.map +0 -1
- package/dist/workspace-C_X0tdv6.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as ownFileURL } from "./root-
|
|
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
|
-
|
|
1516
|
-
], {
|
|
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.
|
|
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: "
|
|
1620
|
+
patch: "b17cde50"
|
|
1610
1621
|
},
|
|
1611
1622
|
antigravity: {
|
|
1612
1623
|
name: "@reeval/harness-antigravity",
|
|
1613
|
-
version: "0.1.
|
|
1624
|
+
version: "0.1.11"
|
|
1614
1625
|
},
|
|
1615
1626
|
opencode: {
|
|
1616
1627
|
name: "@reeval/harness-opencode",
|
|
1617
|
-
version: "0.2.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
2288
|
-
*
|
|
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
|
|
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.#
|
|
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 } =
|
|
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(
|
|
2674
|
-
const
|
|
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:
|
|
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}
|
|
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,
|
|
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
|
-
|
|
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
|
|
2826
|
-
|
|
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
|
-
|
|
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 (!
|
|
2831
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
-
}),
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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://${
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
3337
|
-
*
|
|
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 = [
|
|
3751
|
+
const caps = [
|
|
3752
|
+
...PRIVILEGED_CAPABILITIES,
|
|
3753
|
+
...options.root ? ROOT_CAPABILITIES : [],
|
|
3754
|
+
...options.capabilities ?? []
|
|
3755
|
+
];
|
|
3345
3756
|
return [
|
|
3346
3757
|
"--user",
|
|
3347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
/**
|
|
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()
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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-
|
|
4970
|
+
//# sourceMappingURL=src-CIpKL45U.js.map
|