reeval 0.8.0 → 0.10.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 +52 -40
- package/THIRD_PARTY_NOTICES.md +218 -11
- package/assets/package.json +1 -1
- package/assets/pnpm-lock.yaml +30 -30
- package/dist/adapter-DWlpOtRn.js +279 -0
- package/dist/adapter-DWlpOtRn.js.map +1 -0
- package/dist/{agent-B72lZ6w5.js → agent-CURAwx_q.js} +6 -6
- package/dist/{agent-B72lZ6w5.js.map → agent-CURAwx_q.js.map} +1 -1
- package/dist/{agent-Cfj1wD4D.js → agent-CaS_2Sce.js} +92 -26
- package/dist/agent-CaS_2Sce.js.map +1 -0
- package/dist/{analysis-BTNnKh1w.js → analysis-Bxub9LMT.js} +4 -4
- package/dist/{analysis-BTNnKh1w.js.map → analysis-Bxub9LMT.js.map} +1 -1
- package/dist/{assemble-BbFVjHeS.js → assemble-B9Alds-z.js} +290 -30
- package/dist/assemble-B9Alds-z.js.map +1 -0
- package/dist/assemble-J5MukZFY.js +1 -0
- package/dist/{client-CwzU9CiW.js → assert-EUIoRdIt.js} +38 -112
- package/dist/assert-EUIoRdIt.js.map +1 -0
- package/dist/bridge/index.mjs +248 -55
- package/dist/{capture-BXhBfmsM.js → capture-CNb9SuU5.js} +204 -39
- package/dist/capture-CNb9SuU5.js.map +1 -0
- package/dist/capture-jquXZBKK.js +1 -0
- package/dist/{cli-BMjTKY-D.js → cli-D303Vj5N.js} +257 -146
- package/dist/cli-D303Vj5N.js.map +1 -0
- package/dist/{cli-spec-DSvKsOK4.js → cli-spec-BPZbQG2p.js} +38 -19
- package/dist/cli-spec-BPZbQG2p.js.map +1 -0
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/client-fcwgwOAD.js +106 -0
- package/dist/client-fcwgwOAD.js.map +1 -0
- package/dist/{command-JPGNzFsA.js → command-BLfT7vnQ.js} +19 -18
- package/dist/command-BLfT7vnQ.js.map +1 -0
- package/dist/{command-BK4kOgkH.js → command-BT9a37ZR.js} +381 -78
- package/dist/command-BT9a37ZR.js.map +1 -0
- package/dist/{command-BqJtFiPy.js → command-Bge8F7Ko.js} +384 -101
- package/dist/command-Bge8F7Ko.js.map +1 -0
- package/dist/command-BkFjijN2.js +1 -0
- package/dist/{command-BdstSEHu.js → command-CMJtHKRa.js} +14 -14
- package/dist/{command-BdstSEHu.js.map → command-CMJtHKRa.js.map} +1 -1
- package/dist/commands-1p-bIWhN.js +1 -0
- package/dist/{commands-B618jg3C.js → commands-BFx16WB8.js} +8 -8
- package/dist/commands-BFx16WB8.js.map +1 -0
- package/dist/config-DEgnUiU-.js +756 -0
- package/dist/config-DEgnUiU-.js.map +1 -0
- package/dist/{context-CvUv7DUb.js → context-C1mxXh80.js} +11 -7
- package/dist/context-C1mxXh80.js.map +1 -0
- package/dist/{db-DFbtGIYy.js → db-CdHizc4_.js} +582 -53
- package/dist/db-CdHizc4_.js.map +1 -0
- package/dist/db-Dm1cRJJP.js +2 -0
- package/dist/debug-D7bEv_UV.js +1 -0
- package/dist/{dist-CTzThfu_.js → dist-B8hJImYt.js} +17 -4
- package/dist/dist-B8hJImYt.js.map +1 -0
- package/dist/{docker-2qns7hJ2.js → docker-7b_2SaH7.js} +35 -22
- package/dist/docker-7b_2SaH7.js.map +1 -0
- package/dist/{docker-IFRW8K5f.js → docker-sZrNrr82.js} +3 -3
- package/dist/docker-sZrNrr82.js.map +1 -0
- package/dist/docker-yauK-9X8.js +1 -0
- package/dist/{doctor-B6oYTPoR.js → doctor-BYNok4UO.js} +263 -84
- package/dist/doctor-BYNok4UO.js.map +1 -0
- package/dist/{duration-JsJrIWXq.js → duration-CEOQww97.js} +2 -2
- package/dist/{duration-JsJrIWXq.js.map → duration-CEOQww97.js.map} +1 -1
- package/dist/{egress-IUKdeayc.js → egress-C4PZTSSe.js} +147 -14
- package/dist/egress-C4PZTSSe.js.map +1 -0
- package/dist/environment-DNjRk0nU.js +1133 -0
- package/dist/environment-DNjRk0nU.js.map +1 -0
- package/dist/{errors-yDAEwmZo.js → errors-B6BSMs03.js} +2 -2
- package/dist/errors-B6BSMs03.js.map +1 -0
- package/dist/{errors-C9w_ud7g.js → errors-r025B0Zb.js} +4 -4
- package/dist/errors-r025B0Zb.js.map +1 -0
- package/dist/evaluate-C1UVL21l.d.ts +92 -0
- package/dist/evaluate-D4zeRGBF.js +2351 -0
- package/dist/evaluate-D4zeRGBF.js.map +1 -0
- package/dist/family-D2b7WD10.js +67 -0
- package/dist/family-D2b7WD10.js.map +1 -0
- package/dist/files-BAt0i00X.js +1 -0
- package/dist/{files-CMSUXAEg.js → files-BXUdY8tr.js} +10 -9
- package/dist/{files-CMSUXAEg.js.map → files-BXUdY8tr.js.map} +1 -1
- package/dist/files-BhyltlDB.js +1 -0
- package/dist/files-Bk1mC3AN.js +43 -0
- package/dist/files-Bk1mC3AN.js.map +1 -0
- package/dist/{files-B6aVVGrk.js → files-DQmHE7pb.js} +5 -5
- package/dist/{files-B6aVVGrk.js.map → files-DQmHE7pb.js.map} +1 -1
- package/dist/files-ZZPqfAo6.js +1 -0
- package/dist/{git-EmdFJ4DY.js → git-DfG-YJ1u.js} +2 -2
- package/dist/{git-EmdFJ4DY.js.map → git-DfG-YJ1u.js.map} +1 -1
- package/dist/harness-B8QC9u0O.js +323 -0
- package/dist/harness-B8QC9u0O.js.map +1 -0
- package/dist/harness.d.ts +19 -31
- package/dist/harness.js +1 -2
- package/dist/{header-_F0EWob3.js → header-hK-Z_Po9.js} +10 -3
- package/dist/{header-_F0EWob3.js.map → header-hK-Z_Po9.js.map} +1 -1
- package/dist/{images-CscddrJU.js → images-0On0cztu.js} +114 -45
- package/dist/images-0On0cztu.js.map +1 -0
- package/dist/index.d.ts +10 -26
- package/dist/index.js +6 -2108
- package/dist/{init-NQfIvi-0.js → init-B77Rpcqc.js} +26 -8
- package/dist/init-B77Rpcqc.js.map +1 -0
- package/dist/init-BrOWin3W.js +1 -0
- package/dist/{invocations-nPvrlZPj.js → invocations-DbGfBAA_.js} +4 -4
- package/dist/{invocations-nPvrlZPj.js.map → invocations-DbGfBAA_.js.map} +1 -1
- package/dist/{judge-CeYPN8P_.js → judge-DW_GDOmB.js} +11 -19
- package/dist/{judge-CeYPN8P_.js.map → judge-DW_GDOmB.js.map} +1 -1
- package/dist/judge.d.ts +2 -2
- package/dist/judge.js +1 -1
- package/dist/{live-S0qODb6o.js → live-jlfszAbb.js} +9 -20
- package/dist/live-jlfszAbb.js.map +1 -0
- package/dist/matchers-Ddmc2z7H.js +118 -0
- package/dist/matchers-Ddmc2z7H.js.map +1 -0
- package/dist/{model-DEQo3FkZ.js → model-DaVCY3rb.js} +2 -2
- package/dist/{model-DEQo3FkZ.js.map → model-DaVCY3rb.js.map} +1 -1
- package/dist/model-NM3ID0uR.js +1 -0
- package/dist/{model-Cl507trk.js → model-h_cDtcRr.js} +19 -18
- package/dist/model-h_cDtcRr.js.map +1 -0
- package/dist/model-nwWJvaUR.js +1 -0
- package/dist/{model-BndnmcLm.js → model-oqcDplJG.js} +271 -34
- package/dist/model-oqcDplJG.js.map +1 -0
- package/dist/{node-Co7EXge0.js → node-B3ydHo6t.js} +10 -47
- package/dist/node-B3ydHo6t.js.map +1 -0
- package/dist/object-CLf2-tNq.js +9 -0
- package/dist/object-CLf2-tNq.js.map +1 -0
- package/dist/{opencode-providers-ByD85Q0l.js → opencode-providers-J9colP0-.js} +96 -21
- package/dist/opencode-providers-J9colP0-.js.map +1 -0
- package/dist/options-Bh2W63dK.js +1119 -0
- package/dist/options-Bh2W63dK.js.map +1 -0
- package/dist/{pairs-DDsjgm5F.js → pairs-CnInRu-G.js} +76 -2
- package/dist/{pairs-DDsjgm5F.js.map → pairs-CnInRu-G.js.map} +1 -1
- package/dist/{patch-CGCVipPu.js → patch-Zac5aIRM.js} +6 -6
- package/dist/{patch-CGCVipPu.js.map → patch-Zac5aIRM.js.map} +1 -1
- package/dist/{plan-PPc4-6xd.js → plan-C9Cy27Ay.js} +75 -32
- package/dist/plan-C9Cy27Ay.js.map +1 -0
- package/dist/preflight-CPWpQeBI.js +1 -0
- package/dist/{preflight-CddycK92.js → preflight-D7iFPKeK.js} +8 -7
- package/dist/preflight-D7iFPKeK.js.map +1 -0
- package/dist/prepare-BGjePFQl.js +2 -0
- package/dist/{prepare-CsP-057p.js → prepare-doGxaAGR.js} +38 -30
- package/dist/prepare-doGxaAGR.js.map +1 -0
- package/dist/process-CSVO_jsc.js +119 -0
- package/dist/process-CSVO_jsc.js.map +1 -0
- package/dist/{provenance-CsX6PHDR.js → provenance-DmE9uPVt.js} +4 -4
- package/dist/{provenance-CsX6PHDR.js.map → provenance-DmE9uPVt.js.map} +1 -1
- package/dist/provider-DehR4CMN.js +1 -0
- package/dist/providers-gDMko8fl.js +1 -0
- package/dist/providers-tAOD1NQN.js +165 -0
- package/dist/providers-tAOD1NQN.js.map +1 -0
- package/dist/{query-BUcwCYsI.js → query-Bes9Kh6Y.js} +3 -3
- package/dist/{query-BUcwCYsI.js.map → query-Bes9Kh6Y.js.map} +1 -1
- package/dist/query-C_G_svLU.js +1 -0
- package/dist/questions-DjAwMxZ3.js +524 -0
- package/dist/questions-DjAwMxZ3.js.map +1 -0
- package/dist/{record-BRP9vZrj.js → record-BKmMvZ6O.js} +9 -1
- package/dist/{record-BRP9vZrj.js.map → record-BKmMvZ6O.js.map} +1 -1
- package/dist/remote-HDRt16ew.js +378 -0
- package/dist/remote-HDRt16ew.js.map +1 -0
- package/dist/{results-BcSHWbgB.js → results-DuauQI3Z.js} +4 -4
- package/dist/{results-BcSHWbgB.js.map → results-DuauQI3Z.js.map} +1 -1
- package/dist/{resume-DZ9AmfPv.js → resume-BRfa8E_s.js} +10 -10
- package/dist/{resume-DZ9AmfPv.js.map → resume-BRfa8E_s.js.map} +1 -1
- package/dist/{root-CvdRlRZX.js → root-DICDHCtp.js} +4 -4
- package/dist/{root-CvdRlRZX.js.map → root-DICDHCtp.js.map} +1 -1
- package/dist/{runtime-Dfb2r-cS.js → runtime-PX_oXiCS.js} +105 -11
- package/dist/runtime-PX_oXiCS.js.map +1 -0
- package/dist/sandbox-D3A8oUb7.js +70 -0
- package/dist/sandbox-D3A8oUb7.js.map +1 -0
- package/dist/sandbox.d.ts +22 -8
- package/dist/sandbox.js +1 -2
- package/dist/server-C7sg1ndo.js +1 -0
- package/dist/{server-Da60oe4b.js → server-XDleKaF_.js} +11 -11
- package/dist/server-XDleKaF_.js.map +1 -0
- package/dist/shared-DYpIb1BI.js +166 -0
- package/dist/shared-DYpIb1BI.js.map +1 -0
- package/dist/{source-BVOnL3sn.js → source-C8xumIWj.js} +2 -2
- package/dist/{source-BVOnL3sn.js.map → source-C8xumIWj.js.map} +1 -1
- package/dist/source-fGd0SaKP.js +1 -0
- package/dist/{src-TdJuDYqB.js → src-B7treWa8.js} +1597 -292
- package/dist/src-B7treWa8.js.map +1 -0
- package/dist/{src-C1E9HDWO.js → src-C7_1SqDP.js} +3 -3
- package/dist/{src-C1E9HDWO.js.map → src-C7_1SqDP.js.map} +1 -1
- package/dist/{src-DDNXsD9H.js → src-CF17Ug08.js} +5 -4
- package/dist/src-CF17Ug08.js.map +1 -0
- package/dist/{src-D_cXqmnI.js → src-CFp86GtV.js} +60 -60
- package/dist/src-CFp86GtV.js.map +1 -0
- package/dist/src-C_lPtowT.js +2 -0
- package/dist/src-DIL0jST0.js +368 -0
- package/dist/src-DIL0jST0.js.map +1 -0
- package/dist/src-DNnG4e4R.js +2 -0
- package/dist/src-DTbrY1hi.js +2 -0
- package/dist/src-DdyJEh2v.js +2 -0
- package/dist/src-Dj9PAroc.js +2 -0
- package/dist/src-Du8H85Qq.js +2 -0
- package/dist/src-DvCg0NBs.js +324 -0
- package/dist/src-DvCg0NBs.js.map +1 -0
- package/dist/{src-CxTC5kNI.js → src-DvK0EIZx.js} +8 -10
- package/dist/{src-CxTC5kNI.js.map → src-DvK0EIZx.js.map} +1 -1
- package/dist/{src-DVy58-zZ.js → src-FGXhkEkH.js} +5 -5
- package/dist/{src-DVy58-zZ.js.map → src-FGXhkEkH.js.map} +1 -1
- package/dist/src-jzagbuqH.js +2 -0
- package/dist/src-ofUaYbuY.js +3 -0
- package/dist/src-pJZOrYR9.js +1 -0
- package/dist/{src-D95O-Smv.js → src-u8_ixn_F.js} +15 -11
- package/dist/src-u8_ixn_F.js.map +1 -0
- package/dist/{state-B72h_jEw.js → state-C__9swoG.js} +36 -24
- package/dist/state-C__9swoG.js.map +1 -0
- package/dist/state-CeR6oz0k.js +66 -0
- package/dist/state-CeR6oz0k.js.map +1 -0
- package/dist/state-CgjQo29K.js +1 -0
- package/dist/studio-app/assets/index-CwQN3f_h.css +2 -0
- package/dist/studio-app/assets/{index-Bgr-efZm.js → index-DrQc2Yxd.js} +12 -12
- package/dist/studio-app/index.html +2 -2
- package/dist/{summary-k3mhWWXv.js → summary-BLAVLySU.js} +202 -17
- package/dist/summary-BLAVLySU.js.map +1 -0
- package/dist/{summary-CJdxaRdM.js → summary-DgyjUh_l.js} +7 -7
- package/dist/{summary-CJdxaRdM.js.map → summary-DgyjUh_l.js.map} +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/{tool-target-BU1JnSE9.js → tool-target-D2mcYlSQ.js} +14 -7
- package/dist/tool-target-D2mcYlSQ.js.map +1 -0
- package/dist/{tools-D0gF2QoV.js → tools-GCyNYSc1.js} +7 -7
- package/dist/tools-GCyNYSc1.js.map +1 -0
- package/dist/transcript-BNNANU8A.js +64 -0
- package/dist/transcript-BNNANU8A.js.map +1 -0
- package/dist/types-C5vI3g1C.js.map +1 -1
- package/dist/{types-_LOFdPUf.d.ts → types-g17bRAzt.d.ts} +578 -49
- package/dist/{usage-DDF_6tkV.js → usage-CleNKkly.js} +4 -2
- package/dist/{usage-DDF_6tkV.js.map → usage-CleNKkly.js.map} +1 -1
- package/dist/view/index.d.ts +83 -17
- package/dist/view/index.js +7 -6
- package/dist/views-BW6oCDUQ.js +2 -0
- package/dist/{views-DZ1YDqey.js → views-BmQF4UCI.js} +13 -8
- package/dist/views-BmQF4UCI.js.map +1 -0
- package/dist/{worker-entry-DXeyWY-m.js → worker-entry-yOchGYYv.js} +16 -225
- package/dist/worker-entry-yOchGYYv.js.map +1 -0
- package/dist/{workspace-DpXdrslo.js → workspace-CgyHEM0l.js} +7 -7
- package/dist/{workspace-DpXdrslo.js.map → workspace-CgyHEM0l.js.map} +1 -1
- package/dist/workspace-D8sSgpiy.js +1 -0
- package/docker/Dockerfile +7 -5
- package/package.json +12 -12
- package/dist/agent-Cfj1wD4D.js.map +0 -1
- package/dist/assemble-BbFVjHeS.js.map +0 -1
- package/dist/assemble-CXiTVyHD.js +0 -2
- package/dist/capture-BXhBfmsM.js.map +0 -1
- package/dist/capture-N7r3yeak.js +0 -2
- package/dist/cli-BMjTKY-D.js.map +0 -1
- package/dist/cli-spec-DSvKsOK4.js.map +0 -1
- package/dist/client-CwzU9CiW.js.map +0 -1
- package/dist/command-BK4kOgkH.js.map +0 -1
- package/dist/command-BqJtFiPy.js.map +0 -1
- package/dist/command-CTVGuH8y.js +0 -2
- package/dist/command-JPGNzFsA.js.map +0 -1
- package/dist/commands-B618jg3C.js.map +0 -1
- package/dist/commands-Duo2ur3r.js +0 -2
- package/dist/config-B-E0xRkf.js +0 -447
- package/dist/config-B-E0xRkf.js.map +0 -1
- package/dist/context-CvUv7DUb.js.map +0 -1
- package/dist/db-CnoUy-FZ.js +0 -3
- package/dist/db-DFbtGIYy.js.map +0 -1
- package/dist/debug-SEmW4w8f.js +0 -2
- package/dist/dist-CTzThfu_.js.map +0 -1
- package/dist/docker-2qns7hJ2.js.map +0 -1
- package/dist/docker-D6t23fZl.js +0 -2
- package/dist/docker-IFRW8K5f.js.map +0 -1
- package/dist/doctor-B6oYTPoR.js.map +0 -1
- package/dist/egress-IUKdeayc.js.map +0 -1
- package/dist/environment-DiCljXMD.js +0 -657
- package/dist/environment-DiCljXMD.js.map +0 -1
- package/dist/errors-C9w_ud7g.js.map +0 -1
- package/dist/errors-yDAEwmZo.js.map +0 -1
- package/dist/fake-instructions-CUI33I_F.js +0 -32
- package/dist/fake-instructions-CUI33I_F.js.map +0 -1
- package/dist/files-BM78_nnF.js +0 -2
- package/dist/files-BpYgF2z0.js +0 -2
- package/dist/files-YCCVaTQC.js +0 -2
- package/dist/harness-iqWeJQMM.js +0 -154
- package/dist/harness-iqWeJQMM.js.map +0 -1
- package/dist/images-CscddrJU.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/init-DBHyi92L.js +0 -2
- package/dist/init-NQfIvi-0.js.map +0 -1
- package/dist/jiti-BcJHCtrb.js +0 -46
- package/dist/jiti-BcJHCtrb.js.map +0 -1
- package/dist/live-S0qODb6o.js.map +0 -1
- package/dist/model-BbLGRtOs.js +0 -2
- package/dist/model-BndnmcLm.js.map +0 -1
- package/dist/model-Cl507trk.js.map +0 -1
- package/dist/model-DMOraCcm.js +0 -2
- package/dist/node-Co7EXge0.js.map +0 -1
- package/dist/opencode-providers-ByD85Q0l.js.map +0 -1
- package/dist/options-DPUlm4FI.js +0 -735
- package/dist/options-DPUlm4FI.js.map +0 -1
- package/dist/plan-PPc4-6xd.js.map +0 -1
- package/dist/preflight-CddycK92.js.map +0 -1
- package/dist/preflight-Zumy0adT.js +0 -2
- package/dist/prepare-CKdMoC7e.js +0 -3
- package/dist/prepare-CsP-057p.js.map +0 -1
- package/dist/provider-BDbNOQhQ.js +0 -2
- package/dist/providers-DkwiLKDS.js +0 -107
- package/dist/providers-DkwiLKDS.js.map +0 -1
- package/dist/providers-yXZZcPfx.js +0 -2
- package/dist/query-noGbekGF.js +0 -2
- package/dist/questions-BEPj98fZ.js +0 -234
- package/dist/questions-BEPj98fZ.js.map +0 -1
- package/dist/runtime-Dfb2r-cS.js.map +0 -1
- package/dist/sandbox--ZXuscAZ.js +0 -134
- package/dist/sandbox--ZXuscAZ.js.map +0 -1
- package/dist/server-Da60oe4b.js.map +0 -1
- package/dist/server-DiGR4JxK.js +0 -2
- package/dist/source-Dqu0jKwx.js +0 -2
- package/dist/src-BHNiP-5A.js +0 -4
- package/dist/src-BT4dB5CO.js +0 -2
- package/dist/src-BT5pwco0.js +0 -3
- package/dist/src-BrMbt1B2.js +0 -3
- package/dist/src-D13vpMh0.js +0 -3
- package/dist/src-D95O-Smv.js.map +0 -1
- package/dist/src-DDNXsD9H.js.map +0 -1
- package/dist/src-D_cXqmnI.js.map +0 -1
- package/dist/src-DdRlaiRc.js +0 -3
- package/dist/src-TdJuDYqB.js.map +0 -1
- package/dist/src-xczuQbE6.js +0 -3
- package/dist/state-B72h_jEw.js.map +0 -1
- package/dist/state-Bf_Vq_pV.js +0 -2
- package/dist/studio-app/assets/index-CUx6GoL5.css +0 -2
- package/dist/summary-k3mhWWXv.js.map +0 -1
- package/dist/tool-target-BU1JnSE9.js.map +0 -1
- package/dist/tools-D0gF2QoV.js.map +0 -1
- package/dist/views-DZ1YDqey.js.map +0 -1
- package/dist/views-Ds0GXWtX.js +0 -3
- package/dist/worker-entry-DXeyWY-m.js.map +0 -1
- package/dist/workspace-DzNL-3iO.js +0 -2
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import { t as isObjectLike } from "./object-CLf2-tNq.js";
|
|
2
|
+
import { n as UsageError } from "./errors-B6BSMs03.js";
|
|
3
|
+
import { t as parseDuration } from "./duration-CEOQww97.js";
|
|
4
|
+
import { d as refusal, f as valueAt, i as durationSchema, l as nonEmpty, o as genericProblem, r as durationProblem, s as got, t as closest, u as oneOf } from "./shared-DYpIb1BI.js";
|
|
5
|
+
import { n as SECRET_NAME } from "./redact-DJ9JCShu.js";
|
|
6
|
+
import { i as PACKAGE_DIR, n as COMPILED } from "./root-DICDHCtp.js";
|
|
7
|
+
import picomatch from "picomatch";
|
|
8
|
+
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
9
|
+
import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { isIP } from "node:net";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
//#region src/contract/docker.ts
|
|
14
|
+
/** Docker's memory size: bytes, or a number with `b`, `k`, `m` or `g`. */
|
|
15
|
+
const MEMORY = /^\d+(\.\d+)?[bkmg]?$/i;
|
|
16
|
+
const installStepSchema = z.union([nonEmpty(), z.strictObject({
|
|
17
|
+
run: nonEmpty().describe("The shell command."),
|
|
18
|
+
timeout: durationSchema.optional()
|
|
19
|
+
})]).describe("A `docker({ install })` command, baked into a cached image layer: no secrets, root, cwd `/`.");
|
|
20
|
+
/** Every field but `kind` and `dockerfile`, shared by the descriptor and `docker()`'s options. */
|
|
21
|
+
const fields = {
|
|
22
|
+
id: nonEmpty().optional().describe("Cell id; defaults to `docker` (`podman`), or `docker:<image>` for other images, plus an install hash."),
|
|
23
|
+
image: nonEmpty().optional().describe("An image that already has the harness baked in, used as is."),
|
|
24
|
+
from: nonEmpty().optional().describe("A Debian or Ubuntu based toolchain image; reeval bakes Node and the harness on top, cached."),
|
|
25
|
+
install: z.array(installStepSchema).min(1).optional().describe("Baked onto the sandbox image in one build container: in order, no secrets, no workspace."),
|
|
26
|
+
maxConcurrent: z.number().optional().describe("The most Docker sandboxes at once, across the run, whatever their image."),
|
|
27
|
+
root: z.boolean().optional().describe("Run the agent, setup steps and checks as root, with the capabilities `apt-get` needs, instead of the unprivileged `reeval` user. For evals that install system packages at run time."),
|
|
28
|
+
capabilities: z.array(z.string().regex(/^(CAP_)?[A-Z_]+$/)).optional().describe("Linux capabilities to add back, e.g. `['NET_RAW']`; every other one is dropped."),
|
|
29
|
+
memory: z.union([z.string().regex(MEMORY), z.int().positive()]).optional().describe("The container's memory: `'4g'` (default), `'512m'`, or bytes."),
|
|
30
|
+
swap: z.union([z.string().regex(MEMORY), z.int().nonnegative()]).optional().describe("Swap on top of `memory`, in the same units. Default: as much as `memory`; `0` makes `memory` the hard ceiling. A host without swap has none to give."),
|
|
31
|
+
cpus: z.number().positive().optional().describe("CPUs the container may use, fractions allowed. Default 2, capped at the Docker host's."),
|
|
32
|
+
pids: z.int().positive().optional().describe("Processes and threads at once. Default 1024.")
|
|
33
|
+
};
|
|
34
|
+
const BASES = [
|
|
35
|
+
"image",
|
|
36
|
+
"from",
|
|
37
|
+
"dockerfile"
|
|
38
|
+
];
|
|
39
|
+
function oneRecipe(value, ctx) {
|
|
40
|
+
const bases = BASES.filter((k) => value[k] !== void 0);
|
|
41
|
+
if (bases.length > 1) ctx.addIssue({
|
|
42
|
+
code: "custom",
|
|
43
|
+
message: "one base",
|
|
44
|
+
params: { bases }
|
|
45
|
+
});
|
|
46
|
+
else if (value.dockerfile !== void 0 && value.install !== void 0) ctx.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: "dockerfile or install"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const dockerSandboxSchema = z.strictObject({
|
|
52
|
+
kind: z.literal("docker"),
|
|
53
|
+
engine: z.enum(["docker", "podman"]).optional().describe("The container CLI the sandbox runs on: `podman` for `podman()`, Docker when unset."),
|
|
54
|
+
...fields,
|
|
55
|
+
dockerfile: nonEmpty().optional().describe("A Dockerfile, absolute or relative to the directory of the file it's written in, built from its directory with `REEVAL_BASE` set to reeval's sandbox image and cached by content hash.")
|
|
56
|
+
}).superRefine(oneRecipe).describe("A local Docker container, made by `docker()`, or a Podman one, made by `podman()`.");
|
|
57
|
+
const fakeSandboxSchema = z.strictObject({
|
|
58
|
+
kind: z.literal("fake"),
|
|
59
|
+
id: nonEmpty().optional().describe("Cell id; defaults to `fake`."),
|
|
60
|
+
dir: nonEmpty().optional().describe("Parent directory for the per-eval workspace (defaults to the OS temp dir)."),
|
|
61
|
+
maxConcurrent: z.number().optional().describe("The most attempts on this sandbox kind at once, across the run.")
|
|
62
|
+
}).describe("A temp directory on the host instead of a container, made by `fakeSandbox()`.");
|
|
63
|
+
/** `docker()`'s argument: the descriptor's fields, with `dockerfile` relative or a `file:` URL. */
|
|
64
|
+
const dockerOptionsSchema = z.strictObject({
|
|
65
|
+
...fields,
|
|
66
|
+
dockerfile: z.union([nonEmpty(), z.instanceof(URL)]).optional().describe("Relative to the file that calls `docker()`, or an absolute path or `URL`.")
|
|
67
|
+
}).superRefine(oneRecipe);
|
|
68
|
+
const DOCKER_HINTS = {
|
|
69
|
+
steps: "install",
|
|
70
|
+
run: "install",
|
|
71
|
+
setup: "install",
|
|
72
|
+
image_steps: "install",
|
|
73
|
+
file: "dockerfile",
|
|
74
|
+
build: "dockerfile",
|
|
75
|
+
base: "from",
|
|
76
|
+
user: "root",
|
|
77
|
+
privileged: "root",
|
|
78
|
+
capAdd: "capabilities",
|
|
79
|
+
caps: "capabilities",
|
|
80
|
+
mem: "memory",
|
|
81
|
+
memoryLimit: "memory",
|
|
82
|
+
memorySwap: "swap",
|
|
83
|
+
memswap: "swap",
|
|
84
|
+
cpu: "cpus",
|
|
85
|
+
pidsLimit: "pids"
|
|
86
|
+
};
|
|
87
|
+
/** The sentence for a field whose value the schema refused, given the value. */
|
|
88
|
+
const FIELD = {
|
|
89
|
+
id: (v) => `docker({ id }) must be a non-empty string (got ${got(v)})`,
|
|
90
|
+
image: (v) => `docker({ image }) must be a non-empty string (got ${got(v)})`,
|
|
91
|
+
from: (v) => `docker({ from }) must be a non-empty string (got ${got(v)})`,
|
|
92
|
+
root: (v) => `docker({ root }) is true or false (got ${got(v)})`,
|
|
93
|
+
capabilities: (v) => `docker({ capabilities }) takes Linux capability names, e.g. capabilities: ['NET_RAW'] (got ${got(v)})`,
|
|
94
|
+
memory: (v) => `docker({ memory }) takes a size such as '4g' or '512m', or bytes (got ${got(v)})`,
|
|
95
|
+
swap: (v) => `docker({ swap }) takes a size such as '2g' or '512m', bytes, or 0 for none (got ${got(v)})`,
|
|
96
|
+
cpus: (v) => `docker({ cpus }) takes a number of CPUs above zero, e.g. 1.5 (got ${typeof v === "number" ? String(v) : got(v)})`,
|
|
97
|
+
pids: (v) => `docker({ pids }) takes a whole number of processes above zero (got ${got(v)})`,
|
|
98
|
+
maxConcurrent: (v) => `docker({ maxConcurrent }) takes a number (got ${got(v)})`,
|
|
99
|
+
kind: (v) => `a Docker sandbox's kind is "docker" (got ${got(v)})`,
|
|
100
|
+
engine: (v) => `a Docker sandbox's engine is "docker" or "podman" (got ${got(v)})`,
|
|
101
|
+
dockerfile: (v) => `docker({ dockerfile }) takes a path, e.g. dockerfile: './Dockerfile' (got ${got(v)})`,
|
|
102
|
+
install: (v) => `docker({ install }) takes a non-empty list of shell commands, e.g. install: ['apt-get update && apt-get install -y jq'] (got ${got(v)})`
|
|
103
|
+
};
|
|
104
|
+
/** Which refusal to report when there are several: the order `docker()` has always checked in. */
|
|
105
|
+
const ORDER = [
|
|
106
|
+
"kind",
|
|
107
|
+
"engine",
|
|
108
|
+
"id",
|
|
109
|
+
"image",
|
|
110
|
+
"from",
|
|
111
|
+
"root",
|
|
112
|
+
"capabilities",
|
|
113
|
+
"memory",
|
|
114
|
+
"swap",
|
|
115
|
+
"cpus",
|
|
116
|
+
"pids"
|
|
117
|
+
];
|
|
118
|
+
function installMessage(step) {
|
|
119
|
+
const { run, timeout } = typeof step === "object" && step !== null ? step : {};
|
|
120
|
+
const keys = typeof step === "object" && step !== null ? Object.keys(step) : [];
|
|
121
|
+
if (typeof run === "string" && run.trim()) {
|
|
122
|
+
const extra = keys.find((k) => k !== "run" && k !== "timeout");
|
|
123
|
+
if (extra) return `docker({ install }) command "${run}" has an unknown key "${extra}"; a command is a string or { run, timeout? }.${closest(extra, ["timeout"]) ? " Did you mean \"timeout\"?" : ""}`;
|
|
124
|
+
const problem = timeout === void 0 ? void 0 : durationProblem(timeout);
|
|
125
|
+
if (problem) return `docker({ install }) command "${run}": ${problem}`;
|
|
126
|
+
}
|
|
127
|
+
return `docker({ install }) commands are strings or { run, timeout? } objects (got ${got(step)})`;
|
|
128
|
+
}
|
|
129
|
+
/** reeval's sentence for the first thing Zod refused, in the words `docker()` has always used. */
|
|
130
|
+
function explainDocker(issue, value) {
|
|
131
|
+
const options = value;
|
|
132
|
+
if (issue.code === "unrecognized_keys" && issue.path.length === 0) {
|
|
133
|
+
const key = issue.keys[0];
|
|
134
|
+
if (key === "environment" || key === "env") return `Unknown docker() option "${key}". The sandbox is what's baked into the image; variables and secrets belong in defineEnvironment({ env }).`;
|
|
135
|
+
return `Unknown docker() option "${key}".${DOCKER_HINTS[key] ? ` Did you mean "${DOCKER_HINTS[key]}"?` : ""}`;
|
|
136
|
+
}
|
|
137
|
+
if (issue.code === "custom") {
|
|
138
|
+
const { bases } = issue.params ?? {};
|
|
139
|
+
return bases ? `docker() takes one base image, but got ${bases.join(" and ")}. Use image (has the harness), from (a toolchain image reeval bakes the harness onto) or dockerfile.` : "docker() takes dockerfile or install, not both: put the install commands in the Dockerfile as RUN lines, so there's one recipe for the image.";
|
|
140
|
+
}
|
|
141
|
+
const key = String(issue.path[0]);
|
|
142
|
+
if (key === "install" && issue.path.length > 1) return installMessage(options.install[issue.path[1]]);
|
|
143
|
+
return (FIELD[key] ?? ((v) => `docker({ ${key} }) is invalid (got ${got(v)})`))(options[key]);
|
|
144
|
+
}
|
|
145
|
+
function explainFake(issue, value) {
|
|
146
|
+
if (issue.code === "unrecognized_keys" && issue.path.length === 0) {
|
|
147
|
+
const key = issue.keys[0];
|
|
148
|
+
const near = closest(key, Object.keys(fakeSandboxSchema.shape));
|
|
149
|
+
return `Unknown fakeSandbox() option "${key}".${near ? ` Did you mean "${near}"?` : ""}`;
|
|
150
|
+
}
|
|
151
|
+
const key = String(issue.path[0]);
|
|
152
|
+
const v = value[key];
|
|
153
|
+
return key === "maxConcurrent" ? `fakeSandbox({ maxConcurrent }) takes a number (got ${got(v)})` : `fakeSandbox({ ${key} }) must be a non-empty string (got ${got(v)})`;
|
|
154
|
+
}
|
|
155
|
+
/** A refusal in the words of the function that makes the sandbox: `podman({ memory })`. */
|
|
156
|
+
const named = (problem, engine) => problem && engine === "podman" ? problem.replace(/\bdocker\(/g, "podman(").replace(/\bA Docker sandbox/g, "A Podman sandbox") : problem;
|
|
157
|
+
/** `docker()`'s (or `podman()`'s) options, checked; throws reeval's sentence for the first problem. */
|
|
158
|
+
function checkDockerOptions(options, engine) {
|
|
159
|
+
const problem = named(refusal(dockerOptionsSchema, options, explainDocker, { order: ORDER }), engine);
|
|
160
|
+
if (problem) throw new UsageError(problem);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* What's wrong with a sandbox descriptor of a known kind from any source (a declaration tree, a
|
|
164
|
+
* config), or undefined. `maxConcurrent` is left to the check that names where it was set.
|
|
165
|
+
*/
|
|
166
|
+
function sandboxProblem(value) {
|
|
167
|
+
return value.kind === "docker" ? named(refusal(dockerSandboxSchema, value, explainDocker, {
|
|
168
|
+
order: ORDER,
|
|
169
|
+
skip: ["maxConcurrent"]
|
|
170
|
+
}), value.engine) : refusal(fakeSandboxSchema, value, explainFake, { skip: ["maxConcurrent"] });
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/contract/plain.ts
|
|
174
|
+
/** A host environment variable's name. */
|
|
175
|
+
const VAR = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
176
|
+
/** A host name, or `*.` and a domain for its subdomains. */
|
|
177
|
+
const HOST = /^(?:\*\.)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/i;
|
|
178
|
+
/** Why `host` isn't a host name or `*.` pattern, or undefined when it is one. */
|
|
179
|
+
function badHost(host) {
|
|
180
|
+
if (typeof host === "string" && HOST.test(host)) {
|
|
181
|
+
const name = host.replace(/^\*\./, "");
|
|
182
|
+
if (/(?:^|\.)\d+$/.test(name) && (name !== host || isIP(name) !== 4)) return `'${host}' isn't a host name or an IPv4 address, and a *. pattern never matches an address.`;
|
|
183
|
+
if (name !== host && !name.includes(".")) return `'${host}' would allow every host under .${name}; name the domain, e.g. '*.example.${name}'.`;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (typeof host === "string" && /^[a-z]+:\/\//i.test(host)) return `hosts are host names, not URLs: use '${new URL(host).hostname}' (got '${host}')`;
|
|
187
|
+
if (typeof host === "string" && /^[^:/]+:\d+$/.test(host)) return `hosts are host names without a port: use '${host.split(":")[0]}' (got '${host}')`;
|
|
188
|
+
return `${JSON.stringify(host)} isn't a host. Use a name like 'api.example.com', or '*.example.com' for its subdomains.`;
|
|
189
|
+
}
|
|
190
|
+
const host = z.string().superRefine((value, ctx) => {
|
|
191
|
+
const bad = badHost(value);
|
|
192
|
+
if (bad) ctx.addIssue({
|
|
193
|
+
code: "custom",
|
|
194
|
+
message: bad
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
const gatePolicySchema = z.union([z.literal("all"), z.strictObject({ passRate: z.number().gt(0).max(1) })]).describe("How a cell's attempts decide its outcome: `'all'` (the default) passes only when every attempt passes (pass^k); `{ passRate }` passes when its pass@1 (passes over passes and fails) is at least that share.");
|
|
198
|
+
const egressPolicySchema = z.union([
|
|
199
|
+
z.literal("allow-all"),
|
|
200
|
+
z.literal("deny-all"),
|
|
201
|
+
z.strictObject({ allow: z.array(host).readonly() })
|
|
202
|
+
]).describe("Where the sandbox may connect: anywhere (`'allow-all'`, the default), nowhere (`'deny-all'`), or only the listed hosts (`'*.example.com'` matches subdomains). Under a restricted policy the harness's own hosts, brokered secrets' `hosts` and MCP servers' hosts are allowed too; any other request gets a refusal, and the attempt goes on.");
|
|
203
|
+
const substituteInSchema = z.enum([
|
|
204
|
+
"header",
|
|
205
|
+
"query",
|
|
206
|
+
"body"
|
|
207
|
+
]).describe("`header` values, the `query` string, or the request `body`.");
|
|
208
|
+
const secretSchema = z.strictObject({
|
|
209
|
+
kind: z.literal("secret"),
|
|
210
|
+
from: z.string().regex(VAR, "needs a host variable name").optional().describe("The host variable; defaults to the key it's set under."),
|
|
211
|
+
optional: z.boolean().optional().describe("When the host variable is unset, skip it (the record says so) instead of failing."),
|
|
212
|
+
to: z.enum(["agent", "checks"]).optional().describe("`agent` (default): the container's environment. `checks`: only `c.exec` and `setup` steps."),
|
|
213
|
+
hosts: z.array(host).min(1).optional().describe("Brokers the secret: the sandbox gets a per-attempt placeholder, and the Docker egress gateway swaps in the real value only on HTTPS requests to these hosts (`*.example.com` for subdomains)."),
|
|
214
|
+
in: z.array(substituteInSchema).min(1).optional().describe("Where in a request to those hosts the gateway substitutes; defaults to `['header']`.")
|
|
215
|
+
}).superRefine((secret, ctx) => {
|
|
216
|
+
if (secret.in && !secret.hosts) ctx.addIssue({
|
|
217
|
+
code: "custom",
|
|
218
|
+
path: ["in"],
|
|
219
|
+
message: "in only applies to a brokered secret; add hosts"
|
|
220
|
+
});
|
|
221
|
+
}).describe("A host environment variable, redacted everywhere reeval writes and recorded by name only. Made by the `secret` helper of `defineEnvironment` and `defineConfig` callbacks.");
|
|
222
|
+
const secretTemplateSchema = z.strictObject({
|
|
223
|
+
kind: z.literal("secret-template"),
|
|
224
|
+
template: z.string().describe("The string, with `${NAME}` for each host variable to substitute.")
|
|
225
|
+
}).describe("`secret.template('Bearer ${TOKEN}')`: host variables substituted into a string, for MCP headers.");
|
|
226
|
+
const envValueSchema = z.union([z.string(), secretSchema]).describe("A variable's value: a literal string, or a host secret.");
|
|
227
|
+
const envVarsSchema = z.record(z.string(), envValueSchema).describe("Environment variables by name.");
|
|
228
|
+
const hostFileSchema = z.strictObject({
|
|
229
|
+
kind: z.literal("file"),
|
|
230
|
+
path: nonEmpty().describe("Its path on the host, absolute or relative to the directory of the file it's written in.")
|
|
231
|
+
}).describe("A file on the host, read when an attempt starts. Made by the `file` helper.");
|
|
232
|
+
const skillPlacementSchema = z.enum(["harness", "workspace"]).describe("`harness` (default): the agent's own skills, via its adapter. `workspace`: its repo skill dir.");
|
|
233
|
+
const ONLY = "The skill names to keep; default every one found.";
|
|
234
|
+
const EXCLUDE = "Paths in each skill directory to leave out, gitignore-style.";
|
|
235
|
+
const skill = (source, fields) => z.strictObject({
|
|
236
|
+
kind: z.literal("skill"),
|
|
237
|
+
placement: skillPlacementSchema,
|
|
238
|
+
source: z.literal(source),
|
|
239
|
+
...fields
|
|
240
|
+
});
|
|
241
|
+
const skillSourceSchema = z.discriminatedUnion("source", [
|
|
242
|
+
skill("local", {
|
|
243
|
+
path: nonEmpty().describe("A host directory with a `SKILL.md`, or a directory of them."),
|
|
244
|
+
only: z.array(z.string()).optional().describe(ONLY),
|
|
245
|
+
exclude: z.array(z.string()).optional().describe(EXCLUDE)
|
|
246
|
+
}),
|
|
247
|
+
skill("git", {
|
|
248
|
+
repo: nonEmpty().describe("As written: a URL, `github:owner/repo`, or a GitHub `/tree/<ref>/<path>` URL."),
|
|
249
|
+
ref: z.string().optional().describe("A branch, tag or commit SHA; unset is the remote's `HEAD`."),
|
|
250
|
+
path: z.string().optional().describe("The directory in the repository to read skills from."),
|
|
251
|
+
only: z.array(z.string()).optional().describe(ONLY),
|
|
252
|
+
exclude: z.array(z.string()).optional().describe(EXCLUDE)
|
|
253
|
+
}),
|
|
254
|
+
skill("registry", {
|
|
255
|
+
id: z.string().regex(/^[\w.-]+\/[\w.-]+@[a-z0-9-]+$/).describe("A skills.sh id, `owner/repo@skill`."),
|
|
256
|
+
version: z.string().optional().describe("A git ref of the skill's repository.")
|
|
257
|
+
}),
|
|
258
|
+
skill("npm", {
|
|
259
|
+
spec: nonEmpty().describe("A pinned npm package spec."),
|
|
260
|
+
path: z.string().describe("The directory in the package to read skills from; default `skills`."),
|
|
261
|
+
only: z.array(z.string()).optional().describe(ONLY),
|
|
262
|
+
exclude: z.array(z.string()).optional().describe(EXCLUDE)
|
|
263
|
+
}),
|
|
264
|
+
skill("inline", {
|
|
265
|
+
name: z.string().describe("The skill's name, as in `SKILL.md`'s frontmatter."),
|
|
266
|
+
description: z.string().describe("The skill's description, as in `SKILL.md`'s frontmatter."),
|
|
267
|
+
content: z.string().describe("The body of `SKILL.md`."),
|
|
268
|
+
files: z.record(z.string(), z.string()).describe("Other files in the skill directory, by relative path.")
|
|
269
|
+
})
|
|
270
|
+
]).describe("Skills to place for the agent, made by the `skill` helper.");
|
|
271
|
+
const mcpServerSchema = z.discriminatedUnion("transport", [z.strictObject({
|
|
272
|
+
kind: z.literal("mcp"),
|
|
273
|
+
transport: z.literal("stdio"),
|
|
274
|
+
command: nonEmpty().describe("The command that starts the server in the sandbox."),
|
|
275
|
+
args: z.array(z.string()),
|
|
276
|
+
env: envVarsSchema.describe("Variables the server process gets.")
|
|
277
|
+
}), z.strictObject({
|
|
278
|
+
kind: z.literal("mcp"),
|
|
279
|
+
transport: z.enum(["http", "sse"]).describe("Streamable HTTP, or the older SSE transport."),
|
|
280
|
+
url: z.url("must be a URL"),
|
|
281
|
+
headers: z.record(z.string(), z.union([
|
|
282
|
+
z.string(),
|
|
283
|
+
secretSchema,
|
|
284
|
+
secretTemplateSchema
|
|
285
|
+
])).describe("Request headers, with secrets brokered by the egress gateway.")
|
|
286
|
+
})]).describe("An MCP server the agent gets, made by the `mcp` helper: a command in the sandbox, or a URL.");
|
|
287
|
+
const gitWorkspaceSchema = z.strictObject({
|
|
288
|
+
kind: z.literal("git"),
|
|
289
|
+
repo: nonEmpty().describe("As written: a URL, `github:owner/repo`, or a local path."),
|
|
290
|
+
url: nonEmpty().describe("What reeval fetches: the clone URL, or an absolute path for a local repository."),
|
|
291
|
+
ref: z.string().optional().describe("A branch, tag or commit SHA; unset is the remote's `HEAD`."),
|
|
292
|
+
path: z.string().optional().describe("A directory in the repository whose contents become the workspace."),
|
|
293
|
+
init: z.boolean().optional().describe("Give the workspace a `.git`: one commit of the fetched tree, with no remote."),
|
|
294
|
+
token: z.string().superRefine((token, ctx) => {
|
|
295
|
+
if (!VAR.test(token) || /^(?:gh[opsur]_|github_pat_)/.test(token)) ctx.addIssue({
|
|
296
|
+
code: "custom",
|
|
297
|
+
message: "token names the host variable that holds the token, e.g. token: 'GITHUB_TOKEN'. Don't put the token itself in the eval."
|
|
298
|
+
});
|
|
299
|
+
}).optional().describe("The host variable holding a token for the fetch; read on the host only.")
|
|
300
|
+
}).describe("A git repository (or one directory of it) the workspace starts as, from the `git` helper. `url` is what reeval fetches: the clone URL, or an absolute path for a local repository.");
|
|
301
|
+
/** Every key any object in `schema` takes, for "Did you mean". */
|
|
302
|
+
function keysOf(schema) {
|
|
303
|
+
const def = schema.def;
|
|
304
|
+
if (def.shape) return Object.keys(def.shape);
|
|
305
|
+
if (def.options) return [...new Set(def.options.flatMap(keysOf))];
|
|
306
|
+
return def.innerType ? keysOf(def.innerType) : [];
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* What's wrong with a descriptor that arrived as data, in terms of the helper that makes it
|
|
310
|
+
* (`call`, e.g. `secret` or `skill.git`), or undefined when it's sound.
|
|
311
|
+
*/
|
|
312
|
+
function descriptorProblem(schema, value, call) {
|
|
313
|
+
return refusal(schema, value, (issue, v) => {
|
|
314
|
+
if (issue.code === "unrecognized_keys") {
|
|
315
|
+
const key = issue.keys[0];
|
|
316
|
+
const near = closest(key, keysOf(schema));
|
|
317
|
+
return `${call}() has no "${key}" field${issue.path.length ? ` in ${String(issue.path.join("."))}` : ""}.${near ? ` Did you mean "${near}"?` : ""}`;
|
|
318
|
+
}
|
|
319
|
+
if (issue.code === "custom") return `${call}(): ${issue.message}`;
|
|
320
|
+
if (issue.code === "invalid_union" && issue.path.length === 1) {
|
|
321
|
+
const key = String(issue.path[0]);
|
|
322
|
+
const values = schema.def.options?.flatMap((o) => [...o.shape[key]?._zod.values ?? []]);
|
|
323
|
+
return `${call.split(".")[0]}() ${key} is ${oneOf(values ?? [])} (got ${got(valueAt(v, issue.path))})`;
|
|
324
|
+
}
|
|
325
|
+
return genericProblem(call, issue, v);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const GATE_SHAPE = "is 'all' (every attempt passes) or { passRate } with a share above 0 and at most 1, e.g. gate: { passRate: 0.8 }";
|
|
329
|
+
/** reeval's sentence for a gate that isn't one, or undefined. */
|
|
330
|
+
const gateProblem = (value, where) => refusal(gatePolicySchema, value, () => `gate in ${where} ${GATE_SHAPE} (got ${got(value)})`);
|
|
331
|
+
const EGRESS_SHAPE = `is 'allow-all' (the default), 'deny-all', or { allow: ['api.example.com', '*.example.org'] }`;
|
|
332
|
+
/** reeval's sentence for an egress policy that isn't one, or undefined. */
|
|
333
|
+
function egressProblem(value, where) {
|
|
334
|
+
return refusal(egressPolicySchema, value, () => {
|
|
335
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
336
|
+
const list = Array.isArray(value) ? ` For a list of hosts, use { allow: ${got(value)} }.` : "";
|
|
337
|
+
return `egress in ${where} ${EGRESS_SHAPE} (got ${got(value)}).${list}`;
|
|
338
|
+
}
|
|
339
|
+
const key = Object.keys(value).find((k) => k !== "allow");
|
|
340
|
+
if (key !== void 0) return `Unknown egress key "${key}" in ${where}.${key === "deny" ? " A policy lists what's allowed; everything else is refused." : key === "hosts" || key === "allowedHosts" ? " Did you mean \"allow\"?" : " A policy is { allow: ['api.example.com'] }."}`;
|
|
341
|
+
const allow = value.allow;
|
|
342
|
+
if (!Array.isArray(allow)) return `egress in ${where} ${EGRESS_SHAPE} (got ${got(value)})`;
|
|
343
|
+
return `egress.allow in ${where}: ${allow.map(badHost).find(Boolean)}`;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/contract/environment.ts
|
|
348
|
+
/** An environment's or layer's name, which names its cells. */
|
|
349
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._@+-]*$/;
|
|
350
|
+
const sandboxDescriptorSchema = z.union([fakeSandboxSchema, dockerSandboxSchema]);
|
|
351
|
+
const setupStepSchema = z.union([nonEmpty(), z.strictObject({
|
|
352
|
+
run: nonEmpty().describe("The shell command."),
|
|
353
|
+
timeout: durationSchema.optional().describe("Default 5m."),
|
|
354
|
+
cwd: nonEmpty().optional().describe("Relative to the workspace, or absolute; default the workspace.")
|
|
355
|
+
})]).describe("Runs in every attempt before the agent starts, with secrets, outside the eval timeout: a shell command, or `{ run, timeout?, cwd? }`.");
|
|
356
|
+
const environmentSpecSchema = z.strictObject({
|
|
357
|
+
sandbox: sandboxDescriptorSchema.optional().describe("Where the attempt runs: `docker()` or `fakeSandbox()`. A child's `extend` / `extendEnvironment` replaces the parent's entirely when it sets one; install is not merged onto the parent image."),
|
|
358
|
+
workspace: gitWorkspaceSchema.optional().describe("The code the workspace starts as, before `files`, `rules` and `setup`; a child's replaces its parent's."),
|
|
359
|
+
env: envVarsSchema.optional().describe("Variables and secrets for the sandbox, merged by name over the parent's."),
|
|
360
|
+
egress: egressPolicySchema.optional().describe("Replaces the inherited policy; an `extend` child's replaces its parent's."),
|
|
361
|
+
files: z.record(z.string(), z.union([z.string(), hostFileSchema])).optional().describe("Workspace-relative, or `~/` for home."),
|
|
362
|
+
rules: z.union([z.string(), hostFileSchema]).optional().describe("Project rules, written where the harness's agent reads them: `AGENTS.md`, or `CLAUDE.md` for Claude Code."),
|
|
363
|
+
setup: z.array(setupStepSchema).optional().describe("Shell commands run in every attempt before the agent starts; a child's run after its parent's."),
|
|
364
|
+
skills: z.array(skillSourceSchema).optional().describe("Skills placed for the agent."),
|
|
365
|
+
mcp: z.record(z.string(), mcpServerSchema).optional().describe("MCP servers the agent gets, by name.")
|
|
366
|
+
}).describe("What an eval runs in: the declarative, phased spec of one environment.");
|
|
367
|
+
const environmentLayerSchema = z.strictObject({
|
|
368
|
+
name: z.string().regex(NAME).describe("The name passed to `defineEnvironment` or `extend`."),
|
|
369
|
+
spec: environmentSpecSchema.describe("The spec as that layer wrote it, before merging.")
|
|
370
|
+
}).describe("One environment in an `extend` chain.");
|
|
371
|
+
const environmentDataSchema = z.strictObject({
|
|
372
|
+
kind: z.literal("environment"),
|
|
373
|
+
name: z.string().regex(NAME).describe("The name it was defined or extended under, which names its cells."),
|
|
374
|
+
layers: z.array(environmentLayerSchema).readonly().describe("Its `extend` chain, root first.")
|
|
375
|
+
}).describe("An `Environment` as data, as JSON carries it: without `extend` / `extendEnvironment`.");
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/environment.ts
|
|
378
|
+
var DefinitionError = class extends Error {
|
|
379
|
+
name = "DefinitionError";
|
|
380
|
+
};
|
|
381
|
+
const SPEC_KEYS = [
|
|
382
|
+
"sandbox",
|
|
383
|
+
"workspace",
|
|
384
|
+
"env",
|
|
385
|
+
"egress",
|
|
386
|
+
"files",
|
|
387
|
+
"rules",
|
|
388
|
+
"setup",
|
|
389
|
+
"skills",
|
|
390
|
+
"mcp"
|
|
391
|
+
];
|
|
392
|
+
/** Image fields that belong on `sandbox: docker({ … })`, not as sibling keys. */
|
|
393
|
+
const SANDBOX_KEYS = [
|
|
394
|
+
"image",
|
|
395
|
+
"install",
|
|
396
|
+
"dockerfile",
|
|
397
|
+
"from"
|
|
398
|
+
];
|
|
399
|
+
const SPEC_HINTS = {
|
|
400
|
+
exec: "setup",
|
|
401
|
+
steps: "setup",
|
|
402
|
+
run: "setup",
|
|
403
|
+
vars: "env",
|
|
404
|
+
secrets: "env",
|
|
405
|
+
network: "egress",
|
|
406
|
+
networkPolicy: "egress",
|
|
407
|
+
skill: "skills",
|
|
408
|
+
file: "files",
|
|
409
|
+
agents: "rules",
|
|
410
|
+
use: "extend",
|
|
411
|
+
repo: "workspace",
|
|
412
|
+
git: "workspace",
|
|
413
|
+
template: "workspace"
|
|
414
|
+
};
|
|
415
|
+
const isSecret = (value) => isObjectLike(value) && value.kind === "secret";
|
|
416
|
+
const isSecretTemplate = (value) => isObjectLike(value) && value.kind === "secret-template";
|
|
417
|
+
const isHostFile = (value) => isObjectLike(value) && value.kind === "file";
|
|
418
|
+
const isEnvironment = (value) => isObjectLike(value) && value.kind === "environment" && Array.isArray(value.layers);
|
|
419
|
+
const OWN_DIRS = COMPILED ? [join(PACKAGE_DIR, sep)] : ["src", "dist"].map((dir) => join(PACKAGE_DIR, dir, sep));
|
|
420
|
+
/** The directory of the file that called into reeval, from the stack; relative paths resolve here. */
|
|
421
|
+
function callerDir() {
|
|
422
|
+
const limit = Error.stackTraceLimit;
|
|
423
|
+
Error.stackTraceLimit = 30;
|
|
424
|
+
const stack = (/* @__PURE__ */ new Error()).stack ?? "";
|
|
425
|
+
Error.stackTraceLimit = limit;
|
|
426
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
427
|
+
const match = /(?:\(|at )((?:file:\/\/)?\/[^()]*?|[A-Za-z]:[\\/][^()]*?):\d+:\d+\)?\s*$/.exec(line);
|
|
428
|
+
if (!match?.[1]) continue;
|
|
429
|
+
const file = normalize(match[1].startsWith("file://") ? fileURLToPath(match[1]) : match[1]);
|
|
430
|
+
if (OWN_DIRS.some((d) => file.startsWith(d)) || file.includes(`${sep}node_modules${sep}`)) continue;
|
|
431
|
+
return dirname(file);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** A path as written, or a `file:` URL, made absolute against `base`. */
|
|
435
|
+
const toPath = (value, base) => {
|
|
436
|
+
const path = value instanceof URL ? fileURLToPath(value) : value;
|
|
437
|
+
return isAbsolute(path) ? path : resolve(base, path);
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* A tarball or folder spec (`./x.tgz`, `../pkg`, `/abs`, `file:…`) as an absolute path, resolved
|
|
441
|
+
* like `file()`; npm resolves relative ones against its working directory, reeval's cache.
|
|
442
|
+
*/
|
|
443
|
+
const localNpmSpec = (spec, base) => {
|
|
444
|
+
const path = spec.startsWith("file:") ? spec.slice(5) : spec;
|
|
445
|
+
return /^\.{1,2}(?:\/|$)/.test(path) || isAbsolute(path) ? toPath(path, base) : spec;
|
|
446
|
+
};
|
|
447
|
+
const SUBSTITUTE_IN = [
|
|
448
|
+
"header",
|
|
449
|
+
"query",
|
|
450
|
+
"body"
|
|
451
|
+
];
|
|
452
|
+
/** Throws unless `value` is `'allow-all'`, `'deny-all'` or `{ allow: [hosts] }`. */
|
|
453
|
+
function checkEgress(value, where, Err = DefinitionError) {
|
|
454
|
+
const problem = egressProblem(value, where);
|
|
455
|
+
if (problem) throw new Err(problem);
|
|
456
|
+
}
|
|
457
|
+
const SECRET_HOSTS = Object.freeze({
|
|
458
|
+
gemini: Object.freeze(["generativelanguage.googleapis.com"]),
|
|
459
|
+
openai: Object.freeze(["api.openai.com"]),
|
|
460
|
+
anthropic: Object.freeze(["api.anthropic.com"]),
|
|
461
|
+
github: Object.freeze(["api.github.com", "uploads.github.com"])
|
|
462
|
+
});
|
|
463
|
+
function secretOptions(options, what) {
|
|
464
|
+
const to = options.to ?? "agent";
|
|
465
|
+
if (to !== "agent" && to !== "checks") throw new DefinitionError(`${what}: to is 'agent' or 'checks' (got ${JSON.stringify(to)})`);
|
|
466
|
+
const { hosts } = options;
|
|
467
|
+
if (hosts !== void 0) {
|
|
468
|
+
if (!Array.isArray(hosts) || hosts.length === 0) throw new DefinitionError(`${what}: hosts must list the hosts the secret is sent to, e.g. hosts: ['api.example.com'] or secret.hosts.gemini (got ${JSON.stringify(hosts)})`);
|
|
469
|
+
for (const host of hosts) {
|
|
470
|
+
const bad = badHost(host);
|
|
471
|
+
if (bad) throw new DefinitionError(`${what}: ${bad}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (options.in !== void 0) {
|
|
475
|
+
if (hosts === void 0) throw new DefinitionError(`${what}: in only applies to a brokered secret; add hosts, e.g. { hosts: ['api.example.com'], in: ${JSON.stringify(options.in)} }`);
|
|
476
|
+
if ((Array.isArray(options.in) ? options.in.filter((i) => !SUBSTITUTE_IN.includes(i)) : [options.in]).length || options.in.length === 0) throw new DefinitionError(`${what}: in lists where the gateway substitutes: 'header', 'query' and/or 'body' (got ${JSON.stringify(options.in)})`);
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
...options.optional ? { optional: true } : {},
|
|
480
|
+
...to === "checks" ? { to } : {},
|
|
481
|
+
...hosts ? { hosts: [...hosts] } : {},
|
|
482
|
+
...options.in ? { in: [...new Set(options.in)] } : {}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Where an MCP server's header secrets are brokered to by default: its own host, over HTTPS. */
|
|
486
|
+
function mcpHosts(url) {
|
|
487
|
+
try {
|
|
488
|
+
const u = new URL(url);
|
|
489
|
+
return u.protocol === "https:" ? [u.hostname] : void 0;
|
|
490
|
+
} catch {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Every secret among `env` and the layers' env and MCP servers, layered as the attempt resolves
|
|
496
|
+
* them: a later layer's variable or MCP server replaces an earlier one of the same name.
|
|
497
|
+
*/
|
|
498
|
+
function plannedSecrets(env, layers) {
|
|
499
|
+
const vars = { ...env };
|
|
500
|
+
const servers = /* @__PURE__ */ new Map();
|
|
501
|
+
for (const { spec } of layers) {
|
|
502
|
+
Object.assign(vars, spec.env);
|
|
503
|
+
for (const [name, server] of Object.entries(spec.mcp ?? {})) servers.set(name, server);
|
|
504
|
+
}
|
|
505
|
+
const out = /* @__PURE__ */ new Map();
|
|
506
|
+
const add = (label, from, hosts, checksOnly = false, optional = false) => out.set(`${label}\0${from}`, {
|
|
507
|
+
label,
|
|
508
|
+
from,
|
|
509
|
+
...hosts ? { hosts } : {},
|
|
510
|
+
...checksOnly ? { checksOnly } : {},
|
|
511
|
+
...optional ? { optional } : {}
|
|
512
|
+
});
|
|
513
|
+
const fromVars = (values, prefix) => {
|
|
514
|
+
for (const [name, value] of Object.entries(values)) if (isSecret(value)) add(`${prefix}${name}`, value.from ?? name, value.hosts, value.to === "checks", value.optional === true);
|
|
515
|
+
};
|
|
516
|
+
fromVars(vars, "");
|
|
517
|
+
for (const [server, s] of servers) {
|
|
518
|
+
if (s.transport === "stdio") {
|
|
519
|
+
fromVars(s.env, `mcp.${server}.env.`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const own = mcpHosts(s.url);
|
|
523
|
+
for (const [header, value] of Object.entries(s.headers)) {
|
|
524
|
+
const label = `mcp.${server}.headers.${header}`;
|
|
525
|
+
if (isSecret(value)) add(label, value.from ?? header, value.hosts ?? own);
|
|
526
|
+
if (isSecretTemplate(value)) for (const [, name] of value.template.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)) add(label, name, own);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [...out.values()];
|
|
530
|
+
}
|
|
531
|
+
/** The secrets the sandbox gets as real values: no `hosts`, and none implied by an MCP URL. */
|
|
532
|
+
const unbrokeredSecrets = (env, layers) => plannedSecrets(env, layers).flatMap(({ hosts, ...s }) => hosts ? [] : [s]);
|
|
533
|
+
function secretHelper() {
|
|
534
|
+
const secret = ((options = {}) => ({
|
|
535
|
+
kind: "secret",
|
|
536
|
+
...secretOptions(options, "secret()")
|
|
537
|
+
}));
|
|
538
|
+
secret.fromEnv = (host, options = {}) => {
|
|
539
|
+
if (typeof host !== "string" || !VAR.test(host)) throw new DefinitionError(`secret.fromEnv(${JSON.stringify(host)}) needs a host variable name: letters, digits and _, not starting with a digit, e.g. secret.fromEnv('API_KEY')`);
|
|
540
|
+
return {
|
|
541
|
+
kind: "secret",
|
|
542
|
+
from: host,
|
|
543
|
+
...secretOptions(options, `secret.fromEnv("${host}")`)
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
secret.template = (template) => ({
|
|
547
|
+
kind: "secret-template",
|
|
548
|
+
template
|
|
549
|
+
});
|
|
550
|
+
secret.hosts = SECRET_HOSTS;
|
|
551
|
+
return secret;
|
|
552
|
+
}
|
|
553
|
+
const MOVING = /^(?:main|master|HEAD|trunk|develop|dev|latest)$/;
|
|
554
|
+
/** A git ref that isn't a commit SHA or a version-like tag. */
|
|
555
|
+
const isMovingRef = (ref) => ref === void 0 || MOVING.test(ref) || !/^(?:[0-9a-f]{7,40}|v?\d+(?:\.\d+)*(?:[-+].*)?)$/.test(ref);
|
|
556
|
+
/** `github:owner/repo` and GitHub `/tree/<ref>/<path>` URLs, as a clone URL plus ref and path. */
|
|
557
|
+
function parseGitRepo(repo) {
|
|
558
|
+
const short = /^github:([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(repo);
|
|
559
|
+
if (short) return { url: `https://github.com/${short[1]}.git` };
|
|
560
|
+
const tree = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/tree\/([^/]+)(?:\/(.+))?$/.exec(repo);
|
|
561
|
+
if (tree) return {
|
|
562
|
+
url: `https://github.com/${tree[1]}.git`,
|
|
563
|
+
ref: tree[2],
|
|
564
|
+
path: tree[3]
|
|
565
|
+
};
|
|
566
|
+
return { url: repo };
|
|
567
|
+
}
|
|
568
|
+
/** `repo#ref:path`, as the plan and the record name a git workspace. */
|
|
569
|
+
const workspaceLabel = (w) => `${w.repo}${w.ref ? `#${w.ref}` : ""}${w.path ? `:${w.path}` : ""}`;
|
|
570
|
+
/** The workspace an attempt starts as: the last layer that sets one. */
|
|
571
|
+
const workspaceOf = (layers) => layers.findLast((l) => l.spec.workspace)?.spec.workspace;
|
|
572
|
+
/** The paths the layers' `files` write, sorted. */
|
|
573
|
+
const filesOf = (layers) => [...new Set(layers.flatMap((l) => Object.keys(l.spec.files ?? {})))].sort();
|
|
574
|
+
/** A URL with its password (or an https username, which is where tokens go) masked. */
|
|
575
|
+
function credentialsIn(repo) {
|
|
576
|
+
if (!URL.canParse(repo)) return void 0;
|
|
577
|
+
const u = new URL(repo);
|
|
578
|
+
if (!u.password && !(u.username && /^https?:$/.test(u.protocol))) return void 0;
|
|
579
|
+
u.username = u.username && "***";
|
|
580
|
+
u.password = u.password && "***";
|
|
581
|
+
return u.href;
|
|
582
|
+
}
|
|
583
|
+
const GIT_OPTIONS = [
|
|
584
|
+
"ref",
|
|
585
|
+
"path",
|
|
586
|
+
"init",
|
|
587
|
+
"token"
|
|
588
|
+
];
|
|
589
|
+
const isLocalPath = (repo) => /^\.{1,2}(?:\/|$)/.test(repo) || isAbsolute(repo);
|
|
590
|
+
function gitHelper(base) {
|
|
591
|
+
return (repo, options = {}) => {
|
|
592
|
+
const written = repo instanceof URL ? repo.href : repo;
|
|
593
|
+
if (typeof written !== "string" || !written.trim()) throw new DefinitionError(`git() needs a repository: a URL, github:owner/repo or a local path (got ${JSON.stringify(repo)})`);
|
|
594
|
+
const what = `git(${JSON.stringify(written)})`;
|
|
595
|
+
const masked = credentialsIn(written);
|
|
596
|
+
if (masked) throw new DefinitionError(`git(${JSON.stringify(masked)}): the URL has a password or token in it. reeval fetches the repository on the host with your own git credentials (a credential helper, gh auth setup-git or an SSH key), and the sandbox never gets them; remove it from the URL.`);
|
|
597
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) throw new DefinitionError(`${what}: options are { ref?, path? }`);
|
|
598
|
+
const extra = Object.keys(options).filter((k) => !GIT_OPTIONS.includes(k));
|
|
599
|
+
if (extra.length) {
|
|
600
|
+
const hint = extra.includes("branch") || extra.includes("tag") ? " Use ref for a branch, tag or commit." : "";
|
|
601
|
+
throw new DefinitionError(`${what}: unknown option "${extra.join("\", \"")}"; it takes ${GIT_OPTIONS.slice(0, -1).join(", ")} and ${GIT_OPTIONS.at(-1)}.${hint}`);
|
|
602
|
+
}
|
|
603
|
+
const local = repo instanceof URL ? repo.protocol === "file:" : isLocalPath(written);
|
|
604
|
+
const parsed = local ? { url: toPath(repo, base) } : parseGitRepo(written);
|
|
605
|
+
const ref = options.ref ?? parsed.ref;
|
|
606
|
+
if (ref !== void 0 && (typeof ref !== "string" || !ref.trim() || ref.startsWith("-"))) throw new DefinitionError(`${what}: ref is a branch, tag or commit SHA (got ${JSON.stringify(ref)})`);
|
|
607
|
+
const path = options.path ?? parsed.path;
|
|
608
|
+
if (path !== void 0) {
|
|
609
|
+
const clean = typeof path === "string" ? path.replace(/\/+$/, "") : path;
|
|
610
|
+
if (typeof clean !== "string" || !clean || clean.includes("\\") || clean.startsWith("/") || clean.split("/").some((p) => p === ".." || p === "." || p === "")) throw new DefinitionError(`${what}: path is a directory in the repository, relative, with forward slashes and no . or .. (got ${JSON.stringify(path)})`);
|
|
611
|
+
}
|
|
612
|
+
const cleanPath = path?.replace(/\/+$/, "");
|
|
613
|
+
const { init, token } = options;
|
|
614
|
+
if (init !== void 0 && typeof init !== "boolean") throw new DefinitionError(`${what}: init is true or false (got ${JSON.stringify(init)})`);
|
|
615
|
+
if (token !== void 0) {
|
|
616
|
+
if (typeof token !== "string" || !VAR.test(token) || /^(?:gh[opsur]_|github_pat_)/.test(token)) throw new DefinitionError(`${what}: token names the host variable that holds the token, e.g. token: 'GITHUB_TOKEN'. Don't put the token itself in the eval.`);
|
|
617
|
+
if (local || !/^https?:\/\//.test(parsed.url)) throw new DefinitionError(`${what}: token only works with an https:// URL; for SSH or a local repository, git uses this machine's own credentials`);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
kind: "git",
|
|
621
|
+
repo: "ref" in parsed || "path" in parsed ? parsed.url.replace(/\.git$/, "") : written,
|
|
622
|
+
url: parsed.url,
|
|
623
|
+
...ref ? { ref } : {},
|
|
624
|
+
...cleanPath ? { path: cleanPath } : {},
|
|
625
|
+
...init ? { init } : {},
|
|
626
|
+
...token ? { token } : {}
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* What a skill directory leaves out of the sandbox unless `exclude` says otherwise: eval material
|
|
632
|
+
* (skill-creator keeps `evals/` in the skill), run history and reeval eval files. `.git` is always left out.
|
|
633
|
+
*/
|
|
634
|
+
const DEFAULT_SKILL_EXCLUDE = [
|
|
635
|
+
"/evals/",
|
|
636
|
+
".history/",
|
|
637
|
+
"*.eval.ts"
|
|
638
|
+
];
|
|
639
|
+
function skillHelper(base) {
|
|
640
|
+
const placement = (p) => {
|
|
641
|
+
if (p === void 0 || p === "harness" || p === "workspace") return p ?? "harness";
|
|
642
|
+
throw new DefinitionError(`skill({ placement }) is 'harness' or 'workspace' (got ${JSON.stringify(p)})`);
|
|
643
|
+
};
|
|
644
|
+
const exclude = (e) => {
|
|
645
|
+
if (e === void 0) return {};
|
|
646
|
+
if (!Array.isArray(e) || e.some((p) => typeof p !== "string" || !p.replace(/\//g, ""))) throw new DefinitionError(`skill({ exclude }) must be a list of paths or globs, e.g. ['/evals/', '*.eval.ts'] (got ${JSON.stringify(e)})`);
|
|
647
|
+
return { exclude: [...e] };
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
local: (path, o = {}) => ({
|
|
651
|
+
kind: "skill",
|
|
652
|
+
source: "local",
|
|
653
|
+
path: toPath(path, base),
|
|
654
|
+
...o.only ? { only: [...o.only] } : {},
|
|
655
|
+
...exclude(o.exclude),
|
|
656
|
+
placement: placement(o.placement)
|
|
657
|
+
}),
|
|
658
|
+
git: (repo, o = {}) => {
|
|
659
|
+
if (typeof repo !== "string" || !repo) throw new DefinitionError(`skill.git() needs a repository URL, e.g. skill.git('https://github.com/acme/skills') (got ${JSON.stringify(repo)})`);
|
|
660
|
+
const parsed = parseGitRepo(repo);
|
|
661
|
+
const ref = o.ref ?? parsed.ref;
|
|
662
|
+
const path = o.path ?? parsed.path;
|
|
663
|
+
return {
|
|
664
|
+
kind: "skill",
|
|
665
|
+
source: "git",
|
|
666
|
+
repo,
|
|
667
|
+
...ref ? { ref } : {},
|
|
668
|
+
...path ? { path } : {},
|
|
669
|
+
...o.only ? { only: [...o.only] } : {},
|
|
670
|
+
...exclude(o.exclude),
|
|
671
|
+
placement: placement(o.placement)
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
registry: (id, o = {}) => {
|
|
675
|
+
if (!/^[\w.-]+\/[\w.-]+@[a-z0-9-]+$/.test(id)) throw new DefinitionError(`skill.registry(${JSON.stringify(id)}) needs an id like owner/repo@skill, e.g. skill.registry('acme/skills@review')`);
|
|
676
|
+
return {
|
|
677
|
+
kind: "skill",
|
|
678
|
+
source: "registry",
|
|
679
|
+
id,
|
|
680
|
+
...o.version ? { version: o.version } : {},
|
|
681
|
+
placement: placement(o.placement)
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
npm: (spec, o = {}) => ({
|
|
685
|
+
kind: "skill",
|
|
686
|
+
source: "npm",
|
|
687
|
+
spec: localNpmSpec(spec, base),
|
|
688
|
+
path: o.path ?? "skills",
|
|
689
|
+
...o.only ? { only: [...o.only] } : {},
|
|
690
|
+
...exclude(o.exclude),
|
|
691
|
+
placement: placement(o.placement)
|
|
692
|
+
}),
|
|
693
|
+
inline: (skill, o = {}) => ({
|
|
694
|
+
kind: "skill",
|
|
695
|
+
source: "inline",
|
|
696
|
+
name: skill.name,
|
|
697
|
+
description: skill.description,
|
|
698
|
+
content: skill.content,
|
|
699
|
+
files: { ...skill.files },
|
|
700
|
+
placement: placement(o.placement)
|
|
701
|
+
})
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* What `directory()` leaves out unless `defaultIgnores: false`: dependency, build-output, cache
|
|
706
|
+
* and editor directories, in its `glob` syntax. `build/` and `target/` are anchored to the top of
|
|
707
|
+
* the directory, where Flutter, Dart, Gradle and Cargo put their output, since a nested one can be
|
|
708
|
+
* source. `vendor/` isn't here: a committed Go or PHP vendor directory is what an offline build needs.
|
|
709
|
+
*/
|
|
710
|
+
const DEFAULT_DIRECTORY_IGNORES = Object.freeze([
|
|
711
|
+
"node_modules/",
|
|
712
|
+
".DS_Store",
|
|
713
|
+
"dist/",
|
|
714
|
+
".next/",
|
|
715
|
+
".turbo/",
|
|
716
|
+
"coverage/",
|
|
717
|
+
".cache/",
|
|
718
|
+
"__pycache__/",
|
|
719
|
+
".venv/",
|
|
720
|
+
"venv/",
|
|
721
|
+
".pytest_cache/",
|
|
722
|
+
".mypy_cache/",
|
|
723
|
+
"*.pyc",
|
|
724
|
+
".dart_tool/",
|
|
725
|
+
"/build/",
|
|
726
|
+
"/target/",
|
|
727
|
+
".gradle/",
|
|
728
|
+
".idea/",
|
|
729
|
+
".vscode/"
|
|
730
|
+
]);
|
|
731
|
+
/**
|
|
732
|
+
* A gitignore-style pattern as a test of a file's path, matched by picomatch: a pattern without an
|
|
733
|
+
* inner `/` matches a name at any depth, a leading `/` anchors, a trailing `/` names a directory,
|
|
734
|
+
* and a pattern also matches every file in a directory it names.
|
|
735
|
+
*/
|
|
736
|
+
function globTest(pattern) {
|
|
737
|
+
const dirOnly = pattern.endsWith("/");
|
|
738
|
+
const body = pattern.replace(/\/+$/, "").replaceAll("[!", "[^");
|
|
739
|
+
const glob = body.includes("/") ? body.replace(/^\/+/, "") : `**/${body}`;
|
|
740
|
+
const self = picomatch(glob, { dot: true });
|
|
741
|
+
const inside = picomatch(`${glob}/**/*`, { dot: true });
|
|
742
|
+
return (path, isDir = false) => inside(path) || (isDir || !dirOnly) && self(path);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* `glob` as a test of a directory-relative file path: the last pattern that matches decides, a
|
|
746
|
+
* `!` one excluding; a file no pattern matches is in unless some pattern includes.
|
|
747
|
+
*/
|
|
748
|
+
function globFilter(patterns) {
|
|
749
|
+
const rules = patterns.map((p) => ({
|
|
750
|
+
include: !p.startsWith("!"),
|
|
751
|
+
test: globTest(p.replace(/^!/, ""))
|
|
752
|
+
}));
|
|
753
|
+
const start = !rules.some((r) => r.include);
|
|
754
|
+
return (path, isDir) => rules.reduce((keep, r) => r.test(path, isDir) ? r.include : keep, start);
|
|
755
|
+
}
|
|
756
|
+
const DIRECTORY_OPTIONS = [
|
|
757
|
+
"into",
|
|
758
|
+
"glob",
|
|
759
|
+
"defaultIgnores"
|
|
760
|
+
];
|
|
761
|
+
function directoryHelper(base) {
|
|
762
|
+
return (path, options = {}) => {
|
|
763
|
+
const written = path instanceof URL ? path.href : path;
|
|
764
|
+
const what = `directory(${JSON.stringify(written)})`;
|
|
765
|
+
if (typeof written !== "string" || !written) throw new DefinitionError(`directory() needs a path to a directory, relative to this file (got ${JSON.stringify(path)})`);
|
|
766
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) throw new DefinitionError(`${what}: options are { into?, glob?, defaultIgnores? }`);
|
|
767
|
+
const extra = Object.keys(options).filter((k) => !DIRECTORY_OPTIONS.includes(k));
|
|
768
|
+
if (extra.length) {
|
|
769
|
+
const hint = extra.includes("ignore") || extra.includes("exclude") ? " Exclude with a '!' pattern in glob, e.g. glob: ['!dist/']." : "";
|
|
770
|
+
throw new DefinitionError(`${what}: unknown option "${extra.join("\", \"")}"; it takes into, glob and defaultIgnores.${hint}`);
|
|
771
|
+
}
|
|
772
|
+
const { into, glob = [], defaultIgnores = true } = options;
|
|
773
|
+
if (into !== void 0 && (typeof into !== "string" || into.includes("\\") || isAbsolute(into) || into.split("/").some((p) => p === ".."))) throw new DefinitionError(`${what}: into is a directory relative to the workspace (or ~/…), with forward slashes and no .. (got ${JSON.stringify(into)})`);
|
|
774
|
+
if (!Array.isArray(glob) || glob.some((p) => typeof p !== "string" || !p.replace(/^!/, "").replace(/\//g, ""))) throw new DefinitionError(`${what}: glob is a list of patterns, e.g. ['src/**', 'package.json', '!**/*.test.ts'] (got ${JSON.stringify(glob)})`);
|
|
775
|
+
if (typeof defaultIgnores !== "boolean") throw new DefinitionError(`${what}: defaultIgnores is true or false (got ${JSON.stringify(defaultIgnores)})`);
|
|
776
|
+
const root = toPath(path, base);
|
|
777
|
+
const stat = statSync(root, { throwIfNoEntry: false });
|
|
778
|
+
if (!stat) throw new DefinitionError(`${what}: ${root} doesn't exist`);
|
|
779
|
+
if (!stat.isDirectory()) throw new DefinitionError(`${what}: ${root} is a file, not a directory. Use file(${JSON.stringify(written)}) for one file.`);
|
|
780
|
+
const skip = defaultIgnores ? globFilter(DEFAULT_DIRECTORY_IGNORES) : () => false;
|
|
781
|
+
const keep = globFilter(glob);
|
|
782
|
+
const prefix = into?.replace(/\/+$/, "").replace(/^\.\/?/, "");
|
|
783
|
+
const files = {};
|
|
784
|
+
const walk = (dir, rel, seen) => {
|
|
785
|
+
for (const name of readdirSync(dir).sort()) {
|
|
786
|
+
if (name === ".git") continue;
|
|
787
|
+
const full = join(dir, name);
|
|
788
|
+
const relPath = rel ? `${rel}/${name}` : name;
|
|
789
|
+
const target = statSync(full, { throwIfNoEntry: false });
|
|
790
|
+
if (!target) throw new DefinitionError(`${what}: ${relPath} is a symlink to something that doesn't exist. Remove it, or leave it out with glob: ['!${relPath}'].`);
|
|
791
|
+
if (target.isDirectory()) {
|
|
792
|
+
if (skip(relPath, true)) continue;
|
|
793
|
+
const real = realpathSync(full);
|
|
794
|
+
if (seen.includes(real)) throw new DefinitionError(`${what}: ${relPath} is a symlink to a directory that contains it. Leave it out with glob: ['!${relPath}/'].`);
|
|
795
|
+
walk(full, relPath, [...seen, real]);
|
|
796
|
+
} else if (target.isFile() && !skip(relPath) && keep(relPath)) files[prefix ? `${prefix}/${relPath}` : relPath] = {
|
|
797
|
+
kind: "file",
|
|
798
|
+
path: full
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
walk(root, "", [realpathSync(root)]);
|
|
803
|
+
if (Object.keys(files).length === 0) throw new DefinitionError(`${what}: no files in ${root}${glob.length || defaultIgnores ? ` match (glob ${JSON.stringify(glob)}${defaultIgnores ? ", after the default ignores such as node_modules/ and dist/; defaultIgnores: false keeps them" : ""})` : ""}. Empty directories aren't seeded; create one with a setup step.`);
|
|
804
|
+
return files;
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const mcpHelper = {
|
|
808
|
+
stdio: (command, args = [], o = {}) => {
|
|
809
|
+
if (typeof command !== "string" || !command) throw new DefinitionError(`mcp.stdio() needs a command, e.g. mcp.stdio('npx', ['-y', 'server']) (got ${JSON.stringify(command)})`);
|
|
810
|
+
return {
|
|
811
|
+
kind: "mcp",
|
|
812
|
+
transport: "stdio",
|
|
813
|
+
command,
|
|
814
|
+
args: [...args],
|
|
815
|
+
env: { ...o.env }
|
|
816
|
+
};
|
|
817
|
+
},
|
|
818
|
+
http: (url, o = {}) => ({
|
|
819
|
+
kind: "mcp",
|
|
820
|
+
transport: "http",
|
|
821
|
+
url,
|
|
822
|
+
headers: { ...o.headers }
|
|
823
|
+
}),
|
|
824
|
+
sse: (url, o = {}) => ({
|
|
825
|
+
kind: "mcp",
|
|
826
|
+
transport: "sse",
|
|
827
|
+
url,
|
|
828
|
+
headers: { ...o.headers }
|
|
829
|
+
})
|
|
830
|
+
};
|
|
831
|
+
/** The helpers a callback gets, with relative paths resolving against `base`. */
|
|
832
|
+
function helpersFor(base) {
|
|
833
|
+
return {
|
|
834
|
+
secret: secretHelper(),
|
|
835
|
+
skill: skillHelper(base),
|
|
836
|
+
mcp: mcpHelper,
|
|
837
|
+
file: (path) => ({
|
|
838
|
+
kind: "file",
|
|
839
|
+
path: toPath(path, base)
|
|
840
|
+
}),
|
|
841
|
+
directory: directoryHelper(base),
|
|
842
|
+
git: gitHelper(base)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/** Throws on env variables that aren't strings or secrets, or look like literal secrets. */
|
|
846
|
+
function checkEnv(env, where, Err = DefinitionError) {
|
|
847
|
+
if (typeof env !== "object" || env === null || Array.isArray(env)) throw new Err(`env in ${where} must be an object of variables, e.g. env: { NODE_ENV: 'test' } (got ${JSON.stringify(env)})`);
|
|
848
|
+
for (const [name, value] of Object.entries(env)) {
|
|
849
|
+
if (!VAR.test(name)) throw new Err(`env.${name} in ${where} isn't a valid variable name: use letters, digits and _, not starting with a digit`);
|
|
850
|
+
if (isSecret(value)) {
|
|
851
|
+
const problem = descriptorProblem(secretSchema, value, "secret");
|
|
852
|
+
if (problem) throw new Err(`env.${name} in ${where}: ${problem}`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (typeof value !== "string") throw new Err(`env.${name} in ${where} must be a string or secret.fromEnv() (got ${JSON.stringify(value)})`);
|
|
856
|
+
if (SECRET_NAME.test(name)) throw new Err(`env.${name} in ${where} is a literal string. Pass secrets with secret.fromEnv() so they're redacted and never recorded.`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const stepCommand = (step) => typeof step === "string" ? step : step.run;
|
|
860
|
+
function checkSpec(spec, where) {
|
|
861
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) throw new DefinitionError(`${where} must be an object or a callback that returns one (got ${JSON.stringify(spec)})`);
|
|
862
|
+
for (const key of Object.keys(spec)) {
|
|
863
|
+
if (SPEC_KEYS.includes(key)) continue;
|
|
864
|
+
if (SANDBOX_KEYS.includes(key)) {
|
|
865
|
+
const value = spec[key];
|
|
866
|
+
throw new DefinitionError(`"${key}" isn't an environment field (${where}). Put the image on the sandbox: sandbox: docker({ ${key === "image" ? typeof value === "string" ? "from" : "install" : key}: … }).`);
|
|
867
|
+
}
|
|
868
|
+
const near = SPEC_HINTS[key] ?? closest(key, SPEC_KEYS);
|
|
869
|
+
throw new DefinitionError(`Unknown field "${key}" in ${where}.${near ? ` Did you mean "${near}"?` : ""}`);
|
|
870
|
+
}
|
|
871
|
+
const s = spec;
|
|
872
|
+
if (s.sandbox !== void 0) checkSandbox(s.sandbox, where);
|
|
873
|
+
if (s.workspace !== void 0 && s.workspace?.kind !== "git") throw new DefinitionError(`workspace in ${where} must come from the git helper, e.g. workspace: git('https://github.com/acme/app', { ref: 'v1.0.0' }) (got ${JSON.stringify(s.workspace)})`);
|
|
874
|
+
if (s.workspace !== void 0) {
|
|
875
|
+
const problem = descriptorProblem(gitWorkspaceSchema, s.workspace, "git");
|
|
876
|
+
if (problem) throw new DefinitionError(`workspace in ${where}: ${problem}`);
|
|
877
|
+
}
|
|
878
|
+
if (s.env !== void 0) checkEnv(s.env, where);
|
|
879
|
+
if (s.egress !== void 0) checkEgress(s.egress, where);
|
|
880
|
+
const list = (key, value) => {
|
|
881
|
+
if (value !== void 0 && !Array.isArray(value)) throw new DefinitionError(`${key} in ${where} must be a list (got ${JSON.stringify(value)})`);
|
|
882
|
+
return value ?? [];
|
|
883
|
+
};
|
|
884
|
+
for (const [i, step] of list("setup", s.setup).entries()) {
|
|
885
|
+
const run = typeof step === "string" ? step : isObjectLike(step) ? step.run : void 0;
|
|
886
|
+
if (typeof run !== "string" || !run.trim()) throw new DefinitionError(`setup[${i}] in ${where} must be a command string or { run, timeout?, cwd? } (got ${JSON.stringify(step)})`);
|
|
887
|
+
if (typeof step === "string") continue;
|
|
888
|
+
const extra = Object.keys(step).filter((k) => ![
|
|
889
|
+
"run",
|
|
890
|
+
"timeout",
|
|
891
|
+
"cwd"
|
|
892
|
+
].includes(k));
|
|
893
|
+
if (extra.length) throw new DefinitionError(`setup step "${run}" in ${where} has an unknown key "${extra[0]}"; a step is a string or { run, timeout?, cwd? }.`);
|
|
894
|
+
const { cwd, timeout } = step;
|
|
895
|
+
if (timeout !== void 0) try {
|
|
896
|
+
parseDuration(timeout);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
throw new DefinitionError(`setup step "${run}" in ${where}: ${error.message}`);
|
|
899
|
+
}
|
|
900
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || !cwd.trim())) throw new DefinitionError(`setup step "${run}" in ${where}: cwd must be a directory path, relative to the workspace or absolute (got ${JSON.stringify(cwd)})`);
|
|
901
|
+
}
|
|
902
|
+
for (const [i, skill] of list("skills", s.skills).entries()) {
|
|
903
|
+
if (skill?.kind !== "skill") throw new DefinitionError(`skills in ${where} must come from the skill helper, e.g. skills: [skill.local('./skills')] (got ${JSON.stringify(skill)})`);
|
|
904
|
+
const call = `skill.${String(skill.source)}`;
|
|
905
|
+
const problem = descriptorProblem(skillSourceSchema, skill, call);
|
|
906
|
+
if (problem) throw new DefinitionError(`skills[${i}] in ${where}: ${problem}`);
|
|
907
|
+
}
|
|
908
|
+
for (const [name, server] of Object.entries(s.mcp ?? {})) {
|
|
909
|
+
if (server?.kind !== "mcp") throw new DefinitionError(`mcp.${name} in ${where} must come from the mcp helper, e.g. mcp.stdio('npx', ['-y', 'server']) (got ${JSON.stringify(server)})`);
|
|
910
|
+
const call = `mcp.${String(server.transport)}`;
|
|
911
|
+
const problem = descriptorProblem(mcpServerSchema, server, call);
|
|
912
|
+
if (problem) throw new DefinitionError(`mcp.${name} in ${where}: ${problem}`);
|
|
913
|
+
}
|
|
914
|
+
for (const [path, value] of Object.entries(s.files ?? {})) {
|
|
915
|
+
const key = `files[${JSON.stringify(path)}] in ${where}`;
|
|
916
|
+
if (path.includes("\\")) throw new DefinitionError(`${key}: use forward slashes (${path.replaceAll("\\", "/")})`);
|
|
917
|
+
const rest = path.startsWith("~/") ? path.slice(2) : path;
|
|
918
|
+
if (!rest || isAbsolute(rest) || rest.split("/").includes("..")) throw new DefinitionError(`${key}: paths are relative to the workspace, or start with ~/ for the home directory, and never use ..`);
|
|
919
|
+
if (typeof value !== "string" && !isHostFile(value)) throw new DefinitionError(`${key} must be a string or file(path) (got ${JSON.stringify(value)})`);
|
|
920
|
+
const problem = isHostFile(value) && descriptorProblem(hostFileSchema, value, "file");
|
|
921
|
+
if (problem) throw new DefinitionError(`${key}: ${problem}`);
|
|
922
|
+
}
|
|
923
|
+
if (s.rules !== void 0 && typeof s.rules !== "string" && !isHostFile(s.rules)) throw new DefinitionError(`rules in ${where} must be markdown or file(path) (got ${JSON.stringify(s.rules)})`);
|
|
924
|
+
const rules = isHostFile(s.rules) && descriptorProblem(hostFileSchema, s.rules, "file");
|
|
925
|
+
if (rules) throw new DefinitionError(`rules in ${where}: ${rules}`);
|
|
926
|
+
const rest = refusal(environmentSpecSchema, s, (issue, value) => `${where}: ${genericProblem("", issue, value)}`);
|
|
927
|
+
if (rest) throw new DefinitionError(rest);
|
|
928
|
+
return structuredClone(s);
|
|
929
|
+
}
|
|
930
|
+
const NAME_RULE = "a name of letters, digits, \".\", \"_\", \"@\", \"+\" or \"-\"";
|
|
931
|
+
/** The envelope of environment data: its name and layers, each layer's spec only as an object. */
|
|
932
|
+
const environmentEnvelope = environmentDataSchema.extend({ layers: z.array(z.strictObject({
|
|
933
|
+
name: z.string().regex(NAME),
|
|
934
|
+
spec: z.record(z.string(), z.unknown())
|
|
935
|
+
})) });
|
|
936
|
+
/**
|
|
937
|
+
* What's wrong with an environment as data (from another language, or built by hand), in the
|
|
938
|
+
* words `defineEnvironment` uses for each layer's spec, or undefined. `option` names the option
|
|
939
|
+
* (`environment`, `compare.baseline`, …) and `where` the eval or config that set it.
|
|
940
|
+
*/
|
|
941
|
+
function environmentDataProblem(value, option, at) {
|
|
942
|
+
const env = value;
|
|
943
|
+
const where = `${option} in ${at}`;
|
|
944
|
+
const envelope = refusal(environmentEnvelope, value, (issue) => {
|
|
945
|
+
const [key, index, field] = issue.path;
|
|
946
|
+
const layer = index === void 0 ? void 0 : env.layers[index];
|
|
947
|
+
if (issue.code === "unrecognized_keys") {
|
|
948
|
+
const what = layer ? `layer ${index} of the environment` : "the environment";
|
|
949
|
+
return `${where}: ${what} has no "${issue.keys[0]}" field`;
|
|
950
|
+
}
|
|
951
|
+
if (key === "name") return `${where} needs ${NAME_RULE} (got ${got(env.name)})`;
|
|
952
|
+
if (field === "name") return `${where}: layer ${index} of "${env.name}" needs ${NAME_RULE} (got ${got(layer?.name)})`;
|
|
953
|
+
if (field === "spec") return `${where}: layer "${layer?.name}" of "${env.name}" needs its spec, an object (got ${got(layer?.spec)})`;
|
|
954
|
+
return `${where}: ${genericProblem("environment", issue, value)}`;
|
|
955
|
+
});
|
|
956
|
+
if (envelope) return envelope;
|
|
957
|
+
for (const layer of env.layers) try {
|
|
958
|
+
const of = option === "environment" ? at : where;
|
|
959
|
+
checkSpec(layer.spec, `layer "${layer.name}" of environment "${env.name}" in ${of}`);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error instanceof DefinitionError) return error.message;
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
966
|
+
const isPath = (value) => typeof value === "string" && value !== "";
|
|
967
|
+
/**
|
|
968
|
+
* A sandbox descriptor written as data with its host paths made absolute against `base`, as
|
|
969
|
+
* `docker({ dockerfile })` and `fakeSandbox({ dir })` resolve theirs: a Dockerfile, which must be
|
|
970
|
+
* a file, and a fake sandbox's `dir`. Any other value is left for the checks to name.
|
|
971
|
+
*/
|
|
972
|
+
function sandboxPaths(value, base, field, missing) {
|
|
973
|
+
if (!isRecord(value)) return value;
|
|
974
|
+
if (value.kind === "docker" && isPath(value.dockerfile)) {
|
|
975
|
+
const full = toPath(value.dockerfile, base);
|
|
976
|
+
if (!statSync(full, { throwIfNoEntry: false })?.isFile()) missing(`${field}.dockerfile`, full);
|
|
977
|
+
return {
|
|
978
|
+
...value,
|
|
979
|
+
dockerfile: full
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return value.kind === "fake" && isPath(value.dir) ? {
|
|
983
|
+
...value,
|
|
984
|
+
dir: toPath(value.dir, base)
|
|
985
|
+
} : value;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* An environment spec written as data with its host paths made absolute against `base`, as the
|
|
989
|
+
* helpers resolve theirs: host files in `files` and `rules`, a local skill's `path`, a local npm
|
|
990
|
+
* skill spec, a local git workspace's `url`, and the sandbox's. Only a non-empty string changes,
|
|
991
|
+
* and an absolute path stays as written.
|
|
992
|
+
*/
|
|
993
|
+
function specPaths(value, base, field, missing) {
|
|
994
|
+
if (!isRecord(value)) return value;
|
|
995
|
+
const hostFile = (v) => isRecord(v) && v.kind === "file" && isPath(v.path) ? {
|
|
996
|
+
...v,
|
|
997
|
+
path: toPath(v.path, base)
|
|
998
|
+
} : v;
|
|
999
|
+
const skill = (v) => {
|
|
1000
|
+
if (!isRecord(v)) return v;
|
|
1001
|
+
if (v.source === "local" && isPath(v.path)) return {
|
|
1002
|
+
...v,
|
|
1003
|
+
path: toPath(v.path, base)
|
|
1004
|
+
};
|
|
1005
|
+
return v.source === "npm" && isPath(v.spec) ? {
|
|
1006
|
+
...v,
|
|
1007
|
+
spec: localNpmSpec(v.spec, base)
|
|
1008
|
+
} : v;
|
|
1009
|
+
};
|
|
1010
|
+
const out = { ...value };
|
|
1011
|
+
const prefix = field ? `${field}.` : "";
|
|
1012
|
+
if ("sandbox" in value) out.sandbox = sandboxPaths(value.sandbox, base, `${prefix}sandbox`, missing);
|
|
1013
|
+
const { workspace, files, rules, skills } = value;
|
|
1014
|
+
if (isRecord(workspace) && isPath(workspace.url) && isLocalPath(workspace.url)) out.workspace = {
|
|
1015
|
+
...workspace,
|
|
1016
|
+
url: toPath(workspace.url, base)
|
|
1017
|
+
};
|
|
1018
|
+
if (isRecord(files)) out.files = Object.fromEntries(Object.entries(files).map(([k, v]) => [k, hostFile(v)]));
|
|
1019
|
+
if ("rules" in value) out.rules = hostFile(rules);
|
|
1020
|
+
if (Array.isArray(skills)) out.skills = skills.map(skill);
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* An environment written as data with each layer's spec given absolute host paths against `base`,
|
|
1025
|
+
* as `specPaths` gives one spec. Anything that isn't environment data is left for the checks.
|
|
1026
|
+
*/
|
|
1027
|
+
function environmentPaths(value, base, field, missing) {
|
|
1028
|
+
if (!isRecord(value) || !Array.isArray(value.layers)) return value;
|
|
1029
|
+
return {
|
|
1030
|
+
...value,
|
|
1031
|
+
layers: value.layers.map((layer, i) => isRecord(layer) ? {
|
|
1032
|
+
...layer,
|
|
1033
|
+
spec: specPaths(layer.spec, base, `${field}.layers[${i}].spec`, missing)
|
|
1034
|
+
} : layer)
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function buildSpec(input, where, base) {
|
|
1038
|
+
const spec = typeof input === "function" ? input(helpersFor(base)) : input;
|
|
1039
|
+
if (spec instanceof Promise || typeof spec?.then === "function") throw new DefinitionError(`${where} returned a Promise. The callback runs once at definition time and must return the environment synchronously; fetch remote content in a setup step instead.`);
|
|
1040
|
+
const missing = (field, full) => {
|
|
1041
|
+
throw new DefinitionError(`${field} in ${where}: ${full} doesn't exist. Paths are relative to the file that defines the environment.`);
|
|
1042
|
+
};
|
|
1043
|
+
return checkSpec(specPaths(spec, base, "", missing), where);
|
|
1044
|
+
}
|
|
1045
|
+
/** An environment from its data, as a declaration tree carries it: `extendEnvironment` works again. */
|
|
1046
|
+
const environmentFrom = (data) => makeEnvironment(data.name, [...data.layers]);
|
|
1047
|
+
function makeEnvironment(name, layers) {
|
|
1048
|
+
const env = {
|
|
1049
|
+
kind: "environment",
|
|
1050
|
+
name,
|
|
1051
|
+
layers: Object.freeze(layers)
|
|
1052
|
+
};
|
|
1053
|
+
const extend = (child, input) => {
|
|
1054
|
+
checkName(child, `${name}.extend`);
|
|
1055
|
+
const where = `${name}.extend("${child}")`;
|
|
1056
|
+
const base = callerDir() ?? process.cwd();
|
|
1057
|
+
return makeEnvironment(child, [...layers, {
|
|
1058
|
+
name: child,
|
|
1059
|
+
spec: buildSpec(input, where, base)
|
|
1060
|
+
}]);
|
|
1061
|
+
};
|
|
1062
|
+
Object.defineProperty(env, "extendEnvironment", {
|
|
1063
|
+
enumerable: false,
|
|
1064
|
+
value: extend
|
|
1065
|
+
});
|
|
1066
|
+
Object.defineProperty(env, "extend", {
|
|
1067
|
+
enumerable: false,
|
|
1068
|
+
value: extend
|
|
1069
|
+
});
|
|
1070
|
+
return Object.freeze(env);
|
|
1071
|
+
}
|
|
1072
|
+
function checkName(name, what) {
|
|
1073
|
+
if (typeof name !== "string" || !NAME.test(name)) throw new DefinitionError(`${what} needs a name of letters, digits, ".", "_", "@", "+" or "-" (got ${JSON.stringify(name)})`);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* What an eval runs in: the sandbox, skills, MCP servers, files, rules, variables, secrets and
|
|
1077
|
+
* setup steps. Pass a spec, or a callback that destructures the typed helpers it needs:
|
|
1078
|
+
*
|
|
1079
|
+
* defineEnvironment('genkit', ({ secret, skill }) => ({
|
|
1080
|
+
* sandbox: docker({ from: 'node:24-bookworm' }),
|
|
1081
|
+
* env: { GOOGLE_GENAI_API_KEY: secret.fromEnv('GOOGLE_GENERATIVE_AI_API_KEY') },
|
|
1082
|
+
* skills: [skill.local('../skills/genkit')],
|
|
1083
|
+
* }))
|
|
1084
|
+
*
|
|
1085
|
+
* The callback runs once, synchronously. Relative paths resolve against the calling file.
|
|
1086
|
+
* `extend` / `extendEnvironment` replace `sandbox` entirely when the child sets one; when
|
|
1087
|
+
* the child omits it, the parent's sandbox is kept. A bare `defineEnvironment('plain', {})`
|
|
1088
|
+
* that is not an extend uses the cascaded option sandbox, else implicit Docker.
|
|
1089
|
+
*/
|
|
1090
|
+
function defineEnvironment(name, input) {
|
|
1091
|
+
checkName(name, "defineEnvironment");
|
|
1092
|
+
const base = callerDir() ?? process.cwd();
|
|
1093
|
+
return makeEnvironment(name, [{
|
|
1094
|
+
name,
|
|
1095
|
+
spec: buildSpec(input, `defineEnvironment("${name}")`, base)
|
|
1096
|
+
}]);
|
|
1097
|
+
}
|
|
1098
|
+
/** The control of a compare without a `baseline` when nothing in the cascade sets an environment. */
|
|
1099
|
+
const EMPTY_BASELINE = "baseline";
|
|
1100
|
+
/**
|
|
1101
|
+
* An environment with no layers. As a compare's empty control it uses the first
|
|
1102
|
+
* variant's sandbox when that variant sets one, else the cascaded option sandbox,
|
|
1103
|
+
* else the implicit default Docker sandbox.
|
|
1104
|
+
*/
|
|
1105
|
+
const emptyEnvironment = (name) => makeEnvironment(name, []);
|
|
1106
|
+
/** The last layer that sets `sandbox`, else `fallback` (the cascaded option sandbox). */
|
|
1107
|
+
const sandboxOf = (layers, fallback) => layers.findLast((l) => l.spec.sandbox)?.spec.sandbox ?? fallback;
|
|
1108
|
+
/** The sandbox an environment names, else `fallback`. */
|
|
1109
|
+
const environmentSandbox = (env, fallback) => sandboxOf(env?.layers ?? [], fallback);
|
|
1110
|
+
function checkSandbox(value, where) {
|
|
1111
|
+
const kind = isObjectLike(value) ? value.kind : void 0;
|
|
1112
|
+
if (!isObjectLike(value) || kind !== "docker" && kind !== "fake") throw new DefinitionError(`sandbox in ${where} must come from docker(), podman() or fakeSandbox() (got ${JSON.stringify(value)})`);
|
|
1113
|
+
const max = value.maxConcurrent;
|
|
1114
|
+
if (max !== void 0 && !(typeof max === "number" && Number.isInteger(max) && max >= 1)) throw new DefinitionError(`sandbox.maxConcurrent in ${where} must be a whole number of at least 1 (got ${String(max)})`);
|
|
1115
|
+
const problem = sandboxProblem(value);
|
|
1116
|
+
if (problem) throw new DefinitionError(`sandbox in ${where}: ${problem}`);
|
|
1117
|
+
}
|
|
1118
|
+
function applied(env) {
|
|
1119
|
+
if (!env) return {
|
|
1120
|
+
name: "-",
|
|
1121
|
+
parent: null,
|
|
1122
|
+
layers: []
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
name: env.name,
|
|
1126
|
+
parent: env.layers.at(-2)?.name ?? null,
|
|
1127
|
+
layers: [...env.layers]
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
//#endregion
|
|
1131
|
+
export { workspaceLabel as A, parseGitRepo as C, stepCommand as D, sandboxPaths as E, envVarsSchema as F, gatePolicySchema as I, gateProblem as L, environmentDataSchema as M, sandboxDescriptorSchema as N, toPath as O, egressPolicySchema as P, checkDockerOptions as R, mcpHosts as S, sandboxOf as T, isEnvironment as _, callerDir as a, isSecret as b, defineEnvironment as c, environmentFrom as d, environmentPaths as f, helpersFor as g, globFilter as h, applied as i, workspaceOf as j, unbrokeredSecrets as k, emptyEnvironment as l, filesOf as m, DEFAULT_SKILL_EXCLUDE as n, checkEgress as o, environmentSandbox as p, EMPTY_BASELINE as r, checkEnv as s, DEFAULT_DIRECTORY_IGNORES as t, environmentDataProblem as u, isHostFile as v, plannedSecrets as w, isSecretTemplate as x, isMovingRef as y, sandboxProblem as z };
|
|
1132
|
+
|
|
1133
|
+
//# sourceMappingURL=environment-DNjRk0nU.js.map
|