0sec-cli 0.24.1 → 0.25.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/0.js +64 -60
- package/README.md +121 -82
- package/chunks/{adapt-loop-L77BWMNE.js → adapt-loop-L2PXCD4J.js} +6 -5
- package/chunks/{artifact-scraper-BV67RAP5.js → artifact-scraper-W3LGLXBM.js} +4 -3
- package/chunks/{assumption-mining-RPVR77A3.js → assumption-mining-7IJ6ZWZE.js} +22 -20
- package/chunks/{backend-connections-ACIXNYZN.js → backend-connections-MYH4QRQG.js} +5 -4
- package/chunks/{chunk-QNRYOJEB.js → chunk-27SXCYWH.js} +1 -1
- package/chunks/{chunk-PJU24AKR.js → chunk-2J3STRJT.js} +337 -257
- package/chunks/chunk-36J5ZLFE.js +18729 -0
- package/chunks/chunk-3DF55BWV.js +49 -0
- package/chunks/{chunk-F2P25DJY.js → chunk-3HMO42M2.js} +1417 -316
- package/chunks/{chunk-PVJUPIQG.js → chunk-3T5R7P4T.js} +6 -6
- package/chunks/{chunk-SU4IHQQI.js → chunk-5H2PXZ7Q.js} +1 -1
- package/chunks/chunk-5XOYTAOK.js +1546 -0
- package/chunks/{chunk-X2IPO5JO.js → chunk-6TA3CWND.js} +2656 -2960
- package/chunks/{chunk-TVS6WVSK.js → chunk-76CLUI54.js} +3 -3
- package/chunks/{chunk-KCA35IL3.js → chunk-7PUJI64A.js} +11 -9
- package/chunks/{chunk-2NBZZFRG.js → chunk-7TIWL6E5.js} +8 -10
- package/chunks/chunk-ABKUI3QB.js +196 -0
- package/chunks/{chunk-3LPBJMHB.js → chunk-ACYZMJHC.js} +1 -1
- package/chunks/{chunk-H5QURBBK.js → chunk-AL32F74R.js} +1 -1
- package/chunks/{chunk-RURJYCWY.js → chunk-B5TIXHCO.js} +1 -1
- package/chunks/{chunk-GZA7RGK7.js → chunk-BWUW6ZQR.js} +2052 -1075
- package/chunks/{chunk-I43YYHDU.js → chunk-C6ZMPXN2.js} +5 -50
- package/chunks/{chunk-ILRMLA6J.js → chunk-D4IBEO2K.js} +1 -1
- package/chunks/{chunk-K74DRDC5.js → chunk-DHNYJ2ML.js} +1 -1
- package/chunks/{chunk-YKWARO3T.js → chunk-DJGJECBV.js} +93 -20
- package/chunks/{chunk-RVJVM5L4.js → chunk-DMDWIW3E.js} +5 -5
- package/chunks/{chunk-ZM6ANQZJ.js → chunk-EEP6GBUO.js} +16037 -34417
- package/chunks/{chunk-IJ6UPW3H.js → chunk-EIONVEFN.js} +208 -29
- package/chunks/chunk-EQBC2ZWL.js +2481 -0
- package/chunks/{chunk-3WUPCZGF.js → chunk-FI2H6HV2.js} +1 -1
- package/chunks/{chunk-5NFIY2SJ.js → chunk-FI5YL4R2.js} +3 -3
- package/chunks/{chunk-XA2IBHPB.js → chunk-FOLTHXRH.js} +1 -1
- package/chunks/{chunk-CJRZSWII.js → chunk-GOUGG7IQ.js} +3 -3
- package/chunks/{chunk-NBV4I237.js → chunk-GXSECFUE.js} +1 -1
- package/chunks/{chunk-BD7WULFS.js → chunk-HDJSHLRM.js} +3 -2
- package/chunks/{chunk-5WAHHZBF.js → chunk-HFUOUT2B.js} +61232 -30665
- package/chunks/chunk-HJKJ4NQP.js +196 -0
- package/chunks/{chunk-UJE4WOQS.js → chunk-I4VIAQHM.js} +1 -1
- package/chunks/{chunk-5ETI27Q3.js → chunk-ITEMDJAB.js} +3 -3
- package/chunks/chunk-ITHDAIRY.js +57 -0
- package/chunks/{chunk-R2XBH476.js → chunk-JCRQV4FE.js} +2 -2
- package/chunks/{chunk-SQCBHFWN.js → chunk-LWMEJYCP.js} +2 -2
- package/chunks/{chunk-5PFRE522.js → chunk-MY44EZKD.js} +1 -1
- package/chunks/{chunk-7UHAXEOV.js → chunk-NBLKIQCW.js} +226 -4
- package/chunks/chunk-NO543ZBU.js +90 -0
- package/chunks/{chunk-X2ZMSRTP.js → chunk-OFPF26WL.js} +333 -3
- package/chunks/{chunk-YERUDEXT.js → chunk-PAE7ZAV6.js} +2780 -1255
- package/chunks/{chunk-S2GITRD5.js → chunk-PMAKX6KP.js} +2 -2
- package/chunks/{chunk-LMP2T4G2.js → chunk-RBTJYREN.js} +8 -171
- package/chunks/{chunk-CGBDEST2.js → chunk-RFK5F45W.js} +1 -1
- package/chunks/{chunk-GTED3FSZ.js → chunk-S2WCYEK2.js} +4 -99
- package/chunks/{chunk-GXXX7AGH.js → chunk-S3B6HFSM.js} +144 -14
- package/chunks/{chunk-WUPN3D37.js → chunk-S4DFH2FJ.js} +26 -1
- package/chunks/{chunk-Q7JKIKKF.js → chunk-SEWZ76MN.js} +77 -13
- package/chunks/{chunk-6XMFCVJS.js → chunk-TFKY6D7O.js} +1 -1
- package/chunks/{chunk-BOBIAHKA.js → chunk-WK6VUCKK.js} +3 -333
- package/chunks/{chunk-IXT64DV5.js → chunk-XPDJE537.js} +97 -1
- package/chunks/{chunk-BMDG6T7D.js → chunk-YXTV2FRE.js} +113 -22
- package/chunks/{chunk-SZDD77KX.js → chunk-YYFODVZG.js} +6 -10
- package/chunks/{chunk-JWSOUKFF.js → chunk-ZWAWZU6C.js} +30 -13
- package/chunks/{chunk-GV5WXDHY.js → chunk-ZZV56ACK.js} +11 -9
- package/chunks/{codex-models-6XVNFRXZ.js → codex-models-KCUIVS3R.js} +9 -8
- package/chunks/{commands-FRPOFIO4.js → commands-MPPAADGY.js} +798 -608
- package/chunks/dashboard-MN7GZLLX.js +75 -0
- package/chunks/{db-BXJNWUYJ.js → db-R2AQSESC.js} +5 -4
- package/chunks/{deep-review-GSJEBLCG.js → deep-review-Q6BJKEV4.js} +37 -38
- package/chunks/{disclose-OU6Z6GVT.js → disclose-GGULOW27.js} +6 -5
- package/chunks/{dist-TQMHMSBZ.js → dist-6RNAVFAY.js} +376 -333
- package/chunks/{dist-7KSYDDSG.js → dist-DWN7U5OV.js} +5 -2
- package/chunks/{dist-M2X367VW.js → dist-JAZWL3SC.js} +3 -2
- package/chunks/{dist-RKLQIXDT.js → dist-OLGNKDB2.js} +4 -3
- package/chunks/{dist-LNXGATR2.js → dist-WNAOCQGA.js} +5 -4
- package/chunks/eval-runner-WCTTIYN4.js +38 -0
- package/chunks/{exploit-agent-Y2P4GFIY.js → exploit-agent-M3KUMZJH.js} +1 -1
- package/chunks/{exploit-autoclimb-YRZ3TMS2.js → exploit-autoclimb-QTTWC5WM.js} +1 -1
- package/chunks/{exploit-climb-4LEY4SDW.js → exploit-climb-E4KDELTN.js} +4 -3
- package/chunks/{fix-RQSIG75B.js → fix-U5FFJD4F.js} +9 -8
- package/chunks/{harness-3NVC3DDY.js → harness-FMKIMEH5.js} +5 -4
- package/chunks/{http-conformance-U5F6GKYG.js → http-conformance-AXMUOE6C.js} +3 -2
- package/chunks/hunt-scan-RYPWRUQD.js +43 -0
- package/chunks/{kernel-primitive-T47V3RD3.js → kernel-primitive-CVG2E7MJ.js} +4 -3
- package/chunks/{kernel-vm-runner-AEL5E3SV.js → kernel-vm-runner-62KISULQ.js} +4 -3
- package/chunks/{local-engine-ZCW4YC6E.js → local-engine-WP3I7K7G.js} +7 -6
- package/chunks/managed-operation-642WPTLC.js +59 -0
- package/chunks/{memsafety-scan-L2Z7E7TE.js → memsafety-scan-Q2MBZEGJ.js} +1 -2
- package/chunks/{native-loop-26X2CHBV.js → native-loop-HC77R5NQ.js} +31 -16
- package/chunks/operator-services-E2NONBQS.js +79 -0
- package/chunks/orchestrate-Z3DW265M.js +62 -0
- package/chunks/{prepare-C3KGMBXT.js → prepare-7DTS6RJT.js} +7 -6
- package/chunks/{remote-workflow-runtime-DB52SINR.js → remote-workflow-runtime-6CEVRZXS.js} +4 -3
- package/chunks/routing-ITJZBJIU.js +61 -0
- package/chunks/{runtime-U66JNSAY.js → runtime-PZJ77QES.js} +9 -8
- package/chunks/{runtime-AUBLQUOH.js → runtime-W6JNDQRO.js} +4 -3
- package/chunks/{scan-stream-34F3LTPV.js → scan-stream-6VOJTHTR.js} +6 -4
- package/chunks/{scope-HSTG63TQ.js → scope-FHXW6EX6.js} +4 -3
- package/chunks/service-plugins-CN6Q2NSN.js +63 -0
- package/chunks/{session-store-MKAAP3WL.js → session-store-DBBI544Y.js} +8 -5
- package/chunks/{variant-candidates-LG767HWH.js → variant-candidates-DPR6DJ3N.js} +9 -8
- package/chunks/{web-recon-prepass-NJP6J3E4.js → web-recon-prepass-HWDHDQMN.js} +7 -6
- package/chunks/{workflow-runtime-AR627I26.js → workflow-runtime-PLB32XC3.js} +50 -49
- package/dashboard/assets/{abnfDiagram-VCTEODGH-Cq149Jpk.js → abnfDiagram-VCTEODGH-6e39CQKz.js} +1 -1
- package/dashboard/assets/{arc-CpP-3TRW.js → arc-ClYun86B.js} +1 -1
- package/dashboard/assets/architecture-7GRP2DOG-rM-lWo7E.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-C9gEGZIU.js +36 -0
- package/dashboard/assets/{blockDiagram-I7D4REHJ-CZYkRRDi.js → blockDiagram-I7D4REHJ-Dgh2n5Lf.js} +2 -2
- package/dashboard/assets/{c4Diagram-7LVT6UL2-CV7Iu3_Z.js → c4Diagram-7LVT6UL2-CsorW08z.js} +1 -1
- package/dashboard/assets/channel-BBlDqIBl.js +1 -0
- package/dashboard/assets/{chunk-4HAMMTFA-PKCyW4m-.js → chunk-4HAMMTFA-4_b_LGGQ.js} +1 -1
- package/dashboard/assets/{chunk-75Z2AOVW-C5MlFTc_.js → chunk-75Z2AOVW-qzNlKGCp.js} +1 -1
- package/dashboard/assets/chunk-DU6HZSFF-SAPICW28.js +127 -0
- package/dashboard/assets/{chunk-F27PBJKO-DTAok0qM.js → chunk-F27PBJKO-DlLotIQx.js} +1 -1
- package/dashboard/assets/{chunk-FOHPRMQF-DHwB1DNv.js → chunk-FOHPRMQF-COOliuUh.js} +14 -14
- package/dashboard/assets/{chunk-GMAD6QVW-CPsr2F33.js → chunk-GMAD6QVW-DNnr65Nb.js} +7 -7
- package/dashboard/assets/{chunk-GVQU2GXP-C2AXGf9A.js → chunk-GVQU2GXP-DTwv0VfU.js} +1 -1
- package/dashboard/assets/{chunk-IMKFNOWR-DMG6yD_O.js → chunk-IMKFNOWR-B1fk1q_W.js} +1 -1
- package/dashboard/assets/{chunk-L3NEJ4N5-BSvwdbXx.js → chunk-L3NEJ4N5-ByVjFIox.js} +1 -1
- package/dashboard/assets/{chunk-OSK3NFVY-CfB8V7FU.js → chunk-OSK3NFVY-DUDCzjbh.js} +1 -1
- package/dashboard/assets/{chunk-P2QGCYS3-CvmZvq6n.js → chunk-P2QGCYS3-D_D5qEso.js} +1 -1
- package/dashboard/assets/{chunk-POPQ4Y6H-D_TvwDxL.js → chunk-POPQ4Y6H-CetM-tVm.js} +1 -1
- package/dashboard/assets/{chunk-PWAF6VOD-Da5GnuOY.js → chunk-PWAF6VOD-BcJX4exl.js} +1 -1
- package/dashboard/assets/{chunk-SHT3W25Y-CcsMJpR-.js → chunk-SHT3W25Y-BWQql238.js} +1 -1
- package/dashboard/assets/{chunk-SVP7TREG-COojjieJ.js → chunk-SVP7TREG-BSLOD1JE.js} +1 -1
- package/dashboard/assets/{chunk-TICWLB2K-nBAmJwx6.js → chunk-TICWLB2K-DReOl-g_.js} +2 -2
- package/dashboard/assets/{chunk-XXDRQBXY-r5HyhWDv.js → chunk-XXDRQBXY-DSdSMeei.js} +1 -1
- package/dashboard/assets/classDiagram-ZZMXUADV-CRH2kB8G.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CRH2kB8G.js +1 -0
- package/dashboard/assets/common-CRRsylCH.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-fg-DIBqt.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-XmrbEIFh.js +1 -0
- package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-xodkuQtW.js → cynefinDiagram-5FMLGOSQ-CoQ7QEPl.js} +1 -1
- package/dashboard/assets/cytoscape.esm-BbBREfsg.js +321 -0
- package/dashboard/assets/dagre-D7JM2Yhv.js +1 -0
- package/dashboard/assets/dagre-GXQ25YYZ-qtQtsg6c.js +4 -0
- package/dashboard/assets/{diagram-S7CK7UJ4-DNoTMlCO.js → diagram-S7CK7UJ4-B_ts4_58.js} +2 -2
- package/dashboard/assets/{diagram-UQ7AKVKN-COccuyew.js → diagram-UQ7AKVKN-DwHbbxFy.js} +1 -1
- package/dashboard/assets/{diagram-VSXAHHWV-g6D1tWRb.js → diagram-VSXAHHWV-2Tg_RG89.js} +1 -1
- package/dashboard/assets/{diagram-VX7I27RA-DhO4eVIC.js → diagram-VX7I27RA-BFYp-YjF.js} +1 -1
- package/dashboard/assets/{diagram-Z3DM3KII-DeOQFexd.js → diagram-Z3DM3KII-DrPiS4V0.js} +1 -1
- package/dashboard/assets/{dist-BQYeOQg3.js → dist-cbVssBvx.js} +1 -1
- package/dashboard/assets/{ebnfDiagram-PWID7BFC-MtkX2feM.js → ebnfDiagram-PWID7BFC-DCMOZtU2.js} +1 -1
- package/dashboard/assets/{erDiagram-RLTQ6QDP-DIWSsoYZ.js → erDiagram-RLTQ6QDP-CW4rUJGU.js} +1 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-C2q-Aaav.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-3NOabgG3.js +1 -0
- package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DKggkJe0.js → ganttDiagram-EL5Y4UJY-C1AqcwIF.js} +1 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DYR76gtd.js +1 -0
- package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-r_iR8Qzo.js → gitGraphDiagram-WWUBYQGX-B7hL98CE.js} +1 -1
- package/dashboard/assets/info-A6RAGUB7-TXB43LiH.js +1 -0
- package/dashboard/assets/{infoDiagram-27XIBGKW-ClQk7UyQ.js → infoDiagram-27XIBGKW-Dk5dXmh_.js} +1 -1
- package/dashboard/assets/{ishikawaDiagram-5VMMS53U-tR7Vziog.js → ishikawaDiagram-5VMMS53U-BQDuNMka.js} +2 -2
- package/dashboard/assets/{journeyDiagram-3NMN7TZE-Cpb9IqKL.js → journeyDiagram-3NMN7TZE-DkustddD.js} +1 -1
- package/dashboard/assets/{kanban-definition-UXKFOSKX-BzUuKbA9.js → kanban-definition-UXKFOSKX-BW7Ann84.js} +1 -1
- package/dashboard/assets/{katex-BdBmVSus.js → katex-nLxf_RlZ.js} +2 -2
- package/dashboard/assets/{line-BHY_tSHu.js → line-DVxcLeru.js} +1 -1
- package/dashboard/assets/linear-DOM_l9bO.js +1 -0
- package/dashboard/assets/mermaid-parser.core-CKg9fDvJ.js +7 -0
- package/dashboard/assets/{mermaid.core-D_QsUPU8.js → mermaid.core-DruW_5-Y.js} +9 -9
- package/dashboard/assets/{mindmap-definition-YA3MSWOX-CtfLTPPJ.js → mindmap-definition-YA3MSWOX-Di5W5VB3.js} +1 -1
- package/dashboard/assets/operations-B-t_k60U.js +194 -0
- package/dashboard/assets/operations-D2ZKZFOF.css +1 -0
- package/dashboard/assets/packet-AYTQ26CC-BABwqtNu.js +1 -0
- package/dashboard/assets/{pegDiagram-XKGWAZYB-C1Dclbzj.js → pegDiagram-XKGWAZYB-Cx4ABT6q.js} +1 -1
- package/dashboard/assets/pie-WAS4IAKB-CNdXyrze.js +1 -0
- package/dashboard/assets/{pieDiagram-E7YTZNPT-Cd7XwYo5.js → pieDiagram-E7YTZNPT-BilfQbIs.js} +1 -1
- package/dashboard/assets/{quadrantDiagram-AXDQQJYC-CaS-GQ4X.js → quadrantDiagram-AXDQQJYC-BVmMQHE5.js} +1 -1
- package/dashboard/assets/radar-RG4KPBEZ-fKNmDeoK.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-DhcvDCOh.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-BTRVAmf0.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-CwS8fgC_.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-CXuhXlNp.js +1 -0
- package/dashboard/assets/{railroadDiagram-O6MQD6OU-BeesTrqn.js → railroadDiagram-O6MQD6OU-DnBru-jf.js} +1 -1
- package/dashboard/assets/{requirementDiagram-BXWQKSXE-IjyK0DhW.js → requirementDiagram-BXWQKSXE-FwdvUZ7_.js} +1 -1
- package/dashboard/assets/{sankeyDiagram-P5KCCOFB-DmIluQqo.js → sankeyDiagram-P5KCCOFB-BQvDORai.js} +1 -1
- package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-CUp8ZjTc.js → sequenceDiagram-WJ2MYXX4-FWYQLzcY.js} +2 -2
- package/dashboard/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dashboard/assets/src-BwkPhlcp.js +1 -0
- package/dashboard/assets/{stateDiagram-D77RDMKH-Dy9TQb8P.js → stateDiagram-D77RDMKH-CjPBd_Sa.js} +1 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-DHmenujm.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-SQLNsa4J.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-D1bH4ETZ.js +8 -0
- package/dashboard/assets/{timeline-definition-24CTP7MA-BGdXW-iY.js → timeline-definition-24CTP7MA-BxCf20k1.js} +1 -1
- package/dashboard/assets/treeView-Q6P3EWNA-BGLYAyGp.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-BlzAwncU.js +1 -0
- package/dashboard/assets/{vennDiagram-4TSXK5OY-DzKWHJsl.js → vennDiagram-4TSXK5OY-D76RIZLz.js} +1 -1
- package/dashboard/assets/wardley-WFR3VGLG-Ba_reiVw.js +1 -0
- package/dashboard/assets/{wardleyDiagram-VM6X3IG4-DGsjYmNJ.js → wardleyDiagram-VM6X3IG4-Cux4coOD.js} +1 -1
- package/dashboard/assets/{xychartDiagram-S5SC5T6Z-C7_T_tWt.js → xychartDiagram-S5SC5T6Z-Ch5mvnlH.js} +1 -1
- package/dashboard/index.html +2 -2
- package/package.json +2 -5
- package/chunks/chunk-2XGBEH53.js +0 -4304
- package/chunks/chunk-4ROZFLSR.js +0 -279
- package/chunks/chunk-56UTQKXO.js +0 -56
- package/chunks/chunk-BPU6M42R.js +0 -77
- package/chunks/chunk-IIBNQTOK.js +0 -1532
- package/chunks/chunk-NJLKFC66.js +0 -4790
- package/chunks/chunk-OOMBOLAF.js +0 -295
- package/chunks/chunk-VIT5ALXF.js +0 -4054
- package/chunks/chunk-VU7V2SDW.js +0 -284
- package/chunks/chunk-W624J7NF.js +0 -117
- package/chunks/chunk-YSQWZ67M.js +0 -6027
- package/chunks/dashboard-5HDSNBAU.js +0 -76
- package/chunks/eval-runner-A2DIGVP7.js +0 -31
- package/chunks/hunt-scan-W246VWJC.js +0 -40
- package/chunks/managed-operation-6R2QPPW5.js +0 -60
- package/chunks/orchestrate-OBS37FWQ.js +0 -63
- package/chunks/run-MU3MFL2Z.js +0 -28242
- package/chunks/service-plugins-5B23PWXC.js +0 -64
- package/dashboard/assets/architecture-7GRP2DOG-z5EHYkyH.js +0 -1
- package/dashboard/assets/architectureDiagram-5GKGNRK7-ewtXpjR_.js +0 -36
- package/dashboard/assets/channel-CuYjfiC5.js +0 -1
- package/dashboard/assets/chunk-DU6HZSFF-3M37ulcO.js +0 -127
- package/dashboard/assets/classDiagram-ZZMXUADV-DCuOrWUV.js +0 -1
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-DCuOrWUV.js +0 -1
- package/dashboard/assets/common-C6snVPsk.js +0 -5
- package/dashboard/assets/cose-bilkent-JH36ORCC-BLGcaZHk.js +0 -1
- package/dashboard/assets/cynefin-OW5HDTMX-DbZMK00I.js +0 -1
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +0 -321
- package/dashboard/assets/dagre-GXQ25YYZ-CRk9qfSG.js +0 -4
- package/dashboard/assets/dagre-PrKaheQc.js +0 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-D_3ez4Gv.js +0 -1
- package/dashboard/assets/flowDiagram-HODETNUW-D2boyYEd.js +0 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DFOQYwXc.js +0 -1
- package/dashboard/assets/info-A6RAGUB7-DeednLoB.js +0 -1
- package/dashboard/assets/linear-naZZ1pHk.js +0 -1
- package/dashboard/assets/mermaid-parser.core-CPvd4cEx.js +0 -7
- package/dashboard/assets/operations-BwCzhVnT.js +0 -130
- package/dashboard/assets/operations-QjKT0Hwi.css +0 -1
- package/dashboard/assets/packet-AYTQ26CC-DDQbkT9I.js +0 -1
- package/dashboard/assets/pie-WAS4IAKB-LpU4ULAs.js +0 -1
- package/dashboard/assets/radar-RG4KPBEZ-CcxW3NJm.js +0 -1
- package/dashboard/assets/railroad-74A4TZTK-9P5Rl8fP.js +0 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-CSfpwjna.js +0 -1
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-D6R5qzr-.js +0 -1
- package/dashboard/assets/railroad-peg-WCYAUIDC-DKEX7ITG.js +0 -1
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
- package/dashboard/assets/src-r61wDjcA.js +0 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-CQb0ZtPr.js +0 -1
- package/dashboard/assets/swimlanes-42K2YHIH-CZuDV7V5.js +0 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BxgWMq8X.js +0 -8
- package/dashboard/assets/treeView-Q6P3EWNA-DlRrayjM.js +0 -1
- package/dashboard/assets/treemap-WGGIJYW6-C6UqLKfI.js +0 -1
- package/dashboard/assets/wardley-WFR3VGLG-thmyHKb0.js +0 -1
|
@@ -4,24 +4,21 @@ const require = __0CreateRequire(import.meta.url);
|
|
|
4
4
|
import {
|
|
5
5
|
ConsoleGateway,
|
|
6
6
|
ConsoleGatewayError,
|
|
7
|
-
WebOperatorServices,
|
|
8
7
|
WebWorkflowService,
|
|
9
|
-
WorkflowEngineService
|
|
10
|
-
} from "./chunk-GZA7RGK7.js";
|
|
11
|
-
import {
|
|
12
|
-
assertLocalEngineAvailable,
|
|
13
|
-
registerLocalEngine
|
|
14
|
-
} from "./chunk-RVJVM5L4.js";
|
|
15
|
-
import {
|
|
16
|
-
findingFromRow as findingFromRow2
|
|
17
|
-
} from "./chunk-W624J7NF.js";
|
|
18
|
-
import {
|
|
8
|
+
WorkflowEngineService,
|
|
19
9
|
buildFindingConsoleCommand,
|
|
20
10
|
createLocalConsoleSession,
|
|
21
|
-
createSessionPluginHostManager,
|
|
22
11
|
findingFromRow,
|
|
23
12
|
loadFindingFocus
|
|
24
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-BWUW6ZQR.js";
|
|
14
|
+
import {
|
|
15
|
+
assertLocalEngineAvailable,
|
|
16
|
+
registerLocalEngine
|
|
17
|
+
} from "./chunk-DMDWIW3E.js";
|
|
18
|
+
import {
|
|
19
|
+
WebOperatorServices,
|
|
20
|
+
createSessionPluginHostManager
|
|
21
|
+
} from "./chunk-EQBC2ZWL.js";
|
|
25
22
|
import {
|
|
26
23
|
BackendConnectionRegistry,
|
|
27
24
|
backendBearerFromEnv,
|
|
@@ -29,13 +26,11 @@ import {
|
|
|
29
26
|
handleBackendConnectionRequest,
|
|
30
27
|
loadManagedWorker,
|
|
31
28
|
validateWorkerOptions
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import {
|
|
34
|
-
Option
|
|
35
|
-
} from "./chunk-WUPN3D37.js";
|
|
29
|
+
} from "./chunk-SEWZ76MN.js";
|
|
36
30
|
import {
|
|
31
|
+
Option,
|
|
37
32
|
presentationEventBus
|
|
38
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-S4DFH2FJ.js";
|
|
39
34
|
import {
|
|
40
35
|
source_default
|
|
41
36
|
} from "./chunk-HLFS6WV2.js";
|
|
@@ -61,43 +56,47 @@ import {
|
|
|
61
56
|
validateWorkbenchSourceLesson,
|
|
62
57
|
workbenchConfigPath,
|
|
63
58
|
workbenchNetworkEnabled
|
|
64
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-6TA3CWND.js";
|
|
65
60
|
import {
|
|
66
61
|
connectServicePlugins,
|
|
67
62
|
createConsoleRuntime,
|
|
68
63
|
exportReport,
|
|
69
64
|
isReportExportFormat
|
|
70
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-EEP6GBUO.js";
|
|
71
66
|
import {
|
|
72
67
|
HuntMemoryStore,
|
|
73
68
|
TOOL_DEFINITIONS,
|
|
74
69
|
discoverAgentSkills,
|
|
70
|
+
listBrowserViews,
|
|
75
71
|
loadSkillRegistry,
|
|
72
|
+
parseImpactAssessment,
|
|
76
73
|
readToolCallNames,
|
|
74
|
+
subscribeBrowserView,
|
|
77
75
|
validateAgentSkillContent
|
|
78
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-HFUOUT2B.js";
|
|
79
77
|
import {
|
|
80
78
|
SecurityWorkflowStore,
|
|
81
79
|
WorkflowTriggerStore,
|
|
82
80
|
findingStatuses,
|
|
83
81
|
nextWorkflowTriggerFire,
|
|
84
82
|
osecDB,
|
|
85
|
-
resolveOsecDbPath
|
|
86
|
-
|
|
83
|
+
resolveOsecDbPath,
|
|
84
|
+
restoreFindingReviewFields
|
|
85
|
+
} from "./chunk-YXTV2FRE.js";
|
|
87
86
|
import {
|
|
88
87
|
LlmApiRuntime
|
|
89
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-EIONVEFN.js";
|
|
90
89
|
import {
|
|
91
90
|
DEFAULT_WORKBENCH_BROKER_LIMITS,
|
|
92
91
|
approveSmolvmWorkbenchImage,
|
|
93
92
|
getSmolvmWorkbenchStatus,
|
|
94
93
|
isAdmittedSmolvmWorkbench,
|
|
95
94
|
resolveSmolvmRuntime
|
|
96
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-3T5R7P4T.js";
|
|
97
96
|
import {
|
|
98
97
|
ScopePolicy,
|
|
99
98
|
withScopeEnforcement
|
|
100
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-27SXCYWH.js";
|
|
101
100
|
import {
|
|
102
101
|
BackendDescriptorSchema,
|
|
103
102
|
BackendRequestIdSchema,
|
|
@@ -106,14 +105,14 @@ import {
|
|
|
106
105
|
VERSION,
|
|
107
106
|
createPresentationEvent,
|
|
108
107
|
homeStateDir
|
|
109
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-S3B6HFSM.js";
|
|
110
109
|
import {
|
|
111
110
|
external_exports
|
|
112
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-5XOYTAOK.js";
|
|
113
112
|
|
|
114
113
|
// packages/cli/src/commands/dashboard.ts
|
|
115
114
|
import { createServer as createServer2 } from "node:http";
|
|
116
|
-
import { createHash as createHash8, randomUUID as
|
|
115
|
+
import { createHash as createHash8, randomUUID as randomUUID9, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
117
116
|
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
118
117
|
import { existsSync as existsSync2, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
119
118
|
import { dirname as dirname2, extname, join as join7, resolve as resolve4, sep } from "node:path";
|
|
@@ -132,6 +131,44 @@ import { createServer } from "node:http";
|
|
|
132
131
|
import { randomUUID } from "node:crypto";
|
|
133
132
|
import { spawn } from "node:child_process";
|
|
134
133
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
134
|
+
|
|
135
|
+
// packages/cli/src/tui/findings-data.ts
|
|
136
|
+
function parseJsonColumn(raw) {
|
|
137
|
+
if (!raw) return void 0;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function findingFromRow2(row) {
|
|
145
|
+
const impactAssessment = parseJsonColumn(row.impactAssessment) === void 0 ? void 0 : parseImpactAssessment(row.impactAssessment) ?? void 0;
|
|
146
|
+
const record = {
|
|
147
|
+
id: row.id,
|
|
148
|
+
templateId: row.templateId,
|
|
149
|
+
title: row.title,
|
|
150
|
+
description: row.description,
|
|
151
|
+
severity: row.severity,
|
|
152
|
+
cvssScore: row.cvssScore ?? void 0,
|
|
153
|
+
cvssVector: row.cvssVector ?? void 0,
|
|
154
|
+
category: row.category,
|
|
155
|
+
status: row.status,
|
|
156
|
+
fingerprint: row.fingerprint ?? void 0,
|
|
157
|
+
triageStatus: row.triageStatus ?? void 0,
|
|
158
|
+
timestamp: row.timestamp,
|
|
159
|
+
evidence: {
|
|
160
|
+
request: row.evidenceRequest,
|
|
161
|
+
response: row.evidenceResponse,
|
|
162
|
+
analysis: row.evidenceAnalysis ?? void 0
|
|
163
|
+
},
|
|
164
|
+
verificationSpec: parseJsonColumn(row.verificationSpec),
|
|
165
|
+
impactAssessment,
|
|
166
|
+
...restoreFindingReviewFields(row)
|
|
167
|
+
};
|
|
168
|
+
return record;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// packages/cli/src/workbench-console-guest.ts
|
|
135
172
|
var CALLBACKS = ["requestScope", "requestLocalScope", "approveTool", "escalateScopedAudit", "askOperator"];
|
|
136
173
|
var EVENTS = ["onHarnessUpdate", "onAssistantDelta", "onReasoningDelta", "onToolStart", "onToolResult", "onUsage", "onNotice", "onCompaction"];
|
|
137
174
|
async function runGuest(cli) {
|
|
@@ -156,9 +193,9 @@ async function runGuest(cli) {
|
|
|
156
193
|
let policy;
|
|
157
194
|
const callback = (name, args) => {
|
|
158
195
|
if (decisions.size >= WORKBENCH_MAX_PENDING) return Promise.reject(new Error("Workbench decision limit reached"));
|
|
159
|
-
const
|
|
160
|
-
emit({ type: "decision", id, name, args });
|
|
161
|
-
return new Promise((resolve5, reject) => decisions.set(
|
|
196
|
+
const id2 = randomUUID();
|
|
197
|
+
emit({ type: "decision", id: id2, name, args });
|
|
198
|
+
return new Promise((resolve5, reject) => decisions.set(id2, { resolve: resolve5, reject }));
|
|
162
199
|
};
|
|
163
200
|
const server = createServer(async (request, response) => {
|
|
164
201
|
if (request.method !== "POST" || request.url !== "/provider/request" || providers.size >= 4) {
|
|
@@ -175,12 +212,12 @@ async function runGuest(cli) {
|
|
|
175
212
|
}
|
|
176
213
|
const envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
177
214
|
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) throw new Error("Invalid provider request");
|
|
178
|
-
const
|
|
179
|
-
providers.set(
|
|
215
|
+
const id2 = randomUUID();
|
|
216
|
+
providers.set(id2, response);
|
|
180
217
|
response.once("close", () => {
|
|
181
|
-
if (providers.delete(
|
|
218
|
+
if (providers.delete(id2)) emit({ type: "provider-cancel", id: id2 });
|
|
182
219
|
});
|
|
183
|
-
emit({ type: "provider", id, envelope });
|
|
220
|
+
emit({ type: "provider", id: id2, envelope });
|
|
184
221
|
} catch {
|
|
185
222
|
response.writeHead(400).end("Invalid provider request");
|
|
186
223
|
}
|
|
@@ -212,7 +249,7 @@ async function runGuest(cli) {
|
|
|
212
249
|
};
|
|
213
250
|
await mkdir("/home/zero/.0/controller", { recursive: true, mode: 448 });
|
|
214
251
|
await writeFile("/home/zero/.0/controller/session.json", JSON.stringify(scrub(snapshot)), { mode: 384 });
|
|
215
|
-
emit({ type: "state", snapshot: checkpoint?.harnessRoot ? {
|
|
252
|
+
emit({ type: "state", snapshot: { ...snapshot, ...checkpoint?.harnessRoot ? { checkpoint: void 0 } : {}, sourceContext: session.sourceContext } });
|
|
216
253
|
const db = new osecDB("/home/zero/.0/controller.sqlite");
|
|
217
254
|
try {
|
|
218
255
|
const rows = db.getFindings(session.scanId);
|
|
@@ -373,6 +410,23 @@ async function runGuest(cli) {
|
|
|
373
410
|
}
|
|
374
411
|
});
|
|
375
412
|
};
|
|
413
|
+
if (frame.callbacks.includes("skillRead")) callbacks.skillAuthoring = {
|
|
414
|
+
read: (id2) => callback("skillRead", [id2]),
|
|
415
|
+
create: (content) => callback("skillCreate", [content]),
|
|
416
|
+
update: (id2, input) => callback("skillSave", [id2, input]),
|
|
417
|
+
copy: (id2, input) => callback("skillCopy", [id2, input])
|
|
418
|
+
};
|
|
419
|
+
if (frame.callbacks.includes("skillEditorRead")) callbacks.skillEditor = { read: () => callback("skillEditorRead", []) };
|
|
420
|
+
if (frame.callbacks.includes("workflowList")) callbacks.workflowAuthoring = {
|
|
421
|
+
list: () => callback("workflowList", []),
|
|
422
|
+
save: (input) => callback("workflowSave", [input]),
|
|
423
|
+
...frame.callbacks.includes("workflowEditorRead") && typeof frame.workflowEditorId === "string" ? {
|
|
424
|
+
editor: { id: frame.workflowEditorId, read: () => callback("workflowEditorRead", []) }
|
|
425
|
+
} : {}
|
|
426
|
+
};
|
|
427
|
+
if (frame.callbacks.includes("workflowInvoke")) callbacks.workflowLifecycle = {
|
|
428
|
+
invoke: (name, args) => callback("workflowInvoke", [name, args])
|
|
429
|
+
};
|
|
376
430
|
if (frame.callbacks.includes("historyList")) callbacks.conversationHistory = { list: (...args) => callback("historyList", args), read: (...args) => callback("historyRead", args) };
|
|
377
431
|
pluginManager = await createSessionPluginHostManager({ projectPath: "/workspace", reservedToolNames: Object.values(TOOL_DEFINITIONS).map((tool) => tool.name), coreVersion: VERSION });
|
|
378
432
|
const unavailable = pluginManager.current().status().filter((entry) => entry.state !== "ready");
|
|
@@ -399,7 +453,7 @@ async function runGuest(cli) {
|
|
|
399
453
|
if (typeof frame.text !== "string") throw new Error("Invalid operator message");
|
|
400
454
|
turn = new AbortController();
|
|
401
455
|
const callbacks = Object.fromEntries(EVENTS.map((name) => [name, (...args) => emit({ type: "event", name, args })]));
|
|
402
|
-
const result = withScopeEnforcement(policy, () => session.send(frame.text, callbacks, { signal: turn.signal, generateTitle: frame.generateTitle === true }));
|
|
456
|
+
const result = withScopeEnforcement(policy, () => session.send(frame.text, callbacks, { signal: turn.signal, generateTitle: frame.generateTitle === true, images: frame.images }));
|
|
403
457
|
await state();
|
|
404
458
|
value = await result;
|
|
405
459
|
turn = void 0;
|
|
@@ -514,7 +568,7 @@ async function setupWorkbench(options, home = process.env.HOME || homedir()) {
|
|
|
514
568
|
const workspaceRoot = options.workspace === void 0 ? previous?.workspaceRoot : realpathSync(resolve(options.workspace));
|
|
515
569
|
if (workspaceRoot !== void 0 && !lstatSync(workspaceRoot).isDirectory()) throw new Error("Workbench workspace must be a directory.");
|
|
516
570
|
const providers = options.provider ?? previous?.providers ?? [];
|
|
517
|
-
for (const
|
|
571
|
+
for (const id2 of providers) if (!PROVIDERS.some((provider) => provider.id === id2)) throw new Error(`Unknown provider grant: ${id2}. Use 0 workbench providers.`);
|
|
518
572
|
const imageArchive = options.image ? resolve(options.image) : previous?.image;
|
|
519
573
|
if (!imageArchive) throw new Error("No approved workbench image is available. Supply a locally provisioned archive with 0 workbench setup --image <archive>. Docker is only needed to build an image, never to run this profile.");
|
|
520
574
|
const cpus = resource(options.cpus, previous?.cpus ?? 2, 64, "CPUs");
|
|
@@ -574,7 +628,7 @@ function registerWorkbenchCommand(program) {
|
|
|
574
628
|
workbench.command("console-agent", { hidden: true }).action(async () => {
|
|
575
629
|
await runWorkbenchConsoleGuest();
|
|
576
630
|
});
|
|
577
|
-
workbench.command("setup").description("Verify/provision the signed native runtime, approve a local image, and select SmolVM execution").option("--image <archive>", "Local OCI/Docker archive to digest-pin and approve; never a mutable registry tag").option("--state <directory>", "Private VM state directory (defaults to ~/.0/workbench)").option("--workspace <directory>", "Explicit workspace mount; otherwise each invocation mounts its current directory").option("--provider <id>", "Grant chatgpt-codex requests through the host provider broker; credentials stay on the host", (
|
|
631
|
+
workbench.command("setup").description("Verify/provision the signed native runtime, approve a local image, and select SmolVM execution").option("--image <archive>", "Local OCI/Docker archive to digest-pin and approve; never a mutable registry tag").option("--state <directory>", "Private VM state directory (defaults to ~/.0/workbench)").option("--workspace <directory>", "Explicit workspace mount; otherwise each invocation mounts its current directory").option("--provider <id>", "Grant chatgpt-codex requests through the host provider broker; credentials stay on the host", (id2, ids = []) => [...ids, id2]).option("--github", "Unsupported: GitHub credential forwarding is refused").option("--no-github", "Revoke the GitHub token grant").option("--cpus <count>", "Guest virtual CPUs").option("--memory <MiB>", "Guest RAM in MiB").option("--storage <GiB>", "Guest private writable storage in GiB").option("--sandbox-image <reference=archive>", "Approve an immutable image reference for brokered isolated container actions; repeat for multiple images", (selection, selections = []) => [...selections, selection]).action(async (options) => {
|
|
578
632
|
try {
|
|
579
633
|
const config = await setupWorkbench(options);
|
|
580
634
|
console.log(`SmolVM workbench selected. Approved image: ${config.imageDigest}`);
|
|
@@ -606,7 +660,7 @@ function registerWorkbenchCommand(program) {
|
|
|
606
660
|
try {
|
|
607
661
|
const config = loadWorkbenchConfig();
|
|
608
662
|
if (!config) throw new Error("No workbench is configured. Run 0 workbench setup first.");
|
|
609
|
-
if (options.provider !== void 0) config.providers = options.provider === "none" ? [] : options.provider.split(",").map((
|
|
663
|
+
if (options.provider !== void 0) config.providers = options.provider === "none" ? [] : options.provider.split(",").map((id2) => id2.trim());
|
|
610
664
|
if (options.github !== void 0) config.github = options.github;
|
|
611
665
|
if (options.currentWorkspace) delete config.workspaceRoot;
|
|
612
666
|
if (options.clearSandboxImages) delete config.approvedImages;
|
|
@@ -656,6 +710,771 @@ function findDashboardInstance(origin) {
|
|
|
656
710
|
});
|
|
657
711
|
}
|
|
658
712
|
|
|
713
|
+
// packages/cli/src/web/managed-host-reconnect.ts
|
|
714
|
+
function startManagedHostReconnect(options) {
|
|
715
|
+
const metadata = options.getMetadata ?? loadManagedWorker;
|
|
716
|
+
const now = options.now ?? Date.now;
|
|
717
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
718
|
+
const controller = new AbortController();
|
|
719
|
+
let busy = false;
|
|
720
|
+
const tick = async () => {
|
|
721
|
+
if (busy || controller.signal.aborted) return;
|
|
722
|
+
busy = true;
|
|
723
|
+
try {
|
|
724
|
+
const registry2 = options.getRegistry();
|
|
725
|
+
const hosts = registry2.list().filter((host) => host.id !== "local");
|
|
726
|
+
for (const id2 of attempts.keys()) if (!hosts.some((host) => host.id === id2)) attempts.delete(id2);
|
|
727
|
+
for (const host of hosts) {
|
|
728
|
+
if (controller.signal.aborted) break;
|
|
729
|
+
try {
|
|
730
|
+
if (metadata(host.id).autoReconnect === false || (attempts.get(host.id)?.next ?? 0) > now()) continue;
|
|
731
|
+
const view = await registry2.handshake(host.id).catch(() => null);
|
|
732
|
+
if (view?.backend.status === "connected") {
|
|
733
|
+
attempts.delete(host.id);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (controller.signal.aborted || !registry2.list().some((item) => item.id === host.id) || metadata(host.id).autoReconnect === false) continue;
|
|
737
|
+
try {
|
|
738
|
+
await options.repair(host.id, controller.signal);
|
|
739
|
+
if (!controller.signal.aborted) await options.onRecovered?.(host.id);
|
|
740
|
+
attempts.delete(host.id);
|
|
741
|
+
} catch {
|
|
742
|
+
const count = (attempts.get(host.id)?.count ?? 0) + 1;
|
|
743
|
+
attempts.set(host.id, { count, next: now() + [6e4, 12e4, 3e5][Math.min(count - 1, 2)] });
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
} finally {
|
|
751
|
+
busy = false;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const timer = setInterval(() => {
|
|
755
|
+
void tick();
|
|
756
|
+
}, 3e4);
|
|
757
|
+
timer.unref?.();
|
|
758
|
+
void tick();
|
|
759
|
+
return () => {
|
|
760
|
+
clearInterval(timer);
|
|
761
|
+
controller.abort();
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// packages/cli/src/web/local-provider-relay.ts
|
|
766
|
+
async function createLocalRelayProvider(selection = {}, signal) {
|
|
767
|
+
const [{ createWebConsoleRuntime }, { createWorkbenchProviderBroker }] = await Promise.all([
|
|
768
|
+
import("./operator-services-E2NONBQS.js"),
|
|
769
|
+
import("./dist-6RNAVFAY.js")
|
|
770
|
+
]);
|
|
771
|
+
signal?.throwIfAborted();
|
|
772
|
+
if (selection.providerId && selection.providerId !== "chatgpt-codex") throw new Error("Local model relay currently supports ChatGPT Codex only. Choose the local ChatGPT connection.");
|
|
773
|
+
const { runtime, info } = await createWebConsoleRuntime({ ...selection, providerId: "chatgpt-codex" });
|
|
774
|
+
signal?.throwIfAborted();
|
|
775
|
+
const provider = runtime.resolvedProvider();
|
|
776
|
+
const model = runtime.resolvedModel();
|
|
777
|
+
if (provider !== "chatgpt-codex" || !model || !info.configured || !info.diagnostics.valid) throw new Error("Local model relay requires a connected ChatGPT Codex account and an explicit model.");
|
|
778
|
+
const routing = runtime.modelSelection();
|
|
779
|
+
const models = [.../* @__PURE__ */ new Set([model, ...Object.values(routing.agentModels).filter((value) => value !== "auto")])];
|
|
780
|
+
const broker = createWorkbenchProviderBroker({ provider, models, resolveCredentials: runtime.workbenchCredentialResolver(), signal });
|
|
781
|
+
return { info, grant: broker.grant, transport: { provider, models: broker.grant.models, request: (request, requestSignal) => broker.request(request, requestSignal), close: broker.close } };
|
|
782
|
+
}
|
|
783
|
+
function object(value) {
|
|
784
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
785
|
+
}
|
|
786
|
+
function id(value) {
|
|
787
|
+
return typeof value === "string" && value.length > 0 && value.length <= 160 && !/[\x00-\x20\x7f]/.test(value);
|
|
788
|
+
}
|
|
789
|
+
var RelayResponseError = class extends Error {
|
|
790
|
+
constructor(code, status) {
|
|
791
|
+
super("Provider relay transport failed.");
|
|
792
|
+
this.code = code;
|
|
793
|
+
this.status = status;
|
|
794
|
+
this.name = "RelayResponseError";
|
|
795
|
+
}
|
|
796
|
+
code;
|
|
797
|
+
status;
|
|
798
|
+
};
|
|
799
|
+
function safeRelayFailure(error2) {
|
|
800
|
+
if (error2 instanceof RelayResponseError) return { name: error2.name, code: error2.code };
|
|
801
|
+
const name = error2 instanceof Error && ["Error", "TypeError", "SyntaxError", "AbortError", "TimeoutError", "BackendConnectionError"].includes(error2.name) ? error2.name : "OtherError";
|
|
802
|
+
const known = {
|
|
803
|
+
"Provider request cancelled or timed out": "provider_timeout_or_cancel",
|
|
804
|
+
"Provider response stream failed": "provider_stream_failed",
|
|
805
|
+
"Host provider request failed": "provider_request_failed",
|
|
806
|
+
"Provider broker concurrency limit reached": "provider_concurrency",
|
|
807
|
+
"Provider broker session budget exhausted": "provider_budget",
|
|
808
|
+
"Provider broker is closed": "provider_closed",
|
|
809
|
+
"Provider or model is not granted to this workbench": "provider_grant_rejected",
|
|
810
|
+
"Provider request exceeds its byte limit": "provider_input_limit",
|
|
811
|
+
"Invalid provider request JSON": "provider_input_json",
|
|
812
|
+
"Provider broker permits only stateless streamed Responses requests": "provider_body_shape",
|
|
813
|
+
"Provider broker refuses hosted tools and external tool destinations": "provider_tool_destination",
|
|
814
|
+
"Unsupported provider input item": "provider_input_type",
|
|
815
|
+
"Provider broker permits text input only": "provider_text_only",
|
|
816
|
+
"Remote provider relay scope changed.": "remote_scope_changed",
|
|
817
|
+
"Invalid provider cancellation.": "remote_cancel_invalid",
|
|
818
|
+
"Remote provider request exceeds its approval.": "remote_request_not_approved",
|
|
819
|
+
"Remote provider relay exceeded its request budget.": "relay_budget",
|
|
820
|
+
"Provider output exceeds approval.": "provider_output_limit",
|
|
821
|
+
"Provider SSE event exceeds response limit.": "provider_sse_limit",
|
|
822
|
+
"Backend response identity does not match the registered engine.": "remote_identity_changed",
|
|
823
|
+
"Backend response exceeds the transport limit.": "remote_response_limit",
|
|
824
|
+
"Backend request timed out.": "remote_timeout",
|
|
825
|
+
"Backend transport failed.": "remote_transport_failed",
|
|
826
|
+
"Backend request capacity exceeded.": "remote_capacity"
|
|
827
|
+
};
|
|
828
|
+
return { name, code: error2 instanceof Error ? Object.hasOwn(known, error2.message) ? known[error2.message] : "unclassified" : "unclassified" };
|
|
829
|
+
}
|
|
830
|
+
async function json(response) {
|
|
831
|
+
if (!response.ok) {
|
|
832
|
+
await response.body?.cancel();
|
|
833
|
+
throw new RelayResponseError("remote_http_rejected", response.status);
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
return object(await response.json());
|
|
837
|
+
} catch {
|
|
838
|
+
throw new RelayResponseError("remote_invalid_json", response.status);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function pause(signal, delayMs = 500) {
|
|
842
|
+
if (signal.aborted) return;
|
|
843
|
+
await new Promise((resolve5) => {
|
|
844
|
+
const abort = () => {
|
|
845
|
+
clearTimeout(timer);
|
|
846
|
+
resolve5();
|
|
847
|
+
};
|
|
848
|
+
const timer = setTimeout(() => {
|
|
849
|
+
signal.removeEventListener("abort", abort);
|
|
850
|
+
resolve5();
|
|
851
|
+
}, delayMs);
|
|
852
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function retryablePollFailure(error2) {
|
|
856
|
+
if (error2 instanceof RelayResponseError) return [408, 425, 429, 500, 502, 503, 504].includes(error2.status);
|
|
857
|
+
if (!(error2 instanceof Error) || error2.name !== "BackendConnectionError") return false;
|
|
858
|
+
const status = error2.statusCode;
|
|
859
|
+
return status === 504 && error2.message === "Backend request timed out." || status === 502 && error2.message === "Backend transport failed." || status === 429 && error2.message === "Backend request capacity exceeded.";
|
|
860
|
+
}
|
|
861
|
+
function failureTransportStatus(error2, fallback) {
|
|
862
|
+
if (error2 instanceof RelayResponseError) return error2.status;
|
|
863
|
+
if (error2 instanceof Error && error2.name === "BackendConnectionError" && typeof error2.statusCode === "number") return error2.statusCode;
|
|
864
|
+
return fallback;
|
|
865
|
+
}
|
|
866
|
+
function providerSseCompletion(maximumBytes = 8 * 1024 * 1024) {
|
|
867
|
+
const decoder2 = new TextDecoder();
|
|
868
|
+
let buffer = "", completed = false;
|
|
869
|
+
return (chunk) => {
|
|
870
|
+
if (completed) return true;
|
|
871
|
+
buffer += decoder2.decode(chunk, { stream: true });
|
|
872
|
+
let boundary;
|
|
873
|
+
while (boundary = /\r?\n\r?\n/.exec(buffer)) {
|
|
874
|
+
const block = buffer.slice(0, boundary.index);
|
|
875
|
+
if (Buffer.byteLength(block) > maximumBytes) throw new Error("Provider SSE event exceeds response limit.");
|
|
876
|
+
buffer = buffer.slice(boundary.index + boundary[0].length);
|
|
877
|
+
const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
878
|
+
if (!data) continue;
|
|
879
|
+
if (data === "[DONE]") {
|
|
880
|
+
completed = true;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const event = JSON.parse(data);
|
|
885
|
+
if (event && ["response.completed", "response.failed", "response.incomplete", "error"].includes(String(event.type))) {
|
|
886
|
+
completed = true;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (Buffer.byteLength(buffer) > maximumBytes) throw new Error("Provider SSE event exceeds response limit.");
|
|
893
|
+
return completed;
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function createLocalProviderRelay(options) {
|
|
897
|
+
if (options.backendId === "local" || !id(options.backendId)) throw new Error("Provider relay requires a registered remote host.");
|
|
898
|
+
const lifetime = new AbortController();
|
|
899
|
+
const signal = options.signal ? AbortSignal.any([lifetime.signal, options.signal]) : lifetime.signal;
|
|
900
|
+
signal.throwIfAborted();
|
|
901
|
+
const peer = await options.registry.handshake(options.backendId);
|
|
902
|
+
const epoch2 = peer.handshake?.serverInstanceId;
|
|
903
|
+
if (peer.backend.status !== "connected" || !id(epoch2)) throw new Error("Provider relay requires a connected, identified host.");
|
|
904
|
+
const local = await (options.createProvider ?? createLocalRelayProvider)(options.selection ?? {}, signal);
|
|
905
|
+
const base = "/api/console/provider-relays";
|
|
906
|
+
let grant;
|
|
907
|
+
try {
|
|
908
|
+
const response = await json(await options.registry.request(options.backendId, base, { method: "POST", body: { epoch: epoch2, provider: local.grant.provider, models: local.grant.models, model: local.info.model }, signal }));
|
|
909
|
+
const candidate = object(response.grant);
|
|
910
|
+
if (!id(candidate.id) || candidate.epoch !== epoch2 || candidate.provider !== local.grant.provider || !Array.isArray(candidate.models) || candidate.models.length !== local.grant.models.length || new Set(candidate.models).size !== candidate.models.length || candidate.models.some((model) => typeof model !== "string" || !local.grant.models.includes(model)) || candidate.model !== local.info.model || typeof candidate.expiresAt !== "string" || !Number.isFinite(Date.parse(candidate.expiresAt))) throw new Error("Remote provider relay grant does not match the local approval.");
|
|
911
|
+
grant = candidate;
|
|
912
|
+
} catch (error2) {
|
|
913
|
+
await local.transport.close?.();
|
|
914
|
+
throw error2;
|
|
915
|
+
}
|
|
916
|
+
const diagnostic = { phase: "registered", lastPollAt: null, seenRequests: 0, activeRequests: 0, bytesForwarded: 0 };
|
|
917
|
+
const grantPath = `${base}/${encodeURIComponent(grant.id)}`;
|
|
918
|
+
let revocation;
|
|
919
|
+
const revokeGrant = () => revocation ??= options.registry.request(options.backendId, grantPath, { method: "DELETE", signal: AbortSignal.timeout(5e3) }).then(async (response) => {
|
|
920
|
+
await response.body?.cancel();
|
|
921
|
+
}).catch(() => {
|
|
922
|
+
});
|
|
923
|
+
const active = /* @__PURE__ */ new Map();
|
|
924
|
+
const seen = /* @__PURE__ */ new Set();
|
|
925
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
926
|
+
let boundSession, polling, closing;
|
|
927
|
+
const abortActive = () => {
|
|
928
|
+
for (const controller of active.values()) controller.abort();
|
|
929
|
+
};
|
|
930
|
+
signal.addEventListener("abort", abortActive, { once: true });
|
|
931
|
+
const remoteRequest = async (path, optionsValue) => {
|
|
932
|
+
const response = await options.registry.request(options.backendId, path, optionsValue);
|
|
933
|
+
diagnostic.lastTransportStatus = response.status;
|
|
934
|
+
return response;
|
|
935
|
+
};
|
|
936
|
+
const send = (request, events, callerSignal) => remoteRequest(`${grantPath}/requests/${encodeURIComponent(request.requestId)}/events`, { method: "POST", body: { epoch: epoch2, sessionId: boundSession, events }, signal: callerSignal }).then(json);
|
|
937
|
+
async function service(request) {
|
|
938
|
+
const controller = new AbortController();
|
|
939
|
+
active.set(request.requestId, controller);
|
|
940
|
+
diagnostic.phase = "requesting";
|
|
941
|
+
diagnostic.activeRequests = active.size;
|
|
942
|
+
delete diagnostic.error;
|
|
943
|
+
const requestSignal = AbortSignal.any([signal, controller.signal, AbortSignal.timeout(local.grant.limits.timeoutMs)]);
|
|
944
|
+
let reader;
|
|
945
|
+
const cancelReader = () => {
|
|
946
|
+
void reader?.cancel().catch(() => {
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
requestSignal.addEventListener("abort", cancelReader, { once: true });
|
|
950
|
+
let stage = "provider_request";
|
|
951
|
+
try {
|
|
952
|
+
const response = await local.transport.request({ provider: request.provider, model: request.model, body: request.body }, requestSignal);
|
|
953
|
+
diagnostic.lastUpstreamStatus = response.status;
|
|
954
|
+
stage = "headers_post";
|
|
955
|
+
await send(request, [{ kind: "headers", status: response.status, contentType: response.headers.get("content-type") || "text/event-stream" }], requestSignal);
|
|
956
|
+
diagnostic.phase = "streaming";
|
|
957
|
+
if (response.body) {
|
|
958
|
+
reader = response.body.getReader();
|
|
959
|
+
let bytes = 0;
|
|
960
|
+
const completed = response.headers.get("content-type")?.toLowerCase().includes("text/event-stream") ? providerSseCompletion(local.grant.limits.maxResponseBytes) : void 0;
|
|
961
|
+
while (!requestSignal.aborted) {
|
|
962
|
+
stage = "reader_read";
|
|
963
|
+
const chunk = await reader.read();
|
|
964
|
+
if (chunk.done) break;
|
|
965
|
+
bytes += chunk.value.byteLength;
|
|
966
|
+
if (bytes > local.grant.limits.maxResponseBytes) throw new Error("Provider output exceeds approval.");
|
|
967
|
+
stage = "chunk_post";
|
|
968
|
+
for (let offset = 0; offset < chunk.value.byteLength; offset += 32 * 1024) {
|
|
969
|
+
const portion = chunk.value.subarray(offset, offset + 32 * 1024);
|
|
970
|
+
await send(request, [{ kind: "chunk", data: Buffer.from(portion).toString("base64") }], requestSignal);
|
|
971
|
+
diagnostic.bytesForwarded += portion.byteLength;
|
|
972
|
+
}
|
|
973
|
+
stage = "sse_parse";
|
|
974
|
+
if (completed?.(chunk.value)) break;
|
|
975
|
+
}
|
|
976
|
+
requestSignal.throwIfAborted();
|
|
977
|
+
}
|
|
978
|
+
stage = "end_post";
|
|
979
|
+
await send(request, [{ kind: "end" }], requestSignal);
|
|
980
|
+
} catch (error2) {
|
|
981
|
+
if (!signal.aborted && !controller.signal.aborted) {
|
|
982
|
+
const failure = safeRelayFailure(error2);
|
|
983
|
+
diagnostic.lastFailureStage = stage;
|
|
984
|
+
diagnostic.lastFailureName = failure.name;
|
|
985
|
+
diagnostic.lastFailureCode = failure.code;
|
|
986
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
987
|
+
diagnostic.error = diagnostic.phase === "requesting" ? "upstream_failed" : "response_rejected";
|
|
988
|
+
await send(request, [{ kind: "error" }], AbortSignal.any([signal, AbortSignal.timeout(5e3)])).catch(() => {
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
} finally {
|
|
992
|
+
requestSignal.removeEventListener("abort", cancelReader);
|
|
993
|
+
controller.abort();
|
|
994
|
+
if (reader) {
|
|
995
|
+
await reader.cancel().catch(() => {
|
|
996
|
+
});
|
|
997
|
+
reader.releaseLock();
|
|
998
|
+
}
|
|
999
|
+
active.delete(request.requestId);
|
|
1000
|
+
diagnostic.activeRequests = active.size;
|
|
1001
|
+
if (!signal.aborted && !active.size) diagnostic.phase = "polling";
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async function poll() {
|
|
1005
|
+
let failureCategory = "poll_rejected";
|
|
1006
|
+
try {
|
|
1007
|
+
if (!active.size) diagnostic.phase = "polling";
|
|
1008
|
+
while (!signal.aborted) {
|
|
1009
|
+
failureCategory = "poll_rejected";
|
|
1010
|
+
let body;
|
|
1011
|
+
try {
|
|
1012
|
+
body = await json(await remoteRequest(`${grantPath}/requests`, { method: "GET", signal }));
|
|
1013
|
+
} catch (error2) {
|
|
1014
|
+
if (signal.aborted || !retryablePollFailure(error2)) throw error2;
|
|
1015
|
+
const failure = safeRelayFailure(error2);
|
|
1016
|
+
diagnostic.phase = "reconnecting";
|
|
1017
|
+
diagnostic.error = "poll_rejected";
|
|
1018
|
+
diagnostic.lastFailureStage = "poll";
|
|
1019
|
+
diagnostic.lastFailureName = failure.name;
|
|
1020
|
+
diagnostic.lastFailureCode = failure.code;
|
|
1021
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
1022
|
+
diagnostic.consecutivePollFailures = (diagnostic.consecutivePollFailures ?? 0) + 1;
|
|
1023
|
+
await pause(signal, Math.min(5e3, 500 * 2 ** Math.min(diagnostic.consecutivePollFailures - 1, 4)));
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
diagnostic.consecutivePollFailures = 0;
|
|
1027
|
+
if (diagnostic.error === "poll_rejected") delete diagnostic.error;
|
|
1028
|
+
diagnostic.phase = active.size ? "streaming" : "polling";
|
|
1029
|
+
diagnostic.lastPollAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1030
|
+
failureCategory = "scope_changed";
|
|
1031
|
+
if (body.grantId !== grant.id || body.epoch !== epoch2 || body.sessionId !== void 0 && body.sessionId !== boundSession || !Array.isArray(body.requests) || !Array.isArray(body.cancelled) || body.requests.length > 4 || body.cancelled.length > 256) throw new Error("Remote provider relay scope changed.");
|
|
1032
|
+
for (const requestId of body.cancelled) {
|
|
1033
|
+
if (!id(requestId)) throw new Error("Invalid provider cancellation.");
|
|
1034
|
+
active.get(requestId)?.abort();
|
|
1035
|
+
}
|
|
1036
|
+
for (const value of body.requests) {
|
|
1037
|
+
const request = object(value);
|
|
1038
|
+
failureCategory = "request_rejected";
|
|
1039
|
+
if (!id(request.requestId) || request.grantId !== grant.id || request.epoch !== epoch2 || request.sessionId !== boundSession || request.provider !== local.grant.provider || typeof request.model !== "string" || !local.grant.models.includes(request.model) || typeof request.body !== "string" || Buffer.byteLength(request.body) > local.grant.limits.maxRequestBytes) throw new Error("Remote provider request exceeds its approval.");
|
|
1040
|
+
if (seen.has(request.requestId)) continue;
|
|
1041
|
+
if (seen.size >= local.grant.limits.maxRequests || active.size >= Math.min(4, local.grant.limits.maxConcurrent)) throw new Error("Remote provider relay exceeded its request budget.");
|
|
1042
|
+
seen.add(request.requestId);
|
|
1043
|
+
diagnostic.seenRequests = seen.size;
|
|
1044
|
+
const task = service(request);
|
|
1045
|
+
tasks.add(task);
|
|
1046
|
+
void task.finally(() => tasks.delete(task));
|
|
1047
|
+
}
|
|
1048
|
+
await pause(signal);
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error2) {
|
|
1051
|
+
if (!signal.aborted) {
|
|
1052
|
+
diagnostic.phase = "failed";
|
|
1053
|
+
diagnostic.error = failureCategory;
|
|
1054
|
+
const failure = safeRelayFailure(error2);
|
|
1055
|
+
diagnostic.lastFailureStage = "poll";
|
|
1056
|
+
diagnostic.lastFailureName = failure.name;
|
|
1057
|
+
diagnostic.lastFailureCode = failure.code;
|
|
1058
|
+
diagnostic.lastFailureTransportStatus = failureTransportStatus(error2, diagnostic.lastTransportStatus);
|
|
1059
|
+
}
|
|
1060
|
+
if (!signal.aborted) options.onError?.("The local model connection to this host ended. Reconnect the host before continuing.");
|
|
1061
|
+
lifetime.abort();
|
|
1062
|
+
} finally {
|
|
1063
|
+
if (signal.aborted && diagnostic.phase !== "failed") diagnostic.phase = "closed";
|
|
1064
|
+
abortActive();
|
|
1065
|
+
await local.transport.close?.();
|
|
1066
|
+
await revokeGrant();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
grant,
|
|
1071
|
+
info: local.info,
|
|
1072
|
+
status: () => ({ ...diagnostic }),
|
|
1073
|
+
start(sessionId) {
|
|
1074
|
+
if (!id(sessionId) || signal.aborted || boundSession) throw new Error("Provider relay must bind once to an active remote session.");
|
|
1075
|
+
boundSession = sessionId;
|
|
1076
|
+
polling = poll();
|
|
1077
|
+
},
|
|
1078
|
+
close() {
|
|
1079
|
+
return closing ??= (async () => {
|
|
1080
|
+
diagnostic.phase = "closed";
|
|
1081
|
+
lifetime.abort();
|
|
1082
|
+
abortActive();
|
|
1083
|
+
signal.removeEventListener("abort", abortActive);
|
|
1084
|
+
await local.transport.close?.();
|
|
1085
|
+
await Promise.allSettled([...tasks, ...polling ? [polling] : []]);
|
|
1086
|
+
await revokeGrant();
|
|
1087
|
+
})();
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// packages/cli/src/web/local-provider-sessions.ts
|
|
1093
|
+
var LocalProviderSessionError = class extends Error {
|
|
1094
|
+
constructor(message2, statusCode = 400) {
|
|
1095
|
+
super(message2);
|
|
1096
|
+
this.statusCode = statusCode;
|
|
1097
|
+
}
|
|
1098
|
+
statusCode;
|
|
1099
|
+
};
|
|
1100
|
+
var resourceId = external_exports.string().min(1).max(160).regex(/^[^\x00-\x20\x7f]+$/);
|
|
1101
|
+
var runtimeSchema = external_exports.object({ providerId: external_exports.string().min(1).max(256).optional(), model: external_exports.string().min(1).max(256).optional(), reasoningEffort: external_exports.string().min(1).max(256).optional(), agentModels: external_exports.record(external_exports.string().min(1).max(64), external_exports.string().min(1).max(256)).refine((value) => Object.keys(value).length <= 32).optional(), singleModel: external_exports.boolean().optional(), autoRoute: external_exports.boolean().optional() }).strict();
|
|
1102
|
+
var createSchema = external_exports.object({ workflowId: external_exports.string().min(1).max(128).optional(), target: external_exports.string().max(4096).optional(), title: external_exports.string().max(200).optional(), role: external_exports.enum(["discovery", "attack", "verify", "report", "audit", "review"]).optional(), autonomyMode: external_exports.enum(["standard", "recon", "copilot", "yolo"]).optional(), scope: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional(), runtime: runtimeSchema.optional(), findingId: external_exports.string().max(256).optional(), findingIntent: external_exports.enum(["investigate", "verify", "draft_fix", "impact"]).optional() }).strict();
|
|
1103
|
+
var LocalProviderSessions = class {
|
|
1104
|
+
constructor(options) {
|
|
1105
|
+
this.options = options;
|
|
1106
|
+
}
|
|
1107
|
+
options;
|
|
1108
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1109
|
+
lifetime = new AbortController();
|
|
1110
|
+
creating = 0;
|
|
1111
|
+
reconnecting = /* @__PURE__ */ new Set();
|
|
1112
|
+
key(backendId, sessionId) {
|
|
1113
|
+
return JSON.stringify([backendId, sessionId]);
|
|
1114
|
+
}
|
|
1115
|
+
signal() {
|
|
1116
|
+
return this.options.signal ? AbortSignal.any([this.lifetime.signal, this.options.signal]) : this.lifetime.signal;
|
|
1117
|
+
}
|
|
1118
|
+
assertBackend(backendId) {
|
|
1119
|
+
if (!resourceId.safeParse(backendId).success || backendId === "local") throw new LocalProviderSessionError("Choose a registered remote host.");
|
|
1120
|
+
this.signal().throwIfAborted();
|
|
1121
|
+
}
|
|
1122
|
+
async request(backendId, path, method, body) {
|
|
1123
|
+
const response = await this.options.registry.request(backendId, path, { method, body, signal: this.signal() });
|
|
1124
|
+
if (!response.ok) {
|
|
1125
|
+
await response.body?.cancel();
|
|
1126
|
+
throw new LocalProviderSessionError("The remote host rejected this conversation change.", response.status);
|
|
1127
|
+
}
|
|
1128
|
+
const value = await response.json();
|
|
1129
|
+
if (!value?.session || !resourceId.safeParse(value.session.id).success) throw new LocalProviderSessionError("The remote host returned an invalid conversation.", 502);
|
|
1130
|
+
return value.session;
|
|
1131
|
+
}
|
|
1132
|
+
async create(backendId, input, ownerId = "local") {
|
|
1133
|
+
this.assertBackend(backendId);
|
|
1134
|
+
const parsed = createSchema.safeParse(input);
|
|
1135
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid remote conversation configuration; model grants are issued locally.");
|
|
1136
|
+
if (this.sessions.size + this.creating >= 32) throw new LocalProviderSessionError("Close a local model connection before creating another remote conversation.", 429);
|
|
1137
|
+
this.creating++;
|
|
1138
|
+
let relay;
|
|
1139
|
+
try {
|
|
1140
|
+
relay = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection: parsed.data.runtime, signal: this.signal() });
|
|
1141
|
+
const session = await this.request(backendId, "/api/console/sessions", "POST", { ...parsed.data, runtime: { ...parsed.data.runtime, providerId: relay.info.providerId, model: relay.info.model }, modelSource: { kind: "local", grantId: relay.grant.id } });
|
|
1142
|
+
this.signal().throwIfAborted();
|
|
1143
|
+
const key = this.key(backendId, session.id);
|
|
1144
|
+
if (this.sessions.has(key)) throw new LocalProviderSessionError("The host reused an existing conversation ID.", 409);
|
|
1145
|
+
relay.start(session.id);
|
|
1146
|
+
this.sessions.set(key, { owner: ownerId, relay, selection: parsed.data.runtime ?? {}, configuring: false });
|
|
1147
|
+
return { session };
|
|
1148
|
+
} catch (error2) {
|
|
1149
|
+
await relay?.close();
|
|
1150
|
+
throw error2;
|
|
1151
|
+
} finally {
|
|
1152
|
+
this.creating--;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async configure(backendId, sessionId, runtime, ownerId = "local") {
|
|
1156
|
+
this.assertBackend(backendId);
|
|
1157
|
+
if (!resourceId.safeParse(sessionId).success) throw new LocalProviderSessionError("Invalid conversation ID.");
|
|
1158
|
+
const managed = this.sessions.get(this.key(backendId, sessionId));
|
|
1159
|
+
if (!managed) return this.reconnect(backendId, sessionId, runtime, ownerId);
|
|
1160
|
+
if (managed.owner !== ownerId) throw new LocalProviderSessionError("This local model connection does not belong to the current workspace.", 404);
|
|
1161
|
+
const parsed = runtimeSchema.safeParse(runtime);
|
|
1162
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid local model selection.");
|
|
1163
|
+
if (managed.configuring) throw new LocalProviderSessionError("Wait for the current model change.", 409);
|
|
1164
|
+
managed.configuring = true;
|
|
1165
|
+
let replacement;
|
|
1166
|
+
try {
|
|
1167
|
+
const selection = { ...managed.selection, ...parsed.data };
|
|
1168
|
+
replacement = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection, signal: this.signal() });
|
|
1169
|
+
const session = await this.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}/configuration`, "PATCH", { runtime: { ...selection, providerId: replacement.info.providerId, model: replacement.info.model }, modelSource: { kind: "local", grantId: replacement.grant.id } });
|
|
1170
|
+
if (session.id !== sessionId) throw new LocalProviderSessionError("The host returned a different conversation.", 409);
|
|
1171
|
+
this.signal().throwIfAborted();
|
|
1172
|
+
replacement.start(sessionId);
|
|
1173
|
+
const previous = managed.relay;
|
|
1174
|
+
managed.relay = replacement;
|
|
1175
|
+
managed.selection = selection;
|
|
1176
|
+
replacement = void 0;
|
|
1177
|
+
await previous.close();
|
|
1178
|
+
return { session };
|
|
1179
|
+
} finally {
|
|
1180
|
+
managed.configuring = false;
|
|
1181
|
+
await replacement?.close();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/** Explicitly restore a saved local-model marker after either engine restarted. */
|
|
1185
|
+
async reconnect(backendId, sessionId, runtime, ownerId = "local") {
|
|
1186
|
+
this.assertBackend(backendId);
|
|
1187
|
+
if (!resourceId.safeParse(sessionId).success) throw new LocalProviderSessionError("Invalid conversation ID.");
|
|
1188
|
+
const key = this.key(backendId, sessionId), existing = this.sessions.get(key);
|
|
1189
|
+
if (existing) return this.configure(backendId, sessionId, runtime, ownerId);
|
|
1190
|
+
const parsed = runtimeSchema.safeParse(runtime);
|
|
1191
|
+
if (!parsed.success) throw new LocalProviderSessionError("Invalid local model selection.");
|
|
1192
|
+
if (this.reconnecting.has(key)) throw new LocalProviderSessionError("Wait for this conversation to reconnect.", 409);
|
|
1193
|
+
if (this.sessions.size + this.creating >= 32) throw new LocalProviderSessionError("Close a local model connection before reconnecting another conversation.", 429);
|
|
1194
|
+
this.reconnecting.add(key);
|
|
1195
|
+
this.creating++;
|
|
1196
|
+
let replacement;
|
|
1197
|
+
try {
|
|
1198
|
+
const response = await this.options.registry.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}`, { method: "GET", signal: this.signal() });
|
|
1199
|
+
if (!response.ok) {
|
|
1200
|
+
await response.body?.cancel();
|
|
1201
|
+
throw new LocalProviderSessionError("This remote conversation could not be reopened.", response.status);
|
|
1202
|
+
}
|
|
1203
|
+
const snapshot = (await response.json()).snapshot;
|
|
1204
|
+
if (snapshot?.session?.id !== sessionId || snapshot.runtime?.modelSource !== "local") throw new LocalProviderSessionError("Only an existing local-model conversation can reconnect through this route.", 409);
|
|
1205
|
+
if (snapshot.session.status !== "ready") throw new LocalProviderSessionError("Wait until this remote conversation is idle before reconnecting its model.", 409);
|
|
1206
|
+
const selection = runtimeSchema.parse({ providerId: "chatgpt-codex", ...snapshot.runtime.model ? { model: snapshot.runtime.model } : {}, ...parsed.data });
|
|
1207
|
+
replacement = await (this.options.createRelay ?? createLocalProviderRelay)({ registry: this.options.registry, backendId, selection, signal: this.signal() });
|
|
1208
|
+
const session = await this.request(backendId, `/api/console/sessions/${encodeURIComponent(sessionId)}/configuration`, "PATCH", { runtime: { ...selection, providerId: replacement.info.providerId, model: replacement.info.model }, modelSource: { kind: "local", grantId: replacement.grant.id } });
|
|
1209
|
+
if (session.id !== sessionId) throw new LocalProviderSessionError("The host returned a different conversation.", 409);
|
|
1210
|
+
this.signal().throwIfAborted();
|
|
1211
|
+
replacement.start(sessionId);
|
|
1212
|
+
this.sessions.set(key, { owner: ownerId, relay: replacement, selection, configuring: false });
|
|
1213
|
+
replacement = void 0;
|
|
1214
|
+
return { session };
|
|
1215
|
+
} finally {
|
|
1216
|
+
this.creating--;
|
|
1217
|
+
this.reconnecting.delete(key);
|
|
1218
|
+
await replacement?.close();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async status(backendId, sessionId, ownerId = "local") {
|
|
1222
|
+
this.assertBackend(backendId);
|
|
1223
|
+
const managed = this.sessions.get(this.key(backendId, sessionId));
|
|
1224
|
+
if (!managed || managed.owner !== ownerId) throw new LocalProviderSessionError("This local model connection does not belong to the current workspace.", 404);
|
|
1225
|
+
const local = managed.relay.status();
|
|
1226
|
+
try {
|
|
1227
|
+
const response = await this.options.registry.request(backendId, `/api/console/provider-relays/${encodeURIComponent(managed.relay.grant.id)}/status`, { method: "GET", signal: AbortSignal.any([this.signal(), AbortSignal.timeout(5e3)]) });
|
|
1228
|
+
if (!response.ok) {
|
|
1229
|
+
await response.body?.cancel();
|
|
1230
|
+
return { local, remote: { available: false, status: response.status } };
|
|
1231
|
+
}
|
|
1232
|
+
const body = await response.json();
|
|
1233
|
+
if (body.grantId !== managed.relay.grant.id || body.epoch !== managed.relay.grant.epoch || body.sessionId !== sessionId) throw new Error("scope");
|
|
1234
|
+
const pending = external_exports.array(external_exports.object({ delivered: external_exports.boolean(), headers: external_exports.boolean(), receivedBytes: external_exports.number().finite().nonnegative(), bufferedBytes: external_exports.number().finite().nonnegative(), ageMs: external_exports.number().finite().nonnegative() }).strip()).max(4).parse(body.pending);
|
|
1235
|
+
const cancelledCount = external_exports.number().int().nonnegative().parse(body.cancelledCount);
|
|
1236
|
+
return { local, remote: { available: true, pending, cancelledCount } };
|
|
1237
|
+
} catch {
|
|
1238
|
+
return { local, remote: { available: false } };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async release(backendId, sessionId, ownerId = "local") {
|
|
1242
|
+
const key = this.key(backendId, sessionId), managed = this.sessions.get(key);
|
|
1243
|
+
if (!managed || managed.owner !== ownerId) return;
|
|
1244
|
+
this.sessions.delete(key);
|
|
1245
|
+
await managed.relay.close();
|
|
1246
|
+
}
|
|
1247
|
+
async dispose() {
|
|
1248
|
+
this.lifetime.abort();
|
|
1249
|
+
const entries = [...this.sessions.values()];
|
|
1250
|
+
this.sessions.clear();
|
|
1251
|
+
await Promise.allSettled(entries.map((entry) => entry.relay.close()));
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// packages/cli/src/web/remote-provider-relay.ts
|
|
1256
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1257
|
+
var ProviderRelayError = class extends Error {
|
|
1258
|
+
constructor(message2, statusCode = 400) {
|
|
1259
|
+
super(message2);
|
|
1260
|
+
this.statusCode = statusCode;
|
|
1261
|
+
}
|
|
1262
|
+
statusCode;
|
|
1263
|
+
};
|
|
1264
|
+
var GRANT_IDLE_MS = 30 * 6e4;
|
|
1265
|
+
var REDELIVER_UNACKNOWLEDGED_MS = 5e3;
|
|
1266
|
+
var modelId = external_exports.string().regex(/^[A-Za-z0-9._-]{1,128}$/);
|
|
1267
|
+
var grantInput = external_exports.object({ epoch: external_exports.string().min(1).max(256), provider: external_exports.literal("chatgpt-codex"), models: external_exports.array(modelId).min(1).max(16), model: modelId }).strict();
|
|
1268
|
+
var eventsInput = external_exports.object({ epoch: external_exports.string(), sessionId: external_exports.string().min(1).max(256), events: external_exports.array(external_exports.discriminatedUnion("kind", [
|
|
1269
|
+
external_exports.object({ kind: external_exports.literal("headers"), status: external_exports.number().int().min(200).max(599).refine((status) => ![204, 205, 304].includes(status)), contentType: external_exports.string().max(128).regex(/^(text\/event-stream|application\/json)(?:;[^\r\n]*)?$/i).transform((value) => value.split(";")[0].toLowerCase()) }).strict(),
|
|
1270
|
+
external_exports.object({ kind: external_exports.literal("chunk"), data: external_exports.string().max(65536).regex(/^[A-Za-z0-9+/]*={0,2}$/) }).strict(),
|
|
1271
|
+
external_exports.object({ kind: external_exports.literal("end") }).strict(),
|
|
1272
|
+
external_exports.object({ kind: external_exports.literal("error") }).strict()
|
|
1273
|
+
])).min(1).max(16) }).strict();
|
|
1274
|
+
var RemoteProviderRelayBroker = class {
|
|
1275
|
+
epoch;
|
|
1276
|
+
#grants = /* @__PURE__ */ new Map();
|
|
1277
|
+
#disposed = false;
|
|
1278
|
+
constructor(options) {
|
|
1279
|
+
this.epoch = options.epoch;
|
|
1280
|
+
}
|
|
1281
|
+
create(input) {
|
|
1282
|
+
const value = grantInput.parse(input);
|
|
1283
|
+
if (this.#disposed || value.epoch !== this.epoch) throw new ProviderRelayError("Provider relay engine epoch changed.", 409);
|
|
1284
|
+
if (!value.models.includes(value.model) || new Set(value.models).size !== value.models.length) throw new ProviderRelayError("Provider relay requires an exact model allowlist.");
|
|
1285
|
+
for (const [id2, state] of this.#grants) if (Date.parse(state.grant.expiresAt) <= Date.now()) this.close(id2);
|
|
1286
|
+
if (this.#grants.size >= 32) throw new ProviderRelayError("Provider relay grant limit reached.", 429);
|
|
1287
|
+
const grant = { id: randomUUID2(), epoch: this.epoch, provider: value.provider, models: value.models, model: value.model, expiresAt: new Date(Date.now() + GRANT_IDLE_MS).toISOString() };
|
|
1288
|
+
this.#grants.set(grant.id, { grant, requests: /* @__PURE__ */ new Map(), cancelled: [] });
|
|
1289
|
+
return structuredClone(grant);
|
|
1290
|
+
}
|
|
1291
|
+
#get(id2) {
|
|
1292
|
+
const state = this.#grants.get(id2);
|
|
1293
|
+
if (!state || this.#disposed) throw new ProviderRelayError("Provider relay grant is unavailable. Reconnect the local model source.", 410);
|
|
1294
|
+
if (Date.parse(state.grant.expiresAt) <= Date.now()) {
|
|
1295
|
+
this.close(id2);
|
|
1296
|
+
throw new ProviderRelayError("Provider relay grant expired. Reconnect the local model source.", 410);
|
|
1297
|
+
}
|
|
1298
|
+
return state;
|
|
1299
|
+
}
|
|
1300
|
+
bind(id2, sessionId) {
|
|
1301
|
+
const state = this.#get(id2);
|
|
1302
|
+
if (state.sessionId && state.sessionId !== sessionId) throw new ProviderRelayError("Provider relay grant belongs to another conversation.", 409);
|
|
1303
|
+
state.sessionId = sessionId;
|
|
1304
|
+
return structuredClone(state.grant);
|
|
1305
|
+
}
|
|
1306
|
+
transport(id2, sessionId) {
|
|
1307
|
+
const grant = this.bind(id2, sessionId);
|
|
1308
|
+
return { provider: grant.provider, models: grant.models, identity: `relay:${grant.epoch}:${grant.id}`, request: (envelope, signal) => this.request(id2, sessionId, envelope, signal) };
|
|
1309
|
+
}
|
|
1310
|
+
runtime(id2, sessionId, selection = {}) {
|
|
1311
|
+
const grant = this.bind(id2, sessionId);
|
|
1312
|
+
if (selection.providerId && selection.providerId !== grant.provider) throw new ProviderRelayError("Provider is outside the local model grant.", 403);
|
|
1313
|
+
return new LlmApiRuntime({
|
|
1314
|
+
type: "api",
|
|
1315
|
+
timeout: 12e4,
|
|
1316
|
+
model: selection.model ?? grant.model,
|
|
1317
|
+
provider: "chatgpt-codex",
|
|
1318
|
+
singleModel: selection.singleModel,
|
|
1319
|
+
agentModels: selection.agentModels,
|
|
1320
|
+
autoRoute: selection.autoRoute,
|
|
1321
|
+
providerTransport: this.transport(id2, sessionId)
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
request(id2, sessionId, envelope, signal) {
|
|
1325
|
+
const state = this.#get(id2);
|
|
1326
|
+
if (state.sessionId !== sessionId || envelope.provider !== state.grant.provider || !state.grant.models.includes(envelope.model)) throw new ProviderRelayError("Request is outside the local model grant.", 403);
|
|
1327
|
+
signal?.throwIfAborted();
|
|
1328
|
+
if (state.requests.size >= 4) throw new ProviderRelayError("Local model request limit reached.", 429);
|
|
1329
|
+
if (Buffer.byteLength(envelope.body) > 4 * 1024 * 1024) throw new ProviderRelayError("Local model request exceeds 4 MiB.", 413);
|
|
1330
|
+
const body = JSON.parse(envelope.body);
|
|
1331
|
+
if (body.model !== envelope.model || body.background === true || body.store === true || !Array.isArray(body.input)) throw new ProviderRelayError("Invalid granted Responses request.");
|
|
1332
|
+
return new Promise((resolve5, reject) => {
|
|
1333
|
+
const requestId = randomUUID2();
|
|
1334
|
+
const cancel = () => this.#finish(state, pending, new Error("Local provider request cancelled."));
|
|
1335
|
+
const pending = {
|
|
1336
|
+
...envelope,
|
|
1337
|
+
requestId,
|
|
1338
|
+
grantId: id2,
|
|
1339
|
+
epoch: this.epoch,
|
|
1340
|
+
sessionId,
|
|
1341
|
+
startedAt: Date.now(),
|
|
1342
|
+
delivered: false,
|
|
1343
|
+
received: 0,
|
|
1344
|
+
buffered: 0,
|
|
1345
|
+
ended: false,
|
|
1346
|
+
headers: false,
|
|
1347
|
+
queue: [],
|
|
1348
|
+
resolve: resolve5,
|
|
1349
|
+
reject,
|
|
1350
|
+
timer: setTimeout(() => this.#finish(state, pending, new Error("Local provider relay timed out.")), 12e4),
|
|
1351
|
+
cleanup: () => signal?.removeEventListener("abort", cancel)
|
|
1352
|
+
};
|
|
1353
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1354
|
+
state.requests.set(requestId, pending);
|
|
1355
|
+
if (signal?.aborted) cancel();
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
status(id2) {
|
|
1359
|
+
const state = this.#get(id2);
|
|
1360
|
+
return {
|
|
1361
|
+
grantId: id2,
|
|
1362
|
+
epoch: this.epoch,
|
|
1363
|
+
sessionId: state.sessionId,
|
|
1364
|
+
expiresAt: state.grant.expiresAt,
|
|
1365
|
+
pending: [...state.requests.values()].map((request) => ({ id: request.requestId, delivered: request.delivered, headers: request.headers, receivedBytes: request.received, bufferedBytes: request.buffered, ageMs: Math.max(0, Date.now() - request.startedAt) })),
|
|
1366
|
+
cancelledCount: state.cancelled.length
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
poll(id2) {
|
|
1370
|
+
const state = this.#get(id2);
|
|
1371
|
+
state.grant.expiresAt = new Date(Date.now() + GRANT_IDLE_MS).toISOString();
|
|
1372
|
+
const requests = [...state.requests.values()];
|
|
1373
|
+
const next = requests.find((request) => !request.delivered) ?? requests.find((request) => !request.headers && Date.now() - (request.deliveredAt ?? request.startedAt) >= REDELIVER_UNACKNOWLEDGED_MS);
|
|
1374
|
+
if (next) {
|
|
1375
|
+
next.delivered = true;
|
|
1376
|
+
next.deliveredAt = Date.now();
|
|
1377
|
+
}
|
|
1378
|
+
const cancelled = state.cancelled.splice(0);
|
|
1379
|
+
return { grantId: id2, epoch: this.epoch, sessionId: state.sessionId, expiresAt: state.grant.expiresAt, requests: next ? [{ requestId: next.requestId, grantId: id2, epoch: this.epoch, sessionId: next.sessionId, provider: next.provider, model: next.model, body: next.body }] : [], cancelled };
|
|
1380
|
+
}
|
|
1381
|
+
#finish(state, request, error2) {
|
|
1382
|
+
if (request.ended) return;
|
|
1383
|
+
request.ended = true;
|
|
1384
|
+
clearTimeout(request.timer);
|
|
1385
|
+
request.cleanup();
|
|
1386
|
+
state.requests.delete(request.requestId);
|
|
1387
|
+
if (error2) {
|
|
1388
|
+
state.cancelled.push(request.requestId);
|
|
1389
|
+
state.cancelled = state.cancelled.slice(-64);
|
|
1390
|
+
if (!request.headers) request.reject(error2);
|
|
1391
|
+
request.controller?.error(error2);
|
|
1392
|
+
} else if (!request.headers) request.reject(new Error("Provider relay ended without response headers."));
|
|
1393
|
+
else if (request.queue.length === 0) request.controller?.close();
|
|
1394
|
+
}
|
|
1395
|
+
events(id2, requestId, input) {
|
|
1396
|
+
if (Buffer.byteLength(JSON.stringify(input)) > 512 * 1024) throw new ProviderRelayError("Provider event batch exceeds 512 KiB.", 413);
|
|
1397
|
+
const state = this.#get(id2);
|
|
1398
|
+
const value = eventsInput.parse(input);
|
|
1399
|
+
if (value.epoch !== this.epoch || value.sessionId !== state.sessionId) throw new ProviderRelayError("Provider relay ownership or epoch changed.", 409);
|
|
1400
|
+
const pending = state.requests.get(requestId);
|
|
1401
|
+
if (!pending) throw new ProviderRelayError("Provider request is no longer pending.", 410);
|
|
1402
|
+
for (const event of value.events) {
|
|
1403
|
+
if (pending.ended) throw new ProviderRelayError("Provider response has ended.", 409);
|
|
1404
|
+
if (event.kind === "headers") {
|
|
1405
|
+
if (pending.headers) throw new ProviderRelayError("Duplicate provider response headers.", 409);
|
|
1406
|
+
pending.headers = true;
|
|
1407
|
+
const stream = new ReadableStream({ start: (controller) => {
|
|
1408
|
+
pending.controller = controller;
|
|
1409
|
+
}, pull: (controller) => {
|
|
1410
|
+
const chunk = pending.queue.shift();
|
|
1411
|
+
if (chunk) {
|
|
1412
|
+
pending.buffered -= chunk.byteLength;
|
|
1413
|
+
controller.enqueue(chunk);
|
|
1414
|
+
} else if (pending.ended) controller.close();
|
|
1415
|
+
}, cancel: () => this.#finish(state, pending, new Error("Provider response reader cancelled.")) });
|
|
1416
|
+
pending.resolve(new Response(stream, { status: event.status, headers: { "content-type": event.contentType } }));
|
|
1417
|
+
} else if (event.kind === "chunk") {
|
|
1418
|
+
if (!pending.headers) throw new ProviderRelayError("Provider headers must precede body chunks.", 409);
|
|
1419
|
+
const bytes = Buffer.from(event.data, "base64");
|
|
1420
|
+
if (bytes.toString("base64") !== event.data || bytes.byteLength > 49152) throw new ProviderRelayError("Invalid provider body chunk.");
|
|
1421
|
+
pending.received += bytes.byteLength;
|
|
1422
|
+
if (pending.received > 16 * 1024 * 1024 || pending.buffered + bytes.byteLength > 2 * 1024 * 1024) {
|
|
1423
|
+
this.#finish(state, pending, new Error("Provider response exceeded relay limits."));
|
|
1424
|
+
throw new ProviderRelayError("Provider response exceeded relay limits.", 413);
|
|
1425
|
+
}
|
|
1426
|
+
if ((pending.controller?.desiredSize ?? 0) > 0 && pending.queue.length === 0) pending.controller.enqueue(bytes);
|
|
1427
|
+
else {
|
|
1428
|
+
pending.queue.push(bytes);
|
|
1429
|
+
pending.buffered += bytes.byteLength;
|
|
1430
|
+
}
|
|
1431
|
+
} else this.#finish(state, pending, event.kind === "error" ? new Error("Local model source disconnected or failed.") : void 0);
|
|
1432
|
+
}
|
|
1433
|
+
return { accepted: true };
|
|
1434
|
+
}
|
|
1435
|
+
close(id2) {
|
|
1436
|
+
const state = this.#grants.get(id2);
|
|
1437
|
+
if (!state) return;
|
|
1438
|
+
for (const pending of [...state.requests.values()]) this.#finish(state, pending, new Error("Local model source disconnected."));
|
|
1439
|
+
this.#grants.delete(id2);
|
|
1440
|
+
}
|
|
1441
|
+
dispose() {
|
|
1442
|
+
for (const id2 of [...this.#grants.keys()]) this.close(id2);
|
|
1443
|
+
this.#disposed = true;
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
async function handleRemoteProviderRelayRequest(req, res, url, broker, readBody) {
|
|
1447
|
+
const base = "/api/console/provider-relays";
|
|
1448
|
+
if (url.pathname !== base && !url.pathname.startsWith(`${base}/`)) return false;
|
|
1449
|
+
const send = (status, value) => {
|
|
1450
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
1451
|
+
res.end(JSON.stringify(value));
|
|
1452
|
+
};
|
|
1453
|
+
try {
|
|
1454
|
+
if (url.pathname === base && req.method === "POST") {
|
|
1455
|
+
send(201, { grant: broker.create(await readBody()) });
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
const status = /^\/api\/console\/provider-relays\/([a-f0-9-]{36})\/status$/.exec(url.pathname);
|
|
1459
|
+
if (status) {
|
|
1460
|
+
if (req.method !== "GET") throw new ProviderRelayError("Method not allowed.", 405);
|
|
1461
|
+
send(200, broker.status(status[1]));
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
const match = /^\/api\/console\/provider-relays\/([a-f0-9-]{36})(?:\/requests(?:\/([a-f0-9-]{36})\/events)?)?$/.exec(url.pathname);
|
|
1465
|
+
if (!match) throw new ProviderRelayError("Unknown provider relay route.", 404);
|
|
1466
|
+
if (req.method === "DELETE" && !url.pathname.endsWith("/requests") && !match[2]) {
|
|
1467
|
+
broker.close(match[1]);
|
|
1468
|
+
send(200, { closed: true });
|
|
1469
|
+
} else if (req.method === "GET" && url.pathname.endsWith("/requests")) send(200, broker.poll(match[1]));
|
|
1470
|
+
else if (req.method === "POST" && match[2]) send(200, broker.events(match[1], match[2], await readBody()));
|
|
1471
|
+
else throw new ProviderRelayError("Method not allowed.", 405);
|
|
1472
|
+
} catch (error2) {
|
|
1473
|
+
send(error2 instanceof ProviderRelayError ? error2.statusCode : 400, { error: error2 instanceof ProviderRelayError ? error2.message : "Invalid provider relay request." });
|
|
1474
|
+
}
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
659
1478
|
// packages/cli/src/web/report-artifacts.ts
|
|
660
1479
|
var ReportArtifactError = class extends Error {
|
|
661
1480
|
constructor(message2, statusCode) {
|
|
@@ -711,13 +1530,13 @@ function retainedFindingSnapshot(ids, dbPath) {
|
|
|
711
1530
|
try {
|
|
712
1531
|
const findings = [];
|
|
713
1532
|
const scanIds = /* @__PURE__ */ new Set();
|
|
714
|
-
for (const
|
|
715
|
-
const row = db.getFinding(
|
|
1533
|
+
for (const id2 of new Set(ids)) {
|
|
1534
|
+
const row = db.getFinding(id2);
|
|
716
1535
|
if (!row) throw new ReportArtifactError("A selected finding was not found in this engine.", 404);
|
|
717
1536
|
scanIds.add(row.scanId);
|
|
718
|
-
findings.push(loadFindingFocus(
|
|
1537
|
+
findings.push(loadFindingFocus(id2, { dbPath }).finding);
|
|
719
1538
|
}
|
|
720
|
-
const scans = [...scanIds].map((
|
|
1539
|
+
const scans = [...scanIds].map((id2) => db.getScan(id2)).filter((scan) => scan !== void 0);
|
|
721
1540
|
return {
|
|
722
1541
|
target: scans.length === 1 ? scans[0].target : "Selected findings from multiple runs",
|
|
723
1542
|
scanDepth: "default",
|
|
@@ -763,7 +1582,7 @@ function handleFindingImpactRequest(pathname, method, input, options) {
|
|
|
763
1582
|
}
|
|
764
1583
|
|
|
765
1584
|
// packages/cli/src/web/findings-access-tokens.ts
|
|
766
|
-
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as
|
|
1585
|
+
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
767
1586
|
import { closeSync, constants, fstatSync, mkdirSync, openSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
768
1587
|
import { join } from "node:path";
|
|
769
1588
|
|
|
@@ -1442,13 +2261,13 @@ function createRemoteJWKSet(url, options) {
|
|
|
1442
2261
|
}
|
|
1443
2262
|
const reload = async () => {
|
|
1444
2263
|
if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) {
|
|
1445
|
-
const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((
|
|
1446
|
-
const next = createLocalJWKSet(
|
|
2264
|
+
const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json3) => {
|
|
2265
|
+
const next = createLocalJWKSet(json3);
|
|
1447
2266
|
if (sequence <= appliedSequence)
|
|
1448
2267
|
return;
|
|
1449
2268
|
local = next;
|
|
1450
2269
|
const updatedAt = Date.now();
|
|
1451
|
-
cache2 && (cache2.uat = updatedAt, cache2.jwks =
|
|
2270
|
+
cache2 && (cache2.uat = updatedAt, cache2.jwks = json3), jwksTimestamp = updatedAt, appliedSequence = sequence;
|
|
1452
2271
|
}).finally(() => {
|
|
1453
2272
|
pendingFetch === current && (pendingFetch = void 0);
|
|
1454
2273
|
});
|
|
@@ -1534,8 +2353,8 @@ function parseCookies(header) {
|
|
|
1534
2353
|
return cookies;
|
|
1535
2354
|
}
|
|
1536
2355
|
function getSessionId(req) {
|
|
1537
|
-
const
|
|
1538
|
-
return
|
|
2356
|
+
const id2 = parseCookies(req.headers.cookie)[COOKIE];
|
|
2357
|
+
return id2 && /^[\w-]{43}$/.test(id2) ? id2 : void 0;
|
|
1539
2358
|
}
|
|
1540
2359
|
async function hashTeamPassword(password) {
|
|
1541
2360
|
if (!password || password.length > 1024) throw new TeamAuthError("Password must contain 1\u20131024 characters", 400);
|
|
@@ -1592,8 +2411,8 @@ var TeamAuth = class {
|
|
|
1592
2411
|
cookie(name, value, seconds) {
|
|
1593
2412
|
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${this.origin.startsWith("https:") ? "; Secure" : ""}`;
|
|
1594
2413
|
}
|
|
1595
|
-
lookupUser(
|
|
1596
|
-
const user = this.config?.users.find((row) => row.id ===
|
|
2414
|
+
lookupUser(id2) {
|
|
2415
|
+
const user = this.config?.users.find((row) => row.id === id2);
|
|
1597
2416
|
return user ? { workspaceId: this.config.workspace.id, userId: user.id, displayName: user.name, role: user.role } : null;
|
|
1598
2417
|
}
|
|
1599
2418
|
/** Trusted server configuration only; never serialize this through a browser API. */
|
|
@@ -1617,11 +2436,11 @@ var TeamAuth = class {
|
|
|
1617
2436
|
this.config.users.push(member);
|
|
1618
2437
|
return this.lookupUser(member.id);
|
|
1619
2438
|
}
|
|
1620
|
-
resolveSessionId(
|
|
1621
|
-
if (!
|
|
1622
|
-
const session = this.sessions.get(
|
|
2439
|
+
resolveSessionId(id2) {
|
|
2440
|
+
if (!id2) return null;
|
|
2441
|
+
const session = this.sessions.get(id2);
|
|
1623
2442
|
if (!session || session.expires <= this.now()) {
|
|
1624
|
-
this.sessions.delete(
|
|
2443
|
+
this.sessions.delete(id2);
|
|
1625
2444
|
return null;
|
|
1626
2445
|
}
|
|
1627
2446
|
return this.lookupUser(session.userId);
|
|
@@ -1633,7 +2452,7 @@ var TeamAuth = class {
|
|
|
1633
2452
|
return { enabled: this.enabled, ...this.config ? { workspace: this.config.workspace } : {}, user: this.resolveSession(req), sso: !!this.config?.oidc };
|
|
1634
2453
|
}
|
|
1635
2454
|
issueSession(userId) {
|
|
1636
|
-
for (const [
|
|
2455
|
+
for (const [id2, session] of this.sessions) if (session.expires <= this.now()) this.sessions.delete(id2);
|
|
1637
2456
|
if (this.sessions.size >= 1e4) throw new TeamAuthError("Session capacity reached", 503);
|
|
1638
2457
|
const sessionId = opaque();
|
|
1639
2458
|
this.sessions.set(sessionId, { userId, expires: this.now() + SESSION_MS });
|
|
@@ -1644,7 +2463,7 @@ var TeamAuth = class {
|
|
|
1644
2463
|
const parsed = external_exports.object({ userId: external_exports.string().min(1).max(160), password: external_exports.string().min(1).max(1024) }).strict().safeParse(input);
|
|
1645
2464
|
if (!parsed.success) throw new TeamAuthError("User ID and password are required", 400);
|
|
1646
2465
|
const key = rateKey;
|
|
1647
|
-
for (const [
|
|
2466
|
+
for (const [id2, row] of this.attempts) if (row.expires <= this.now()) this.attempts.delete(id2);
|
|
1648
2467
|
const attempt = this.attempts.get(key) ?? { count: 0, expires: this.now() + 6e4 };
|
|
1649
2468
|
if (attempt.count >= 10 || this.attempts.size >= 1e4) throw new TeamAuthError("Too many sign-in attempts; try again shortly", 429);
|
|
1650
2469
|
attempt.count++;
|
|
@@ -1655,8 +2474,8 @@ var TeamAuth = class {
|
|
|
1655
2474
|
return this.issueSession(user.id);
|
|
1656
2475
|
}
|
|
1657
2476
|
logout(req) {
|
|
1658
|
-
const
|
|
1659
|
-
if (
|
|
2477
|
+
const id2 = getSessionId(req);
|
|
2478
|
+
if (id2) this.sessions.delete(id2);
|
|
1660
2479
|
return { setCookie: this.cookie(COOKIE, "", 0) };
|
|
1661
2480
|
}
|
|
1662
2481
|
requireProviderURL(value) {
|
|
@@ -1680,7 +2499,7 @@ var TeamAuth = class {
|
|
|
1680
2499
|
async beginSSO() {
|
|
1681
2500
|
const discovery = await this.getDiscovery();
|
|
1682
2501
|
const oidc = this.config.oidc;
|
|
1683
|
-
for (const [
|
|
2502
|
+
for (const [id2, pending] of this.pending) if (pending.expires <= this.now()) this.pending.delete(id2);
|
|
1684
2503
|
if (this.pending.size >= 1e3) throw new TeamAuthError("Too many pending sign-ins", 429);
|
|
1685
2504
|
const state = opaque(), nonce = opaque(), verifier = opaque(), browser = opaque();
|
|
1686
2505
|
this.pending.set(state, { nonce, verifier, browser, expires: this.now() + SSO_MS });
|
|
@@ -1739,7 +2558,7 @@ var publicTokenSchema = external_exports.object({
|
|
|
1739
2558
|
}).strict();
|
|
1740
2559
|
var tokenSchema = publicTokenSchema.extend({ tokenHash: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
1741
2560
|
var stateSchema = external_exports.object({ schemaVersion: external_exports.literal(1), workspaceId: external_exports.string(), tokens: external_exports.array(tokenSchema).max(1e3) }).strict();
|
|
1742
|
-
var
|
|
2561
|
+
var createSchema2 = external_exports.object({
|
|
1743
2562
|
name: external_exports.string().trim().min(1).max(160),
|
|
1744
2563
|
scopes: scopesSchema.optional(),
|
|
1745
2564
|
expiresAt: external_exports.string().datetime().nullable().optional()
|
|
@@ -1788,7 +2607,7 @@ var FindingsAccessTokenStore = class {
|
|
|
1788
2607
|
#write(records) {
|
|
1789
2608
|
const state = stateSchema.safeParse({ schemaVersion: 1, workspaceId: this.workspaceId, tokens: records });
|
|
1790
2609
|
if (!state.success) throw new FindingsAccessError("API credential limit reached.", 409);
|
|
1791
|
-
const temporary = this.#path + "." +
|
|
2610
|
+
const temporary = this.#path + "." + randomUUID3() + ".tmp";
|
|
1792
2611
|
try {
|
|
1793
2612
|
writeFileSync(temporary, JSON.stringify(state.data), { flag: "wx", mode: 384 });
|
|
1794
2613
|
renameSync(temporary, this.#path);
|
|
@@ -1822,22 +2641,22 @@ var FindingsAccessTokenStore = class {
|
|
|
1822
2641
|
/** Caller supplies a verified owner identity, never createdBy from the HTTP body. */
|
|
1823
2642
|
create(input, createdBy) {
|
|
1824
2643
|
return this.#mutate(() => {
|
|
1825
|
-
const parsed =
|
|
2644
|
+
const parsed = createSchema2.safeParse(input);
|
|
1826
2645
|
if (!parsed.success || !createdBy || createdBy.length > 256) throw new FindingsAccessError("Use a name, optional read:findings scope and future expiry.");
|
|
1827
2646
|
const records = this.#read();
|
|
1828
2647
|
const now = this.#now();
|
|
1829
2648
|
const expiresAt = parsed.data.expiresAt === void 0 ? new Date(now + 90 * 864e5).toISOString() : parsed.data.expiresAt;
|
|
1830
2649
|
if (expiresAt !== null && Date.parse(expiresAt) <= now) throw new FindingsAccessError("API credential expiry must be in the future.");
|
|
1831
2650
|
const token = "0_find_" + randomBytes2(32).toString("base64url");
|
|
1832
|
-
const record = { id:
|
|
2651
|
+
const record = { id: randomUUID3(), workspaceId: this.workspaceId, name: parsed.data.name, scopes: ["read:findings"], createdBy, createdAt: new Date(now).toISOString(), expiresAt, revokedAt: null, tokenHash: createHash2("sha256").update(token).digest("hex") };
|
|
1833
2652
|
this.#write([...records, record]);
|
|
1834
2653
|
return { credential: redact(record), token };
|
|
1835
2654
|
});
|
|
1836
2655
|
}
|
|
1837
|
-
revoke(
|
|
2656
|
+
revoke(id2) {
|
|
1838
2657
|
return this.#mutate(() => {
|
|
1839
2658
|
const records = this.#read();
|
|
1840
|
-
const record = records.find((token) => token.id ===
|
|
2659
|
+
const record = records.find((token) => token.id === id2);
|
|
1841
2660
|
if (!record) throw new FindingsAccessError("API credential not found.", 404);
|
|
1842
2661
|
record.revokedAt ??= new Date(this.#now()).toISOString();
|
|
1843
2662
|
this.#write(records);
|
|
@@ -1894,12 +2713,12 @@ var FindingsReadApi = class {
|
|
|
1894
2713
|
this.#dbPath = resolveOsecDbPath(options.dbPath);
|
|
1895
2714
|
this.workspaceId = options.workspaceId;
|
|
1896
2715
|
}
|
|
1897
|
-
#item(db,
|
|
1898
|
-
const row = db.getFinding(
|
|
2716
|
+
#item(db, id2) {
|
|
2717
|
+
const row = db.getFinding(id2);
|
|
1899
2718
|
if (!row) throw new FindingsAccessError("Finding not found.", 404);
|
|
1900
2719
|
const scan = db.getScan(row.scanId);
|
|
1901
2720
|
if (!scan) throw new FindingsAccessError("Finding source scan was not retained.", 404);
|
|
1902
|
-
return { scanId: row.scanId, target: scan.target, finding: findingFromRow(row, { ...db.getFindingReviewFields(
|
|
2721
|
+
return { scanId: row.scanId, target: scan.target, finding: findingFromRow(row, { ...db.getFindingReviewFields(id2) }) };
|
|
1903
2722
|
}
|
|
1904
2723
|
list(query) {
|
|
1905
2724
|
const raw = {};
|
|
@@ -1934,11 +2753,11 @@ var FindingsReadApi = class {
|
|
|
1934
2753
|
db.close();
|
|
1935
2754
|
}
|
|
1936
2755
|
}
|
|
1937
|
-
detail(
|
|
1938
|
-
if (!boundedId.safeParse(
|
|
2756
|
+
detail(id2) {
|
|
2757
|
+
if (!boundedId.safeParse(id2).success) throw new FindingsAccessError("Invalid finding ID.");
|
|
1939
2758
|
const db = new osecDB(this.#dbPath, { readOnly: true });
|
|
1940
2759
|
try {
|
|
1941
|
-
return { schemaVersion: 1, workspaceId: this.workspaceId, ...this.#item(db,
|
|
2760
|
+
return { schemaVersion: 1, workspaceId: this.workspaceId, ...this.#item(db, id2) };
|
|
1942
2761
|
} finally {
|
|
1943
2762
|
db.close();
|
|
1944
2763
|
}
|
|
@@ -1946,7 +2765,7 @@ var FindingsReadApi = class {
|
|
|
1946
2765
|
export(query) {
|
|
1947
2766
|
if ([...query.keys()].some((key) => key !== "id")) throw new FindingsAccessError("Use repeated id parameters to select export findings.");
|
|
1948
2767
|
const ids = query.getAll("id");
|
|
1949
|
-
if (!ids.length || ids.length > 500 || ids.some((
|
|
2768
|
+
if (!ids.length || ids.length > 500 || ids.some((id2) => !boundedId.safeParse(id2).success) || new Set(ids).size !== ids.length) throw new FindingsAccessError("Select 1\u2013500 distinct exact finding IDs.");
|
|
1950
2769
|
return { schemaVersion: 1, workspaceId: this.workspaceId, coverage: "selected-retained-findings", report: retainedFindingSnapshot(ids, this.#dbPath) };
|
|
1951
2770
|
}
|
|
1952
2771
|
};
|
|
@@ -2023,8 +2842,11 @@ function engagementMarkdown(report) {
|
|
|
2023
2842
|
return lines.filter((line, index) => line !== "" || lines[index - 1] !== "").join("\n");
|
|
2024
2843
|
}
|
|
2025
2844
|
|
|
2845
|
+
// packages/cli/src/web/workspace-services.ts
|
|
2846
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2847
|
+
|
|
2026
2848
|
// packages/cli/src/web/skills.ts
|
|
2027
|
-
import { createHash as createHash4, randomUUID as
|
|
2849
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
|
|
2028
2850
|
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, readdirSync, realpathSync as realpathSync2, renameSync as renameSync2, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2029
2851
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
2030
2852
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -2121,12 +2943,12 @@ var SkillsStore = class _SkillsStore {
|
|
|
2121
2943
|
#discovery() {
|
|
2122
2944
|
return discoverAgentSkills(this.discoveryOptions());
|
|
2123
2945
|
}
|
|
2124
|
-
#builtin(
|
|
2125
|
-
const builtinId =
|
|
2946
|
+
#builtin(id2) {
|
|
2947
|
+
const builtinId = id2.startsWith("builtin/") ? id2.slice(8) : id2.includes("/") ? void 0 : id2;
|
|
2126
2948
|
if (!builtinId) return;
|
|
2127
2949
|
const skill = loadSkillRegistry().get(builtinId);
|
|
2128
2950
|
if (!skill) return;
|
|
2129
|
-
|
|
2951
|
+
id2 = `builtin/${builtinId}`;
|
|
2130
2952
|
const name = skill.id;
|
|
2131
2953
|
const content = `---
|
|
2132
2954
|
name: ${name}
|
|
@@ -2136,20 +2958,20 @@ description: ${JSON.stringify(skill.description)}
|
|
|
2136
2958
|
${skill.content}
|
|
2137
2959
|
`;
|
|
2138
2960
|
const files = [{ path: "SKILL.md", bytes: Buffer.from(content) }];
|
|
2139
|
-
return { metadata: { id, name, description: skill.description, scope: "builtin", revision: revision(files), writable: false, fileCount: 1, source: "Built in" }, files };
|
|
2961
|
+
return { metadata: { id: id2, name, description: skill.description, scope: "builtin", revision: revision(files), writable: false, fileCount: 1, source: "Built in" }, files };
|
|
2140
2962
|
}
|
|
2141
|
-
#entry(
|
|
2142
|
-
const built = this.#builtin(
|
|
2963
|
+
#entry(id2) {
|
|
2964
|
+
const built = this.#builtin(id2);
|
|
2143
2965
|
if (built) return built;
|
|
2144
|
-
const skill = this.#discovery().skills.find((row) => row.id ===
|
|
2966
|
+
const skill = this.#discovery().skills.find((row) => row.id === id2);
|
|
2145
2967
|
if (!skill) throw new SkillsError("Skill not found.", 404);
|
|
2146
2968
|
return this.#describe(skill);
|
|
2147
2969
|
}
|
|
2148
2970
|
#describe(skill) {
|
|
2149
|
-
const
|
|
2971
|
+
const id2 = skill.id;
|
|
2150
2972
|
const files = readFiles(skill.directory);
|
|
2151
2973
|
const shared = skill.root === this.#shared || skill.directory.startsWith(this.#shared + "/");
|
|
2152
|
-
return { metadata: { id, name: skill.name, description: skill.description, scope: shared ? "workspace" : skill.source === "mount" ? "mounted" : skill.source, revision: revision(files), writable: shared, fileCount: files.length, source: shared ? "Workspace" : skill.directory }, files, directory: skill.directory };
|
|
2974
|
+
return { metadata: { id: id2, name: skill.name, description: skill.description, scope: shared ? "workspace" : skill.source === "mount" ? "mounted" : skill.source, revision: revision(files), writable: shared, fileCount: files.length, source: shared ? "Workspace" : skill.directory }, files, directory: skill.directory };
|
|
2153
2975
|
}
|
|
2154
2976
|
#access(metadata, access = {}) {
|
|
2155
2977
|
return { ...metadata, writable: access.canWrite !== false && metadata.scope !== "builtin" && (metadata.scope === "workspace" || access.allowHostEdits === true) };
|
|
@@ -2168,12 +2990,12 @@ ${skill.content}
|
|
|
2168
2990
|
for (const skill of loadSkillRegistry().values()) skills.push(this.#builtin(`builtin/${skill.id}`).metadata);
|
|
2169
2991
|
return { skills, mounts: this.mounts(), diagnostics, workspacePath: this.workspacePath };
|
|
2170
2992
|
}
|
|
2171
|
-
get(
|
|
2172
|
-
const row = this.#entry(
|
|
2993
|
+
get(id2, access = {}) {
|
|
2994
|
+
const row = this.#entry(id2);
|
|
2173
2995
|
return { ...this.#access(row.metadata, access), content: row.files.find((file) => file.path === "SKILL.md").bytes.toString("utf8"), files: row.files.map((file) => ({ path: file.path, size: file.bytes.length })) };
|
|
2174
2996
|
}
|
|
2175
|
-
export(
|
|
2176
|
-
const row = this.#entry(
|
|
2997
|
+
export(id2) {
|
|
2998
|
+
const row = this.#entry(id2);
|
|
2177
2999
|
return { format: "agent-skill", version: 1, name: row.metadata.name, files: row.files.map((file) => ({ path: file.path, encoding: "base64", content: file.bytes.toString("base64") })) };
|
|
2178
3000
|
}
|
|
2179
3001
|
#validate(content, name) {
|
|
@@ -2241,7 +3063,7 @@ ${skill.content}
|
|
|
2241
3063
|
} catch (cause) {
|
|
2242
3064
|
if (cause.code !== "ENOENT") throw cause;
|
|
2243
3065
|
}
|
|
2244
|
-
const temporary = join2(this.#state, `.import-${
|
|
3066
|
+
const temporary = join2(this.#state, `.import-${randomUUID4()}`);
|
|
2245
3067
|
mkdirSync2(temporary, { mode: 448 });
|
|
2246
3068
|
try {
|
|
2247
3069
|
for (const file of files) {
|
|
@@ -2280,31 +3102,31 @@ ${skill.content}
|
|
|
2280
3102
|
if (!names.has("SKILL.md")) throw new SkillsError("Skill bundle needs SKILL.md.");
|
|
2281
3103
|
return this.get(this.#write(files, bundle.name));
|
|
2282
3104
|
}
|
|
2283
|
-
update(
|
|
2284
|
-
return this.#transaction(() => this.#update(
|
|
3105
|
+
update(id2, input, access = {}) {
|
|
3106
|
+
return this.#transaction(() => this.#update(id2, input, access));
|
|
2285
3107
|
}
|
|
2286
|
-
#update(
|
|
3108
|
+
#update(id2, input, access) {
|
|
2287
3109
|
const { content, expectedRevision } = parse(external_exports.object({ content: external_exports.string().min(1).max(MAX_BYTES), expectedRevision: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict(), input);
|
|
2288
|
-
const row = this.#entry(
|
|
3110
|
+
const row = this.#entry(id2);
|
|
2289
3111
|
if (!this.#access(row.metadata, access).writable) throw new SkillsError(row.metadata.scope === "builtin" ? "Save a workspace copy to edit this built-in skill." : "This account cannot edit this skill folder. Save a workspace copy instead.", 403);
|
|
2290
3112
|
if (row.metadata.revision !== expectedRevision) throw new SkillsError("This skill changed. Reload it before saving.", 409);
|
|
2291
3113
|
this.#validate(content, row.metadata.name);
|
|
2292
3114
|
if (row.files.reduce((total, file) => total + (file.path === "SKILL.md" ? Buffer.byteLength(content) : file.bytes.length), 0) > MAX_BYTES) throw new SkillsError("Skill bundle exceeds its size limit.");
|
|
2293
3115
|
const target = join2(row.directory, "SKILL.md");
|
|
2294
|
-
const pending = join2(row.directory, `.skill-edit-${
|
|
3116
|
+
const pending = join2(row.directory, `.skill-edit-${randomUUID4()}.tmp`);
|
|
2295
3117
|
try {
|
|
2296
3118
|
writeFileSync2(pending, content, { flag: "wx", mode: lstatSync2(target).mode & 511 });
|
|
2297
3119
|
renameSync2(pending, target);
|
|
2298
3120
|
} finally {
|
|
2299
3121
|
rmSync(pending, { force: true });
|
|
2300
3122
|
}
|
|
2301
|
-
return this.get(
|
|
3123
|
+
return this.get(id2, access);
|
|
2302
3124
|
}
|
|
2303
|
-
copy(
|
|
3125
|
+
copy(id2, input, access = {}) {
|
|
2304
3126
|
if (access.canWrite === false) throw new SkillsError("This account has read-only workspace access.", 403);
|
|
2305
3127
|
return this.#transaction(() => {
|
|
2306
3128
|
const { content, expectedRevision } = parse(external_exports.object({ content: external_exports.string().min(1).max(MAX_BYTES), expectedRevision: external_exports.string().regex(/^[a-f0-9]{64}$/) }).strict(), input);
|
|
2307
|
-
const row = this.#entry(
|
|
3129
|
+
const row = this.#entry(id2);
|
|
2308
3130
|
if (row.metadata.revision !== expectedRevision) throw new SkillsError("This skill changed. Reload it before copying.", 409);
|
|
2309
3131
|
const { name } = this.#validate(content);
|
|
2310
3132
|
const files = row.files.map((file) => file.path === "SKILL.md" ? { path: file.path, bytes: Buffer.from(content) } : file);
|
|
@@ -2319,18 +3141,18 @@ ${skill.content}
|
|
|
2319
3141
|
};
|
|
2320
3142
|
return {
|
|
2321
3143
|
list: () => this.list(access),
|
|
2322
|
-
read: (
|
|
3144
|
+
read: (id2) => this.get(id2, access),
|
|
2323
3145
|
create: (content) => {
|
|
2324
3146
|
assertWrite();
|
|
2325
3147
|
return this.create({ content });
|
|
2326
3148
|
},
|
|
2327
|
-
update: (
|
|
3149
|
+
update: (id2, input) => {
|
|
2328
3150
|
assertWrite();
|
|
2329
|
-
return this.update(
|
|
3151
|
+
return this.update(id2, input, access);
|
|
2330
3152
|
},
|
|
2331
|
-
copy: (
|
|
3153
|
+
copy: (id2, input) => {
|
|
2332
3154
|
assertWrite();
|
|
2333
|
-
return this.copy(
|
|
3155
|
+
return this.copy(id2, input, access);
|
|
2334
3156
|
}
|
|
2335
3157
|
};
|
|
2336
3158
|
}
|
|
@@ -2349,24 +3171,24 @@ ${skill.content}
|
|
|
2349
3171
|
const mounts = this.mounts();
|
|
2350
3172
|
if (mounts.length >= 100) throw new SkillsError("At most 100 folders can be mounted.", 409);
|
|
2351
3173
|
if (mounts.some((item) => item.path === canonical)) throw new SkillsError("This folder is already mounted.", 409);
|
|
2352
|
-
const mount = { id:
|
|
3174
|
+
const mount = { id: randomUUID4(), path: canonical };
|
|
2353
3175
|
const found = discoverAgentSkills({ homeDir: null, mounts: [mount] });
|
|
2354
3176
|
if (!found.skills.length) throw new SkillsError("No valid SKILL.md folders found.");
|
|
2355
3177
|
mounts.push(mount);
|
|
2356
3178
|
this.#saveMounts(mounts);
|
|
2357
3179
|
return mount;
|
|
2358
3180
|
}
|
|
2359
|
-
unmount(
|
|
2360
|
-
return this.#transaction(() => this.#unmount(
|
|
3181
|
+
unmount(id2) {
|
|
3182
|
+
return this.#transaction(() => this.#unmount(id2));
|
|
2361
3183
|
}
|
|
2362
|
-
#unmount(
|
|
3184
|
+
#unmount(id2) {
|
|
2363
3185
|
const mounts = this.mounts();
|
|
2364
|
-
if (!mounts.some((item) => item.id ===
|
|
2365
|
-
this.#saveMounts(mounts.filter((item) => item.id !==
|
|
3186
|
+
if (!mounts.some((item) => item.id === id2)) throw new SkillsError("Mount not found.", 404);
|
|
3187
|
+
this.#saveMounts(mounts.filter((item) => item.id !== id2));
|
|
2366
3188
|
}
|
|
2367
3189
|
#saveMounts(mounts) {
|
|
2368
3190
|
mkdirSync2(this.#state, { recursive: true, mode: 448 });
|
|
2369
|
-
const temporary = join2(this.#state, `.mounts-${
|
|
3191
|
+
const temporary = join2(this.#state, `.mounts-${randomUUID4()}`);
|
|
2370
3192
|
try {
|
|
2371
3193
|
writeFileSync2(temporary, JSON.stringify(mounts), { flag: "wx", mode: 384 });
|
|
2372
3194
|
renameSync2(temporary, join2(this.#state, "mounts.json"));
|
|
@@ -2377,7 +3199,7 @@ ${skill.content}
|
|
|
2377
3199
|
};
|
|
2378
3200
|
|
|
2379
3201
|
// packages/cli/src/web/engagements.ts
|
|
2380
|
-
import { createHash as createHash5, randomUUID as
|
|
3202
|
+
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
2381
3203
|
import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync4, realpathSync as realpathSync3, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2382
3204
|
import { join as join3 } from "node:path";
|
|
2383
3205
|
var EngagementError = class extends Error {
|
|
@@ -2438,7 +3260,7 @@ var EngagementStore = class {
|
|
|
2438
3260
|
const body = JSON.stringify({ schemaVersion: 1, workspace: this.workspace, engagements });
|
|
2439
3261
|
if (Buffer.byteLength(body) > MAX_STATE_BYTES) throw new EngagementError("Engagement storage limit reached.", 409);
|
|
2440
3262
|
mkdirSync3(this.#directory, { recursive: true, mode: 448 });
|
|
2441
|
-
const temporary = `${this.#path}.${
|
|
3263
|
+
const temporary = `${this.#path}.${randomUUID5()}.tmp`;
|
|
2442
3264
|
try {
|
|
2443
3265
|
writeFileSync3(temporary, body, { flag: "wx", mode: 384 });
|
|
2444
3266
|
renameSync3(temporary, this.#path);
|
|
@@ -2454,7 +3276,7 @@ var EngagementStore = class {
|
|
|
2454
3276
|
if (!ids.length) return;
|
|
2455
3277
|
const db = new osecDB(this.#dbPath);
|
|
2456
3278
|
try {
|
|
2457
|
-
for (const
|
|
3279
|
+
for (const id2 of ids) if (!db.getScan(id2)) throw new EngagementError(`Selected scan was not found: ${id2}`, 404);
|
|
2458
3280
|
} finally {
|
|
2459
3281
|
db.close();
|
|
2460
3282
|
}
|
|
@@ -2514,8 +3336,8 @@ var EngagementStore = class {
|
|
|
2514
3336
|
list() {
|
|
2515
3337
|
return this.#read().sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2516
3338
|
}
|
|
2517
|
-
get(
|
|
2518
|
-
const row = this.#read().find((item) => item.id ===
|
|
3339
|
+
get(id2) {
|
|
3340
|
+
const row = this.#read().find((item) => item.id === id2);
|
|
2519
3341
|
if (!row) throw new EngagementError("Engagement not found in this workspace.", 404);
|
|
2520
3342
|
return row;
|
|
2521
3343
|
}
|
|
@@ -2526,17 +3348,17 @@ var EngagementStore = class {
|
|
|
2526
3348
|
return this.#transaction((rows) => {
|
|
2527
3349
|
if (rows.length >= 200) throw new EngagementError("Engagement limit reached.", 409);
|
|
2528
3350
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2529
|
-
const row = { ...fields, id:
|
|
3351
|
+
const row = { ...fields, id: randomUUID5(), revision: 1, createdAt: now, updatedAt: now, ...attribution ? { createdBy: attribution, updatedBy: attribution } : {} };
|
|
2530
3352
|
rows.push(row);
|
|
2531
3353
|
return structuredClone(row);
|
|
2532
3354
|
});
|
|
2533
3355
|
}
|
|
2534
|
-
update(
|
|
3356
|
+
update(id2, input, actor) {
|
|
2535
3357
|
const { expectedRevision, ...patch } = parse2(updateSchema, input);
|
|
2536
3358
|
const attribution = actor ? parse2(actorSchema, actor) : void 0;
|
|
2537
3359
|
if (patch.scanIds) this.#validateScans(patch.scanIds);
|
|
2538
3360
|
return this.#transaction((rows) => {
|
|
2539
|
-
const index = rows.findIndex((row2) => row2.id ===
|
|
3361
|
+
const index = rows.findIndex((row2) => row2.id === id2);
|
|
2540
3362
|
if (index < 0) throw new EngagementError("Engagement not found in this workspace.", 404);
|
|
2541
3363
|
if (attribution && expectedRevision === void 0) throw new EngagementError("Review the latest report before saving. Your draft has been kept.", 409);
|
|
2542
3364
|
if (expectedRevision !== void 0 && rows[index].revision !== expectedRevision) throw new EngagementError("Report changed. Your draft has been kept. Reload the latest version before saving.", 409);
|
|
@@ -2545,8 +3367,8 @@ var EngagementStore = class {
|
|
|
2545
3367
|
return structuredClone(row);
|
|
2546
3368
|
});
|
|
2547
3369
|
}
|
|
2548
|
-
report(
|
|
2549
|
-
const engagement = this.get(
|
|
3370
|
+
report(id2) {
|
|
3371
|
+
const engagement = this.get(id2);
|
|
2550
3372
|
const db = new osecDB(this.#dbPath);
|
|
2551
3373
|
try {
|
|
2552
3374
|
const sources = engagement.scanIds.map((scanId) => {
|
|
@@ -2568,22 +3390,22 @@ function reviewStatus(finding) {
|
|
|
2568
3390
|
function assembleEngagementReport(engagement, sources) {
|
|
2569
3391
|
const record = parse2(recordSchema, engagement);
|
|
2570
3392
|
const byScan = new Map(sources.map((source) => [source.scan.id, source]));
|
|
2571
|
-
if (byScan.size !== sources.length || sources.length !== record.scanIds.length || record.scanIds.some((
|
|
3393
|
+
if (byScan.size !== sources.length || sources.length !== record.scanIds.length || record.scanIds.some((id2) => !byScan.has(id2))) throw new EngagementError("Report sources must match exactly the selected scan IDs.", 404);
|
|
2572
3394
|
const groups = /* @__PURE__ */ new Map();
|
|
2573
3395
|
let findingCount = 0;
|
|
2574
|
-
const scans = record.scanIds.map((
|
|
2575
|
-
const source = byScan.get(
|
|
3396
|
+
const scans = record.scanIds.map((id2) => {
|
|
3397
|
+
const source = byScan.get(id2);
|
|
2576
3398
|
for (const finding of source.report.findings) {
|
|
2577
3399
|
findingCount++;
|
|
2578
3400
|
const fingerprint = finding.fingerprint?.trim();
|
|
2579
|
-
const key = fingerprint ? `fingerprint:${fingerprint}` : `source:${
|
|
3401
|
+
const key = fingerprint ? `fingerprint:${fingerprint}` : `source:${id2}:${finding.id}`;
|
|
2580
3402
|
const status = reviewStatus(finding);
|
|
2581
3403
|
let group = groups.get(key);
|
|
2582
3404
|
if (!group) {
|
|
2583
3405
|
group = { key, ...fingerprint ? { fingerprint } : {}, reviewStatus: status, occurrences: [] };
|
|
2584
3406
|
groups.set(key, group);
|
|
2585
3407
|
} else if (group.reviewStatus !== status) group.reviewStatus = "unreviewed";
|
|
2586
|
-
group.occurrences.push({ scanId:
|
|
3408
|
+
group.occurrences.push({ scanId: id2, findingId: finding.id, finding: structuredClone(finding) });
|
|
2587
3409
|
}
|
|
2588
3410
|
return { ...source.scan, warnings: structuredClone(source.report.warnings), executionSuccessful: source.report.executionSuccessful, exitReason: source.report.exitReason, ...source.report.error ? { error: source.report.error } : {} };
|
|
2589
3411
|
});
|
|
@@ -2606,8 +3428,8 @@ function assembleEngagementReport(engagement, sources) {
|
|
|
2606
3428
|
}
|
|
2607
3429
|
|
|
2608
3430
|
// packages/cli/src/web/workflow-triggers.ts
|
|
2609
|
-
var
|
|
2610
|
-
var toggleSchema = external_exports.object({ enabled: external_exports.boolean(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict();
|
|
3431
|
+
var createSchema3 = external_exports.object({ workflowId: external_exports.string().min(1).max(160), workflowRevision: external_exports.number().int().positive(), sessionId: external_exports.string().min(1).max(160), cadence: external_exports.enum(["hourly", "daily", "weekly"]), startAt: external_exports.string().datetime({ offset: true }), timezone: external_exports.string().min(1).max(100), enabled: external_exports.boolean(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict();
|
|
3432
|
+
var toggleSchema = external_exports.object({ enabled: external_exports.boolean().optional(), cadence: external_exports.enum(["hourly", "daily", "weekly"]).optional(), startAt: external_exports.string().datetime({ offset: true }).optional(), timezone: external_exports.string().min(1).max(100).optional(), workflowRevision: external_exports.number().int().positive().optional(), expectedVersion: external_exports.number().int().positive().optional(), approval: external_exports.literal("enable-reviewed-trigger").optional() }).strict().refine((value) => ["enabled", "cadence", "startAt", "timezone", "workflowRevision"].some((key) => key in value), "Provide a schedule change.");
|
|
2611
3433
|
var PREFIX = "/api/console/workflow-triggers";
|
|
2612
3434
|
var terminal = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "interrupted"]);
|
|
2613
3435
|
var WorkflowTriggerService = class {
|
|
@@ -2632,10 +3454,10 @@ var WorkflowTriggerService = class {
|
|
|
2632
3454
|
async handle(method, url, input) {
|
|
2633
3455
|
if (url.pathname !== PREFIX && !url.pathname.startsWith(`${PREFIX}/`)) return null;
|
|
2634
3456
|
try {
|
|
2635
|
-
const
|
|
3457
|
+
const id2 = url.pathname.slice(PREFIX.length + 1);
|
|
2636
3458
|
if (url.pathname === PREFIX && method === "GET") return { status: 200, data: { triggers: this.#store.list(url.searchParams.get("workflowId") ?? void 0).map(publicTrigger) } };
|
|
2637
3459
|
if (url.pathname === PREFIX && method === "POST") {
|
|
2638
|
-
const parsed =
|
|
3460
|
+
const parsed = createSchema3.parse(input);
|
|
2639
3461
|
try {
|
|
2640
3462
|
new Intl.DateTimeFormat("en", { timeZone: parsed.timezone });
|
|
2641
3463
|
} catch {
|
|
@@ -2654,22 +3476,42 @@ var WorkflowTriggerService = class {
|
|
|
2654
3476
|
}
|
|
2655
3477
|
return { status: 201, data: { trigger: publicTrigger(this.#store.create({ ...fields, reviewedConnectionIdentity: identity?.connectionIdentity, reviewedModel: identity?.model, reviewedProviderId: identity?.providerId })) } };
|
|
2656
3478
|
}
|
|
2657
|
-
const trigger = this.#store.get(
|
|
3479
|
+
const trigger = this.#store.get(id2);
|
|
2658
3480
|
if (!trigger) return error(404, "Schedule not found.");
|
|
2659
3481
|
if (method === "PATCH") {
|
|
2660
3482
|
const patch = toggleSchema.parse(input);
|
|
3483
|
+
if (patch.expectedVersion !== void 0 && patch.expectedVersion !== (trigger.version ?? 1)) return error(409, "Schedule changed. Review it again before enabling.");
|
|
3484
|
+
if (trigger.activeClaim && ["cadence", "startAt", "timezone", "workflowRevision"].some((key) => key in patch)) return error(409, "Pause the schedule and wait for its active execution before editing.");
|
|
3485
|
+
const changedTiming = patch.cadence !== void 0 || patch.startAt !== void 0 || patch.timezone !== void 0 || patch.workflowRevision !== void 0;
|
|
3486
|
+
const enabled = patch.enabled ?? (changedTiming ? false : trigger.enabled);
|
|
3487
|
+
const next = { ...trigger, ...patch, enabled };
|
|
3488
|
+
try {
|
|
3489
|
+
new Intl.DateTimeFormat("en", { timeZone: next.timezone });
|
|
3490
|
+
} catch {
|
|
3491
|
+
return error(400, "Choose a valid timezone.");
|
|
3492
|
+
}
|
|
2661
3493
|
let identity = void 0;
|
|
2662
|
-
if (
|
|
3494
|
+
if (enabled) {
|
|
2663
3495
|
if (!patch.approval) return error(400, "Review this schedule before enabling it.");
|
|
2664
3496
|
const definition = this.#workflows.get(trigger.workflowId);
|
|
2665
|
-
if (definition?.revision !==
|
|
2666
|
-
identity = await this.#adapter.validate(
|
|
2667
|
-
this.#verifyIdentity(trigger, identity);
|
|
3497
|
+
if (definition?.revision !== next.workflowRevision) return error(409, "Workflow changed. Review its current revision before scheduling.");
|
|
3498
|
+
identity = await this.#adapter.validate(next);
|
|
3499
|
+
if (!changedTiming) this.#verifyIdentity(trigger, identity);
|
|
2668
3500
|
}
|
|
2669
|
-
return { status: 200, data: { trigger: publicTrigger(this.#store.update(
|
|
3501
|
+
return { status: 200, data: { trigger: publicTrigger(this.#store.update(id2, {
|
|
3502
|
+
cadence: next.cadence,
|
|
3503
|
+
startAt: next.startAt,
|
|
3504
|
+
timezone: next.timezone,
|
|
3505
|
+
workflowRevision: next.workflowRevision,
|
|
3506
|
+
...enabled ? { reviewedModel: identity?.model, reviewedProviderId: identity?.providerId, reviewedConnectionIdentity: identity?.connectionIdentity } : {},
|
|
3507
|
+
enabled,
|
|
3508
|
+
lastStatus: enabled ? "scheduled" : "paused",
|
|
3509
|
+
lastError: void 0,
|
|
3510
|
+
nextFireAt: nextWorkflowTriggerFire(next.startAt, next.cadence, Date.now())
|
|
3511
|
+
}, enabled ? trigger.version ?? 1 : patch.expectedVersion)) } };
|
|
2670
3512
|
}
|
|
2671
3513
|
if (method === "DELETE") {
|
|
2672
|
-
if (!this.#store.delete(
|
|
3514
|
+
if (!this.#store.delete(id2)) return error(409, "Pause the schedule and wait for its active execution before deleting.");
|
|
2673
3515
|
return { status: 200, data: { deleted: true } };
|
|
2674
3516
|
}
|
|
2675
3517
|
return error(405, "Unsupported schedule action.");
|
|
@@ -2677,6 +3519,43 @@ var WorkflowTriggerService = class {
|
|
|
2677
3519
|
return error(cause instanceof external_exports.ZodError ? 400 : 409, cause instanceof Error ? cause.message.slice(0, 1e3) : "Schedule could not be updated.");
|
|
2678
3520
|
}
|
|
2679
3521
|
}
|
|
3522
|
+
/** Chat transport shares the same validation and scheduler as the HTTP editor. */
|
|
3523
|
+
async invokeLifecycle(sessionId, name, args, context) {
|
|
3524
|
+
const identifier = external_exports.string().min(1).max(160);
|
|
3525
|
+
if (name === "list_schedules") {
|
|
3526
|
+
const request = external_exports.object({ workflowId: identifier.optional() }).strict().parse(args);
|
|
3527
|
+
if (context?.workflowId && request.workflowId && context.workflowId !== request.workflowId) throw new Error("This editor is bound to another workflow.");
|
|
3528
|
+
return { schedules: this.#store.list(context?.workflowId ?? request.workflowId).filter((trigger) => context?.workflowId || trigger.sessionId === sessionId).map(publicTrigger) };
|
|
3529
|
+
}
|
|
3530
|
+
let method;
|
|
3531
|
+
let path = PREFIX;
|
|
3532
|
+
let input;
|
|
3533
|
+
if (name === "create_schedule") {
|
|
3534
|
+
const request = createSchema3.omit({ sessionId: true, approval: true }).extend({ enabled: external_exports.boolean().default(false) }).parse(args);
|
|
3535
|
+
if (context?.workflowId && context.workflowId !== request.workflowId) throw new Error("This editor is bound to another workflow.");
|
|
3536
|
+
if (request.enabled && !context?.activationApproved) throw new Error("Schedule activation requires operator approval.");
|
|
3537
|
+
method = "POST";
|
|
3538
|
+
input = { ...request, sessionId, ...request.enabled ? { approval: "enable-reviewed-trigger" } : {} };
|
|
3539
|
+
} else if (name === "update_schedule" || name === "delete_schedule") {
|
|
3540
|
+
const { id: id2, ...fields } = external_exports.object({ id: identifier }).passthrough().parse(args);
|
|
3541
|
+
const trigger = this.#store.get(id2);
|
|
3542
|
+
if (!trigger || (context?.workflowId ? trigger.workflowId !== context.workflowId : trigger.sessionId !== sessionId)) throw new Error("Schedule not found for this editor.");
|
|
3543
|
+
path += `/${encodeURIComponent(id2)}`;
|
|
3544
|
+
if (name === "delete_schedule") {
|
|
3545
|
+
external_exports.object({}).strict().parse(fields);
|
|
3546
|
+
method = "DELETE";
|
|
3547
|
+
input = void 0;
|
|
3548
|
+
} else {
|
|
3549
|
+
const patch = toggleSchema.parse(fields);
|
|
3550
|
+
if (patch.enabled && !context?.activationApproved) throw new Error("Schedule activation requires operator approval.");
|
|
3551
|
+
method = "PATCH";
|
|
3552
|
+
input = { ...patch, ...patch.enabled ? { approval: "enable-reviewed-trigger" } : {} };
|
|
3553
|
+
}
|
|
3554
|
+
} else throw new Error("Unknown schedule action.");
|
|
3555
|
+
const result = await this.handle(method, new URL(path, "http://localhost"), input);
|
|
3556
|
+
if (!result || result.status >= 400) throw new Error(result?.data?.error ?? "Schedule could not be updated.");
|
|
3557
|
+
return result.data;
|
|
3558
|
+
}
|
|
2680
3559
|
async tick(now = Date.now()) {
|
|
2681
3560
|
if (this.#closed || this.#polling) return;
|
|
2682
3561
|
this.#polling = true;
|
|
@@ -2852,7 +3731,9 @@ var GitHubPublicationAuth = class {
|
|
|
2852
3731
|
// packages/cli/src/web/workspace-services.ts
|
|
2853
3732
|
async function createWorkspaceServices(options) {
|
|
2854
3733
|
const skills = new SkillsStore({ workspace: options.workspace ?? process.cwd(), stateDir: options.stateDir ?? homeStateDir(), team: options.auth.enabled });
|
|
3734
|
+
const remoteProviderRelays = new RemoteProviderRelayBroker({ epoch: options.relayEpoch ?? randomUUID6() });
|
|
2855
3735
|
const gateway = new ConsoleGateway({
|
|
3736
|
+
remoteProviderRelays,
|
|
2856
3737
|
dbPath: options.dbPath,
|
|
2857
3738
|
projectPath: options.workspace,
|
|
2858
3739
|
skillAuthoring: (author, workspaceRoot) => {
|
|
@@ -2869,7 +3750,7 @@ async function createWorkspaceServices(options) {
|
|
|
2869
3750
|
let triggers;
|
|
2870
3751
|
let github;
|
|
2871
3752
|
const dispose = async () => {
|
|
2872
|
-
const actions = [() => engine?.dispose(), () => triggers?.dispose(), () => gateway.closeAll(), () => workflows?.dispose(), () => operator.dispose(), () => github?.dispose()];
|
|
3753
|
+
const actions = [() => remoteProviderRelays.dispose(), () => engine?.dispose(), () => triggers?.dispose(), () => gateway.closeAll(), () => workflows?.dispose(), () => operator.dispose(), () => github?.dispose()];
|
|
2873
3754
|
let failure;
|
|
2874
3755
|
for (const action of actions) {
|
|
2875
3756
|
try {
|
|
@@ -2900,8 +3781,10 @@ async function createWorkspaceServices(options) {
|
|
|
2900
3781
|
return { executionId: result.execution.id };
|
|
2901
3782
|
}
|
|
2902
3783
|
} });
|
|
3784
|
+
const schedules = triggers;
|
|
3785
|
+
gateway.attachScheduleLifecycle({ invoke: (sessionId, name, args, context) => schedules.invokeLifecycle(sessionId, name, args, context) });
|
|
2903
3786
|
github = new GitHubPublicationAuth();
|
|
2904
|
-
return { skills, gateway, engagements, operator, workflows: workflow, engine, triggers, github, dispose };
|
|
3787
|
+
return { remoteProviderRelays, skills, gateway, engagements, operator, workflows: workflow, engine, triggers, github, dispose };
|
|
2905
3788
|
} catch (cause) {
|
|
2906
3789
|
await dispose().catch(() => {
|
|
2907
3790
|
});
|
|
@@ -2911,7 +3794,7 @@ async function createWorkspaceServices(options) {
|
|
|
2911
3794
|
|
|
2912
3795
|
// packages/cli/src/web/team-setup.ts
|
|
2913
3796
|
import { closeSync as closeSync4, constants as constants4, existsSync, fstatSync as fstatSync4, fsyncSync, mkdirSync as mkdirSync4, openSync as openSync4, readFileSync as readFileSync5, realpathSync as realpathSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2914
|
-
import { createHash as createHash6, randomUUID as
|
|
3797
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
|
|
2915
3798
|
import { dirname, join as join4, resolve as resolve3 } from "node:path";
|
|
2916
3799
|
var setupSchema = external_exports.object({
|
|
2917
3800
|
workspaceName: external_exports.string().trim().min(1).max(160),
|
|
@@ -2977,7 +3860,7 @@ var TeamSetupService = class {
|
|
|
2977
3860
|
if (auth.members().length >= 1e3) throw new TeamAuthError("Workspace member limit reached", 409);
|
|
2978
3861
|
this.busy = true;
|
|
2979
3862
|
const lockPath = `${this.configPath}.lock`;
|
|
2980
|
-
const tempPath = `${this.configPath}.${
|
|
3863
|
+
const tempPath = `${this.configPath}.${randomUUID7()}.tmp`;
|
|
2981
3864
|
let lockFD;
|
|
2982
3865
|
let temporaryFD;
|
|
2983
3866
|
try {
|
|
@@ -3034,7 +3917,7 @@ var TeamSetupService = class {
|
|
|
3034
3917
|
try {
|
|
3035
3918
|
const data = parsed.data;
|
|
3036
3919
|
const config = {
|
|
3037
|
-
workspace: { id:
|
|
3920
|
+
workspace: { id: randomUUID7(), name: data.workspaceName },
|
|
3038
3921
|
users: [{ id: data.userId, name: data.displayName, role: "owner", passwordHash: await hashTeamPassword(data.password) }]
|
|
3039
3922
|
};
|
|
3040
3923
|
const auth = new TeamAuth({ config, origin: this.options.origin() });
|
|
@@ -3087,7 +3970,7 @@ async function handleTeamSetupRequest(req, url, setup, readBody) {
|
|
|
3087
3970
|
}
|
|
3088
3971
|
|
|
3089
3972
|
// packages/cli/src/web/collaboration.ts
|
|
3090
|
-
import { createHash as createHash7, randomUUID as
|
|
3973
|
+
import { createHash as createHash7, randomUUID as randomUUID8 } from "node:crypto";
|
|
3091
3974
|
import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, mkdirSync as mkdirSync5, openSync as openSync5, readFileSync as readFileSync6, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3092
3975
|
import { join as join5 } from "node:path";
|
|
3093
3976
|
var CollaborationError = class extends Error {
|
|
@@ -3172,9 +4055,9 @@ var CollaborationService = class {
|
|
|
3172
4055
|
});
|
|
3173
4056
|
}
|
|
3174
4057
|
/** Trusted post-write adapter only: the caller proves this resource exists in its workspace. */
|
|
3175
|
-
notifyChanged(kind,
|
|
3176
|
-
if (!external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id }).success) throw new CollaborationError("Invalid changed resource.");
|
|
3177
|
-
this.#publish({ type: "changed", kind, id });
|
|
4058
|
+
notifyChanged(kind, id2) {
|
|
4059
|
+
if (!external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id: id2 }).success) throw new CollaborationError("Invalid changed resource.");
|
|
4060
|
+
this.#publish({ type: "changed", kind, id: id2 });
|
|
3178
4061
|
}
|
|
3179
4062
|
#actor(sessionId) {
|
|
3180
4063
|
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 4096) throw new CollaborationError("An authenticated team session is required.", 401);
|
|
@@ -3201,7 +4084,7 @@ var CollaborationService = class {
|
|
|
3201
4084
|
if (bytes.length > 32 * 1024 * 1024) throw new Error("size");
|
|
3202
4085
|
let source = JSON.parse(bytes.toString("utf8"));
|
|
3203
4086
|
const legacy = external_exports.object({ schemaVersion: external_exports.literal(1), workspaceId: external_exports.string(), conversations: external_exports.array(external_exports.object({ id: external_exports.string(), revision: external_exports.number(), createdBy: attributionSchema, createdAt: external_exports.number(), members: external_exports.array(memberSchema2) }).passthrough()).max(1e3) }).strict().safeParse(source);
|
|
3204
|
-
if (legacy.success) source = { schemaVersion: 2, workspaceId: legacy.data.workspaceId, rooms: legacy.data.conversations.map(({ id, revision: revision2, createdBy, createdAt, members }) => ({ kind: "conversation", id, revision: revision2, createdBy, createdAt, members })) };
|
|
4087
|
+
if (legacy.success) source = { schemaVersion: 2, workspaceId: legacy.data.workspaceId, rooms: legacy.data.conversations.map(({ id: id2, revision: revision2, createdBy, createdAt, members }) => ({ kind: "conversation", id: id2, revision: revision2, createdBy, createdAt, members })) };
|
|
3205
4088
|
const parsed = stateSchema3.parse(source);
|
|
3206
4089
|
if (parsed.workspaceId !== this.#options.workspaceId || new Set(parsed.rooms.map((row) => this.#roomKey(row.kind, row.id))).size !== parsed.rooms.length) throw new Error("workspace");
|
|
3207
4090
|
return parsed.rooms;
|
|
@@ -3216,7 +4099,7 @@ var CollaborationService = class {
|
|
|
3216
4099
|
if (!state.success) throw new CollaborationError("Collaboration record limit reached.", 409);
|
|
3217
4100
|
const data = JSON.stringify(state.data);
|
|
3218
4101
|
if (Buffer.byteLength(data) > 32 * 1024 * 1024) throw new CollaborationError("Collaboration storage limit reached.", 409);
|
|
3219
|
-
const temporary = this.#path + "." +
|
|
4102
|
+
const temporary = this.#path + "." + randomUUID8() + ".tmp";
|
|
3220
4103
|
try {
|
|
3221
4104
|
writeFileSync5(temporary, data, { flag: "wx", mode: 384 });
|
|
3222
4105
|
renameSync5(temporary, this.#path);
|
|
@@ -3278,11 +4161,11 @@ var CollaborationService = class {
|
|
|
3278
4161
|
unlinkSync5(this.#lock);
|
|
3279
4162
|
}
|
|
3280
4163
|
}
|
|
3281
|
-
#roomKey(kind,
|
|
3282
|
-
return kind + ":" +
|
|
4164
|
+
#roomKey(kind, id2) {
|
|
4165
|
+
return kind + ":" + id2;
|
|
3283
4166
|
}
|
|
3284
|
-
#room(rows, kind,
|
|
3285
|
-
const row = rows.find((item) => item.kind === kind && item.id ===
|
|
4167
|
+
#room(rows, kind, id2) {
|
|
4168
|
+
const row = rows.find((item) => item.kind === kind && item.id === id2);
|
|
3286
4169
|
if (!row) throw new CollaborationError("Shared resource room not found.", 404);
|
|
3287
4170
|
return row;
|
|
3288
4171
|
}
|
|
@@ -3304,8 +4187,8 @@ var CollaborationService = class {
|
|
|
3304
4187
|
const { workspaceId: _workspace, ...viewer } = actor;
|
|
3305
4188
|
return viewer;
|
|
3306
4189
|
}
|
|
3307
|
-
#presenceFor(kind,
|
|
3308
|
-
const entries = this.#presence.get(this.#roomKey(kind,
|
|
4190
|
+
#presenceFor(kind, id2) {
|
|
4191
|
+
const entries = this.#presence.get(this.#roomKey(kind, id2));
|
|
3309
4192
|
const presence = /* @__PURE__ */ new Map();
|
|
3310
4193
|
for (const [key, entry] of entries ?? []) {
|
|
3311
4194
|
if (!entry.viewing || entry.updatedAt + this.#presenceMs <= this.#now()) {
|
|
@@ -3332,15 +4215,15 @@ var CollaborationService = class {
|
|
|
3332
4215
|
return [...presence.values()];
|
|
3333
4216
|
}
|
|
3334
4217
|
/** Routing must validate that the underlying resource exists before registration. */
|
|
3335
|
-
registerRoom(kind,
|
|
4218
|
+
registerRoom(kind, id2, sessionId) {
|
|
3336
4219
|
const actor = this.#actor(sessionId);
|
|
3337
|
-
const valid = external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id });
|
|
4220
|
+
const valid = external_exports.object({ kind: resourceKindSchema, id: external_exports.string().min(1).max(160) }).safeParse({ kind, id: id2 });
|
|
3338
4221
|
if (!valid.success) throw new CollaborationError("Invalid resource room.");
|
|
3339
4222
|
let joined = false;
|
|
3340
4223
|
this.#transaction((rows) => {
|
|
3341
|
-
let row = rows.find((item) => item.kind === kind && item.id ===
|
|
4224
|
+
let row = rows.find((item) => item.kind === kind && item.id === id2);
|
|
3342
4225
|
if (!row) {
|
|
3343
|
-
row = { kind, id, revision: 0, createdBy: this.#attribution(actor), createdAt: this.#now(), members: [] };
|
|
4226
|
+
row = { kind, id: id2, revision: 0, createdBy: this.#attribution(actor), createdAt: this.#now(), members: [] };
|
|
3344
4227
|
rows.push(row);
|
|
3345
4228
|
}
|
|
3346
4229
|
const revision2 = row.revision;
|
|
@@ -3348,7 +4231,7 @@ var CollaborationService = class {
|
|
|
3348
4231
|
joined = row.revision !== revision2;
|
|
3349
4232
|
});
|
|
3350
4233
|
if (joined) this.#notifyPresence();
|
|
3351
|
-
return this.roomSnapshot(kind,
|
|
4234
|
+
return this.roomSnapshot(kind, id2, sessionId);
|
|
3352
4235
|
}
|
|
3353
4236
|
register(conversationId, sessionId) {
|
|
3354
4237
|
this.registerRoom("conversation", conversationId, sessionId);
|
|
@@ -3357,10 +4240,10 @@ var CollaborationService = class {
|
|
|
3357
4240
|
hasConversation(conversationId) {
|
|
3358
4241
|
return this.#read().some((row) => row.kind === "conversation" && row.id === conversationId);
|
|
3359
4242
|
}
|
|
3360
|
-
roomSnapshot(kind,
|
|
4243
|
+
roomSnapshot(kind, id2, sessionId) {
|
|
3361
4244
|
const actor = this.#actor(sessionId);
|
|
3362
|
-
const row = this.#room(this.#read(), kind,
|
|
3363
|
-
return { kind, id, workspaceId: this.#options.workspaceId, revision: row.revision, viewer: this.#viewer(actor), canWrite: actor.role !== "viewer", members: structuredClone(row.members), presence: this.#presenceFor(kind,
|
|
4245
|
+
const row = this.#room(this.#read(), kind, id2);
|
|
4246
|
+
return { kind, id: id2, workspaceId: this.#options.workspaceId, revision: row.revision, viewer: this.#viewer(actor), canWrite: actor.role !== "viewer", members: structuredClone(row.members), presence: this.#presenceFor(kind, id2) };
|
|
3364
4247
|
}
|
|
3365
4248
|
snapshot(conversationId, sessionId) {
|
|
3366
4249
|
const { kind: _kind, id: _id, ...snapshot } = this.roomSnapshot("conversation", conversationId, sessionId);
|
|
@@ -3374,19 +4257,19 @@ var CollaborationService = class {
|
|
|
3374
4257
|
});
|
|
3375
4258
|
return { workspaceId: this.#options.workspaceId, viewer: this.#viewer(actor), rooms };
|
|
3376
4259
|
}
|
|
3377
|
-
heartbeatRoom(kind,
|
|
4260
|
+
heartbeatRoom(kind, id2, sessionId, input) {
|
|
3378
4261
|
const actor = this.#actor(sessionId);
|
|
3379
4262
|
const parsed = external_exports.object({ typing: external_exports.boolean(), viewing: external_exports.boolean().default(true), clientId: external_exports.string().uuid().optional() }).strict().safeParse(input);
|
|
3380
4263
|
if (!parsed.success) throw new CollaborationError("Invalid presence update.");
|
|
3381
|
-
this.#presenceFor(kind,
|
|
4264
|
+
this.#presenceFor(kind, id2);
|
|
3382
4265
|
let joined = false;
|
|
3383
4266
|
this.#transaction((rows) => {
|
|
3384
|
-
const row = this.#room(rows, kind,
|
|
4267
|
+
const row = this.#room(rows, kind, id2);
|
|
3385
4268
|
const revision2 = row.revision;
|
|
3386
4269
|
this.#join(row, actor);
|
|
3387
4270
|
joined = row.revision !== revision2;
|
|
3388
4271
|
});
|
|
3389
|
-
const key = this.#roomKey(kind,
|
|
4272
|
+
const key = this.#roomKey(kind, id2);
|
|
3390
4273
|
let entries = this.#presence.get(key);
|
|
3391
4274
|
if (!entries) {
|
|
3392
4275
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -3398,7 +4281,7 @@ var CollaborationService = class {
|
|
|
3398
4281
|
if (parsed.data.viewing) entries.set(entryKey, { ...this.#attribution(actor), sessionId, viewing: true, typing: parsed.data.typing, updatedAt: this.#now() });
|
|
3399
4282
|
else entries.delete(entryKey);
|
|
3400
4283
|
if (joined || previous?.typing !== (parsed.data.viewing ? parsed.data.typing : void 0) || previous?.displayName !== (parsed.data.viewing ? actor.displayName : void 0)) this.#notifyPresence();
|
|
3401
|
-
return this.roomSnapshot(kind,
|
|
4284
|
+
return this.roomSnapshot(kind, id2, sessionId);
|
|
3402
4285
|
}
|
|
3403
4286
|
heartbeat(conversationId, sessionId, input) {
|
|
3404
4287
|
this.heartbeatRoom("conversation", conversationId, sessionId, input);
|
|
@@ -3457,29 +4340,133 @@ async function handleTeamRequest(req, url, auth, collaboration, gateway, readBod
|
|
|
3457
4340
|
if (resource2) {
|
|
3458
4341
|
if (method !== "POST") throw new TeamAuthError("Method not allowed.", 405);
|
|
3459
4342
|
const kind = resource2[1];
|
|
3460
|
-
const
|
|
3461
|
-
if (kind === "conversation") gateway.get(
|
|
3462
|
-
else if (validateResource) await validateResource(kind,
|
|
4343
|
+
const id3 = resource2[2];
|
|
4344
|
+
if (kind === "conversation") gateway.get(id3);
|
|
4345
|
+
else if (validateResource) await validateResource(kind, id3);
|
|
3463
4346
|
else throw new TeamAuthError("Resource presence is not configured for this engine.", 404);
|
|
3464
|
-
collaboration.registerRoom(kind,
|
|
3465
|
-
return { status: 200, data: collaboration.heartbeatRoom(kind,
|
|
4347
|
+
collaboration.registerRoom(kind, id3, session);
|
|
4348
|
+
return { status: 200, data: collaboration.heartbeatRoom(kind, id3, session, await readBody()) };
|
|
3466
4349
|
}
|
|
3467
4350
|
const match = /^conversations\/([A-Za-z0-9_.:-]+)(?:\/(presence))?$/.exec(path);
|
|
3468
4351
|
if (!match) throw new TeamAuthError("Team resource not found.", 404);
|
|
3469
|
-
const
|
|
3470
|
-
gateway.get(
|
|
3471
|
-
if (!collaboration.hasConversation(
|
|
3472
|
-
if (!match[2] && method === "GET") return { status: 200, data: collaboration.snapshot(
|
|
3473
|
-
if (match[2] === "presence" && method === "POST") return { status: 200, data: collaboration.heartbeat(
|
|
4352
|
+
const id2 = match[1];
|
|
4353
|
+
gateway.get(id2);
|
|
4354
|
+
if (!collaboration.hasConversation(id2)) collaboration.register(id2, session);
|
|
4355
|
+
if (!match[2] && method === "GET") return { status: 200, data: collaboration.snapshot(id2, session) };
|
|
4356
|
+
if (match[2] === "presence" && method === "POST") return { status: 200, data: collaboration.heartbeat(id2, session, await readBody()) };
|
|
3474
4357
|
throw new TeamAuthError("Method not allowed.", 405);
|
|
3475
4358
|
}
|
|
3476
|
-
function assertTeamConsoleMutation(req,
|
|
4359
|
+
function assertTeamConsoleMutation(req, id2, auth, collaboration, gateway) {
|
|
3477
4360
|
if (!auth.enabled) return;
|
|
3478
|
-
gateway.get(
|
|
4361
|
+
gateway.get(id2);
|
|
3479
4362
|
const session = getSessionId(req);
|
|
3480
4363
|
if (!session || !collaboration) throw new TeamAuthError("Sign in to this workspace.");
|
|
3481
|
-
if (!collaboration.hasConversation(
|
|
3482
|
-
collaboration.assertWriter(
|
|
4364
|
+
if (!collaboration.hasConversation(id2)) collaboration.register(id2, session);
|
|
4365
|
+
collaboration.assertWriter(id2, session);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
// packages/cli/src/web/browser-live-routes.ts
|
|
4369
|
+
var registry = { list: listBrowserViews, subscribe: subscribeBrowserView };
|
|
4370
|
+
var HEARTBEAT_MS = 15e3;
|
|
4371
|
+
var STREAM_LIFETIME_MS = 5 * 6e4;
|
|
4372
|
+
async function handleBrowserLiveRequest(req, res, url, ownerId, pageId, source = registry) {
|
|
4373
|
+
const reply = (status, body) => {
|
|
4374
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
4375
|
+
res.end(JSON.stringify(body));
|
|
4376
|
+
};
|
|
4377
|
+
if (url.search) {
|
|
4378
|
+
reply(400, { error: "Browser view endpoints do not accept query parameters." });
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
if (req.method !== "GET") {
|
|
4382
|
+
reply(405, { error: "Use GET for browser views." });
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
const views = source.list(ownerId);
|
|
4386
|
+
if (!pageId) {
|
|
4387
|
+
reply(200, { views });
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
const view = views.find((item) => item.pageId === pageId);
|
|
4391
|
+
if (!view) {
|
|
4392
|
+
reply(404, { error: "Browser view was closed or does not belong to this conversation." });
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
if (!view.liveAvailable) {
|
|
4396
|
+
reply(409, { error: "Live browser view is unavailable for this execution backend." });
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
res.writeHead(200, {
|
|
4400
|
+
"Content-Type": "text/event-stream",
|
|
4401
|
+
"Cache-Control": "no-cache, no-store, no-transform",
|
|
4402
|
+
"X-Accel-Buffering": "no",
|
|
4403
|
+
Connection: "keep-alive"
|
|
4404
|
+
});
|
|
4405
|
+
res.flushHeaders?.();
|
|
4406
|
+
let closed = false;
|
|
4407
|
+
let blocked = false;
|
|
4408
|
+
let unsubscribe;
|
|
4409
|
+
let heartbeat;
|
|
4410
|
+
let lifetime;
|
|
4411
|
+
const cleanup = () => {
|
|
4412
|
+
if (closed) return;
|
|
4413
|
+
closed = true;
|
|
4414
|
+
clearInterval(heartbeat);
|
|
4415
|
+
clearTimeout(lifetime);
|
|
4416
|
+
unsubscribe?.();
|
|
4417
|
+
unsubscribe = void 0;
|
|
4418
|
+
res.off("drain", drain);
|
|
4419
|
+
res.off("close", cleanup);
|
|
4420
|
+
res.off("error", cleanup);
|
|
4421
|
+
req.off("aborted", cleanup);
|
|
4422
|
+
};
|
|
4423
|
+
const drain = () => {
|
|
4424
|
+
blocked = false;
|
|
4425
|
+
};
|
|
4426
|
+
const write = (event, data) => {
|
|
4427
|
+
if (closed || blocked || res.destroyed || res.writableEnded) return;
|
|
4428
|
+
blocked = !res.write(`event: ${event}
|
|
4429
|
+
data: ${JSON.stringify(data)}
|
|
4430
|
+
|
|
4431
|
+
`);
|
|
4432
|
+
};
|
|
4433
|
+
const finish = (status) => {
|
|
4434
|
+
write("state", { status });
|
|
4435
|
+
cleanup();
|
|
4436
|
+
if (!res.destroyed && !res.writableEnded) res.end();
|
|
4437
|
+
};
|
|
4438
|
+
res.on("drain", drain);
|
|
4439
|
+
res.on("close", cleanup);
|
|
4440
|
+
res.on("error", cleanup);
|
|
4441
|
+
req.on("aborted", cleanup);
|
|
4442
|
+
write("state", { status: "connecting" });
|
|
4443
|
+
heartbeat = setInterval(() => {
|
|
4444
|
+
try {
|
|
4445
|
+
if (!source.list(ownerId).some((item) => item.pageId === pageId)) {
|
|
4446
|
+
finish("closed");
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
if (!closed && !blocked) blocked = !res.write(": heartbeat\n\n");
|
|
4450
|
+
} catch {
|
|
4451
|
+
finish("unavailable");
|
|
4452
|
+
}
|
|
4453
|
+
}, HEARTBEAT_MS);
|
|
4454
|
+
lifetime = setTimeout(() => finish("expired"), STREAM_LIFETIME_MS);
|
|
4455
|
+
heartbeat.unref?.();
|
|
4456
|
+
lifetime.unref?.();
|
|
4457
|
+
try {
|
|
4458
|
+
const stop = await source.subscribe(ownerId, pageId, (frame) => {
|
|
4459
|
+
if (closed || blocked || frame.mimeType !== "image/jpeg" || frame.data.length > 4 * 1024 * 1024) return;
|
|
4460
|
+
write("frame", frame);
|
|
4461
|
+
});
|
|
4462
|
+
if (closed) stop();
|
|
4463
|
+
else {
|
|
4464
|
+
unsubscribe = stop;
|
|
4465
|
+
write("state", { status: "live" });
|
|
4466
|
+
}
|
|
4467
|
+
} catch {
|
|
4468
|
+
finish("unavailable");
|
|
4469
|
+
}
|
|
3483
4470
|
}
|
|
3484
4471
|
|
|
3485
4472
|
// packages/cli/src/web/team-presence-stream.ts
|
|
@@ -3701,7 +4688,7 @@ function openBrowser(url) {
|
|
|
3701
4688
|
execFile2(cmd, args, () => {
|
|
3702
4689
|
});
|
|
3703
4690
|
}
|
|
3704
|
-
function
|
|
4691
|
+
function json2(res, status, body) {
|
|
3705
4692
|
res.writeHead(status, {
|
|
3706
4693
|
"Content-Type": "application/json; charset=utf-8",
|
|
3707
4694
|
"Cache-Control": "no-store"
|
|
@@ -4387,7 +5374,7 @@ function requireControlToken(req, res, controlToken, engineBearer) {
|
|
|
4387
5374
|
const matches = (a, b) => typeof a === "string" && Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual3(Buffer.from(a), Buffer.from(b));
|
|
4388
5375
|
const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice(7) : void 0;
|
|
4389
5376
|
if (!matches(provided, controlToken) && !matches(bearer, controlToken) && !(engineBearer && matches(bearer, engineBearer))) {
|
|
4390
|
-
|
|
5377
|
+
json2(res, 403, { error: "Invalid or missing control token" });
|
|
4391
5378
|
return false;
|
|
4392
5379
|
}
|
|
4393
5380
|
return true;
|
|
@@ -4414,52 +5401,52 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4414
5401
|
const bodyId = input && typeof input === "object" && "sessionId" in input && typeof input.sessionId === "string" ? input.sessionId : void 0;
|
|
4415
5402
|
const savedId = /^saved\/([^/]+)/.exec(path)?.[1];
|
|
4416
5403
|
const savedLiveId = savedId ? gateway.list().find((item) => item.id === savedId || item.savedId === savedId)?.id : void 0;
|
|
4417
|
-
const
|
|
4418
|
-
if (
|
|
5404
|
+
const id2 = liveId ?? bodyId ?? savedLiveId;
|
|
5405
|
+
if (id2 && !readResume) assertTeamConsoleMutation(req, id2, team.auth, team.collaboration, gateway);
|
|
4419
5406
|
}
|
|
4420
5407
|
try {
|
|
4421
5408
|
if (path === "execution" && method === "GET") {
|
|
4422
|
-
|
|
5409
|
+
json2(res, 200, await workbenchStatus());
|
|
4423
5410
|
return true;
|
|
4424
5411
|
}
|
|
4425
5412
|
if (path === "github" && method === "GET") {
|
|
4426
|
-
|
|
5413
|
+
json2(res, 200, { github: await github.status() });
|
|
4427
5414
|
return true;
|
|
4428
5415
|
}
|
|
4429
5416
|
if (path === "github/connect" && method === "POST") {
|
|
4430
|
-
|
|
5417
|
+
json2(res, 200, { github: await github.connect(input) });
|
|
4431
5418
|
return true;
|
|
4432
5419
|
}
|
|
4433
5420
|
if (path === "github/device-auth" && method === "POST") {
|
|
4434
|
-
|
|
5421
|
+
json2(res, 202, { github: await github.start() });
|
|
4435
5422
|
return true;
|
|
4436
5423
|
}
|
|
4437
5424
|
if (path === "github/device-auth" && method === "DELETE") {
|
|
4438
|
-
|
|
5425
|
+
json2(res, 200, { github: await github.cancel() });
|
|
4439
5426
|
return true;
|
|
4440
5427
|
}
|
|
4441
5428
|
const trigger = await triggers.handle(method, requestUrl, input);
|
|
4442
5429
|
if (trigger) {
|
|
4443
|
-
|
|
5430
|
+
json2(res, trigger.status, trigger.data);
|
|
4444
5431
|
return true;
|
|
4445
5432
|
}
|
|
4446
5433
|
const workflow = await workflows.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4447
5434
|
if (workflow) {
|
|
4448
5435
|
if (team?.collaboration && method !== "GET" && method !== "HEAD" && workflow.status >= 200 && workflow.status < 300) {
|
|
4449
5436
|
const definition = workflow.data && typeof workflow.data === "object" && "definition" in workflow.data ? workflow.data.definition : void 0;
|
|
4450
|
-
const
|
|
4451
|
-
if (
|
|
5437
|
+
const id3 = definition && typeof definition === "object" && "id" in definition && typeof definition.id === "string" ? definition.id : /^\/api\/console\/workflow-definitions\/([^/]+)/.exec(requestUrl.pathname)?.[1];
|
|
5438
|
+
if (id3) team.collaboration.notifyChanged("workflow", decodeURIComponent(id3));
|
|
4452
5439
|
}
|
|
4453
|
-
|
|
5440
|
+
json2(res, workflow.status, workflow.data);
|
|
4454
5441
|
return true;
|
|
4455
5442
|
}
|
|
4456
5443
|
const service = await operator.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4457
5444
|
if (service) {
|
|
4458
|
-
|
|
5445
|
+
json2(res, service.status, service.data);
|
|
4459
5446
|
return true;
|
|
4460
5447
|
}
|
|
4461
5448
|
if (path === "search" && method === "GET") {
|
|
4462
|
-
|
|
5449
|
+
json2(res, 200, await gateway.search({
|
|
4463
5450
|
q: requestUrl.searchParams.get("q") ?? "",
|
|
4464
5451
|
...requestUrl.searchParams.has("limit") ? { limit: Number(requestUrl.searchParams.get("limit")) } : {},
|
|
4465
5452
|
...requestUrl.searchParams.has("offset") ? { offset: Number(requestUrl.searchParams.get("offset")) } : {}
|
|
@@ -4467,84 +5454,99 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4467
5454
|
return true;
|
|
4468
5455
|
}
|
|
4469
5456
|
if (path === "sessions") {
|
|
4470
|
-
if (method === "GET")
|
|
5457
|
+
if (method === "GET") json2(res, 200, { sessions: gateway.list() });
|
|
4471
5458
|
else if (method === "POST") {
|
|
4472
5459
|
const session = gateway.create(input);
|
|
4473
5460
|
if (team?.auth.enabled && team.collaboration) team.collaboration.register(session.id, getSessionId(req));
|
|
4474
|
-
|
|
4475
|
-
} else
|
|
5461
|
+
json2(res, 201, { session });
|
|
5462
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4476
5463
|
return true;
|
|
4477
5464
|
}
|
|
4478
5465
|
const parts = path.split("/").map((part) => decodeURIComponent(part));
|
|
4479
5466
|
if (parts.some((part) => !/^[A-Za-z0-9_.:-]+$/.test(part))) throw new ConsoleGatewayError("Invalid resource identifier.", 400);
|
|
4480
5467
|
if (parts[0] === "saved") {
|
|
4481
|
-
const
|
|
4482
|
-
if (!
|
|
5468
|
+
const id3 = parts[1];
|
|
5469
|
+
if (!id3 && method === "GET") {
|
|
4483
5470
|
const limit = requestUrl.searchParams.get("limit");
|
|
4484
|
-
|
|
5471
|
+
json2(res, 200, { sessions: gateway.listSaved({
|
|
4485
5472
|
...requestUrl.searchParams.get("cwd") ? { cwd: requestUrl.searchParams.get("cwd") } : {},
|
|
4486
5473
|
...limit ? { limit: Number(limit) } : {}
|
|
4487
5474
|
}) });
|
|
4488
|
-
} else if (
|
|
4489
|
-
else if (
|
|
4490
|
-
gateway.deleteSaved(
|
|
4491
|
-
|
|
4492
|
-
} else if (
|
|
4493
|
-
else if (
|
|
4494
|
-
else if (
|
|
4495
|
-
else
|
|
5475
|
+
} else if (id3 && parts.length === 2 && method === "GET") json2(res, 200, gateway.loadSaved(id3));
|
|
5476
|
+
else if (id3 && parts.length === 2 && method === "DELETE") {
|
|
5477
|
+
gateway.deleteSaved(id3);
|
|
5478
|
+
json2(res, 200, { ok: true });
|
|
5479
|
+
} else if (id3 && parts[2] === "archive" && method === "POST") json2(res, 200, { session: gateway.archiveSaved(id3, input) });
|
|
5480
|
+
else if (id3 && parts[2] === "resume" && method === "POST") json2(res, 201, { session: await gateway.resume(id3, input) });
|
|
5481
|
+
else if (id3 && parts[2] === "export" && method === "GET") json2(res, 200, gateway.exportSaved(id3));
|
|
5482
|
+
else json2(res, 405, { error: "Method not allowed." });
|
|
4496
5483
|
return true;
|
|
4497
5484
|
}
|
|
4498
5485
|
if (parts[0] !== "sessions" || !parts[1]) return false;
|
|
4499
|
-
const
|
|
5486
|
+
const id2 = parts[1];
|
|
4500
5487
|
const action = parts[2];
|
|
4501
|
-
if (
|
|
5488
|
+
if (action === "browser" && parts[3] === "views" && (parts.length === 4 || parts.length === 6 && parts[5] === "stream")) {
|
|
5489
|
+
gateway.get(id2);
|
|
5490
|
+
if (team?.auth.enabled) {
|
|
5491
|
+
const sessionId = getSessionId(req);
|
|
5492
|
+
if (!sessionId || !team.collaboration) throw new WebRequestError("Sign in to this workspace.", 403);
|
|
5493
|
+
if (!team.collaboration.hasConversation(id2)) team.collaboration.register(id2, sessionId);
|
|
5494
|
+
team.collaboration.snapshot(id2, sessionId);
|
|
5495
|
+
}
|
|
5496
|
+
await handleBrowserLiveRequest(req, res, requestUrl, id2, parts[4]);
|
|
5497
|
+
return true;
|
|
5498
|
+
}
|
|
5499
|
+
if (!action && method === "GET") json2(res, 200, { snapshot: gateway.get(id2) });
|
|
4502
5500
|
else if (!action && method === "DELETE") {
|
|
4503
|
-
workflows.cancelSession(
|
|
4504
|
-
await gateway.close(
|
|
4505
|
-
|
|
5501
|
+
workflows.cancelSession(id2);
|
|
5502
|
+
await gateway.close(id2);
|
|
5503
|
+
json2(res, 200, { ok: true });
|
|
4506
5504
|
} else if (action === "events" && method === "GET") {
|
|
4507
|
-
|
|
5505
|
+
json2(res, 200, gateway.eventsAfter(id2, consoleEventsAfter(requestUrl.searchParams.get("after"))));
|
|
4508
5506
|
} else if (action === "messages" && method === "POST") {
|
|
4509
5507
|
const message2 = author && input && typeof input === "object" && !("workerId" in input) && (!("mode" in input) || input.mode === "send") ? { ...input, mode: "queue" } : input;
|
|
4510
|
-
|
|
4511
|
-
|
|
5508
|
+
const { queuedMessageId, ...session } = await gateway.send(id2, message2, author);
|
|
5509
|
+
json2(res, 202, { session, ...queuedMessageId ? { queuedMessageId } : {} });
|
|
5510
|
+
} else if (action === "cancel" && method === "POST") json2(res, 200, { session: await gateway.cancel(id2) });
|
|
4512
5511
|
else if (action === "connection" && parts[3] === "reload" && parts.length === 4 && method === "POST") {
|
|
4513
5512
|
external_exports.object({}).strict().parse(input ?? {});
|
|
4514
|
-
|
|
4515
|
-
} else if (action === "configuration" && method === "PATCH")
|
|
4516
|
-
else if (action === "clear" && method === "POST")
|
|
4517
|
-
else if (action === "continue" && method === "POST")
|
|
4518
|
-
else if (action === "harness" && method === "POST")
|
|
4519
|
-
else if (action === "save" && method === "POST")
|
|
5513
|
+
json2(res, 200, { session: await gateway.reloadConnection(id2) });
|
|
5514
|
+
} else if (action === "configuration" && method === "PATCH") json2(res, 200, { session: await gateway.configure(id2, input) });
|
|
5515
|
+
else if (action === "clear" && method === "POST") json2(res, 200, { session: await gateway.clear(id2) });
|
|
5516
|
+
else if (action === "continue" && method === "POST") json2(res, 202, { session: await gateway.continue(id2, input, author) });
|
|
5517
|
+
else if (action === "harness" && method === "POST") json2(res, 200, await gateway.harness(id2, input));
|
|
5518
|
+
else if (action === "save" && method === "POST") json2(res, 200, { session: gateway.save(id2) });
|
|
4520
5519
|
else if (action === "archive" && method === "POST") {
|
|
4521
|
-
workflows.cancelSession(
|
|
4522
|
-
|
|
5520
|
+
workflows.cancelSession(id2);
|
|
5521
|
+
json2(res, 200, { session: await gateway.archive(id2) });
|
|
4523
5522
|
} else if (action === "delete" && method === "POST") {
|
|
4524
|
-
workflows.cancelSession(
|
|
4525
|
-
|
|
4526
|
-
} else if (action === "export" && method === "GET")
|
|
4527
|
-
else if (action === "queue" &&
|
|
4528
|
-
|
|
5523
|
+
workflows.cancelSession(id2);
|
|
5524
|
+
json2(res, 200, await gateway.delete(id2));
|
|
5525
|
+
} else if (action === "export" && method === "GET") json2(res, 200, gateway.export(id2));
|
|
5526
|
+
else if (action === "queue" && parts[3] && parts[4] === "send" && parts.length === 5 && method === "POST") {
|
|
5527
|
+
external_exports.object({}).strict().parse(input ?? {});
|
|
5528
|
+
json2(res, 202, { session: gateway.sendQueuedNow(id2, parts[3]) });
|
|
5529
|
+
} else if (action === "queue" && method === "DELETE") json2(res, 200, { session: gateway.removeQueued(id2, parts[3]) });
|
|
5530
|
+
else if (action === "decisions" && parts[3] && method === "POST") json2(res, 200, { session: gateway.resolveDecision(id2, parts[3], input) });
|
|
4529
5531
|
else if (action === "workers") {
|
|
4530
5532
|
const workerId = parts[3];
|
|
4531
|
-
if (!workerId && method === "GET")
|
|
4532
|
-
else if (workerId === "stop" && method === "POST")
|
|
4533
|
-
else if (workerId && parts[4] === "export" && method === "GET")
|
|
4534
|
-
else if (workerId && parts[4] === "stop" && method === "POST")
|
|
5533
|
+
if (!workerId && method === "GET") json2(res, 200, { workers: gateway.workers(id2) });
|
|
5534
|
+
else if (workerId === "stop" && method === "POST") json2(res, 200, { session: await gateway.drainWorkers(id2) });
|
|
5535
|
+
else if (workerId && parts[4] === "export" && method === "GET") json2(res, 200, gateway.exportWorker(id2, workerId));
|
|
5536
|
+
else if (workerId && parts[4] === "stop" && method === "POST") json2(res, 200, { worker: await gateway.stopWorker(id2, workerId) });
|
|
4535
5537
|
else if (workerId && parts[4] === "messages" && method === "POST") {
|
|
4536
5538
|
const body = external_exports.object({ text: external_exports.unknown() }).strict().parse(input);
|
|
4537
|
-
|
|
4538
|
-
} else
|
|
4539
|
-
} else
|
|
5539
|
+
json2(res, 202, { session: gateway.sendWorker(id2, workerId, body.text, author) });
|
|
5540
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
5541
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4540
5542
|
return true;
|
|
4541
5543
|
} catch (error2) {
|
|
4542
5544
|
if (error2 instanceof external_exports.ZodError) {
|
|
4543
|
-
|
|
5545
|
+
json2(res, 400, { error: "Invalid console request payload." });
|
|
4544
5546
|
return true;
|
|
4545
5547
|
}
|
|
4546
5548
|
if (error2 instanceof ConsoleGatewayError) {
|
|
4547
|
-
|
|
5549
|
+
json2(res, error2.statusCode, { error: error2.message });
|
|
4548
5550
|
return true;
|
|
4549
5551
|
}
|
|
4550
5552
|
throw error2;
|
|
@@ -4572,10 +5574,10 @@ data: ${JSON.stringify(event)}
|
|
|
4572
5574
|
`);
|
|
4573
5575
|
}
|
|
4574
5576
|
async function handleApiRequest(req, res, pathname, dbPath, controlToken, engineBearer) {
|
|
4575
|
-
const { osecDB: osecDB2 } = await import("./dist-
|
|
5577
|
+
const { osecDB: osecDB2 } = await import("./dist-OLGNKDB2.js");
|
|
4576
5578
|
if (isPresentationEventsStreamPath(pathname)) {
|
|
4577
5579
|
if (req.method !== "GET") {
|
|
4578
|
-
|
|
5580
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4579
5581
|
return true;
|
|
4580
5582
|
}
|
|
4581
5583
|
const db = new osecDB2(dbPath);
|
|
@@ -4619,27 +5621,27 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4619
5621
|
const controlPath = parseControlPath(pathname);
|
|
4620
5622
|
if (controlPath) {
|
|
4621
5623
|
if (req.method !== "POST") {
|
|
4622
|
-
|
|
5624
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4623
5625
|
return true;
|
|
4624
5626
|
}
|
|
4625
5627
|
if (!requireControlToken(req, res, controlToken, engineBearer)) {
|
|
4626
5628
|
return true;
|
|
4627
5629
|
}
|
|
4628
5630
|
if (controlPath.action === "recover-stale-workers") {
|
|
4629
|
-
const { recoverStaleWorkers } = await import("./orchestrate-
|
|
5631
|
+
const { recoverStaleWorkers } = await import("./orchestrate-Z3DW265M.js");
|
|
4630
5632
|
const body = await readJson(req);
|
|
4631
5633
|
const recovered = recoverStaleWorkers(
|
|
4632
5634
|
dbPath,
|
|
4633
5635
|
typeof body.staleAfterMs === "number" ? body.staleAfterMs : 3e4
|
|
4634
5636
|
);
|
|
4635
|
-
|
|
5637
|
+
json2(res, 200, { ok: true, recovered });
|
|
4636
5638
|
return true;
|
|
4637
5639
|
}
|
|
4638
5640
|
if (controlPath.action === "prune-stopped-workers") {
|
|
4639
5641
|
const db = new osecDB2(dbPath);
|
|
4640
5642
|
try {
|
|
4641
5643
|
const deleted = db.deleteWorkersByStatus("stopped");
|
|
4642
|
-
|
|
5644
|
+
json2(res, 200, { ok: true, deleted });
|
|
4643
5645
|
} finally {
|
|
4644
5646
|
db.close();
|
|
4645
5647
|
}
|
|
@@ -4653,7 +5655,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4653
5655
|
(worker) => worker.status !== "stopped" && Date.now() - Date.parse(worker.heartbeatAt) < 2e4
|
|
4654
5656
|
);
|
|
4655
5657
|
if (activeWorkers.length > 0) {
|
|
4656
|
-
|
|
5658
|
+
json2(res, 409, { error: "An active daemon is already heartbeating. Stop it before starting another local daemon." });
|
|
4657
5659
|
return true;
|
|
4658
5660
|
}
|
|
4659
5661
|
} finally {
|
|
@@ -4664,21 +5666,21 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4664
5666
|
label: typeof body.label === "string" && body.label.trim() ? body.label.trim() : "control-plane-1",
|
|
4665
5667
|
pollIntervalMs: typeof body.pollIntervalMs === "number" ? body.pollIntervalMs : 2e3
|
|
4666
5668
|
});
|
|
4667
|
-
|
|
5669
|
+
json2(res, 200, { ok: true, ...started });
|
|
4668
5670
|
return true;
|
|
4669
5671
|
}
|
|
4670
5672
|
if (controlPath.action === "stop-daemon") {
|
|
4671
5673
|
const stopped = stopDaemonWorkers(osecDB2, dbPath);
|
|
4672
|
-
|
|
5674
|
+
json2(res, 200, { ok: true, stopped });
|
|
4673
5675
|
return true;
|
|
4674
5676
|
}
|
|
4675
5677
|
if (controlPath.action === "reset-database") {
|
|
4676
|
-
const { resetOsecDatabase } = await import("./dist-
|
|
4677
|
-
const { seedVerificationWorkbench } = await import("./db-
|
|
5678
|
+
const { resetOsecDatabase } = await import("./dist-OLGNKDB2.js");
|
|
5679
|
+
const { seedVerificationWorkbench } = await import("./db-R2AQSESC.js");
|
|
4678
5680
|
const body = await readJson(req);
|
|
4679
5681
|
const seed = typeof body.seed === "string" ? body.seed.trim().toLowerCase() : "verification";
|
|
4680
5682
|
if (!["verification", "empty"].includes(seed)) {
|
|
4681
|
-
|
|
5683
|
+
json2(res, 400, { error: `Unsupported seed preset: ${seed}` });
|
|
4682
5684
|
return true;
|
|
4683
5685
|
}
|
|
4684
5686
|
const activeWorkerDb = new osecDB2(dbPath);
|
|
@@ -4687,7 +5689,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4687
5689
|
(worker) => ["idle", "claiming", "running", "sleeping"].includes(worker.status) && Date.now() - Date.parse(worker.heartbeatAt) < 2e4 && (worker.pid ? isLiveLocalPid(worker.pid) : true)
|
|
4688
5690
|
);
|
|
4689
5691
|
if (hasActiveWorker) {
|
|
4690
|
-
|
|
5692
|
+
json2(res, 409, { error: "Stop active orchestration daemons before resetting the local database." });
|
|
4691
5693
|
return true;
|
|
4692
5694
|
}
|
|
4693
5695
|
} finally {
|
|
@@ -4697,7 +5699,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4697
5699
|
const resetDb = new osecDB2(dbPath);
|
|
4698
5700
|
try {
|
|
4699
5701
|
const seeded = seed === "verification" ? seedVerificationWorkbench(resetDb) : { scans: 0, families: 0, workers: 0 };
|
|
4700
|
-
|
|
5702
|
+
json2(res, 200, { ok: true, path, seed, ...seeded });
|
|
4701
5703
|
} finally {
|
|
4702
5704
|
resetDb.close();
|
|
4703
5705
|
}
|
|
@@ -4733,7 +5735,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4733
5735
|
});
|
|
4734
5736
|
const casesById = new Map(cases.map((item) => [item.id, item]));
|
|
4735
5737
|
const workItemsById = new Map(workItems.map((item) => [item.id, item]));
|
|
4736
|
-
|
|
5738
|
+
json2(res, 200, {
|
|
4737
5739
|
scans: scans.map(summarizeScan),
|
|
4738
5740
|
cases,
|
|
4739
5741
|
groups,
|
|
@@ -4749,7 +5751,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4749
5751
|
const db = new osecDB2(dbPath);
|
|
4750
5752
|
try {
|
|
4751
5753
|
const scans = db.listScans(100);
|
|
4752
|
-
|
|
5754
|
+
json2(res, 200, { scans: scans.map(summarizeScan) });
|
|
4753
5755
|
} finally {
|
|
4754
5756
|
db.close();
|
|
4755
5757
|
}
|
|
@@ -4783,7 +5785,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4783
5785
|
presentation
|
|
4784
5786
|
};
|
|
4785
5787
|
});
|
|
4786
|
-
|
|
5788
|
+
json2(res, 200, { events });
|
|
4787
5789
|
} finally {
|
|
4788
5790
|
db.close();
|
|
4789
5791
|
}
|
|
@@ -4795,7 +5797,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4795
5797
|
try {
|
|
4796
5798
|
const scan = db.getScan(scanPath.scanId);
|
|
4797
5799
|
if (!scan) {
|
|
4798
|
-
|
|
5800
|
+
json2(res, 404, { error: "Scan not found" });
|
|
4799
5801
|
return true;
|
|
4800
5802
|
}
|
|
4801
5803
|
if (scanPath.suffix === "events") {
|
|
@@ -4803,7 +5805,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4803
5805
|
...event,
|
|
4804
5806
|
payload: parsePayload(event.payload)
|
|
4805
5807
|
}));
|
|
4806
|
-
|
|
5808
|
+
json2(res, 200, { scan: summarizeScan(scan), events });
|
|
4807
5809
|
return true;
|
|
4808
5810
|
}
|
|
4809
5811
|
if (scanPath.suffix === "findings") {
|
|
@@ -4813,7 +5815,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4813
5815
|
scanIds: [scanPath.scanId],
|
|
4814
5816
|
status: "running"
|
|
4815
5817
|
});
|
|
4816
|
-
|
|
5818
|
+
json2(res, 200, {
|
|
4817
5819
|
scan: summarizeScan(scan),
|
|
4818
5820
|
findings,
|
|
4819
5821
|
groups: groupFindings(
|
|
@@ -4824,7 +5826,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4824
5826
|
});
|
|
4825
5827
|
return true;
|
|
4826
5828
|
}
|
|
4827
|
-
|
|
5829
|
+
json2(res, 200, { scan: summarizeScan(scan) });
|
|
4828
5830
|
} finally {
|
|
4829
5831
|
db.close();
|
|
4830
5832
|
}
|
|
@@ -4844,7 +5846,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4844
5846
|
normalizeTriageStatus(body.triageStatus),
|
|
4845
5847
|
typeof body.triageNote === "string" ? body.triageNote : void 0
|
|
4846
5848
|
);
|
|
4847
|
-
|
|
5849
|
+
json2(res, 200, { ok: true });
|
|
4848
5850
|
return true;
|
|
4849
5851
|
}
|
|
4850
5852
|
if (req.method === "POST" && familyPath.action === "workflow") {
|
|
@@ -4857,12 +5859,12 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4857
5859
|
normalizeWorkflowStatus(body.workflowStatus),
|
|
4858
5860
|
typeof body.workflowAssignee === "string" ? body.workflowAssignee.trim() : null
|
|
4859
5861
|
);
|
|
4860
|
-
|
|
5862
|
+
json2(res, 200, { ok: true });
|
|
4861
5863
|
return true;
|
|
4862
5864
|
}
|
|
4863
5865
|
const rows = db.getRelatedFindings(familyPath.fingerprint).map(normalizeFinding);
|
|
4864
5866
|
if (rows.length === 0) {
|
|
4865
|
-
|
|
5867
|
+
json2(res, 404, { error: "Not found" });
|
|
4866
5868
|
return true;
|
|
4867
5869
|
}
|
|
4868
5870
|
const verdicts = db.listVerdicts(rows.map((row) => row.id));
|
|
@@ -4904,7 +5906,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4904
5906
|
sessions,
|
|
4905
5907
|
events: allEvents
|
|
4906
5908
|
});
|
|
4907
|
-
|
|
5909
|
+
json2(res, 200, {
|
|
4908
5910
|
consoleCommand: buildFindingConsoleCommand(rows[0], dbPath),
|
|
4909
5911
|
fingerprint: familyPath.fingerprint,
|
|
4910
5912
|
case: familyCase,
|
|
@@ -4972,7 +5974,7 @@ function registerDashboardCommand(program) {
|
|
|
4972
5974
|
const timeCapMs = opts.engineTimeCap === void 0 ? 6e5 : Number(opts.engineTimeCap);
|
|
4973
5975
|
const costCapUsd = opts.engineCostCap === void 0 ? 5 : Number(opts.engineCostCap);
|
|
4974
5976
|
if (!Number.isInteger(timeCapMs) || timeCapMs < 1 || timeCapMs > 864e5 || !Number.isFinite(costCapUsd) || costCapUsd <= 0 || costCapUsd > 1e3) throw new Error("Engine limits must be positive, with time at most 86400000 ms and cost at most $1000.");
|
|
4975
|
-
const controlToken =
|
|
5977
|
+
const controlToken = randomUUID9();
|
|
4976
5978
|
let teamAuth = new TeamAuth({ configPath: teamConfigPath, origin });
|
|
4977
5979
|
let teamDirectory = teamAuth.enabled ? join7(homeStateDir(), "teams", createHash8("sha256").update(teamAuth.workspaceId).digest("hex")) : void 0;
|
|
4978
5980
|
if (teamDirectory) {
|
|
@@ -4982,9 +5984,51 @@ function registerDashboardCommand(program) {
|
|
|
4982
5984
|
}
|
|
4983
5985
|
let collaboration = teamAuth.enabled ? new CollaborationService({ workspaceId: teamAuth.workspaceId, stateDir: join7(teamDirectory, "collaboration"), resolveSession: (sessionId) => teamAuth.resolveSessionId(sessionId) }) : void 0;
|
|
4984
5986
|
assertLocalEngineAvailable(opts.dbPath);
|
|
4985
|
-
const capabilities = ["sessions", "workflows", "schedules", "approvals", "events", "workspaces", "artifacts", "model-connections", "operator-services", "process-controls", "learning", "workflow-engine"];
|
|
5987
|
+
const capabilities = ["sessions", "workflows", "schedules", "approvals", "events", "workspaces", "artifacts", "model-connections", "provider-relay", "operator-services", "process-controls", "learning", "workflow-engine"];
|
|
4986
5988
|
let localHandshake = createBackendHandshake(opts.dbPath, capabilities);
|
|
4987
5989
|
let backends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake });
|
|
5990
|
+
let modelBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake });
|
|
5991
|
+
const modelRegistry = {
|
|
5992
|
+
handshake: (id2) => {
|
|
5993
|
+
modelBackends.reload();
|
|
5994
|
+
return modelBackends.handshake(id2);
|
|
5995
|
+
},
|
|
5996
|
+
request: (...args) => {
|
|
5997
|
+
modelBackends.reload();
|
|
5998
|
+
return modelBackends.request(...args);
|
|
5999
|
+
}
|
|
6000
|
+
};
|
|
6001
|
+
let localProviderSessions = new LocalProviderSessions({ registry: modelRegistry });
|
|
6002
|
+
const hostRepairs = /* @__PURE__ */ new Map();
|
|
6003
|
+
const hostRepairAbort = new AbortController();
|
|
6004
|
+
const repairManagedHost = (backendId, signal) => {
|
|
6005
|
+
const pending = hostRepairs.get(backendId);
|
|
6006
|
+
if (pending) return pending;
|
|
6007
|
+
const compiled = import.meta.url.startsWith("file:///$bunfs/") || import.meta.url.startsWith("/$bunfs/");
|
|
6008
|
+
const args = [...compiled ? [] : [resolveCliEntrypoint()], "worker", "repair", backendId];
|
|
6009
|
+
const task = new Promise((resolveRepair, reject) => {
|
|
6010
|
+
execFile2(process.execPath, args, { timeout: 75e3, maxBuffer: 65536, signal: signal ? AbortSignal.any([signal, hostRepairAbort.signal]) : hostRepairAbort.signal }, (error2, stdout) => {
|
|
6011
|
+
if (error2) {
|
|
6012
|
+
reject(new WebRequestError("This host could not reconnect. Check its SSH connection and managed engine installation.", 409));
|
|
6013
|
+
return;
|
|
6014
|
+
}
|
|
6015
|
+
try {
|
|
6016
|
+
resolveRepair(JSON.parse(stdout));
|
|
6017
|
+
} catch {
|
|
6018
|
+
reject(new WebRequestError("Host reconnect returned an invalid result.", 502));
|
|
6019
|
+
}
|
|
6020
|
+
});
|
|
6021
|
+
}).finally(() => {
|
|
6022
|
+
hostRepairs.delete(backendId);
|
|
6023
|
+
});
|
|
6024
|
+
hostRepairs.set(backendId, task);
|
|
6025
|
+
return task;
|
|
6026
|
+
};
|
|
6027
|
+
let stopHostReconnect;
|
|
6028
|
+
const watchManagedHosts = () => startManagedHostReconnect({ getRegistry: () => backends, repair: repairManagedHost, onRecovered: async (id2) => {
|
|
6029
|
+
backends.reload();
|
|
6030
|
+
await backends.handshake(id2);
|
|
6031
|
+
} });
|
|
4988
6032
|
let engine;
|
|
4989
6033
|
let startupGateway;
|
|
4990
6034
|
let startupOperator;
|
|
@@ -4995,8 +6039,9 @@ function registerDashboardCommand(program) {
|
|
|
4995
6039
|
try {
|
|
4996
6040
|
const { path: assetDir, cleanup: cleanupAssetDir } = resolveDashboardAssetDir(opts.assetDir);
|
|
4997
6041
|
startupAssetCleanup = cleanupAssetDir;
|
|
4998
|
-
const buildServices = (auth, dbPath = opts.dbPath, stateDir = teamDirectory) => createWorkspaceServices({ auth, dbPath, stateDir, workspace: opts.engineWorkspace, scopePath: opts.engineScope, target: opts.engineTarget, allowApply: opts.engineAllowApply, controlToken, engineBearer, timeCapMs, costCapUsd });
|
|
6042
|
+
const buildServices = (auth, dbPath = opts.dbPath, stateDir = teamDirectory, relayEpoch = localHandshake.serverInstanceId) => createWorkspaceServices({ relayEpoch, auth, dbPath, stateDir, workspace: opts.engineWorkspace, scopePath: opts.engineScope, target: opts.engineTarget, allowApply: opts.engineAllowApply, controlToken, engineBearer, timeCapMs, costCapUsd });
|
|
4999
6043
|
let services = await buildServices(teamAuth);
|
|
6044
|
+
stopHostReconnect = watchManagedHosts();
|
|
5000
6045
|
let { skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = services;
|
|
5001
6046
|
engine = services.engine;
|
|
5002
6047
|
startupGateway = consoleGateway;
|
|
@@ -5015,11 +6060,11 @@ function registerDashboardCommand(program) {
|
|
|
5015
6060
|
currentAuth: () => teamAuth,
|
|
5016
6061
|
activate: async (prepared) => {
|
|
5017
6062
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5018
|
-
const
|
|
6063
|
+
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
6064
|
+
const next = await buildServices(prepared.auth, prepared.dbPath, prepared.stateDir, nextHandshake.serverInstanceId);
|
|
5019
6065
|
let nextBackends;
|
|
5020
6066
|
let nextUnregister;
|
|
5021
6067
|
try {
|
|
5022
|
-
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
5023
6068
|
nextBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
5024
6069
|
const nextAccess = new FindingsAccessTokenStore({ workspaceId: prepared.workspaceId, stateDir: join7(prepared.stateDir, "findings-api-access") });
|
|
5025
6070
|
const nextRead = new FindingsReadApi({ workspaceId: prepared.workspaceId, dbPath: prepared.dbPath });
|
|
@@ -5028,6 +6073,9 @@ function registerDashboardCommand(program) {
|
|
|
5028
6073
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5029
6074
|
await services.dispose();
|
|
5030
6075
|
for (const response of activeResponses) if (response !== setupResponse) response.end();
|
|
6076
|
+
stopHostReconnect?.();
|
|
6077
|
+
await localProviderSessions.dispose();
|
|
6078
|
+
modelBackends.dispose();
|
|
5031
6079
|
backends.dispose();
|
|
5032
6080
|
unregisterLocalEngine?.();
|
|
5033
6081
|
services = next;
|
|
@@ -5037,6 +6085,8 @@ function registerDashboardCommand(program) {
|
|
|
5037
6085
|
collaboration = nextCollaboration;
|
|
5038
6086
|
localHandshake = nextHandshake;
|
|
5039
6087
|
backends = nextBackends;
|
|
6088
|
+
modelBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
6089
|
+
localProviderSessions = new LocalProviderSessions({ registry: modelRegistry });
|
|
5040
6090
|
unregisterLocalEngine = nextUnregister;
|
|
5041
6091
|
({ skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = next);
|
|
5042
6092
|
engine = next.engine;
|
|
@@ -5047,6 +6097,7 @@ function registerDashboardCommand(program) {
|
|
|
5047
6097
|
startupWorkflows = workflows;
|
|
5048
6098
|
startupTriggers = triggers;
|
|
5049
6099
|
startupGitHub = github;
|
|
6100
|
+
stopHostReconnect = watchManagedHosts();
|
|
5050
6101
|
} catch (cause) {
|
|
5051
6102
|
nextBackends?.dispose();
|
|
5052
6103
|
nextUnregister?.();
|
|
@@ -5064,12 +6115,12 @@ function registerDashboardCommand(program) {
|
|
|
5064
6115
|
try {
|
|
5065
6116
|
authorizeWebRequest(req, origin);
|
|
5066
6117
|
} catch (error2) {
|
|
5067
|
-
|
|
6118
|
+
json2(res, error2 instanceof WebRequestError ? error2.statusCode : 403, { error: error2 instanceof Error ? error2.message : "Invalid request origin." });
|
|
5068
6119
|
return;
|
|
5069
6120
|
}
|
|
5070
6121
|
let requestUrl = new URL2(req.url ?? "/", origin);
|
|
5071
6122
|
const requestId = BackendRequestIdSchema.safeParse(req.headers["x-0-request-id"]);
|
|
5072
|
-
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data :
|
|
6123
|
+
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data : randomUUID9());
|
|
5073
6124
|
try {
|
|
5074
6125
|
if (requestUrl.pathname.startsWith("/api/")) {
|
|
5075
6126
|
const findingsCredential = findingsAccess.authenticate(req, requestUrl);
|
|
@@ -5078,7 +6129,7 @@ function registerDashboardCommand(program) {
|
|
|
5078
6129
|
if (!findingsCredential) throw new FindingsAccessError("A findings API key is required in the Authorization header.", 401);
|
|
5079
6130
|
const reply = handleFindingsReadRequest(req, requestUrl, findingsRead);
|
|
5080
6131
|
if (!reply) throw new FindingsAccessError("Findings API route was not found.", 404);
|
|
5081
|
-
|
|
6132
|
+
json2(res, reply.status, reply.data);
|
|
5082
6133
|
return;
|
|
5083
6134
|
}
|
|
5084
6135
|
const ssoNavigation = requestUrl.pathname === "/api/team/auth/sso" || requestUrl.pathname === "/api/team/auth/callback";
|
|
@@ -5089,7 +6140,7 @@ function registerDashboardCommand(program) {
|
|
|
5089
6140
|
const reply = await handleTeamSetupRequest(req, requestUrl, setup, () => readJson(req));
|
|
5090
6141
|
if (reply) {
|
|
5091
6142
|
for (const [name, value] of Object.entries(reply.headers ?? {})) res.setHeader(name, value);
|
|
5092
|
-
|
|
6143
|
+
json2(res, reply.status, reply.data);
|
|
5093
6144
|
return;
|
|
5094
6145
|
}
|
|
5095
6146
|
} finally {
|
|
@@ -5098,12 +6149,12 @@ function registerDashboardCommand(program) {
|
|
|
5098
6149
|
}
|
|
5099
6150
|
if (setup.status().pending && requestUrl.pathname !== "/api/team/session") throw new WebRequestError("Workspace setup is in progress. Try again shortly.", 503);
|
|
5100
6151
|
if (handleTeamPresenceStream(req, res, requestUrl, teamAuth, collaboration)) return;
|
|
5101
|
-
const teamReply = await handleTeamRequest(req, requestUrl, teamAuth, collaboration, consoleGateway, () => readJson(req), async (kind,
|
|
6152
|
+
const teamReply = await handleTeamRequest(req, requestUrl, teamAuth, collaboration, consoleGateway, () => readJson(req), async (kind, id2) => {
|
|
5102
6153
|
if (kind === "report") {
|
|
5103
|
-
engagements.get(
|
|
6154
|
+
engagements.get(id2);
|
|
5104
6155
|
return;
|
|
5105
6156
|
}
|
|
5106
|
-
const definition = await workflows.handle(`/api/console/workflow-definitions/${encodeURIComponent(
|
|
6157
|
+
const definition = await workflows.handle(`/api/console/workflow-definitions/${encodeURIComponent(id2)}`, "GET", void 0, new URLSearchParams());
|
|
5107
6158
|
if (!definition || definition.status !== 200) throw new WebRequestError("Workflow was not found.", 404);
|
|
5108
6159
|
});
|
|
5109
6160
|
if (teamReply) {
|
|
@@ -5111,7 +6162,7 @@ function registerDashboardCommand(program) {
|
|
|
5111
6162
|
if (teamReply.redirect) {
|
|
5112
6163
|
res.writeHead(teamReply.status, { Location: teamReply.redirect, "Cache-Control": "no-store" });
|
|
5113
6164
|
res.end();
|
|
5114
|
-
} else
|
|
6165
|
+
} else json2(res, teamReply.status, teamReply.data);
|
|
5115
6166
|
return;
|
|
5116
6167
|
}
|
|
5117
6168
|
const bearer = req.headers.authorization;
|
|
@@ -5122,11 +6173,41 @@ function registerDashboardCommand(program) {
|
|
|
5122
6173
|
if (expectedEngineId !== void 0 && expectedEngineId !== localHandshake.engineId) throw new WebRequestError("Request belongs to a different engine identity.", 409);
|
|
5123
6174
|
const findingsAccessReply = await handleFindingsAccessTokenRequest(req, requestUrl, teamAuth, findingsAccess, () => readJson(req));
|
|
5124
6175
|
if (findingsAccessReply) {
|
|
5125
|
-
|
|
6176
|
+
json2(res, findingsAccessReply.status, findingsAccessReply.data);
|
|
6177
|
+
return;
|
|
6178
|
+
}
|
|
6179
|
+
const remoteSessionRoute = /^\/api\/console\/remote-sessions(?:\/([^/]+)\/(configuration|status))?$/.exec(requestUrl.pathname);
|
|
6180
|
+
if (remoteSessionRoute) {
|
|
6181
|
+
const ownerId = engineRequest ? "engine" : teamAuth.resolveSession(req)?.userId ?? "local";
|
|
6182
|
+
try {
|
|
6183
|
+
if (remoteSessionRoute[1] && remoteSessionRoute[2] === "status" && req.method === "GET") {
|
|
6184
|
+
const backendId = BackendDescriptorSchema.shape.id.parse(requestUrl.searchParams.get("backendId"));
|
|
6185
|
+
json2(res, 200, await localProviderSessions.status(backendId, decodeURIComponent(remoteSessionRoute[1]), ownerId));
|
|
6186
|
+
} else if (!remoteSessionRoute[1] && req.method === "POST") {
|
|
6187
|
+
const body = external_exports.object({ backendId: external_exports.string().min(1).max(128), configuration: external_exports.unknown() }).strict().parse(await readJson(req));
|
|
6188
|
+
json2(res, 201, await localProviderSessions.create(body.backendId, body.configuration, ownerId));
|
|
6189
|
+
} else if (remoteSessionRoute[1] && remoteSessionRoute[2] === "configuration" && req.method === "PATCH") {
|
|
6190
|
+
const body = external_exports.object({ backendId: external_exports.string().min(1).max(128), runtime: external_exports.unknown() }).strict().parse(await readJson(req));
|
|
6191
|
+
json2(res, 200, await localProviderSessions.configure(body.backendId, decodeURIComponent(remoteSessionRoute[1]), body.runtime, ownerId));
|
|
6192
|
+
} else throw new WebRequestError("Method not allowed.", 405);
|
|
6193
|
+
} catch (error2) {
|
|
6194
|
+
if (error2 instanceof LocalProviderSessionError) {
|
|
6195
|
+
json2(res, error2.statusCode, { error: error2.message });
|
|
6196
|
+
return;
|
|
6197
|
+
}
|
|
6198
|
+
throw error2;
|
|
6199
|
+
}
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
6202
|
+
if (requestUrl.pathname === "/api/console/provider-relays" || requestUrl.pathname.startsWith("/api/console/provider-relays/")) {
|
|
6203
|
+
if (!engineRequest) throw new WebRequestError("Provider relay transport requires an authenticated engine connection.", 403);
|
|
6204
|
+
if (await handleRemoteProviderRelayRequest(req, res, requestUrl, services.remoteProviderRelays, () => readJson(req))) return;
|
|
6205
|
+
}
|
|
6206
|
+
const backendRoute = await handleBackendConnectionRequest(req, res, requestUrl, backends, (backendId, sessionId) => localProviderSessions.release(backendId, sessionId, engineRequest ? "engine" : teamAuth.resolveSession(req)?.userId ?? "local"));
|
|
6207
|
+
if (backendRoute.handled) {
|
|
6208
|
+
modelBackends.reload();
|
|
5126
6209
|
return;
|
|
5127
6210
|
}
|
|
5128
|
-
const backendRoute = await handleBackendConnectionRequest(req, res, requestUrl, backends);
|
|
5129
|
-
if (backendRoute.handled) return;
|
|
5130
6211
|
if (backendRoute.localPath) requestUrl = new URL2(backendRoute.localPath, origin);
|
|
5131
6212
|
const skillsRoute = requestUrl.pathname.match(/^\/api\/skills(?:\/(.*))?$/);
|
|
5132
6213
|
if (skillsRoute) {
|
|
@@ -5140,21 +6221,21 @@ function registerDashboardCommand(program) {
|
|
|
5140
6221
|
throw new WebRequestError("Invalid skill identifier.", 400);
|
|
5141
6222
|
}
|
|
5142
6223
|
const method = req.method ?? "GET";
|
|
5143
|
-
if (!path && method === "GET")
|
|
5144
|
-
else if (!path && method === "POST")
|
|
5145
|
-
else if (path === "import" && method === "POST")
|
|
5146
|
-
else if (path === "mounts" && method === "POST")
|
|
6224
|
+
if (!path && method === "GET") json2(res, 200, skills.list(access));
|
|
6225
|
+
else if (!path && method === "POST") json2(res, 201, { skill: skills.create(await readJson(req)) });
|
|
6226
|
+
else if (path === "import" && method === "POST") json2(res, 201, { skill: skills.import(await readJson(req, 12 * 1024 * 1024)) });
|
|
6227
|
+
else if (path === "mounts" && method === "POST") json2(res, 201, { mount: skills.mount(await readJson(req)) });
|
|
5147
6228
|
else if (path.startsWith("mounts/") && method === "DELETE") {
|
|
5148
6229
|
skills.unmount(decodedPath.slice(7));
|
|
5149
|
-
|
|
5150
|
-
} else if (path.endsWith("/copy") && method === "POST")
|
|
6230
|
+
json2(res, 200, { ok: true });
|
|
6231
|
+
} else if (path.endsWith("/copy") && method === "POST") json2(res, 201, { skill: skills.copy(decodedPath.slice(0, -5), await readJson(req), access) });
|
|
5151
6232
|
else if (path.endsWith("/export") && method === "GET") {
|
|
5152
|
-
const
|
|
5153
|
-
const bundle = skills.export(
|
|
6233
|
+
const id2 = decodedPath.slice(0, -7);
|
|
6234
|
+
const bundle = skills.export(id2);
|
|
5154
6235
|
res.setHeader("Content-Disposition", `attachment; filename="${bundle.name}.skill.json"`);
|
|
5155
|
-
|
|
5156
|
-
} else if (path && method === "GET")
|
|
5157
|
-
else if (path && method === "PUT")
|
|
6236
|
+
json2(res, 200, bundle);
|
|
6237
|
+
} else if (path && method === "GET") json2(res, 200, { skill: skills.get(decodedPath, access) });
|
|
6238
|
+
else if (path && method === "PUT") json2(res, 200, { skill: skills.update(decodedPath, await readJson(req), access) });
|
|
5158
6239
|
else throw new WebRequestError("Method not allowed.", 405);
|
|
5159
6240
|
return;
|
|
5160
6241
|
}
|
|
@@ -5162,34 +6243,34 @@ function registerDashboardCommand(program) {
|
|
|
5162
6243
|
if (engagementRoute) {
|
|
5163
6244
|
const member = teamAuth.resolveSession(req);
|
|
5164
6245
|
const engagementActor = member ? { userId: member.userId, displayName: member.displayName } : void 0;
|
|
5165
|
-
const
|
|
6246
|
+
const id2 = engagementRoute[1] ? decodeURIComponent(engagementRoute[1]) : void 0;
|
|
5166
6247
|
if (engagementRoute[2]) {
|
|
5167
6248
|
if (req.method !== "GET") throw new WebRequestError("Use GET to read an engagement report.", 405);
|
|
5168
6249
|
const format = requestUrl.searchParams.get("format") ?? "json";
|
|
5169
6250
|
if (format !== "json" && format !== "markdown") throw new WebRequestError("Use json or markdown for an engagement report.", 400);
|
|
5170
|
-
const report = engagements.report(
|
|
6251
|
+
const report = engagements.report(id2);
|
|
5171
6252
|
if (format === "json") {
|
|
5172
|
-
res.setHeader("Content-Disposition", `attachment; filename="engagement-${
|
|
5173
|
-
|
|
6253
|
+
res.setHeader("Content-Disposition", `attachment; filename="engagement-${id2}.json"`);
|
|
6254
|
+
json2(res, 200, report);
|
|
5174
6255
|
} else {
|
|
5175
|
-
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="engagement-${
|
|
6256
|
+
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="engagement-${id2}.md"`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
5176
6257
|
res.end(engagementMarkdown(report));
|
|
5177
6258
|
}
|
|
5178
|
-
} else if (req.method === "GET")
|
|
5179
|
-
else if (req.method === "POST" && !
|
|
6259
|
+
} else if (req.method === "GET") json2(res, 200, id2 ? { engagement: engagements.get(id2) } : { engagements: engagements.list() });
|
|
6260
|
+
else if (req.method === "POST" && !id2) {
|
|
5180
6261
|
const engagement = engagements.create(await readJson(req), engagementActor);
|
|
5181
6262
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5182
|
-
|
|
5183
|
-
} else if (req.method === "PATCH" &&
|
|
5184
|
-
const engagement = engagements.update(
|
|
6263
|
+
json2(res, 201, { engagement });
|
|
6264
|
+
} else if (req.method === "PATCH" && id2) {
|
|
6265
|
+
const engagement = engagements.update(id2, await readJson(req), engagementActor);
|
|
5185
6266
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5186
|
-
|
|
6267
|
+
json2(res, 200, { engagement });
|
|
5187
6268
|
} else throw new WebRequestError("Method not allowed.", 405);
|
|
5188
6269
|
return;
|
|
5189
6270
|
}
|
|
5190
6271
|
if (requestUrl.pathname === "/api/workers/hosts") {
|
|
5191
6272
|
if (req.method !== "GET") throw new WebRequestError("Method not allowed.", 405);
|
|
5192
|
-
|
|
6273
|
+
json2(res, 200, { hosts: discoverSshHosts() });
|
|
5193
6274
|
return;
|
|
5194
6275
|
}
|
|
5195
6276
|
if (requestUrl.pathname === "/api/workers/managed") {
|
|
@@ -5202,7 +6283,15 @@ function registerDashboardCommand(program) {
|
|
|
5202
6283
|
return [];
|
|
5203
6284
|
}
|
|
5204
6285
|
});
|
|
5205
|
-
|
|
6286
|
+
json2(res, 200, { workers });
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
if (requestUrl.pathname === "/api/control/repair-worker") {
|
|
6290
|
+
if (req.method !== "POST") throw new WebRequestError("Use POST to reconnect a host.", 405);
|
|
6291
|
+
const body = external_exports.object({ backendId: BackendDescriptorSchema.shape.id }).strict().parse(await readJson(req));
|
|
6292
|
+
const repaired = await repairManagedHost(body.backendId);
|
|
6293
|
+
backends.reload();
|
|
6294
|
+
json2(res, 200, repaired);
|
|
5206
6295
|
return;
|
|
5207
6296
|
}
|
|
5208
6297
|
const workerStop = requestUrl.pathname.match(/^\/api\/workers\/([^/]+)\/stop$/);
|
|
@@ -5224,7 +6313,7 @@ function registerDashboardCommand(program) {
|
|
|
5224
6313
|
}
|
|
5225
6314
|
});
|
|
5226
6315
|
});
|
|
5227
|
-
|
|
6316
|
+
json2(res, 200, result);
|
|
5228
6317
|
return;
|
|
5229
6318
|
}
|
|
5230
6319
|
if (requestUrl.pathname === "/api/workers/deploy") {
|
|
@@ -5257,7 +6346,7 @@ function registerDashboardCommand(program) {
|
|
|
5257
6346
|
});
|
|
5258
6347
|
});
|
|
5259
6348
|
backends.reload();
|
|
5260
|
-
|
|
6349
|
+
json2(res, 202, result);
|
|
5261
6350
|
return;
|
|
5262
6351
|
}
|
|
5263
6352
|
if (requestUrl.pathname === "/api/control/launch-run") {
|
|
@@ -5267,15 +6356,15 @@ function registerDashboardCommand(program) {
|
|
|
5267
6356
|
if (body.ensureDaemon) throw new WebRequestError("Engine runs do not require a separate orchestration daemon. Configure a workflow schedule for recurring work.", 400);
|
|
5268
6357
|
const session = await engine.invoke("create_session", { config: { target: body.target, title: `Assessment: ${body.target}` } });
|
|
5269
6358
|
const execution = await engine.invoke("start_assessment", { sessionId: session.id, target: body.target, plan: { goal: "unknown-vulnerabilities", depth: body.depth ?? "default", runCount: 1, executionMode: "sequential", timeCapMs, costCapUsd } });
|
|
5270
|
-
|
|
6359
|
+
json2(res, 202, { ok: true, engineOwned: true, sessionId: session.id, runId: execution.id, jobId: execution.jobId });
|
|
5271
6360
|
return;
|
|
5272
6361
|
}
|
|
5273
6362
|
if (requestUrl.pathname === "/api/workflow-engine/call") {
|
|
5274
6363
|
if (!engine) throw new WebRequestError("This engine does not admit persistent workflow calls.", 403);
|
|
5275
6364
|
if (req.method !== "POST") throw new WebRequestError("Method not allowed.", 405);
|
|
5276
|
-
const input = external_exports.object({ name: external_exports.string().min(1).max(128), args: external_exports.record(external_exports.unknown()) }).strict().parse(await readJson(req));
|
|
6365
|
+
const input = external_exports.object({ name: external_exports.string().min(1).max(128), args: external_exports.record(external_exports.string(), external_exports.unknown()) }).strict().parse(await readJson(req));
|
|
5277
6366
|
if (!engineRequest && typeof input.args.sessionId === "string") assertTeamConsoleMutation(req, input.args.sessionId, teamAuth, collaboration, consoleGateway);
|
|
5278
|
-
|
|
6367
|
+
json2(res, 200, await engine.invoke(input.name, input.args));
|
|
5279
6368
|
return;
|
|
5280
6369
|
}
|
|
5281
6370
|
const resumeScan = requestUrl.pathname.match(/^\/api\/scans\/([^/]+)\/resume$/);
|
|
@@ -5283,14 +6372,14 @@ function registerDashboardCommand(program) {
|
|
|
5283
6372
|
if (req.method !== "POST") throw new WebRequestError("Use POST to resume a scan.", 405);
|
|
5284
6373
|
const request = ScanResumeRequestSchema.parse(await readJson(req));
|
|
5285
6374
|
const result = await workflows.resumeScan(decodeURIComponent(resumeScan[1]), { ...request, timeCapMs: Math.min(request.timeCapMs ?? timeCapMs, timeCapMs), costCapUsd: Math.min(request.costCapUsd ?? costCapUsd, costCapUsd) });
|
|
5286
|
-
|
|
6375
|
+
json2(res, result.status, result.data);
|
|
5287
6376
|
return;
|
|
5288
6377
|
}
|
|
5289
6378
|
if (/^\/api\/findings\/[^/]+\/impact-assessment$/.test(requestUrl.pathname)) {
|
|
5290
6379
|
const input = req.method === "POST" || req.method === "DELETE" ? await readJson(req) : void 0;
|
|
5291
6380
|
const result = handleFindingImpactRequest(requestUrl.pathname, req.method ?? "GET", input, { dbPath: opts.dbPath });
|
|
5292
6381
|
if (result) {
|
|
5293
|
-
|
|
6382
|
+
json2(res, result.status, result.data);
|
|
5294
6383
|
return;
|
|
5295
6384
|
}
|
|
5296
6385
|
}
|
|
@@ -5309,7 +6398,7 @@ function registerDashboardCommand(program) {
|
|
|
5309
6398
|
const consoleHandled = await handleWebConsoleApiRequest(req, res, requestUrl, consoleGateway, operator, workflows, triggers, github, engineRequest ? void 0 : { auth: teamAuth, collaboration });
|
|
5310
6399
|
if (consoleHandled) return;
|
|
5311
6400
|
const handled = await handleApiRequest(req, res, requestUrl.pathname, opts.dbPath, controlToken, engineBearer);
|
|
5312
|
-
if (!handled)
|
|
6401
|
+
if (!handled) json2(res, 404, { error: "Not found" });
|
|
5313
6402
|
return;
|
|
5314
6403
|
}
|
|
5315
6404
|
if (devUrl) {
|
|
@@ -5330,12 +6419,12 @@ function registerDashboardCommand(program) {
|
|
|
5330
6419
|
return;
|
|
5331
6420
|
}
|
|
5332
6421
|
if (extname(requestUrl.pathname)) {
|
|
5333
|
-
|
|
6422
|
+
json2(res, 404, { error: "Asset not found" });
|
|
5334
6423
|
return;
|
|
5335
6424
|
}
|
|
5336
6425
|
sendFile(res, join7(assetDir, "index.html"), controlToken);
|
|
5337
6426
|
} catch (err) {
|
|
5338
|
-
|
|
6427
|
+
json2(res, errorStatusCode(err), { error: err instanceof Error ? err.message : "Web application request failed." });
|
|
5339
6428
|
}
|
|
5340
6429
|
});
|
|
5341
6430
|
let dashboardAssetsCleaned = false;
|
|
@@ -5383,19 +6472,31 @@ function registerDashboardCommand(program) {
|
|
|
5383
6472
|
const shutdown = () => {
|
|
5384
6473
|
if (shuttingDown) return;
|
|
5385
6474
|
shuttingDown = true;
|
|
5386
|
-
|
|
5387
|
-
|
|
6475
|
+
stopHostReconnect?.();
|
|
6476
|
+
hostRepairAbort.abort();
|
|
6477
|
+
services.remoteProviderRelays.dispose();
|
|
6478
|
+
void localProviderSessions.dispose().then(() => {
|
|
6479
|
+
modelBackends.dispose();
|
|
6480
|
+
backends.dispose();
|
|
6481
|
+
return engine?.dispose();
|
|
6482
|
+
}).then(() => triggers.dispose()).then(() => consoleGateway.closeAll()).then(() => workflows.dispose()).finally(() => {
|
|
5388
6483
|
operator.dispose();
|
|
5389
6484
|
github.dispose();
|
|
6485
|
+
for (const response of activeResponses) response.end();
|
|
5390
6486
|
server.close(() => {
|
|
5391
6487
|
cleanupDashboardAssets();
|
|
5392
6488
|
process.exit(0);
|
|
5393
6489
|
});
|
|
6490
|
+
server.closeAllConnections();
|
|
5394
6491
|
});
|
|
5395
6492
|
};
|
|
5396
6493
|
process.once("SIGINT", shutdown);
|
|
5397
6494
|
process.once("SIGTERM", shutdown);
|
|
5398
6495
|
} catch (error2) {
|
|
6496
|
+
stopHostReconnect?.();
|
|
6497
|
+
hostRepairAbort.abort();
|
|
6498
|
+
await localProviderSessions.dispose();
|
|
6499
|
+
modelBackends.dispose();
|
|
5399
6500
|
backends.dispose();
|
|
5400
6501
|
await startupTriggers?.dispose().catch(() => {
|
|
5401
6502
|
});
|