@hona/openeval 0.4.0 → 0.5.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/JUDGING.md +6 -1
- package/VERIFICATION.md +100 -0
- package/package.json +2 -2
- package/src/app/grade-recording.ts +16 -0
- package/src/app/input-fingerprints.ts +8 -2
- package/src/app/judge-evidence.ts +8 -2
- package/src/app/judge-run.ts +10 -0
- package/src/app/load-benchmark.ts +15 -2
- package/src/app/prepare-inputs.ts +40 -0
- package/src/app/read-results.ts +21 -4
- package/src/app/rejudge.ts +5 -0
- package/src/app/run-benchmark.ts +2 -1
- package/src/app/serve-results.ts +14 -0
- package/src/cli.ts +21 -2
- package/src/index.ts +8 -0
- package/src/infra/containers/oci.ts +4 -0
- package/src/infra/judging/code-result.ts +21 -3
- package/src/infra/judging/code-source.ts +10 -2
- package/src/infra/judging/code-worker.ts +5 -3
- package/src/infra/judging/code.ts +19 -3
- package/src/infra/judging/contract.ts +10 -2
- package/src/infra/judging/index.ts +1 -0
- package/src/infra/recording/index.ts +14 -2
- package/src/infra/verification/image.ts +33 -0
- package/src/infra/verification/runtime/Dockerfile +18 -0
- package/src/infra/verification/runtime/bun.lock +16 -0
- package/src/infra/verification/runtime/package.json +5 -0
- package/src/infra/verification/session.ts +199 -0
- package/src/judge-context.ts +66 -0
- package/src/types.ts +10 -2
- package/viewer/assets/{abnfDiagram-VCTEODGH-D-idCGaW.js → abnfDiagram-VCTEODGH-tXaAmCdr.js} +1 -1
- package/viewer/assets/{angular-html-B-7vkhmj.js → angular-html-CNhJPCRr.js} +1 -1
- package/viewer/assets/{angular-ts-CqJWLTIZ.js → angular-ts-BnGgGecY.js} +1 -1
- package/viewer/assets/{apl-DVTjqAhB.js → apl-ZOvmk1hE.js} +1 -1
- package/viewer/assets/{arc-Bmz8zsvq.js → arc-BalFY-0o.js} +1 -1
- package/viewer/assets/architecture-7GRP2DOG-Dwn_SxkP.js +1 -0
- package/viewer/assets/{architectureDiagram-5GKGNRK7-CxqP2ijS.js → architectureDiagram-5GKGNRK7-DpSDAEgb.js} +1 -1
- package/viewer/assets/{astro-Ih6QH4I8.js → astro-BJzTFZ3R.js} +1 -1
- package/viewer/assets/{blade-vzkWgA61.js → blade-CvS8_w1j.js} +1 -1
- package/viewer/assets/{blockDiagram-I7D4REHJ-Dm35S95Q.js → blockDiagram-I7D4REHJ-BY3upSR4.js} +1 -1
- package/viewer/assets/{c-092Q-y5e.js → c-D5ZSBtUG.js} +1 -1
- package/viewer/assets/{c4Diagram-7LVT6UL2-DArgyJNC.js → c4Diagram-7LVT6UL2-CA9keRc2.js} +1 -1
- package/viewer/assets/channel-CgM1_6P7.js +1 -0
- package/viewer/assets/{chapel-DUOt2X3X.js → chapel-1aYDxJri.js} +1 -1
- package/viewer/assets/{chunk-4HAMMTFA-Bm3UCwQ6.js → chunk-4HAMMTFA-B0uITxv4.js} +1 -1
- package/viewer/assets/{chunk-75Z2AOVW-CpmjsCJX.js → chunk-75Z2AOVW-CbTHx8Lz.js} +1 -1
- package/viewer/assets/{chunk-DU6HZSFF-DYT2KEwe.js → chunk-DU6HZSFF-C7mZpnch.js} +1 -1
- package/viewer/assets/{chunk-F27PBJKO-BFGd2OPj.js → chunk-F27PBJKO-iUZG9l9x.js} +1 -1
- package/viewer/assets/{chunk-GMAD6QVW-D8gzxWqz.js → chunk-GMAD6QVW-BYg8qa71.js} +1 -1
- package/viewer/assets/{chunk-GVQU2GXP-BZJsi-wS.js → chunk-GVQU2GXP-rnsdTI4Q.js} +1 -1
- package/viewer/assets/{chunk-IMKFNOWR-BKbF8JvM.js → chunk-IMKFNOWR-D0e2NIBw.js} +1 -1
- package/viewer/assets/{chunk-L3NEJ4N5-SWKb6tCy.js → chunk-L3NEJ4N5-Itlw7HBQ.js} +1 -1
- package/viewer/assets/{chunk-OSK3NFVY-BrqoFOmR.js → chunk-OSK3NFVY-DjAAQhPw.js} +1 -1
- package/viewer/assets/{chunk-P2QGCYS3-B10Cyxbx.js → chunk-P2QGCYS3-XOxrUS-o.js} +1 -1
- package/viewer/assets/{chunk-POPQ4Y6H-DXpfBPGQ.js → chunk-POPQ4Y6H-SHuwgciA.js} +1 -1
- package/viewer/assets/{chunk-PWAF6VOD-DZmaXnfr.js → chunk-PWAF6VOD-BtrRW-Sl.js} +1 -1
- package/viewer/assets/{chunk-SHT3W25Y-1U6zFGxP.js → chunk-SHT3W25Y-6Pacfssp.js} +1 -1
- package/viewer/assets/{chunk-SVP7TREG-CaW6KcpM.js → chunk-SVP7TREG-eTVS9a20.js} +1 -1
- package/viewer/assets/{chunk-TICWLB2K-X4pj7G4S.js → chunk-TICWLB2K-CPnAfOZj.js} +1 -1
- package/viewer/assets/{chunk-XXDRQBXY-0afAM3OP.js → chunk-XXDRQBXY-ciXhM2Hb.js} +1 -1
- package/viewer/assets/classDiagram-ZZMXUADV-BtFc7wo5.js +1 -0
- package/viewer/assets/classDiagram-v2-VYDZK3BY-BtFc7wo5.js +1 -0
- package/viewer/assets/{cobol-CtspwbZ4.js → cobol-CEoq_5kf.js} +1 -1
- package/viewer/assets/{coffee-qbHB9gj8.js → coffee-CZTWsTxw.js} +1 -1
- package/viewer/assets/{cose-bilkent-JH36ORCC-7s0vcDw1.js → cose-bilkent-JH36ORCC-NCkOcFkY.js} +1 -1
- package/viewer/assets/{cpp-Dx37X5A_.js → cpp-Hh-o0ivR.js} +1 -1
- package/viewer/assets/{crystal-CiWjJvk4.js → crystal-CL_KTsac.js} +1 -1
- package/viewer/assets/{css-CLeuyJyb.js → css-9neLgVLd.js} +1 -1
- package/viewer/assets/{cynefin-OW5HDTMX-B4XFGNNZ.js → cynefin-OW5HDTMX-Ba1tlgBB.js} +1 -1
- package/viewer/assets/{cynefinDiagram-5FMLGOSQ-DdNMKbw6.js → cynefinDiagram-5FMLGOSQ-CMrzWo3p.js} +1 -1
- package/viewer/assets/{dagre-GXQ25YYZ-BkDo4A91.js → dagre-GXQ25YYZ-DwjOnUDD.js} +1 -1
- package/viewer/assets/{diagram-S7CK7UJ4-DU8cnnVy.js → diagram-S7CK7UJ4-DA0PXsM6.js} +1 -1
- package/viewer/assets/{diagram-UQ7AKVKN-Dg2emYKa.js → diagram-UQ7AKVKN-C5BBFVvO.js} +1 -1
- package/viewer/assets/{diagram-VSXAHHWV-C_-YSzcn.js → diagram-VSXAHHWV-COgLeOyi.js} +1 -1
- package/viewer/assets/{diagram-VX7I27RA-BLCEnxqe.js → diagram-VX7I27RA-CEu-Pm3B.js} +1 -1
- package/viewer/assets/{diagram-Z3DM3KII-62CpIiAg.js → diagram-Z3DM3KII-BGq1_7BE.js} +1 -1
- package/viewer/assets/{dist-CQIgbdem.js → dist-CX8-lyNa.js} +1 -1
- package/viewer/assets/{ebnfDiagram-PWID7BFC-FkGGETgC.js → ebnfDiagram-PWID7BFC-CGoEp9fJ.js} +1 -1
- package/viewer/assets/{edge-CzmGSXHr.js → edge-CTz4kCIs.js} +1 -1
- package/viewer/assets/{elixir-DDfD_-oS.js → elixir-Bi8YL7XX.js} +1 -1
- package/viewer/assets/{elm-l7aRfQIq.js → elm-C-UxMmSh.js} +1 -1
- package/viewer/assets/{erDiagram-RLTQ6QDP-GlCpsS1v.js → erDiagram-RLTQ6QDP-Bb0ICmqZ.js} +1 -1
- package/viewer/assets/{erb-qVffN5xM.js → erb-ZSq62Y_u.js} +1 -1
- package/viewer/assets/eventmodeling-NTZA5JFV-BCWgnTvv.js +1 -0
- package/viewer/assets/flowDiagram-HODETNUW-B4hhZ1QN.js +1 -0
- package/viewer/assets/{ganttDiagram-EL5Y4UJY-CH_Tk4Ex.js → ganttDiagram-EL5Y4UJY-B4KN9ny5.js} +1 -1
- package/viewer/assets/{git-rebase-jEJ8nKk1.js → git-rebase-Cw9GoTpw.js} +1 -1
- package/viewer/assets/{gitGraph-4MIJSDKK-Cc9o1l4Y.js → gitGraph-4MIJSDKK-CI3xWhLt.js} +1 -1
- package/viewer/assets/{gitGraphDiagram-WWUBYQGX-UdfHBATc.js → gitGraphDiagram-WWUBYQGX-CPv8-MbI.js} +1 -1
- package/viewer/assets/{glimmer-js-BMk2JCW8.js → glimmer-js-Bb_1emHy.js} +1 -1
- package/viewer/assets/{glimmer-ts-BH7RwQxU.js → glimmer-ts-D2kOXMB0.js} +1 -1
- package/viewer/assets/{glsl-CAWrZoEq.js → glsl-DEqjHGFr.js} +1 -1
- package/viewer/assets/{graphql-Bac_hecs.js → graphql-Bi0OE4k4.js} +1 -1
- package/viewer/assets/{hack-1OgpAtm-.js → hack-aU4VRzRW.js} +1 -1
- package/viewer/assets/{haml-CvwG3BzW.js → haml-B-2wwgyn.js} +1 -1
- package/viewer/assets/{handlebars-Cq4oEEp8.js → handlebars-Bmq7hbFI.js} +1 -1
- package/viewer/assets/{html-D5wZ_JR6.js → html-BBsNbpya.js} +1 -1
- package/viewer/assets/{html-derivative-ZfzPj0aS.js → html-derivative-CkVRX-vn.js} +1 -1
- package/viewer/assets/{http-BDE2UDvO.js → http-cQ-nDL4L.js} +1 -1
- package/viewer/assets/{hurl-CQbXNLlk.js → hurl-D5phPwZN.js} +1 -1
- package/viewer/assets/{index-OL_rrWNy.css → index-DdaXmlLv.css} +1 -1
- package/viewer/assets/{index-BezQIu6a.js → index-Dhnl4xDm.js} +4 -4
- package/viewer/assets/{info-A6RAGUB7-YD13oMfx.js → info-A6RAGUB7-CRDxOY87.js} +1 -1
- package/viewer/assets/{infoDiagram-27XIBGKW-jYKjA_l7.js → infoDiagram-27XIBGKW-D6y32CfO.js} +1 -1
- package/viewer/assets/{ishikawaDiagram-5VMMS53U-BBPzHiVd.js → ishikawaDiagram-5VMMS53U-BWFBTCh5.js} +1 -1
- package/viewer/assets/{java-Cbpu4oyT.js → java-CT3DXSYV.js} +1 -1
- package/viewer/assets/{javascript-UMuq64YD.js → javascript-BPmsAXaP.js} +1 -1
- package/viewer/assets/{jinja-DVvZtqgU.js → jinja-Ct_36dEc.js} +1 -1
- package/viewer/assets/{jison-CZCXBIV3.js → jison-BYrmy85c.js} +1 -1
- package/viewer/assets/{journeyDiagram-3NMN7TZE-CMqG5ndb.js → journeyDiagram-3NMN7TZE-6XCObBEx.js} +1 -1
- package/viewer/assets/{json-CzTvWngu.js → json-BCFP8udh.js} +1 -1
- package/viewer/assets/{jsx-CNJnEGR4.js → jsx-Dgzhn0Dq.js} +1 -1
- package/viewer/assets/{julia-Bj0q4uoH.js → julia-DIddcjVY.js} +1 -1
- package/viewer/assets/{just-93-MRGUC.js → just-C5t963St.js} +1 -1
- package/viewer/assets/{kanban-definition-UXKFOSKX-VyFYPq9b.js → kanban-definition-UXKFOSKX-DU1B45HL.js} +1 -1
- package/viewer/assets/{latex-CXd1tMjA.js → latex-BKZooW5w.js} +1 -1
- package/viewer/assets/{line-D5nvVDHp.js → line-lYgs1iW7.js} +1 -1
- package/viewer/assets/{linear-Cq-FJZ_z.js → linear-sHqbNFGm.js} +1 -1
- package/viewer/assets/{liquid-Cb-ALXSr.js → liquid-_PLVu-8C.js} +1 -1
- package/viewer/assets/{lua-BxTQiamn.js → lua-BWNAOGrF.js} +1 -1
- package/viewer/assets/{marko-mFYyU5jn.js → marko-DC7qz2de.js} +1 -1
- package/viewer/assets/{mdc-Oryqox7_.js → mdc-Bn_2wB99.js} +1 -1
- package/viewer/assets/{mermaid-parser.core-CCDsanlH.js → mermaid-parser.core-jzj7JZTj.js} +3 -3
- package/viewer/assets/{mermaid.core-Crd1YMgi.js → mermaid.core-DpRyWAPv.js} +4 -4
- package/viewer/assets/{mindmap-definition-YA3MSWOX-BKLrEFWt.js → mindmap-definition-YA3MSWOX-CIZjqKbE.js} +1 -1
- package/viewer/assets/{nginx-C1PwWP7d.js → nginx-Ce7JLwNg.js} +1 -1
- package/viewer/assets/{nim-PszXW-l4.js → nim-CP_MVph6.js} +1 -1
- package/viewer/assets/{org-DO0CuJlO.js → org-BVKft-Jg.js} +1 -1
- package/viewer/assets/{packet-AYTQ26CC-BNPwLE6g.js → packet-AYTQ26CC-BwRTCCV1.js} +1 -1
- package/viewer/assets/{pegDiagram-XKGWAZYB-DjBg_iPh.js → pegDiagram-XKGWAZYB-3nilNfrm.js} +1 -1
- package/viewer/assets/{perl-CHhAgoDL.js → perl-DRhmHTw_.js} +1 -1
- package/viewer/assets/{php-CA4H6qnu.js → php-CrnivzIK.js} +1 -1
- package/viewer/assets/{pie-WAS4IAKB-BzNmHX-Y.js → pie-WAS4IAKB-Dn0ztV-V.js} +1 -1
- package/viewer/assets/{pieDiagram-E7YTZNPT-cgMB-fBw.js → pieDiagram-E7YTZNPT-BFj69QcO.js} +1 -1
- package/viewer/assets/{pug-DDuKTe7C.js → pug-BfP-YwOD.js} +1 -1
- package/viewer/assets/{qml-DSd2VypD.js → qml-BHf9C17M.js} +1 -1
- package/viewer/assets/{quadrantDiagram-AXDQQJYC-Crzhs7Np.js → quadrantDiagram-AXDQQJYC-2t5I-_4_.js} +1 -1
- package/viewer/assets/{r-qf-5qR5Q.js → r-DjPtVJdp.js} +1 -1
- package/viewer/assets/{radar-RG4KPBEZ-B3oI70pB.js → radar-RG4KPBEZ-D14Z-9c0.js} +1 -1
- package/viewer/assets/{railroad-74A4TZTK-DOM5Od17.js → railroad-74A4TZTK-BuxyK73P.js} +1 -1
- package/viewer/assets/railroad-abnf-HS5TGJTU-BM7gtNXd.js +1 -0
- package/viewer/assets/railroad-ebnf-LZEXJU2U-DPlBcoln.js +1 -0
- package/viewer/assets/railroad-peg-WCYAUIDC-B6tOGY79.js +1 -0
- package/viewer/assets/{railroadDiagram-O6MQD6OU-M3SvSYf-.js → railroadDiagram-O6MQD6OU-Dm2mv6N0.js} +1 -1
- package/viewer/assets/{razor-B9n9DtIL.js → razor-BH3XX51e.js} +1 -1
- package/viewer/assets/{regexp-DhGN0EOR.js → regexp-DPzTmgy2.js} +1 -1
- package/viewer/assets/{requirementDiagram-BXWQKSXE-CFisiZTo.js → requirementDiagram-BXWQKSXE-BJTZ8ml9.js} +1 -1
- package/viewer/assets/{rst-D1SdhuXd.js → rst-DQbfRM9X.js} +1 -1
- package/viewer/assets/{ruby-BybsgZgf.js → ruby-DxybHg9D.js} +1 -1
- package/viewer/assets/{sankeyDiagram-P5KCCOFB-BKf2TMWg.js → sankeyDiagram-P5KCCOFB-eBLIKN_A.js} +1 -1
- package/viewer/assets/{sas-Io7QwCtD.js → sas-DoUzRWOs.js} +1 -1
- package/viewer/assets/{scss-D6XY0yo3.js → scss-BSqQF9Aq.js} +1 -1
- package/viewer/assets/{sequenceDiagram-WJ2MYXX4-CHawNQZ9.js → sequenceDiagram-WJ2MYXX4-BxHxbxqV.js} +1 -1
- package/viewer/assets/{shellscript-DSk8kvCh.js → shellscript-DkZJ1HjW.js} +1 -1
- package/viewer/assets/{shellsession-Be1CN8DO.js → shellsession-ykUTtcwM.js} +1 -1
- package/viewer/assets/{soy-CGkkqGyT.js → soy-4rmF0gor.js} +1 -1
- package/viewer/assets/{sql-DIgb716V.js → sql-wJX-vNm_.js} +1 -1
- package/viewer/assets/{src-DP6Z6M4G.js → src-IhQtBUXO.js} +1 -1
- package/viewer/assets/{stata-CS_2tb9p.js → stata-Bjw5vh7C.js} +1 -1
- package/viewer/assets/{stateDiagram-D77RDMKH-Cx9Rf6fY.js → stateDiagram-D77RDMKH-DNPBpLlw.js} +1 -1
- package/viewer/assets/stateDiagram-v2-MP3YSRHH-sLT-i9tN.js +1 -0
- package/viewer/assets/{surrealql-Do4o8w1B.js → surrealql-B-cZs35d.js} +1 -1
- package/viewer/assets/{svelte-3bKxeV6-.js → svelte-BtYh0aTf.js} +1 -1
- package/viewer/assets/{swimlanes-42K2YHIH-yL2oIw_D.js → swimlanes-42K2YHIH-CnHbZ76W.js} +1 -1
- package/viewer/assets/swimlanesDiagram-VR7AAH4N-DIpLefsN.js +8 -0
- package/viewer/assets/{templ-BgEYPNGP.js → templ-eYWdvrUr.js} +1 -1
- package/viewer/assets/{tex-D_p6whQw.js → tex-DQ-rt_Oj.js} +1 -1
- package/viewer/assets/{timeline-definition-24CTP7MA-BJeLq4a5.js → timeline-definition-24CTP7MA-vOTFZgf6.js} +1 -1
- package/viewer/assets/{treeView-Q6P3EWNA-COcnImiE.js → treeView-Q6P3EWNA-CU_mSunf.js} +1 -1
- package/viewer/assets/{treemap-WGGIJYW6-DpbxWots.js → treemap-WGGIJYW6-BbLri-XL.js} +1 -1
- package/viewer/assets/{ts-tags-GBmMk7Oh.js → ts-tags-CspMWJ3s.js} +1 -1
- package/viewer/assets/{tsx-BSD-yNhi.js → tsx-CXUmELmz.js} +1 -1
- package/viewer/assets/{twig-dwAG5SzX.js → twig-B0yioFX1.js} +1 -1
- package/viewer/assets/{typescript-B8YTPl9v.js → typescript-vMN-etbi.js} +1 -1
- package/viewer/assets/{typst-Dpzojbzo.js → typst-Cvk0bEbK.js} +1 -1
- package/viewer/assets/{vennDiagram-4TSXK5OY-nF34Gjtn.js → vennDiagram-4TSXK5OY-PBkXHE6T.js} +1 -1
- package/viewer/assets/{vue-Djbmk2DC.js → vue-Ce21QNcM.js} +1 -1
- package/viewer/assets/{vue-html-BXc5rfco.js → vue-html-DsNf3O2s.js} +1 -1
- package/viewer/assets/{vue-vine-DHCfQh17.js → vue-vine-BsGtGXV3.js} +1 -1
- package/viewer/assets/{wardley-WFR3VGLG-C1qhMifc.js → wardley-WFR3VGLG-kwPRtpGA.js} +1 -1
- package/viewer/assets/{wardleyDiagram-VM6X3IG4-B0BfyYWQ.js → wardleyDiagram-VM6X3IG4-BacMGxmX.js} +1 -1
- package/viewer/assets/{xml-CdCEskcV.js → xml-BmjAaPhA.js} +1 -1
- package/viewer/assets/{xsl-BbNukwXP.js → xsl-CtfA1v1E.js} +1 -1
- package/viewer/assets/{xychartDiagram-S5SC5T6Z-DJKYce1U.js → xychartDiagram-S5SC5T6Z-BDIbR1Lm.js} +1 -1
- package/viewer/assets/{yaml-BYLDET4A.js → yaml-A9EbijaD.js} +1 -1
- package/viewer/index.html +2 -2
- package/viewer/assets/architecture-7GRP2DOG-D3Kw34KW.js +0 -1
- package/viewer/assets/channel-D2lVv6ST.js +0 -1
- package/viewer/assets/classDiagram-ZZMXUADV-DqBNvABf.js +0 -1
- package/viewer/assets/classDiagram-v2-VYDZK3BY-DqBNvABf.js +0 -1
- package/viewer/assets/eventmodeling-NTZA5JFV-07UfGVCm.js +0 -1
- package/viewer/assets/flowDiagram-HODETNUW-CyIUf7TW.js +0 -1
- package/viewer/assets/railroad-abnf-HS5TGJTU-ScZ2h_5Q.js +0 -1
- package/viewer/assets/railroad-ebnf-LZEXJU2U-_Cl5zxJ-.js +0 -1
- package/viewer/assets/railroad-peg-WCYAUIDC-CRlGg7vV.js +0 -1
- package/viewer/assets/stateDiagram-v2-MP3YSRHH-DRb-6t_S.js +0 -1
- package/viewer/assets/swimlanesDiagram-VR7AAH4N-cDiCT6xi.js +0 -8
|
@@ -15,7 +15,9 @@ export async function compileCodeJudge(
|
|
|
15
15
|
dependencies[sdkManifest] = await Bun.file(sdkManifest).text();
|
|
16
16
|
const external = new Set<string>();
|
|
17
17
|
const built = await Bun.build({
|
|
18
|
-
|
|
18
|
+
// Import only the judge function: unused reporting metadata is tree-shaken.
|
|
19
|
+
// If grading reads metadata, it remains executable and must affect the hash.
|
|
20
|
+
entrypoints: ["openeval:judge-entry"],
|
|
19
21
|
root: dirname(file),
|
|
20
22
|
target: "bun",
|
|
21
23
|
format: "esm",
|
|
@@ -25,6 +27,11 @@ export async function compileCodeJudge(
|
|
|
25
27
|
{
|
|
26
28
|
name: "judge-dependencies",
|
|
27
29
|
setup(build) {
|
|
30
|
+
build.onResolve({ filter: /^openeval:judge-entry$/ }, () => ({ path: "judge.js", namespace: "openeval-judge" }));
|
|
31
|
+
build.onLoad({ filter: /.*/, namespace: "openeval-judge" }, () => ({
|
|
32
|
+
contents: `export { default } from ${JSON.stringify(file.replaceAll("\\", "/"))};`,
|
|
33
|
+
loader: "js", resolveDir: dirname(file),
|
|
34
|
+
}));
|
|
28
35
|
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
29
36
|
if (
|
|
30
37
|
args.path.startsWith("node:") ||
|
|
@@ -91,6 +98,7 @@ export async function compileCodeJudge(
|
|
|
91
98
|
source,
|
|
92
99
|
sourceMap,
|
|
93
100
|
dependencies,
|
|
94
|
-
|
|
101
|
+
// Debug mappings and original-source metadata are retained, but not executable inputs.
|
|
102
|
+
hash: fingerprint({ source, dependencies }),
|
|
95
103
|
};
|
|
96
104
|
}
|
|
@@ -2,25 +2,27 @@ import { pathToFileURL } from "node:url";
|
|
|
2
2
|
import { openRecording, type RecordingInput } from "../recording";
|
|
3
3
|
import { jsonOutput } from "./code-result";
|
|
4
4
|
import { errorMessage, writeJson } from "../files";
|
|
5
|
-
import type { JudgeFunction, RunMetrics } from "../../judge-context";
|
|
5
|
+
import type { JudgeFunction, RunMetrics, VerificationResult } from "../../judge-context";
|
|
6
6
|
|
|
7
7
|
type Job = RecordingInput & { module: string; output: string; scratch: string };
|
|
8
8
|
const job: Job = JSON.parse(await Bun.stdin.text());
|
|
9
9
|
let metrics: RunMetrics | undefined;
|
|
10
|
+
let verifications: VerificationResult[] | undefined;
|
|
10
11
|
async function grade() {
|
|
11
12
|
await using context = await openRecording(job, job.scratch);
|
|
12
13
|
metrics = context.metrics;
|
|
14
|
+
verifications = context.verificationResults;
|
|
13
15
|
const module = await import(pathToFileURL(job.module).href);
|
|
14
16
|
if (typeof module.default !== "function")
|
|
15
17
|
throw new Error("judge.ts must default-export a function");
|
|
16
18
|
const output = jsonOutput(await (module.default as JudgeFunction)(context));
|
|
17
|
-
return { output, metrics: context.metrics };
|
|
19
|
+
return { output, metrics: context.metrics, verifications: context.verificationResults };
|
|
18
20
|
}
|
|
19
21
|
try {
|
|
20
22
|
await writeJson(job.output, await grade());
|
|
21
23
|
process.exit(0);
|
|
22
24
|
} catch (error) {
|
|
23
|
-
await writeJson(job.output, { error: errorMessage(error), metrics });
|
|
25
|
+
await writeJson(job.output, { error: errorMessage(error), metrics, verifications });
|
|
24
26
|
console.error(error);
|
|
25
27
|
process.exit(1);
|
|
26
28
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, rm, readdir } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import type {
|
|
5
6
|
CodeJudgeDefinition,
|
|
6
7
|
CodeJudgeExecution,
|
|
7
8
|
JsonValue,
|
|
8
9
|
RunMetrics,
|
|
10
|
+
VerificationRuntime,
|
|
11
|
+
VerificationResult,
|
|
9
12
|
} from "../../judge-context";
|
|
10
13
|
import type { RecordingInput } from "../recording";
|
|
11
14
|
import { codeJudgment, jsonOutput } from "./code-result";
|
|
12
15
|
import { errorMessage, writeJson } from "../files";
|
|
16
|
+
import { closeVerification } from "../verification/session";
|
|
13
17
|
|
|
14
18
|
/** A separate process bounds synchronous loops as well as asynchronous judges. */
|
|
15
19
|
export async function executeCodeJudge(
|
|
@@ -17,6 +21,7 @@ export async function executeCodeJudge(
|
|
|
17
21
|
recording: RecordingInput,
|
|
18
22
|
directory: string,
|
|
19
23
|
timeoutMs: number,
|
|
24
|
+
verification?: VerificationRuntime,
|
|
20
25
|
): Promise<CodeJudgeExecution> {
|
|
21
26
|
directory = resolve(directory);
|
|
22
27
|
await mkdir(directory, { recursive: true });
|
|
@@ -31,7 +36,8 @@ export async function executeCodeJudge(
|
|
|
31
36
|
const startedAt = new Date().toISOString();
|
|
32
37
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
33
38
|
let timedOut = false;
|
|
34
|
-
let raw: JsonValue | undefined, metrics: RunMetrics | undefined;
|
|
39
|
+
let raw: JsonValue | undefined, metrics: RunMetrics | undefined, verifications: VerificationResult[] | undefined;
|
|
40
|
+
const owner = randomUUID();
|
|
35
41
|
const base = { startedAt, sourceHash: code.hash, stdout, stderr };
|
|
36
42
|
try {
|
|
37
43
|
const child = Bun.spawn(
|
|
@@ -52,7 +58,9 @@ export async function executeCodeJudge(
|
|
|
52
58
|
child.kill();
|
|
53
59
|
}, timeoutMs);
|
|
54
60
|
await child.stdin.write(
|
|
55
|
-
JSON.stringify({ ...recording, module, output, scratch
|
|
61
|
+
JSON.stringify({ ...recording, module, output, scratch, verification: {
|
|
62
|
+
environment: verification, directory, owner, deadlineAt: Date.now() + timeoutMs,
|
|
63
|
+
} }),
|
|
56
64
|
);
|
|
57
65
|
child.stdin.end();
|
|
58
66
|
const exit = await child.exited;
|
|
@@ -60,6 +68,7 @@ export async function executeCodeJudge(
|
|
|
60
68
|
throw new Error(`judge.ts exceeded its ${timeoutMs}ms time limit`);
|
|
61
69
|
const result = await Bun.file(output).json();
|
|
62
70
|
metrics = result.metrics;
|
|
71
|
+
verifications = result.verifications;
|
|
63
72
|
if (exit !== 0 || result.error)
|
|
64
73
|
throw new Error(result.error ?? `judge.ts exited with code ${exit}`);
|
|
65
74
|
raw = jsonOutput(result.output);
|
|
@@ -71,11 +80,16 @@ export async function executeCodeJudge(
|
|
|
71
80
|
elapsedMs: Date.now() - Date.parse(startedAt),
|
|
72
81
|
output: raw,
|
|
73
82
|
metrics,
|
|
83
|
+
verifications,
|
|
74
84
|
scores: Object.fromEntries(
|
|
75
85
|
Object.entries(judgment.scores).map(([id, score]) => [id, score.value]),
|
|
76
86
|
),
|
|
77
87
|
};
|
|
78
88
|
} catch (error) {
|
|
89
|
+
// Completed receipts survive a killed worker or a later malformed submission.
|
|
90
|
+
verifications ??= await Promise.all((await readdir(resolve(directory, "verification")).catch(() => []))
|
|
91
|
+
.map(id => Bun.file(resolve(directory, "verification", id, "receipt.json")).json().catch(() => undefined)))
|
|
92
|
+
.then(items => items.filter((item): item is VerificationResult => !!item));
|
|
79
93
|
return {
|
|
80
94
|
...base,
|
|
81
95
|
state: timedOut ? "timed_out" : "failed",
|
|
@@ -84,9 +98,11 @@ export async function executeCodeJudge(
|
|
|
84
98
|
error: errorMessage(error),
|
|
85
99
|
output: raw,
|
|
86
100
|
metrics,
|
|
101
|
+
verifications,
|
|
87
102
|
};
|
|
88
103
|
} finally {
|
|
89
104
|
clearTimeout(timer);
|
|
105
|
+
if (verification) await closeVerification(verification.engine, owner).catch(() => {});
|
|
90
106
|
await rm(scratch, {
|
|
91
107
|
recursive: true,
|
|
92
108
|
force: true,
|
|
@@ -81,10 +81,10 @@ export function validateCriteria(
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function parseCitation(value: unknown): EvidenceCitation {
|
|
84
|
+
export function parseCitation(value: unknown): EvidenceCitation {
|
|
85
85
|
if (
|
|
86
86
|
!object(value) ||
|
|
87
|
-
!["response", "tool", "message", "event", "artifact", "metric"].includes(
|
|
87
|
+
!["response", "recording", "tool", "message", "event", "artifact", "metric", "verification"].includes(
|
|
88
88
|
String(value.kind),
|
|
89
89
|
)
|
|
90
90
|
)
|
|
@@ -104,6 +104,12 @@ function parseCitation(value: unknown): EvidenceCitation {
|
|
|
104
104
|
...(value.offset !== undefined ? { offset: value.offset as number } : {}),
|
|
105
105
|
};
|
|
106
106
|
if (value.kind === "response") return { kind: "response", ...extra };
|
|
107
|
+
if (value.kind === "recording") return { kind: "recording", ...extra };
|
|
108
|
+
if (value.kind === "verification") {
|
|
109
|
+
if (!reason(value.id) || (value.path !== undefined && !reason(value.path)))
|
|
110
|
+
throw new Error("Verification citations require an ID and an optional output path");
|
|
111
|
+
return { kind: "verification", id: value.id, ...(value.path ? { path: value.path as string } : {}), ...extra };
|
|
112
|
+
}
|
|
107
113
|
if (value.kind === "artifact") {
|
|
108
114
|
if (
|
|
109
115
|
!reason(value.path) ||
|
|
@@ -134,6 +140,8 @@ export async function validateCitations(
|
|
|
134
140
|
) {
|
|
135
141
|
for (const [id, score] of Object.entries(judgment.scores))
|
|
136
142
|
for (const citation of score.evidence) {
|
|
143
|
+
if (citation.kind === "verification")
|
|
144
|
+
throw new Error("Verification citations must be checked against their JudgeRun receipts, not candidate evidence");
|
|
137
145
|
const query: EvidenceQuery =
|
|
138
146
|
citation.kind === "recording"
|
|
139
147
|
? { action: "summary" }
|
|
@@ -39,6 +39,7 @@ export const judgingFingerprint = async () =>
|
|
|
39
39
|
composition: await Bun.file(
|
|
40
40
|
fileURLToPath(new URL("../../app/grade-recording.ts", import.meta.url)),
|
|
41
41
|
).text(),
|
|
42
|
+
verification: await treeHash(fileURLToPath(new URL("../verification/", import.meta.url)), { ignore: /\.test\.[jt]sx?$/ }),
|
|
42
43
|
useCase: await Bun.file(
|
|
43
44
|
fileURLToPath(new URL("../../app/judge-run.ts", import.meta.url)),
|
|
44
45
|
).text(),
|
|
@@ -6,24 +6,28 @@ import type {
|
|
|
6
6
|
JudgeContext,
|
|
7
7
|
RecordedMessage,
|
|
8
8
|
Revision,
|
|
9
|
+
VerificationRuntime,
|
|
10
|
+
VerificationResult,
|
|
9
11
|
} from "../../judge-context";
|
|
10
12
|
import type { EvalRun, EvidenceRef } from "../../types";
|
|
11
13
|
import { CandidateEvidence } from "../evidence";
|
|
12
14
|
import { contained, hash } from "../files";
|
|
13
15
|
import { OPENCODE_VERSION } from "../opencode/version";
|
|
14
16
|
import { measureRecording } from "./metrics";
|
|
17
|
+
import { verificationSession } from "../verification/session";
|
|
15
18
|
|
|
16
19
|
export type RecordingInput = {
|
|
17
20
|
evidence: EvidenceRef;
|
|
18
21
|
run?: EvalRun;
|
|
19
22
|
runDirectory?: string;
|
|
23
|
+
verification?: { environment?: VerificationRuntime; directory: string; owner: string; deadlineAt: number };
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
/** Own disposable native readers; the authoritative archive is only read. */
|
|
23
27
|
export async function openRecording(
|
|
24
28
|
input: RecordingInput,
|
|
25
29
|
workDirectory: string,
|
|
26
|
-
): Promise<JudgeContext & AsyncDisposable> {
|
|
30
|
+
): Promise<JudgeContext & AsyncDisposable & { verificationResults: VerificationResult[] }> {
|
|
27
31
|
const reference = {
|
|
28
32
|
...input.evidence,
|
|
29
33
|
directory: resolve(input.evidence.directory),
|
|
@@ -86,7 +90,7 @@ export async function openRecording(
|
|
|
86
90
|
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
87
91
|
await evidence.readFile(path, revision),
|
|
88
92
|
);
|
|
89
|
-
|
|
93
|
+
const context: Omit<JudgeContext, "verification"> & AsyncDisposable = {
|
|
90
94
|
response: { text: evidence.manifest.response?.text ?? "" },
|
|
91
95
|
prompt: evidence.manifest.prompt,
|
|
92
96
|
run: run ? structuredClone(run) : null,
|
|
@@ -237,4 +241,12 @@ export async function openRecording(
|
|
|
237
241
|
}
|
|
238
242
|
},
|
|
239
243
|
};
|
|
244
|
+
const verifier = verificationSession({
|
|
245
|
+
...input.verification,
|
|
246
|
+
directory: input.verification?.directory ?? workDirectory,
|
|
247
|
+
owner: input.verification?.owner ?? "unconfigured",
|
|
248
|
+
deadlineAt: input.verification?.deadlineAt ?? Date.now() + 120_000,
|
|
249
|
+
materialize: context.workspace.materialize,
|
|
250
|
+
});
|
|
251
|
+
return { ...context, verification: verifier.context, verificationResults: verifier.results };
|
|
240
252
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { VerificationEnvironment, VerificationRuntime } from "../../judge-context";
|
|
3
|
+
import { engineCommand } from "../containers/docker";
|
|
4
|
+
import { treeHash } from "../files";
|
|
5
|
+
|
|
6
|
+
export const VERIFICATION_IMAGE = "openeval-verification:0.5.0";
|
|
7
|
+
const directory = fileURLToPath(new URL("./runtime/", import.meta.url));
|
|
8
|
+
|
|
9
|
+
export async function buildVerificationImage(environment: VerificationEnvironment) {
|
|
10
|
+
if (environment.image !== VERIFICATION_IMAGE)
|
|
11
|
+
throw new Error("Build custom verification images with your container engine; image --verification builds the standard image only");
|
|
12
|
+
await engineCommand(environment.engine ?? "docker", [
|
|
13
|
+
"build", "--label", `openeval.verification=${await treeHash(directory)}`,
|
|
14
|
+
"-t", environment.image, directory,
|
|
15
|
+
], { timeoutMs: 600_000 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function verificationRuntime(environment?: VerificationEnvironment): Promise<VerificationRuntime | undefined> {
|
|
19
|
+
if (!environment) return undefined;
|
|
20
|
+
const engine = environment.engine ?? "docker";
|
|
21
|
+
if (!["docker", "podman"].includes(engine) || !environment.image || /\s/.test(environment.image) ||
|
|
22
|
+
[environment.cpus ?? 2, environment.memoryMiB ?? 4096].some(value => !Number.isSafeInteger(value) || value < 1))
|
|
23
|
+
throw new Error("Verification requires a valid image, engine, and positive integer resource limits");
|
|
24
|
+
const output = await engineCommand(engine, [
|
|
25
|
+
"image", "inspect", environment.image, "--format",
|
|
26
|
+
'{{.Id}}|{{index .Config.Labels "openeval.verification"}}',
|
|
27
|
+
]);
|
|
28
|
+
const [imageId, label] = output.split("|");
|
|
29
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(imageId)) throw new Error("Verification image has no immutable image ID");
|
|
30
|
+
if (environment.image === VERIFICATION_IMAGE && label !== await treeHash(directory))
|
|
31
|
+
throw new Error("Verification runtime changed. Run image --verification first.");
|
|
32
|
+
return { engine, image: environment.image, imageId, cpus: environment.cpus ?? 2, memoryMiB: environment.memoryMiB ?? 4096 };
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM mcr.microsoft.com/playwright:v1.63.0-noble
|
|
2
|
+
COPY --from=oven/bun:1.4.2 /usr/local/bin/bun /usr/local/bin/bun
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git ripgrep python3 g++ make tar coreutils \
|
|
4
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5
|
+
WORKDIR /opt/verify
|
|
6
|
+
COPY package.json bun.lock ./
|
|
7
|
+
RUN bun install --frozen-lockfile --ignore-scripts \
|
|
8
|
+
&& useradd --create-home --uid 10001 verify \
|
|
9
|
+
&& mkdir -p /workspace /verification \
|
|
10
|
+
&& chown verify:verify /workspace \
|
|
11
|
+
&& git config --system --add safe.directory '*'
|
|
12
|
+
ENV HOME=/tmp/home
|
|
13
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
|
14
|
+
ENV NODE_PATH=/opt/verify/node_modules
|
|
15
|
+
WORKDIR /workspace
|
|
16
|
+
USER 10001:10001
|
|
17
|
+
ENTRYPOINT []
|
|
18
|
+
CMD ["sleep", "infinity"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 2,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"playwright": "1.63.0",
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
"packages": {
|
|
12
|
+
"playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="],
|
|
13
|
+
|
|
14
|
+
"playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="],
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rm, lstat } from "node:fs/promises";
|
|
3
|
+
import { resolve, dirname, posix } from "node:path";
|
|
4
|
+
import type { VerificationRequest, VerificationResult, VerificationRuntime } from "../../judge-context";
|
|
5
|
+
import type { Engine } from "../../types";
|
|
6
|
+
import { engineCommand } from "../containers/docker";
|
|
7
|
+
import { extractWorkspaceArchive } from "../containers/transfer";
|
|
8
|
+
import { contained, hash, relativePath, writeJson } from "../files";
|
|
9
|
+
|
|
10
|
+
const LOG_BYTES = 1024 * 1024;
|
|
11
|
+
const OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const INPUT_BYTES = 128 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
export function verificationPath(value: string): string {
|
|
15
|
+
relativePath(value, "Verification path");
|
|
16
|
+
if (value.includes("\\") || value.includes("\0") || value.startsWith("/") || /^[a-z]:/i.test(value))
|
|
17
|
+
throw new Error("Verification paths must use contained POSIX-relative paths");
|
|
18
|
+
return posix.normalize(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** No host mounts, credentials, published ports, network, or elevated capabilities. */
|
|
22
|
+
export function verificationArgs(runtime: VerificationRuntime, owner: string, name: string, timeoutMs: number) {
|
|
23
|
+
return [
|
|
24
|
+
"run", "--detach", "--rm", "--init", "--name", name,
|
|
25
|
+
"--label", `openeval.verification-owner=${owner}`,
|
|
26
|
+
"--network=none", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--read-only",
|
|
27
|
+
"--pids-limit", "128", "--cpus", String(runtime.cpus), "--memory", `${runtime.memoryMiB}m`,
|
|
28
|
+
"--memory-swap", `${runtime.memoryMiB}m`, "--user", "10001:10001",
|
|
29
|
+
"--tmpfs", "/workspace:rw,exec,nosuid,nodev,mode=1777,size=512m",
|
|
30
|
+
"--tmpfs", "/verification:rw,exec,nosuid,nodev,mode=0755,size=64m",
|
|
31
|
+
"--tmpfs", "/tmp:rw,exec,nosuid,nodev,mode=1777,size=256m",
|
|
32
|
+
"--env", "HOME=/tmp/home", "--env", "CI=1",
|
|
33
|
+
"--entrypoint", "timeout", runtime.imageId,
|
|
34
|
+
"--signal=TERM", "--kill-after=5s", `${Math.ceil(timeoutMs / 1000)}s`, "sleep", "infinity",
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function bounded(stream: ReadableStream<Uint8Array>, limit: number, overflow?: () => void) {
|
|
39
|
+
const chunks: Uint8Array[] = [];
|
|
40
|
+
let bytes = 0, truncated = false;
|
|
41
|
+
for await (const chunk of stream) {
|
|
42
|
+
const room = Math.max(0, limit - bytes);
|
|
43
|
+
if (room) chunks.push(chunk.slice(0, room));
|
|
44
|
+
bytes += Math.min(room, chunk.length);
|
|
45
|
+
if (chunk.length > room) {
|
|
46
|
+
truncated = true;
|
|
47
|
+
overflow?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { bytes: Buffer.concat(chunks), truncated };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Remove only containers owned by this code-judge execution, including after worker death. */
|
|
54
|
+
export async function closeVerification(engine: Engine, owner: string) {
|
|
55
|
+
const names = await engineCommand(engine, ["ps", "-aq", "--filter", `label=openeval.verification-owner=${owner}`]);
|
|
56
|
+
if (names.trim()) await engineCommand(engine, ["rm", "--force", ...names.split(/\s+/)]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function verificationSession(options: {
|
|
60
|
+
environment?: VerificationRuntime;
|
|
61
|
+
directory: string;
|
|
62
|
+
owner: string;
|
|
63
|
+
deadlineAt: number;
|
|
64
|
+
materialize: (revision: "initial" | "final") => Promise<string>;
|
|
65
|
+
}) {
|
|
66
|
+
const results: VerificationResult[] = [];
|
|
67
|
+
const outputs = new Map<string, VerificationResult>();
|
|
68
|
+
const read = async (result: VerificationResult, path: string) => {
|
|
69
|
+
const retained = outputs.get(result.id);
|
|
70
|
+
if (!retained) throw new Error("Read outputs from a verification produced by this context");
|
|
71
|
+
const artifact = retained.artifacts.find(item => item.path === path);
|
|
72
|
+
const file = artifact?.file ?? (path === "stdout" ? retained.stdout : path === "stderr" ? retained.stderr : undefined);
|
|
73
|
+
if (!file) throw new Error(`Verification did not retain ${path}`);
|
|
74
|
+
const bytes = await Bun.file(contained(options.directory, file)).bytes();
|
|
75
|
+
if (artifact && hash(bytes) !== artifact.sha256) throw new Error("Verification artifact hash mismatch");
|
|
76
|
+
return bytes;
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
results,
|
|
80
|
+
context: {
|
|
81
|
+
read,
|
|
82
|
+
async text(result: VerificationResult, path: string) {
|
|
83
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(await read(result, path));
|
|
84
|
+
},
|
|
85
|
+
async run(request: VerificationRequest): Promise<VerificationResult> {
|
|
86
|
+
const runtime = options.environment;
|
|
87
|
+
if (!runtime) throw new Error("Set judge.verification and build its image before verifying artifacts");
|
|
88
|
+
if (!Array.isArray(request.commands) || !request.commands.length || request.commands.some(argv =>
|
|
89
|
+
!Array.isArray(argv) || !argv.length || argv.some(arg => typeof arg !== "string" || !arg || arg.includes("\0"))))
|
|
90
|
+
throw new Error("Verification requires non-empty command argv arrays");
|
|
91
|
+
const requested = request.timeoutMs ?? 120_000;
|
|
92
|
+
if (!Number.isSafeInteger(requested) || requested < 1) throw new Error("Verification timeout must be a positive integer");
|
|
93
|
+
const timeoutMs = Math.min(requested, options.deadlineAt - Date.now());
|
|
94
|
+
if (timeoutMs <= 0) throw new Error("Code-judge deadline was reached before verification");
|
|
95
|
+
const cwd = verificationPath(request.cwd ?? ".");
|
|
96
|
+
const artifacts = (request.artifacts ?? []).map(verificationPath);
|
|
97
|
+
if (artifacts.some(path => path === "stdout" || path === "stderr"))
|
|
98
|
+
throw new Error("stdout and stderr are reserved verification log names");
|
|
99
|
+
const revision = request.revision ?? "final";
|
|
100
|
+
if (revision !== "initial" && revision !== "final") throw new Error("Unknown verification revision");
|
|
101
|
+
const id = `verification_${randomUUID()}`;
|
|
102
|
+
const directory = resolve(options.directory, "verification", id);
|
|
103
|
+
const staging = resolve(directory, ".transfer");
|
|
104
|
+
const trusted = resolve(staging, "trusted");
|
|
105
|
+
await mkdir(trusted, { recursive: true });
|
|
106
|
+
let trustedBytes = 0;
|
|
107
|
+
for (const [path, content] of Object.entries(request.files ?? {})) {
|
|
108
|
+
if (typeof content !== "string") throw new Error("Verification file contents must be text");
|
|
109
|
+
trustedBytes += Buffer.byteLength(content);
|
|
110
|
+
if (trustedBytes > OUTPUT_BYTES) throw new Error("Trusted verification inputs exceed 32 MiB");
|
|
111
|
+
const target = contained(trusted, verificationPath(path));
|
|
112
|
+
await mkdir(dirname(target), { recursive: true });
|
|
113
|
+
await Bun.write(target, content);
|
|
114
|
+
}
|
|
115
|
+
const workspace = await options.materialize(revision);
|
|
116
|
+
const archive = resolve(staging, "workspace.tar");
|
|
117
|
+
const pack = Bun.spawn(["tar", "-cf", archive, "-C", workspace, "."], { stdout: "ignore", stderr: "pipe" });
|
|
118
|
+
const [packed, packError] = await Promise.all([pack.exited, new Response(pack.stderr).text()]);
|
|
119
|
+
if (packed || (await lstat(archive)).size > INPUT_BYTES)
|
|
120
|
+
throw new Error(packed ? `Verification transfer failed: ${packError}` : "Restored verification workspace exceeds 128 MiB");
|
|
121
|
+
const name = `openeval-verify-${randomUUID()}`;
|
|
122
|
+
const startedAt = new Date().toISOString();
|
|
123
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
124
|
+
const commands: VerificationResult["commands"] = [];
|
|
125
|
+
let stdout = "", stderr = "", logsTruncated = false, timedOut = false, exitCode: number | null = null;
|
|
126
|
+
const result: VerificationResult = {
|
|
127
|
+
id, state: "completed", revision, environment: runtime, startedAt, elapsedMs: 0,
|
|
128
|
+
commands, exitCode, stdout: "", stderr: "", logsTruncated: false, artifacts: [], missingArtifacts: [],
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await engineCommand(runtime.engine, verificationArgs(runtime, options.owner, name, timeoutMs));
|
|
132
|
+
await engineCommand(runtime.engine, ["exec", "--interactive", "--user", "10001:10001", name,
|
|
133
|
+
"tar", "--no-same-owner", "--no-overwrite-dir", "-xf", "-", "-C", "/workspace"], { input: archive, timeoutMs });
|
|
134
|
+
// docker cp rejects a read-only rootfs even when its target is tmpfs.
|
|
135
|
+
// Only this trusted upload runs as root; artifact commands remain non-root.
|
|
136
|
+
const trustedArchive = resolve(staging, "trusted.tar");
|
|
137
|
+
const packTrusted = Bun.spawn(["tar", "-cf", trustedArchive, "-C", trusted, "."], { stdout: "ignore", stderr: "pipe" });
|
|
138
|
+
const [trustedExit, trustedError] = await Promise.all([packTrusted.exited, new Response(packTrusted.stderr).text()]);
|
|
139
|
+
if (trustedExit) throw new Error(`Cannot transfer trusted check inputs: ${trustedError}`);
|
|
140
|
+
await engineCommand(runtime.engine, ["exec", "--interactive", "--user", "0:0", name,
|
|
141
|
+
"tar", "--no-same-owner", "--no-overwrite-dir", "-xf", "-", "-C", "/verification"], { input: trustedArchive, timeoutMs });
|
|
142
|
+
for (const argv of request.commands) {
|
|
143
|
+
const started = Date.now();
|
|
144
|
+
const remaining = deadlineAt - started;
|
|
145
|
+
if (remaining <= 0) { timedOut = true; break; }
|
|
146
|
+
const child = Bun.spawn([runtime.engine, "exec", "--user", "10001:10001", "--workdir",
|
|
147
|
+
posix.join("/workspace", cwd), name, ...argv], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
148
|
+
const timer = setTimeout(() => { timedOut = true; child.kill(); }, remaining);
|
|
149
|
+
try {
|
|
150
|
+
const [code, out, err] = await Promise.all([child.exited, bounded(child.stdout, LOG_BYTES), bounded(child.stderr, LOG_BYTES)]);
|
|
151
|
+
exitCode = timedOut ? null : code;
|
|
152
|
+
commands.push({ argv: [...argv], exitCode, elapsedMs: Date.now() - started });
|
|
153
|
+
stdout += out.bytes.toString("utf8"); stderr += err.bytes.toString("utf8");
|
|
154
|
+
logsTruncated ||= out.truncated || err.truncated;
|
|
155
|
+
if (timedOut || code !== 0) break;
|
|
156
|
+
} finally { clearTimeout(timer); }
|
|
157
|
+
}
|
|
158
|
+
if (!timedOut) for (const path of artifacts) {
|
|
159
|
+
const tar = resolve(staging, `output-${randomUUID()}.tar`);
|
|
160
|
+
// Read inside the running mount namespace: docker cp cannot reliably
|
|
161
|
+
// see files in a container's tmpfs mounts.
|
|
162
|
+
const child = Bun.spawn([runtime.engine, "exec", "--user", "10001:10001", name,
|
|
163
|
+
"tar", "-cf", "-", "-C", posix.dirname(`/workspace/${path}`), "--", posix.basename(path)], { stdout: "pipe", stderr: "pipe" });
|
|
164
|
+
const timer = setTimeout(() => child.kill(), 30_000);
|
|
165
|
+
const [code, output, error] = await Promise.all([child.exited, bounded(child.stdout, OUTPUT_BYTES, () => child.kill()), bounded(child.stderr, LOG_BYTES)]).finally(() => clearTimeout(timer));
|
|
166
|
+
if (output.truncated) throw new Error("Verification output exceeds 32 MiB");
|
|
167
|
+
if (code !== 0) {
|
|
168
|
+
if (/not found|no such file|could not find/i.test(error.bytes.toString())) { result.missingArtifacts.push(path); continue; }
|
|
169
|
+
throw new Error(`Cannot retain verification output: ${error.bytes.toString()}`);
|
|
170
|
+
}
|
|
171
|
+
await Bun.write(tar, output.bytes);
|
|
172
|
+
const extracted = resolve(staging, `output-${randomUUID()}`);
|
|
173
|
+
await mkdir(extracted);
|
|
174
|
+
await extractWorkspaceArchive(tar, extracted, { sourceRoot: "/workspace" });
|
|
175
|
+
const file = contained(extracted, posix.basename(path));
|
|
176
|
+
if (!(await lstat(file)).isFile()) throw new Error("Verification artifacts must be regular files, not links or directories");
|
|
177
|
+
const bytes = await Bun.file(file).bytes();
|
|
178
|
+
const relative = `verification/${id}/artifacts/${path}`;
|
|
179
|
+
const target = contained(options.directory, relative);
|
|
180
|
+
await mkdir(dirname(target), { recursive: true });
|
|
181
|
+
await Bun.write(target, bytes);
|
|
182
|
+
result.artifacts.push({ path, bytes: bytes.length, sha256: hash(bytes), file: relative });
|
|
183
|
+
}
|
|
184
|
+
Object.assign(result, { state: timedOut ? "timed_out" : "completed", elapsedMs: Date.now() - Date.parse(startedAt), exitCode, logsTruncated });
|
|
185
|
+
result.stdout = `verification/${id}/stdout.log`; result.stderr = `verification/${id}/stderr.log`;
|
|
186
|
+
await Bun.write(resolve(directory, "stdout.log"), stdout.slice(0, LOG_BYTES));
|
|
187
|
+
await Bun.write(resolve(directory, "stderr.log"), stderr.slice(0, LOG_BYTES));
|
|
188
|
+
result.logsTruncated ||= stdout.length > LOG_BYTES || stderr.length > LOG_BYTES;
|
|
189
|
+
await writeJson(resolve(directory, "receipt.json"), result);
|
|
190
|
+
outputs.set(id, result); results.push(result);
|
|
191
|
+
return structuredClone(result);
|
|
192
|
+
} finally {
|
|
193
|
+
await engineCommand(runtime.engine, ["rm", "--force", name]).catch(() => {});
|
|
194
|
+
await rm(staging, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
package/src/judge-context.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ToolCall,
|
|
13
13
|
Cost,
|
|
14
14
|
Accounting,
|
|
15
|
+
Engine,
|
|
16
|
+
EvidenceCitation,
|
|
15
17
|
} from "./types";
|
|
16
18
|
|
|
17
19
|
export type JsonValue =
|
|
@@ -23,6 +25,62 @@ export type JsonValue =
|
|
|
23
25
|
| { [key: string]: JsonValue };
|
|
24
26
|
export type ScoreValue = boolean | number | null;
|
|
25
27
|
|
|
28
|
+
/** Scalars remain sufficient. Add explanations only when they help a reader. */
|
|
29
|
+
export type CodeScore = ScoreValue | {
|
|
30
|
+
value: ScoreValue;
|
|
31
|
+
reason: string;
|
|
32
|
+
evidence?: EvidenceCitation[];
|
|
33
|
+
measurements?: Record<string, JsonValue>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type VerificationEnvironment = {
|
|
37
|
+
engine?: Engine;
|
|
38
|
+
/** A locally available OCI image; resolved to an immutable ID before collection. */
|
|
39
|
+
image: string;
|
|
40
|
+
cpus?: number;
|
|
41
|
+
memoryMiB?: number;
|
|
42
|
+
};
|
|
43
|
+
export type VerificationRuntime = {
|
|
44
|
+
engine: Engine;
|
|
45
|
+
image: string;
|
|
46
|
+
imageId: string;
|
|
47
|
+
cpus: number;
|
|
48
|
+
memoryMiB: number;
|
|
49
|
+
};
|
|
50
|
+
export type VerificationRequest = {
|
|
51
|
+
revision?: Revision;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
/** Commands run in order; a nonzero exit stops this verification. No implicit shell. */
|
|
54
|
+
commands: readonly (readonly string[])[];
|
|
55
|
+
/** Trusted, frozen judge inputs, copied to /verification; never to the candidate. */
|
|
56
|
+
files?: Record<string, string>;
|
|
57
|
+
/** Relative paths under the restored workspace; missing outputs are reported. */
|
|
58
|
+
artifacts?: readonly string[];
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
};
|
|
61
|
+
export type VerificationArtifact = {
|
|
62
|
+
path: string;
|
|
63
|
+
bytes: number;
|
|
64
|
+
sha256: string;
|
|
65
|
+
/** Relative to the code-judge execution directory, not the candidate workspace. */
|
|
66
|
+
file: string;
|
|
67
|
+
};
|
|
68
|
+
export type VerificationResult = {
|
|
69
|
+
id: string;
|
|
70
|
+
state: "completed" | "timed_out";
|
|
71
|
+
revision: Revision;
|
|
72
|
+
environment: VerificationRuntime;
|
|
73
|
+
startedAt: string;
|
|
74
|
+
elapsedMs: number;
|
|
75
|
+
commands: Array<{ argv: string[]; exitCode: number | null; elapsedMs: number }>;
|
|
76
|
+
exitCode: number | null;
|
|
77
|
+
stdout: string;
|
|
78
|
+
stderr: string;
|
|
79
|
+
logsTruncated: boolean;
|
|
80
|
+
artifacts: VerificationArtifact[];
|
|
81
|
+
missingArtifacts: string[];
|
|
82
|
+
};
|
|
83
|
+
|
|
26
84
|
/** Code judges are ordinary functions. Only an optional scores map has grading semantics. */
|
|
27
85
|
export type JudgeFunction = (
|
|
28
86
|
context: JudgeContext,
|
|
@@ -106,6 +164,13 @@ export interface JudgeContext {
|
|
|
106
164
|
/** Restore a disposable snapshot for author-owned verification commands. */
|
|
107
165
|
materialize(revision?: Revision): Promise<string>;
|
|
108
166
|
};
|
|
167
|
+
readonly verification: {
|
|
168
|
+
/** Execute an artifact in a credential-free, network-isolated OCI container. */
|
|
169
|
+
run(request: VerificationRequest): Promise<VerificationResult>;
|
|
170
|
+
/** Read one retained output, checking its hash. No original runtime-state claim. */
|
|
171
|
+
read(result: VerificationResult, path: string): Promise<Uint8Array>;
|
|
172
|
+
text(result: VerificationResult, path: string): Promise<string>;
|
|
173
|
+
};
|
|
109
174
|
readonly native: {
|
|
110
175
|
readonly version: string | null;
|
|
111
176
|
readonly readerVersion: string;
|
|
@@ -138,4 +203,5 @@ export type CodeJudgeExecution = {
|
|
|
138
203
|
sourceHash: string;
|
|
139
204
|
stdout: string;
|
|
140
205
|
stderr: string;
|
|
206
|
+
verifications?: VerificationResult[];
|
|
141
207
|
};
|