0sec-cli 0.24.0 → 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-TEJPXWFG.js → adapt-loop-L2PXCD4J.js} +6 -5
- package/chunks/{artifact-scraper-AJZ3LOQY.js → artifact-scraper-W3LGLXBM.js} +4 -3
- package/chunks/{assumption-mining-EATDC4BB.js → assumption-mining-7IJ6ZWZE.js} +22 -20
- package/chunks/{backend-connections-QE4NASAK.js → backend-connections-MYH4QRQG.js} +5 -4
- package/chunks/{chunk-RBOMXUYL.js → chunk-27SXCYWH.js} +1 -1
- package/chunks/{chunk-E42XEJTK.js → chunk-2J3STRJT.js} +337 -257
- package/chunks/chunk-36J5ZLFE.js +18729 -0
- package/chunks/chunk-3DF55BWV.js +49 -0
- package/chunks/{chunk-GHEBZKUS.js → chunk-3HMO42M2.js} +1426 -321
- package/chunks/{chunk-BIJPMYFA.js → chunk-3T5R7P4T.js} +6 -6
- package/chunks/{chunk-RD4FKFWR.js → chunk-5H2PXZ7Q.js} +1 -1
- package/chunks/chunk-5XOYTAOK.js +1546 -0
- package/chunks/{chunk-QEA2VHBD.js → chunk-6TA3CWND.js} +2656 -2960
- package/chunks/{chunk-H76JYUPE.js → chunk-76CLUI54.js} +3 -3
- package/chunks/{chunk-KCA35IL3.js → chunk-7PUJI64A.js} +11 -9
- package/chunks/{chunk-W6IYTSZH.js → chunk-7TIWL6E5.js} +8 -10
- package/chunks/chunk-ABKUI3QB.js +196 -0
- package/chunks/{chunk-YORL5S5V.js → chunk-ACYZMJHC.js} +1 -1
- package/chunks/{chunk-5FHFG5P2.js → chunk-AL32F74R.js} +1 -1
- package/chunks/{chunk-J6G73WYF.js → chunk-B5TIXHCO.js} +1 -1
- package/chunks/{chunk-G2EJ3R5I.js → chunk-BWUW6ZQR.js} +2052 -1075
- package/chunks/{chunk-3XLEDCFX.js → chunk-C6ZMPXN2.js} +5 -50
- package/chunks/{chunk-4TUR47H5.js → chunk-D4IBEO2K.js} +1 -1
- package/chunks/{chunk-EXTN4SQV.js → chunk-DHNYJ2ML.js} +1 -1
- package/chunks/{chunk-Y7KV7WV6.js → chunk-DJGJECBV.js} +93 -20
- package/chunks/{chunk-UHUW45S3.js → chunk-DMDWIW3E.js} +5 -5
- package/chunks/{chunk-V5I5GAQH.js → chunk-EEP6GBUO.js} +16037 -34417
- package/chunks/{chunk-EYBMTELO.js → chunk-EIONVEFN.js} +208 -29
- package/chunks/chunk-EQBC2ZWL.js +2481 -0
- package/chunks/{chunk-FKGFHLLS.js → chunk-FI2H6HV2.js} +1 -1
- package/chunks/{chunk-4YRXIVPZ.js → chunk-FI5YL4R2.js} +3 -3
- package/chunks/{chunk-DNV3ZMPX.js → chunk-FOLTHXRH.js} +1 -1
- package/chunks/{chunk-ZPFBQ3SD.js → chunk-GOUGG7IQ.js} +3 -3
- package/chunks/{chunk-O3ADFN2A.js → chunk-GXSECFUE.js} +1 -1
- package/chunks/{chunk-YYOQADBB.js → chunk-HDJSHLRM.js} +3 -2
- package/chunks/{chunk-A3EKBUEM.js → chunk-HFUOUT2B.js} +61232 -30665
- package/chunks/chunk-HJKJ4NQP.js +196 -0
- package/chunks/{chunk-RPTO5FY2.js → chunk-I4VIAQHM.js} +1 -1
- package/chunks/{chunk-A6AZZ6XX.js → chunk-ITEMDJAB.js} +3 -3
- package/chunks/chunk-ITHDAIRY.js +57 -0
- package/chunks/{chunk-TOFIGQ63.js → chunk-JCRQV4FE.js} +2 -2
- package/chunks/{chunk-SQCBHFWN.js → chunk-LWMEJYCP.js} +2 -2
- package/chunks/{chunk-52AWIBCT.js → chunk-MY44EZKD.js} +1 -1
- package/chunks/{chunk-SGILXLR3.js → chunk-NBLKIQCW.js} +226 -4
- package/chunks/chunk-NO543ZBU.js +90 -0
- package/chunks/{chunk-CIMN2AO6.js → chunk-OFPF26WL.js} +333 -3
- package/chunks/{chunk-YERUDEXT.js → chunk-PAE7ZAV6.js} +2780 -1255
- package/chunks/{chunk-V4BD7GSP.js → chunk-PMAKX6KP.js} +2 -2
- package/chunks/{chunk-62PGWN7Y.js → chunk-RBTJYREN.js} +8 -171
- package/chunks/{chunk-IK27KMVV.js → chunk-RFK5F45W.js} +1 -1
- package/chunks/{chunk-554D4AYM.js → chunk-S2WCYEK2.js} +4 -99
- package/chunks/{chunk-RZR2XXAK.js → chunk-S3B6HFSM.js} +144 -14
- package/chunks/{chunk-WUPN3D37.js → chunk-S4DFH2FJ.js} +26 -1
- package/chunks/{chunk-TZRHWP7R.js → chunk-SEWZ76MN.js} +77 -13
- package/chunks/{chunk-HRBOZBMC.js → chunk-TFKY6D7O.js} +1 -1
- package/chunks/{chunk-TOAAYJVN.js → chunk-WK6VUCKK.js} +3 -333
- package/chunks/{chunk-IXT64DV5.js → chunk-XPDJE537.js} +97 -1
- package/chunks/{chunk-RCHAFSWS.js → chunk-YXTV2FRE.js} +113 -22
- package/chunks/{chunk-TTDFBFNZ.js → chunk-YYFODVZG.js} +6 -10
- package/chunks/{chunk-3XLFB46W.js → chunk-ZWAWZU6C.js} +30 -13
- package/chunks/{chunk-5POT5OYF.js → chunk-ZZV56ACK.js} +11 -9
- package/chunks/{codex-models-FQPCWRPC.js → codex-models-KCUIVS3R.js} +9 -8
- package/chunks/{commands-I5L3VD2A.js → commands-MPPAADGY.js} +798 -608
- package/chunks/dashboard-MN7GZLLX.js +75 -0
- package/chunks/{db-P5EIKMLP.js → db-R2AQSESC.js} +5 -4
- package/chunks/{deep-review-I7RD5QSS.js → deep-review-Q6BJKEV4.js} +37 -38
- package/chunks/{disclose-PFBWESVJ.js → disclose-GGULOW27.js} +6 -5
- package/chunks/{dist-AJU25UXA.js → dist-6RNAVFAY.js} +376 -333
- package/chunks/{dist-VK2M4HNW.js → dist-DWN7U5OV.js} +5 -2
- package/chunks/{dist-M2X367VW.js → dist-JAZWL3SC.js} +3 -2
- package/chunks/{dist-3SWWY5FZ.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-A4RTIJ4J.js → exploit-agent-M3KUMZJH.js} +1 -1
- package/chunks/{exploit-autoclimb-ABMMBC3M.js → exploit-autoclimb-QTTWC5WM.js} +1 -1
- package/chunks/{exploit-climb-BQ2SVU63.js → exploit-climb-E4KDELTN.js} +4 -3
- package/chunks/{fix-4YCLHFDT.js → fix-U5FFJD4F.js} +9 -8
- package/chunks/{harness-DHWULEIZ.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-SF4NALVR.js → kernel-primitive-CVG2E7MJ.js} +4 -3
- package/chunks/{kernel-vm-runner-WEBEZEIC.js → kernel-vm-runner-62KISULQ.js} +4 -3
- package/chunks/{local-engine-XUG7XRKM.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-ZNAELJGB.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-SODWQN6O.js → prepare-7DTS6RJT.js} +7 -6
- package/chunks/{remote-workflow-runtime-XNNGI2PG.js → remote-workflow-runtime-6CEVRZXS.js} +4 -3
- package/chunks/routing-ITJZBJIU.js +61 -0
- package/chunks/{runtime-MB4GEFBY.js → runtime-PZJ77QES.js} +9 -8
- package/chunks/{runtime-HFFRUOPL.js → runtime-W6JNDQRO.js} +4 -3
- package/chunks/{scan-stream-GXBJHCDF.js → scan-stream-6VOJTHTR.js} +6 -4
- package/chunks/{scope-M7L3CD6X.js → scope-FHXW6EX6.js} +4 -3
- package/chunks/service-plugins-CN6Q2NSN.js +63 -0
- package/chunks/{session-store-K7I4GLNH.js → session-store-DBBI544Y.js} +8 -5
- package/chunks/{variant-candidates-2KSP5EHH.js → variant-candidates-DPR6DJ3N.js} +9 -8
- package/chunks/{web-recon-prepass-RAXXTEWH.js → web-recon-prepass-HWDHDQMN.js} +7 -6
- package/chunks/{workflow-runtime-V26HIJO3.js → workflow-runtime-PLB32XC3.js} +50 -49
- package/dashboard/assets/{abnfDiagram-VCTEODGH-CGCueOp5.js → abnfDiagram-VCTEODGH-6e39CQKz.js} +1 -1
- package/dashboard/assets/{arc-BjZaQcCc.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-C-byT12_.js → blockDiagram-I7D4REHJ-Dgh2n5Lf.js} +2 -2
- package/dashboard/assets/{c4Diagram-7LVT6UL2-DoYjz-Uv.js → c4Diagram-7LVT6UL2-CsorW08z.js} +1 -1
- package/dashboard/assets/channel-BBlDqIBl.js +1 -0
- package/dashboard/assets/{chunk-4HAMMTFA-NBNp2zdt.js → chunk-4HAMMTFA-4_b_LGGQ.js} +1 -1
- package/dashboard/assets/{chunk-75Z2AOVW-BHmkuP0B.js → chunk-75Z2AOVW-qzNlKGCp.js} +1 -1
- package/dashboard/assets/chunk-DU6HZSFF-SAPICW28.js +127 -0
- package/dashboard/assets/{chunk-F27PBJKO-3wwNFUgZ.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-Btg7huZW.js → chunk-GMAD6QVW-DNnr65Nb.js} +7 -7
- package/dashboard/assets/{chunk-GVQU2GXP-ihCURK5O.js → chunk-GVQU2GXP-DTwv0VfU.js} +1 -1
- package/dashboard/assets/{chunk-IMKFNOWR-FsXj7slg.js → chunk-IMKFNOWR-B1fk1q_W.js} +1 -1
- package/dashboard/assets/{chunk-L3NEJ4N5-DorFIxJY.js → chunk-L3NEJ4N5-ByVjFIox.js} +1 -1
- package/dashboard/assets/{chunk-OSK3NFVY-DKun_KfC.js → chunk-OSK3NFVY-DUDCzjbh.js} +1 -1
- package/dashboard/assets/{chunk-P2QGCYS3-XGgiU1Bz.js → chunk-P2QGCYS3-D_D5qEso.js} +1 -1
- package/dashboard/assets/{chunk-POPQ4Y6H-BTXEnGhJ.js → chunk-POPQ4Y6H-CetM-tVm.js} +1 -1
- package/dashboard/assets/{chunk-PWAF6VOD-DYZcGkTL.js → chunk-PWAF6VOD-BcJX4exl.js} +1 -1
- package/dashboard/assets/{chunk-SHT3W25Y-C4WQfwf8.js → chunk-SHT3W25Y-BWQql238.js} +1 -1
- package/dashboard/assets/{chunk-SVP7TREG-C4nCh0-1.js → chunk-SVP7TREG-BSLOD1JE.js} +1 -1
- package/dashboard/assets/{chunk-TICWLB2K-DSB-IiGc.js → chunk-TICWLB2K-DReOl-g_.js} +2 -2
- package/dashboard/assets/{chunk-XXDRQBXY-CS6fjUcj.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-CgDOOY67.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-CrKqRXZb.js → diagram-S7CK7UJ4-B_ts4_58.js} +2 -2
- package/dashboard/assets/{diagram-UQ7AKVKN-Cu6Es0-v.js → diagram-UQ7AKVKN-DwHbbxFy.js} +1 -1
- package/dashboard/assets/{diagram-VSXAHHWV-eK4d8FwS.js → diagram-VSXAHHWV-2Tg_RG89.js} +1 -1
- package/dashboard/assets/{diagram-VX7I27RA-bFxeFKOc.js → diagram-VX7I27RA-BFYp-YjF.js} +1 -1
- package/dashboard/assets/{diagram-Z3DM3KII-C0ClmnhC.js → diagram-Z3DM3KII-DrPiS4V0.js} +1 -1
- package/dashboard/assets/{dist-BZh8Kb3O.js → dist-cbVssBvx.js} +1 -1
- package/dashboard/assets/{ebnfDiagram-PWID7BFC-CUqxFPzf.js → ebnfDiagram-PWID7BFC-DCMOZtU2.js} +1 -1
- package/dashboard/assets/{erDiagram-RLTQ6QDP-CcYM7pLi.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-UmZfbMyD.js → ganttDiagram-EL5Y4UJY-C1AqcwIF.js} +1 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DYR76gtd.js +1 -0
- package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-CvzeCxVy.js → gitGraphDiagram-WWUBYQGX-B7hL98CE.js} +1 -1
- package/dashboard/assets/info-A6RAGUB7-TXB43LiH.js +1 -0
- package/dashboard/assets/{infoDiagram-27XIBGKW-DjkpNtVW.js → infoDiagram-27XIBGKW-Dk5dXmh_.js} +1 -1
- package/dashboard/assets/{ishikawaDiagram-5VMMS53U-8FhjJaWT.js → ishikawaDiagram-5VMMS53U-BQDuNMka.js} +2 -2
- package/dashboard/assets/{journeyDiagram-3NMN7TZE-DyNJ755f.js → journeyDiagram-3NMN7TZE-DkustddD.js} +1 -1
- package/dashboard/assets/{kanban-definition-UXKFOSKX-_jCmtaY1.js → kanban-definition-UXKFOSKX-BW7Ann84.js} +1 -1
- package/dashboard/assets/{katex-BdBmVSus.js → katex-nLxf_RlZ.js} +2 -2
- package/dashboard/assets/{line-Bf0nxyWf.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-B4yCwiN-.js → mermaid.core-DruW_5-Y.js} +9 -9
- package/dashboard/assets/{mindmap-definition-YA3MSWOX-D5DjWyXj.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-Bf6a1vaf.js → pegDiagram-XKGWAZYB-Cx4ABT6q.js} +1 -1
- package/dashboard/assets/pie-WAS4IAKB-CNdXyrze.js +1 -0
- package/dashboard/assets/{pieDiagram-E7YTZNPT-D0L1qbch.js → pieDiagram-E7YTZNPT-BilfQbIs.js} +1 -1
- package/dashboard/assets/{quadrantDiagram-AXDQQJYC-CbFyy2Pe.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-t7Gp9g2R.js → railroadDiagram-O6MQD6OU-DnBru-jf.js} +1 -1
- package/dashboard/assets/{requirementDiagram-BXWQKSXE-LYmqeBXi.js → requirementDiagram-BXWQKSXE-FwdvUZ7_.js} +1 -1
- package/dashboard/assets/{sankeyDiagram-P5KCCOFB-DnpfA_94.js → sankeyDiagram-P5KCCOFB-BQvDORai.js} +1 -1
- package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-C8s3pTUE.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-BMzm6fK1.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-BfLJMVeG.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-BYJRHEaL.js → vennDiagram-4TSXK5OY-D76RIZLz.js} +1 -1
- package/dashboard/assets/wardley-WFR3VGLG-Ba_reiVw.js +1 -0
- package/dashboard/assets/{wardleyDiagram-VM6X3IG4-DgPN4nSe.js → wardleyDiagram-VM6X3IG4-Cux4coOD.js} +1 -1
- package/dashboard/assets/{xychartDiagram-S5SC5T6Z-BhkAMAOT.js → xychartDiagram-S5SC5T6Z-Ch5mvnlH.js} +1 -1
- package/dashboard/index.html +2 -2
- package/package.json +2 -5
- package/chunks/chunk-AV5BF4V7.js +0 -4304
- package/chunks/chunk-CSPF47S3.js +0 -6027
- package/chunks/chunk-ESM447R6.js +0 -77
- package/chunks/chunk-FEOPHBMY.js +0 -56
- package/chunks/chunk-GNR3APR6.js +0 -295
- package/chunks/chunk-IIBNQTOK.js +0 -1532
- package/chunks/chunk-JSTLXPJS.js +0 -284
- package/chunks/chunk-NJLKFC66.js +0 -4790
- package/chunks/chunk-O7Q2G2KW.js +0 -117
- package/chunks/chunk-VIT5ALXF.js +0 -4054
- package/chunks/chunk-YJU7QACE.js +0 -279
- package/chunks/dashboard-B2AEGCFM.js +0 -76
- package/chunks/eval-runner-XVRX3R2Y.js +0 -31
- package/chunks/hunt-scan-DXV5H6YN.js +0 -40
- package/chunks/managed-operation-SQFMBWHG.js +0 -60
- package/chunks/orchestrate-YHFJXGID.js +0 -63
- package/chunks/run-5T4Z3Q2D.js +0 -28242
- package/chunks/service-plugins-BGS6QKSZ.js +0 -64
- package/dashboard/assets/architecture-7GRP2DOG-hcbO-9zX.js +0 -1
- package/dashboard/assets/architectureDiagram-5GKGNRK7-DReOXTSq.js +0 -36
- package/dashboard/assets/channel-CUURBnpZ.js +0 -1
- package/dashboard/assets/chunk-DU6HZSFF-DgfLNaL2.js +0 -127
- package/dashboard/assets/classDiagram-ZZMXUADV-CD1Buc26.js +0 -1
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CD1Buc26.js +0 -1
- package/dashboard/assets/common-BMdQBwfR.js +0 -5
- package/dashboard/assets/cose-bilkent-JH36ORCC-CUjQHTML.js +0 -1
- package/dashboard/assets/cynefin-OW5HDTMX-BC7zCtFq.js +0 -1
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +0 -321
- package/dashboard/assets/dagre-GXQ25YYZ-Dqc8FS9x.js +0 -4
- package/dashboard/assets/dagre-PrKaheQc.js +0 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-D8ZNnPkV.js +0 -1
- package/dashboard/assets/flowDiagram-HODETNUW-pDb1AYm8.js +0 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-mvNUo0ZA.js +0 -1
- package/dashboard/assets/info-A6RAGUB7-DChKqZMi.js +0 -1
- package/dashboard/assets/linear-Bl6JKbf4.js +0 -1
- package/dashboard/assets/mermaid-parser.core-Cc6sgCzs.js +0 -7
- package/dashboard/assets/operations-B7JAWOi6.js +0 -130
- package/dashboard/assets/operations-QjKT0Hwi.css +0 -1
- package/dashboard/assets/packet-AYTQ26CC-bQXLDhQC.js +0 -1
- package/dashboard/assets/pie-WAS4IAKB-DfzoABIi.js +0 -1
- package/dashboard/assets/radar-RG4KPBEZ-CZ2pyeUT.js +0 -1
- package/dashboard/assets/railroad-74A4TZTK-CP4Jazfd.js +0 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-3MvEiHwD.js +0 -1
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-BWto6cXE.js +0 -1
- package/dashboard/assets/railroad-peg-WCYAUIDC-DasEjbyf.js +0 -1
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
- package/dashboard/assets/src-CaNUT1AQ.js +0 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-B3REMjpY.js +0 -1
- package/dashboard/assets/swimlanes-42K2YHIH-uJE05Tt_.js +0 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-CIaVMZEy.js +0 -8
- package/dashboard/assets/treeView-Q6P3EWNA-ZOE2enu2.js +0 -1
- package/dashboard/assets/treemap-WGGIJYW6-yIZoPBF5.js +0 -1
- package/dashboard/assets/wardley-WFR3VGLG-D5V-jLbB.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-G2EJ3R5I.js";
|
|
11
|
-
import {
|
|
12
|
-
assertLocalEngineAvailable,
|
|
13
|
-
registerLocalEngine
|
|
14
|
-
} from "./chunk-UHUW45S3.js";
|
|
15
|
-
import {
|
|
16
|
-
findingFromRow as findingFromRow2
|
|
17
|
-
} from "./chunk-O7Q2G2KW.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"
|
|
@@ -4355,11 +5342,7 @@ function resolveDashboardAssetDir(explicitAssetDir) {
|
|
|
4355
5342
|
const candidates = [
|
|
4356
5343
|
...explicitAssetDir ? [resolve4(explicitAssetDir)] : [],
|
|
4357
5344
|
join7(moduleDir, "dashboard"),
|
|
4358
|
-
join7(moduleDir, "..", "dashboard")
|
|
4359
|
-
// A source checkout must serve the freshly built workspace dashboard,
|
|
4360
|
-
// not an unrelated stale root bundle left by an earlier packaging run.
|
|
4361
|
-
join7(process.cwd(), "packages", "dashboard", "dist"),
|
|
4362
|
-
join7(process.cwd(), "dist", "dashboard")
|
|
5345
|
+
join7(moduleDir, "..", "dashboard")
|
|
4363
5346
|
];
|
|
4364
5347
|
for (const candidate of candidates) {
|
|
4365
5348
|
if (existsSync2(join7(candidate, "index.html"))) {
|
|
@@ -4368,6 +5351,14 @@ function resolveDashboardAssetDir(explicitAssetDir) {
|
|
|
4368
5351
|
}
|
|
4369
5352
|
const embedded = materializeEmbeddedDashboardAssets(DASHBOARD_ASSETS);
|
|
4370
5353
|
if (embedded) return embedded;
|
|
5354
|
+
for (const candidate of [
|
|
5355
|
+
join7(process.cwd(), "packages", "dashboard", "dist"),
|
|
5356
|
+
join7(process.cwd(), "dist", "dashboard")
|
|
5357
|
+
]) {
|
|
5358
|
+
if (existsSync2(join7(candidate, "index.html"))) {
|
|
5359
|
+
return { path: candidate };
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
4371
5362
|
throw new Error("Dashboard assets not found. Run `pnpm build` to generate the dashboard app.");
|
|
4372
5363
|
}
|
|
4373
5364
|
function resolveAssetPath(assetDir, pathname) {
|
|
@@ -4383,7 +5374,7 @@ function requireControlToken(req, res, controlToken, engineBearer) {
|
|
|
4383
5374
|
const matches = (a, b) => typeof a === "string" && Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual3(Buffer.from(a), Buffer.from(b));
|
|
4384
5375
|
const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice(7) : void 0;
|
|
4385
5376
|
if (!matches(provided, controlToken) && !matches(bearer, controlToken) && !(engineBearer && matches(bearer, engineBearer))) {
|
|
4386
|
-
|
|
5377
|
+
json2(res, 403, { error: "Invalid or missing control token" });
|
|
4387
5378
|
return false;
|
|
4388
5379
|
}
|
|
4389
5380
|
return true;
|
|
@@ -4410,52 +5401,52 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4410
5401
|
const bodyId = input && typeof input === "object" && "sessionId" in input && typeof input.sessionId === "string" ? input.sessionId : void 0;
|
|
4411
5402
|
const savedId = /^saved\/([^/]+)/.exec(path)?.[1];
|
|
4412
5403
|
const savedLiveId = savedId ? gateway.list().find((item) => item.id === savedId || item.savedId === savedId)?.id : void 0;
|
|
4413
|
-
const
|
|
4414
|
-
if (
|
|
5404
|
+
const id2 = liveId ?? bodyId ?? savedLiveId;
|
|
5405
|
+
if (id2 && !readResume) assertTeamConsoleMutation(req, id2, team.auth, team.collaboration, gateway);
|
|
4415
5406
|
}
|
|
4416
5407
|
try {
|
|
4417
5408
|
if (path === "execution" && method === "GET") {
|
|
4418
|
-
|
|
5409
|
+
json2(res, 200, await workbenchStatus());
|
|
4419
5410
|
return true;
|
|
4420
5411
|
}
|
|
4421
5412
|
if (path === "github" && method === "GET") {
|
|
4422
|
-
|
|
5413
|
+
json2(res, 200, { github: await github.status() });
|
|
4423
5414
|
return true;
|
|
4424
5415
|
}
|
|
4425
5416
|
if (path === "github/connect" && method === "POST") {
|
|
4426
|
-
|
|
5417
|
+
json2(res, 200, { github: await github.connect(input) });
|
|
4427
5418
|
return true;
|
|
4428
5419
|
}
|
|
4429
5420
|
if (path === "github/device-auth" && method === "POST") {
|
|
4430
|
-
|
|
5421
|
+
json2(res, 202, { github: await github.start() });
|
|
4431
5422
|
return true;
|
|
4432
5423
|
}
|
|
4433
5424
|
if (path === "github/device-auth" && method === "DELETE") {
|
|
4434
|
-
|
|
5425
|
+
json2(res, 200, { github: await github.cancel() });
|
|
4435
5426
|
return true;
|
|
4436
5427
|
}
|
|
4437
5428
|
const trigger = await triggers.handle(method, requestUrl, input);
|
|
4438
5429
|
if (trigger) {
|
|
4439
|
-
|
|
5430
|
+
json2(res, trigger.status, trigger.data);
|
|
4440
5431
|
return true;
|
|
4441
5432
|
}
|
|
4442
5433
|
const workflow = await workflows.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4443
5434
|
if (workflow) {
|
|
4444
5435
|
if (team?.collaboration && method !== "GET" && method !== "HEAD" && workflow.status >= 200 && workflow.status < 300) {
|
|
4445
5436
|
const definition = workflow.data && typeof workflow.data === "object" && "definition" in workflow.data ? workflow.data.definition : void 0;
|
|
4446
|
-
const
|
|
4447
|
-
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));
|
|
4448
5439
|
}
|
|
4449
|
-
|
|
5440
|
+
json2(res, workflow.status, workflow.data);
|
|
4450
5441
|
return true;
|
|
4451
5442
|
}
|
|
4452
5443
|
const service = await operator.handle(requestUrl.pathname, method, input, requestUrl.searchParams);
|
|
4453
5444
|
if (service) {
|
|
4454
|
-
|
|
5445
|
+
json2(res, service.status, service.data);
|
|
4455
5446
|
return true;
|
|
4456
5447
|
}
|
|
4457
5448
|
if (path === "search" && method === "GET") {
|
|
4458
|
-
|
|
5449
|
+
json2(res, 200, await gateway.search({
|
|
4459
5450
|
q: requestUrl.searchParams.get("q") ?? "",
|
|
4460
5451
|
...requestUrl.searchParams.has("limit") ? { limit: Number(requestUrl.searchParams.get("limit")) } : {},
|
|
4461
5452
|
...requestUrl.searchParams.has("offset") ? { offset: Number(requestUrl.searchParams.get("offset")) } : {}
|
|
@@ -4463,84 +5454,99 @@ async function handleWebConsoleApiRequest(req, res, requestUrl, gateway, operato
|
|
|
4463
5454
|
return true;
|
|
4464
5455
|
}
|
|
4465
5456
|
if (path === "sessions") {
|
|
4466
|
-
if (method === "GET")
|
|
5457
|
+
if (method === "GET") json2(res, 200, { sessions: gateway.list() });
|
|
4467
5458
|
else if (method === "POST") {
|
|
4468
5459
|
const session = gateway.create(input);
|
|
4469
5460
|
if (team?.auth.enabled && team.collaboration) team.collaboration.register(session.id, getSessionId(req));
|
|
4470
|
-
|
|
4471
|
-
} else
|
|
5461
|
+
json2(res, 201, { session });
|
|
5462
|
+
} else json2(res, 405, { error: "Method not allowed." });
|
|
4472
5463
|
return true;
|
|
4473
5464
|
}
|
|
4474
5465
|
const parts = path.split("/").map((part) => decodeURIComponent(part));
|
|
4475
5466
|
if (parts.some((part) => !/^[A-Za-z0-9_.:-]+$/.test(part))) throw new ConsoleGatewayError("Invalid resource identifier.", 400);
|
|
4476
5467
|
if (parts[0] === "saved") {
|
|
4477
|
-
const
|
|
4478
|
-
if (!
|
|
5468
|
+
const id3 = parts[1];
|
|
5469
|
+
if (!id3 && method === "GET") {
|
|
4479
5470
|
const limit = requestUrl.searchParams.get("limit");
|
|
4480
|
-
|
|
5471
|
+
json2(res, 200, { sessions: gateway.listSaved({
|
|
4481
5472
|
...requestUrl.searchParams.get("cwd") ? { cwd: requestUrl.searchParams.get("cwd") } : {},
|
|
4482
5473
|
...limit ? { limit: Number(limit) } : {}
|
|
4483
5474
|
}) });
|
|
4484
|
-
} else if (
|
|
4485
|
-
else if (
|
|
4486
|
-
gateway.deleteSaved(
|
|
4487
|
-
|
|
4488
|
-
} else if (
|
|
4489
|
-
else if (
|
|
4490
|
-
else if (
|
|
4491
|
-
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." });
|
|
4492
5483
|
return true;
|
|
4493
5484
|
}
|
|
4494
5485
|
if (parts[0] !== "sessions" || !parts[1]) return false;
|
|
4495
|
-
const
|
|
5486
|
+
const id2 = parts[1];
|
|
4496
5487
|
const action = parts[2];
|
|
4497
|
-
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) });
|
|
4498
5500
|
else if (!action && method === "DELETE") {
|
|
4499
|
-
workflows.cancelSession(
|
|
4500
|
-
await gateway.close(
|
|
4501
|
-
|
|
5501
|
+
workflows.cancelSession(id2);
|
|
5502
|
+
await gateway.close(id2);
|
|
5503
|
+
json2(res, 200, { ok: true });
|
|
4502
5504
|
} else if (action === "events" && method === "GET") {
|
|
4503
|
-
|
|
5505
|
+
json2(res, 200, gateway.eventsAfter(id2, consoleEventsAfter(requestUrl.searchParams.get("after"))));
|
|
4504
5506
|
} else if (action === "messages" && method === "POST") {
|
|
4505
5507
|
const message2 = author && input && typeof input === "object" && !("workerId" in input) && (!("mode" in input) || input.mode === "send") ? { ...input, mode: "queue" } : input;
|
|
4506
|
-
|
|
4507
|
-
|
|
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) });
|
|
4508
5511
|
else if (action === "connection" && parts[3] === "reload" && parts.length === 4 && method === "POST") {
|
|
4509
5512
|
external_exports.object({}).strict().parse(input ?? {});
|
|
4510
|
-
|
|
4511
|
-
} else if (action === "configuration" && method === "PATCH")
|
|
4512
|
-
else if (action === "clear" && method === "POST")
|
|
4513
|
-
else if (action === "continue" && method === "POST")
|
|
4514
|
-
else if (action === "harness" && method === "POST")
|
|
4515
|
-
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) });
|
|
4516
5519
|
else if (action === "archive" && method === "POST") {
|
|
4517
|
-
workflows.cancelSession(
|
|
4518
|
-
|
|
5520
|
+
workflows.cancelSession(id2);
|
|
5521
|
+
json2(res, 200, { session: await gateway.archive(id2) });
|
|
4519
5522
|
} else if (action === "delete" && method === "POST") {
|
|
4520
|
-
workflows.cancelSession(
|
|
4521
|
-
|
|
4522
|
-
} else if (action === "export" && method === "GET")
|
|
4523
|
-
else if (action === "queue" &&
|
|
4524
|
-
|
|
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) });
|
|
4525
5531
|
else if (action === "workers") {
|
|
4526
5532
|
const workerId = parts[3];
|
|
4527
|
-
if (!workerId && method === "GET")
|
|
4528
|
-
else if (workerId === "stop" && method === "POST")
|
|
4529
|
-
else if (workerId && parts[4] === "export" && method === "GET")
|
|
4530
|
-
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) });
|
|
4531
5537
|
else if (workerId && parts[4] === "messages" && method === "POST") {
|
|
4532
5538
|
const body = external_exports.object({ text: external_exports.unknown() }).strict().parse(input);
|
|
4533
|
-
|
|
4534
|
-
} else
|
|
4535
|
-
} 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." });
|
|
4536
5542
|
return true;
|
|
4537
5543
|
} catch (error2) {
|
|
4538
5544
|
if (error2 instanceof external_exports.ZodError) {
|
|
4539
|
-
|
|
5545
|
+
json2(res, 400, { error: "Invalid console request payload." });
|
|
4540
5546
|
return true;
|
|
4541
5547
|
}
|
|
4542
5548
|
if (error2 instanceof ConsoleGatewayError) {
|
|
4543
|
-
|
|
5549
|
+
json2(res, error2.statusCode, { error: error2.message });
|
|
4544
5550
|
return true;
|
|
4545
5551
|
}
|
|
4546
5552
|
throw error2;
|
|
@@ -4568,10 +5574,10 @@ data: ${JSON.stringify(event)}
|
|
|
4568
5574
|
`);
|
|
4569
5575
|
}
|
|
4570
5576
|
async function handleApiRequest(req, res, pathname, dbPath, controlToken, engineBearer) {
|
|
4571
|
-
const { osecDB: osecDB2 } = await import("./dist-
|
|
5577
|
+
const { osecDB: osecDB2 } = await import("./dist-OLGNKDB2.js");
|
|
4572
5578
|
if (isPresentationEventsStreamPath(pathname)) {
|
|
4573
5579
|
if (req.method !== "GET") {
|
|
4574
|
-
|
|
5580
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4575
5581
|
return true;
|
|
4576
5582
|
}
|
|
4577
5583
|
const db = new osecDB2(dbPath);
|
|
@@ -4615,27 +5621,27 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4615
5621
|
const controlPath = parseControlPath(pathname);
|
|
4616
5622
|
if (controlPath) {
|
|
4617
5623
|
if (req.method !== "POST") {
|
|
4618
|
-
|
|
5624
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
4619
5625
|
return true;
|
|
4620
5626
|
}
|
|
4621
5627
|
if (!requireControlToken(req, res, controlToken, engineBearer)) {
|
|
4622
5628
|
return true;
|
|
4623
5629
|
}
|
|
4624
5630
|
if (controlPath.action === "recover-stale-workers") {
|
|
4625
|
-
const { recoverStaleWorkers } = await import("./orchestrate-
|
|
5631
|
+
const { recoverStaleWorkers } = await import("./orchestrate-Z3DW265M.js");
|
|
4626
5632
|
const body = await readJson(req);
|
|
4627
5633
|
const recovered = recoverStaleWorkers(
|
|
4628
5634
|
dbPath,
|
|
4629
5635
|
typeof body.staleAfterMs === "number" ? body.staleAfterMs : 3e4
|
|
4630
5636
|
);
|
|
4631
|
-
|
|
5637
|
+
json2(res, 200, { ok: true, recovered });
|
|
4632
5638
|
return true;
|
|
4633
5639
|
}
|
|
4634
5640
|
if (controlPath.action === "prune-stopped-workers") {
|
|
4635
5641
|
const db = new osecDB2(dbPath);
|
|
4636
5642
|
try {
|
|
4637
5643
|
const deleted = db.deleteWorkersByStatus("stopped");
|
|
4638
|
-
|
|
5644
|
+
json2(res, 200, { ok: true, deleted });
|
|
4639
5645
|
} finally {
|
|
4640
5646
|
db.close();
|
|
4641
5647
|
}
|
|
@@ -4649,7 +5655,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4649
5655
|
(worker) => worker.status !== "stopped" && Date.now() - Date.parse(worker.heartbeatAt) < 2e4
|
|
4650
5656
|
);
|
|
4651
5657
|
if (activeWorkers.length > 0) {
|
|
4652
|
-
|
|
5658
|
+
json2(res, 409, { error: "An active daemon is already heartbeating. Stop it before starting another local daemon." });
|
|
4653
5659
|
return true;
|
|
4654
5660
|
}
|
|
4655
5661
|
} finally {
|
|
@@ -4660,21 +5666,21 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4660
5666
|
label: typeof body.label === "string" && body.label.trim() ? body.label.trim() : "control-plane-1",
|
|
4661
5667
|
pollIntervalMs: typeof body.pollIntervalMs === "number" ? body.pollIntervalMs : 2e3
|
|
4662
5668
|
});
|
|
4663
|
-
|
|
5669
|
+
json2(res, 200, { ok: true, ...started });
|
|
4664
5670
|
return true;
|
|
4665
5671
|
}
|
|
4666
5672
|
if (controlPath.action === "stop-daemon") {
|
|
4667
5673
|
const stopped = stopDaemonWorkers(osecDB2, dbPath);
|
|
4668
|
-
|
|
5674
|
+
json2(res, 200, { ok: true, stopped });
|
|
4669
5675
|
return true;
|
|
4670
5676
|
}
|
|
4671
5677
|
if (controlPath.action === "reset-database") {
|
|
4672
|
-
const { resetOsecDatabase } = await import("./dist-
|
|
4673
|
-
const { seedVerificationWorkbench } = await import("./db-
|
|
5678
|
+
const { resetOsecDatabase } = await import("./dist-OLGNKDB2.js");
|
|
5679
|
+
const { seedVerificationWorkbench } = await import("./db-R2AQSESC.js");
|
|
4674
5680
|
const body = await readJson(req);
|
|
4675
5681
|
const seed = typeof body.seed === "string" ? body.seed.trim().toLowerCase() : "verification";
|
|
4676
5682
|
if (!["verification", "empty"].includes(seed)) {
|
|
4677
|
-
|
|
5683
|
+
json2(res, 400, { error: `Unsupported seed preset: ${seed}` });
|
|
4678
5684
|
return true;
|
|
4679
5685
|
}
|
|
4680
5686
|
const activeWorkerDb = new osecDB2(dbPath);
|
|
@@ -4683,7 +5689,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4683
5689
|
(worker) => ["idle", "claiming", "running", "sleeping"].includes(worker.status) && Date.now() - Date.parse(worker.heartbeatAt) < 2e4 && (worker.pid ? isLiveLocalPid(worker.pid) : true)
|
|
4684
5690
|
);
|
|
4685
5691
|
if (hasActiveWorker) {
|
|
4686
|
-
|
|
5692
|
+
json2(res, 409, { error: "Stop active orchestration daemons before resetting the local database." });
|
|
4687
5693
|
return true;
|
|
4688
5694
|
}
|
|
4689
5695
|
} finally {
|
|
@@ -4693,7 +5699,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4693
5699
|
const resetDb = new osecDB2(dbPath);
|
|
4694
5700
|
try {
|
|
4695
5701
|
const seeded = seed === "verification" ? seedVerificationWorkbench(resetDb) : { scans: 0, families: 0, workers: 0 };
|
|
4696
|
-
|
|
5702
|
+
json2(res, 200, { ok: true, path, seed, ...seeded });
|
|
4697
5703
|
} finally {
|
|
4698
5704
|
resetDb.close();
|
|
4699
5705
|
}
|
|
@@ -4729,7 +5735,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4729
5735
|
});
|
|
4730
5736
|
const casesById = new Map(cases.map((item) => [item.id, item]));
|
|
4731
5737
|
const workItemsById = new Map(workItems.map((item) => [item.id, item]));
|
|
4732
|
-
|
|
5738
|
+
json2(res, 200, {
|
|
4733
5739
|
scans: scans.map(summarizeScan),
|
|
4734
5740
|
cases,
|
|
4735
5741
|
groups,
|
|
@@ -4745,7 +5751,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4745
5751
|
const db = new osecDB2(dbPath);
|
|
4746
5752
|
try {
|
|
4747
5753
|
const scans = db.listScans(100);
|
|
4748
|
-
|
|
5754
|
+
json2(res, 200, { scans: scans.map(summarizeScan) });
|
|
4749
5755
|
} finally {
|
|
4750
5756
|
db.close();
|
|
4751
5757
|
}
|
|
@@ -4779,7 +5785,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4779
5785
|
presentation
|
|
4780
5786
|
};
|
|
4781
5787
|
});
|
|
4782
|
-
|
|
5788
|
+
json2(res, 200, { events });
|
|
4783
5789
|
} finally {
|
|
4784
5790
|
db.close();
|
|
4785
5791
|
}
|
|
@@ -4791,7 +5797,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4791
5797
|
try {
|
|
4792
5798
|
const scan = db.getScan(scanPath.scanId);
|
|
4793
5799
|
if (!scan) {
|
|
4794
|
-
|
|
5800
|
+
json2(res, 404, { error: "Scan not found" });
|
|
4795
5801
|
return true;
|
|
4796
5802
|
}
|
|
4797
5803
|
if (scanPath.suffix === "events") {
|
|
@@ -4799,7 +5805,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4799
5805
|
...event,
|
|
4800
5806
|
payload: parsePayload(event.payload)
|
|
4801
5807
|
}));
|
|
4802
|
-
|
|
5808
|
+
json2(res, 200, { scan: summarizeScan(scan), events });
|
|
4803
5809
|
return true;
|
|
4804
5810
|
}
|
|
4805
5811
|
if (scanPath.suffix === "findings") {
|
|
@@ -4809,7 +5815,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4809
5815
|
scanIds: [scanPath.scanId],
|
|
4810
5816
|
status: "running"
|
|
4811
5817
|
});
|
|
4812
|
-
|
|
5818
|
+
json2(res, 200, {
|
|
4813
5819
|
scan: summarizeScan(scan),
|
|
4814
5820
|
findings,
|
|
4815
5821
|
groups: groupFindings(
|
|
@@ -4820,7 +5826,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4820
5826
|
});
|
|
4821
5827
|
return true;
|
|
4822
5828
|
}
|
|
4823
|
-
|
|
5829
|
+
json2(res, 200, { scan: summarizeScan(scan) });
|
|
4824
5830
|
} finally {
|
|
4825
5831
|
db.close();
|
|
4826
5832
|
}
|
|
@@ -4840,7 +5846,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4840
5846
|
normalizeTriageStatus(body.triageStatus),
|
|
4841
5847
|
typeof body.triageNote === "string" ? body.triageNote : void 0
|
|
4842
5848
|
);
|
|
4843
|
-
|
|
5849
|
+
json2(res, 200, { ok: true });
|
|
4844
5850
|
return true;
|
|
4845
5851
|
}
|
|
4846
5852
|
if (req.method === "POST" && familyPath.action === "workflow") {
|
|
@@ -4853,12 +5859,12 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4853
5859
|
normalizeWorkflowStatus(body.workflowStatus),
|
|
4854
5860
|
typeof body.workflowAssignee === "string" ? body.workflowAssignee.trim() : null
|
|
4855
5861
|
);
|
|
4856
|
-
|
|
5862
|
+
json2(res, 200, { ok: true });
|
|
4857
5863
|
return true;
|
|
4858
5864
|
}
|
|
4859
5865
|
const rows = db.getRelatedFindings(familyPath.fingerprint).map(normalizeFinding);
|
|
4860
5866
|
if (rows.length === 0) {
|
|
4861
|
-
|
|
5867
|
+
json2(res, 404, { error: "Not found" });
|
|
4862
5868
|
return true;
|
|
4863
5869
|
}
|
|
4864
5870
|
const verdicts = db.listVerdicts(rows.map((row) => row.id));
|
|
@@ -4900,7 +5906,7 @@ async function handleApiRequest(req, res, pathname, dbPath, controlToken, engine
|
|
|
4900
5906
|
sessions,
|
|
4901
5907
|
events: allEvents
|
|
4902
5908
|
});
|
|
4903
|
-
|
|
5909
|
+
json2(res, 200, {
|
|
4904
5910
|
consoleCommand: buildFindingConsoleCommand(rows[0], dbPath),
|
|
4905
5911
|
fingerprint: familyPath.fingerprint,
|
|
4906
5912
|
case: familyCase,
|
|
@@ -4968,7 +5974,7 @@ function registerDashboardCommand(program) {
|
|
|
4968
5974
|
const timeCapMs = opts.engineTimeCap === void 0 ? 6e5 : Number(opts.engineTimeCap);
|
|
4969
5975
|
const costCapUsd = opts.engineCostCap === void 0 ? 5 : Number(opts.engineCostCap);
|
|
4970
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.");
|
|
4971
|
-
const controlToken =
|
|
5977
|
+
const controlToken = randomUUID9();
|
|
4972
5978
|
let teamAuth = new TeamAuth({ configPath: teamConfigPath, origin });
|
|
4973
5979
|
let teamDirectory = teamAuth.enabled ? join7(homeStateDir(), "teams", createHash8("sha256").update(teamAuth.workspaceId).digest("hex")) : void 0;
|
|
4974
5980
|
if (teamDirectory) {
|
|
@@ -4978,9 +5984,51 @@ function registerDashboardCommand(program) {
|
|
|
4978
5984
|
}
|
|
4979
5985
|
let collaboration = teamAuth.enabled ? new CollaborationService({ workspaceId: teamAuth.workspaceId, stateDir: join7(teamDirectory, "collaboration"), resolveSession: (sessionId) => teamAuth.resolveSessionId(sessionId) }) : void 0;
|
|
4980
5986
|
assertLocalEngineAvailable(opts.dbPath);
|
|
4981
|
-
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"];
|
|
4982
5988
|
let localHandshake = createBackendHandshake(opts.dbPath, capabilities);
|
|
4983
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
|
+
} });
|
|
4984
6032
|
let engine;
|
|
4985
6033
|
let startupGateway;
|
|
4986
6034
|
let startupOperator;
|
|
@@ -4991,8 +6039,9 @@ function registerDashboardCommand(program) {
|
|
|
4991
6039
|
try {
|
|
4992
6040
|
const { path: assetDir, cleanup: cleanupAssetDir } = resolveDashboardAssetDir(opts.assetDir);
|
|
4993
6041
|
startupAssetCleanup = cleanupAssetDir;
|
|
4994
|
-
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 });
|
|
4995
6043
|
let services = await buildServices(teamAuth);
|
|
6044
|
+
stopHostReconnect = watchManagedHosts();
|
|
4996
6045
|
let { skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = services;
|
|
4997
6046
|
engine = services.engine;
|
|
4998
6047
|
startupGateway = consoleGateway;
|
|
@@ -5011,11 +6060,11 @@ function registerDashboardCommand(program) {
|
|
|
5011
6060
|
currentAuth: () => teamAuth,
|
|
5012
6061
|
activate: async (prepared) => {
|
|
5013
6062
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5014
|
-
const
|
|
6063
|
+
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
6064
|
+
const next = await buildServices(prepared.auth, prepared.dbPath, prepared.stateDir, nextHandshake.serverInstanceId);
|
|
5015
6065
|
let nextBackends;
|
|
5016
6066
|
let nextUnregister;
|
|
5017
6067
|
try {
|
|
5018
|
-
const nextHandshake = createBackendHandshake(prepared.dbPath, capabilities);
|
|
5019
6068
|
nextBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
5020
6069
|
const nextAccess = new FindingsAccessTokenStore({ workspaceId: prepared.workspaceId, stateDir: join7(prepared.stateDir, "findings-api-access") });
|
|
5021
6070
|
const nextRead = new FindingsReadApi({ workspaceId: prepared.workspaceId, dbPath: prepared.dbPath });
|
|
@@ -5024,6 +6073,9 @@ function registerDashboardCommand(program) {
|
|
|
5024
6073
|
if (consoleGateway.hasActiveTurns() || workflows.hasActiveWork()) throw new TeamAuthError("Finish active work before creating a team workspace.", 409);
|
|
5025
6074
|
await services.dispose();
|
|
5026
6075
|
for (const response of activeResponses) if (response !== setupResponse) response.end();
|
|
6076
|
+
stopHostReconnect?.();
|
|
6077
|
+
await localProviderSessions.dispose();
|
|
6078
|
+
modelBackends.dispose();
|
|
5027
6079
|
backends.dispose();
|
|
5028
6080
|
unregisterLocalEngine?.();
|
|
5029
6081
|
services = next;
|
|
@@ -5033,6 +6085,8 @@ function registerDashboardCommand(program) {
|
|
|
5033
6085
|
collaboration = nextCollaboration;
|
|
5034
6086
|
localHandshake = nextHandshake;
|
|
5035
6087
|
backends = nextBackends;
|
|
6088
|
+
modelBackends = new BackendConnectionRegistry({ configPath: opts.backendsConfig, localHandshake: nextHandshake });
|
|
6089
|
+
localProviderSessions = new LocalProviderSessions({ registry: modelRegistry });
|
|
5036
6090
|
unregisterLocalEngine = nextUnregister;
|
|
5037
6091
|
({ skills, gateway: consoleGateway, engagements, operator, workflows, triggers, github } = next);
|
|
5038
6092
|
engine = next.engine;
|
|
@@ -5043,6 +6097,7 @@ function registerDashboardCommand(program) {
|
|
|
5043
6097
|
startupWorkflows = workflows;
|
|
5044
6098
|
startupTriggers = triggers;
|
|
5045
6099
|
startupGitHub = github;
|
|
6100
|
+
stopHostReconnect = watchManagedHosts();
|
|
5046
6101
|
} catch (cause) {
|
|
5047
6102
|
nextBackends?.dispose();
|
|
5048
6103
|
nextUnregister?.();
|
|
@@ -5060,12 +6115,12 @@ function registerDashboardCommand(program) {
|
|
|
5060
6115
|
try {
|
|
5061
6116
|
authorizeWebRequest(req, origin);
|
|
5062
6117
|
} catch (error2) {
|
|
5063
|
-
|
|
6118
|
+
json2(res, error2 instanceof WebRequestError ? error2.statusCode : 403, { error: error2 instanceof Error ? error2.message : "Invalid request origin." });
|
|
5064
6119
|
return;
|
|
5065
6120
|
}
|
|
5066
6121
|
let requestUrl = new URL2(req.url ?? "/", origin);
|
|
5067
6122
|
const requestId = BackendRequestIdSchema.safeParse(req.headers["x-0-request-id"]);
|
|
5068
|
-
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data :
|
|
6123
|
+
res.setHeader("X-0-Request-ID", requestId.success ? requestId.data : randomUUID9());
|
|
5069
6124
|
try {
|
|
5070
6125
|
if (requestUrl.pathname.startsWith("/api/")) {
|
|
5071
6126
|
const findingsCredential = findingsAccess.authenticate(req, requestUrl);
|
|
@@ -5074,7 +6129,7 @@ function registerDashboardCommand(program) {
|
|
|
5074
6129
|
if (!findingsCredential) throw new FindingsAccessError("A findings API key is required in the Authorization header.", 401);
|
|
5075
6130
|
const reply = handleFindingsReadRequest(req, requestUrl, findingsRead);
|
|
5076
6131
|
if (!reply) throw new FindingsAccessError("Findings API route was not found.", 404);
|
|
5077
|
-
|
|
6132
|
+
json2(res, reply.status, reply.data);
|
|
5078
6133
|
return;
|
|
5079
6134
|
}
|
|
5080
6135
|
const ssoNavigation = requestUrl.pathname === "/api/team/auth/sso" || requestUrl.pathname === "/api/team/auth/callback";
|
|
@@ -5085,7 +6140,7 @@ function registerDashboardCommand(program) {
|
|
|
5085
6140
|
const reply = await handleTeamSetupRequest(req, requestUrl, setup, () => readJson(req));
|
|
5086
6141
|
if (reply) {
|
|
5087
6142
|
for (const [name, value] of Object.entries(reply.headers ?? {})) res.setHeader(name, value);
|
|
5088
|
-
|
|
6143
|
+
json2(res, reply.status, reply.data);
|
|
5089
6144
|
return;
|
|
5090
6145
|
}
|
|
5091
6146
|
} finally {
|
|
@@ -5094,12 +6149,12 @@ function registerDashboardCommand(program) {
|
|
|
5094
6149
|
}
|
|
5095
6150
|
if (setup.status().pending && requestUrl.pathname !== "/api/team/session") throw new WebRequestError("Workspace setup is in progress. Try again shortly.", 503);
|
|
5096
6151
|
if (handleTeamPresenceStream(req, res, requestUrl, teamAuth, collaboration)) return;
|
|
5097
|
-
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) => {
|
|
5098
6153
|
if (kind === "report") {
|
|
5099
|
-
engagements.get(
|
|
6154
|
+
engagements.get(id2);
|
|
5100
6155
|
return;
|
|
5101
6156
|
}
|
|
5102
|
-
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());
|
|
5103
6158
|
if (!definition || definition.status !== 200) throw new WebRequestError("Workflow was not found.", 404);
|
|
5104
6159
|
});
|
|
5105
6160
|
if (teamReply) {
|
|
@@ -5107,7 +6162,7 @@ function registerDashboardCommand(program) {
|
|
|
5107
6162
|
if (teamReply.redirect) {
|
|
5108
6163
|
res.writeHead(teamReply.status, { Location: teamReply.redirect, "Cache-Control": "no-store" });
|
|
5109
6164
|
res.end();
|
|
5110
|
-
} else
|
|
6165
|
+
} else json2(res, teamReply.status, teamReply.data);
|
|
5111
6166
|
return;
|
|
5112
6167
|
}
|
|
5113
6168
|
const bearer = req.headers.authorization;
|
|
@@ -5118,11 +6173,41 @@ function registerDashboardCommand(program) {
|
|
|
5118
6173
|
if (expectedEngineId !== void 0 && expectedEngineId !== localHandshake.engineId) throw new WebRequestError("Request belongs to a different engine identity.", 409);
|
|
5119
6174
|
const findingsAccessReply = await handleFindingsAccessTokenRequest(req, requestUrl, teamAuth, findingsAccess, () => readJson(req));
|
|
5120
6175
|
if (findingsAccessReply) {
|
|
5121
|
-
|
|
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();
|
|
5122
6209
|
return;
|
|
5123
6210
|
}
|
|
5124
|
-
const backendRoute = await handleBackendConnectionRequest(req, res, requestUrl, backends);
|
|
5125
|
-
if (backendRoute.handled) return;
|
|
5126
6211
|
if (backendRoute.localPath) requestUrl = new URL2(backendRoute.localPath, origin);
|
|
5127
6212
|
const skillsRoute = requestUrl.pathname.match(/^\/api\/skills(?:\/(.*))?$/);
|
|
5128
6213
|
if (skillsRoute) {
|
|
@@ -5136,21 +6221,21 @@ function registerDashboardCommand(program) {
|
|
|
5136
6221
|
throw new WebRequestError("Invalid skill identifier.", 400);
|
|
5137
6222
|
}
|
|
5138
6223
|
const method = req.method ?? "GET";
|
|
5139
|
-
if (!path && method === "GET")
|
|
5140
|
-
else if (!path && method === "POST")
|
|
5141
|
-
else if (path === "import" && method === "POST")
|
|
5142
|
-
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)) });
|
|
5143
6228
|
else if (path.startsWith("mounts/") && method === "DELETE") {
|
|
5144
6229
|
skills.unmount(decodedPath.slice(7));
|
|
5145
|
-
|
|
5146
|
-
} 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) });
|
|
5147
6232
|
else if (path.endsWith("/export") && method === "GET") {
|
|
5148
|
-
const
|
|
5149
|
-
const bundle = skills.export(
|
|
6233
|
+
const id2 = decodedPath.slice(0, -7);
|
|
6234
|
+
const bundle = skills.export(id2);
|
|
5150
6235
|
res.setHeader("Content-Disposition", `attachment; filename="${bundle.name}.skill.json"`);
|
|
5151
|
-
|
|
5152
|
-
} else if (path && method === "GET")
|
|
5153
|
-
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) });
|
|
5154
6239
|
else throw new WebRequestError("Method not allowed.", 405);
|
|
5155
6240
|
return;
|
|
5156
6241
|
}
|
|
@@ -5158,34 +6243,34 @@ function registerDashboardCommand(program) {
|
|
|
5158
6243
|
if (engagementRoute) {
|
|
5159
6244
|
const member = teamAuth.resolveSession(req);
|
|
5160
6245
|
const engagementActor = member ? { userId: member.userId, displayName: member.displayName } : void 0;
|
|
5161
|
-
const
|
|
6246
|
+
const id2 = engagementRoute[1] ? decodeURIComponent(engagementRoute[1]) : void 0;
|
|
5162
6247
|
if (engagementRoute[2]) {
|
|
5163
6248
|
if (req.method !== "GET") throw new WebRequestError("Use GET to read an engagement report.", 405);
|
|
5164
6249
|
const format = requestUrl.searchParams.get("format") ?? "json";
|
|
5165
6250
|
if (format !== "json" && format !== "markdown") throw new WebRequestError("Use json or markdown for an engagement report.", 400);
|
|
5166
|
-
const report = engagements.report(
|
|
6251
|
+
const report = engagements.report(id2);
|
|
5167
6252
|
if (format === "json") {
|
|
5168
|
-
res.setHeader("Content-Disposition", `attachment; filename="engagement-${
|
|
5169
|
-
|
|
6253
|
+
res.setHeader("Content-Disposition", `attachment; filename="engagement-${id2}.json"`);
|
|
6254
|
+
json2(res, 200, report);
|
|
5170
6255
|
} else {
|
|
5171
|
-
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" });
|
|
5172
6257
|
res.end(engagementMarkdown(report));
|
|
5173
6258
|
}
|
|
5174
|
-
} else if (req.method === "GET")
|
|
5175
|
-
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) {
|
|
5176
6261
|
const engagement = engagements.create(await readJson(req), engagementActor);
|
|
5177
6262
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5178
|
-
|
|
5179
|
-
} else if (req.method === "PATCH" &&
|
|
5180
|
-
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);
|
|
5181
6266
|
collaboration?.notifyChanged("report", engagement.id);
|
|
5182
|
-
|
|
6267
|
+
json2(res, 200, { engagement });
|
|
5183
6268
|
} else throw new WebRequestError("Method not allowed.", 405);
|
|
5184
6269
|
return;
|
|
5185
6270
|
}
|
|
5186
6271
|
if (requestUrl.pathname === "/api/workers/hosts") {
|
|
5187
6272
|
if (req.method !== "GET") throw new WebRequestError("Method not allowed.", 405);
|
|
5188
|
-
|
|
6273
|
+
json2(res, 200, { hosts: discoverSshHosts() });
|
|
5189
6274
|
return;
|
|
5190
6275
|
}
|
|
5191
6276
|
if (requestUrl.pathname === "/api/workers/managed") {
|
|
@@ -5198,7 +6283,15 @@ function registerDashboardCommand(program) {
|
|
|
5198
6283
|
return [];
|
|
5199
6284
|
}
|
|
5200
6285
|
});
|
|
5201
|
-
|
|
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);
|
|
5202
6295
|
return;
|
|
5203
6296
|
}
|
|
5204
6297
|
const workerStop = requestUrl.pathname.match(/^\/api\/workers\/([^/]+)\/stop$/);
|
|
@@ -5220,7 +6313,7 @@ function registerDashboardCommand(program) {
|
|
|
5220
6313
|
}
|
|
5221
6314
|
});
|
|
5222
6315
|
});
|
|
5223
|
-
|
|
6316
|
+
json2(res, 200, result);
|
|
5224
6317
|
return;
|
|
5225
6318
|
}
|
|
5226
6319
|
if (requestUrl.pathname === "/api/workers/deploy") {
|
|
@@ -5253,7 +6346,7 @@ function registerDashboardCommand(program) {
|
|
|
5253
6346
|
});
|
|
5254
6347
|
});
|
|
5255
6348
|
backends.reload();
|
|
5256
|
-
|
|
6349
|
+
json2(res, 202, result);
|
|
5257
6350
|
return;
|
|
5258
6351
|
}
|
|
5259
6352
|
if (requestUrl.pathname === "/api/control/launch-run") {
|
|
@@ -5263,15 +6356,15 @@ function registerDashboardCommand(program) {
|
|
|
5263
6356
|
if (body.ensureDaemon) throw new WebRequestError("Engine runs do not require a separate orchestration daemon. Configure a workflow schedule for recurring work.", 400);
|
|
5264
6357
|
const session = await engine.invoke("create_session", { config: { target: body.target, title: `Assessment: ${body.target}` } });
|
|
5265
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 } });
|
|
5266
|
-
|
|
6359
|
+
json2(res, 202, { ok: true, engineOwned: true, sessionId: session.id, runId: execution.id, jobId: execution.jobId });
|
|
5267
6360
|
return;
|
|
5268
6361
|
}
|
|
5269
6362
|
if (requestUrl.pathname === "/api/workflow-engine/call") {
|
|
5270
6363
|
if (!engine) throw new WebRequestError("This engine does not admit persistent workflow calls.", 403);
|
|
5271
6364
|
if (req.method !== "POST") throw new WebRequestError("Method not allowed.", 405);
|
|
5272
|
-
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));
|
|
5273
6366
|
if (!engineRequest && typeof input.args.sessionId === "string") assertTeamConsoleMutation(req, input.args.sessionId, teamAuth, collaboration, consoleGateway);
|
|
5274
|
-
|
|
6367
|
+
json2(res, 200, await engine.invoke(input.name, input.args));
|
|
5275
6368
|
return;
|
|
5276
6369
|
}
|
|
5277
6370
|
const resumeScan = requestUrl.pathname.match(/^\/api\/scans\/([^/]+)\/resume$/);
|
|
@@ -5279,14 +6372,14 @@ function registerDashboardCommand(program) {
|
|
|
5279
6372
|
if (req.method !== "POST") throw new WebRequestError("Use POST to resume a scan.", 405);
|
|
5280
6373
|
const request = ScanResumeRequestSchema.parse(await readJson(req));
|
|
5281
6374
|
const result = await workflows.resumeScan(decodeURIComponent(resumeScan[1]), { ...request, timeCapMs: Math.min(request.timeCapMs ?? timeCapMs, timeCapMs), costCapUsd: Math.min(request.costCapUsd ?? costCapUsd, costCapUsd) });
|
|
5282
|
-
|
|
6375
|
+
json2(res, result.status, result.data);
|
|
5283
6376
|
return;
|
|
5284
6377
|
}
|
|
5285
6378
|
if (/^\/api\/findings\/[^/]+\/impact-assessment$/.test(requestUrl.pathname)) {
|
|
5286
6379
|
const input = req.method === "POST" || req.method === "DELETE" ? await readJson(req) : void 0;
|
|
5287
6380
|
const result = handleFindingImpactRequest(requestUrl.pathname, req.method ?? "GET", input, { dbPath: opts.dbPath });
|
|
5288
6381
|
if (result) {
|
|
5289
|
-
|
|
6382
|
+
json2(res, result.status, result.data);
|
|
5290
6383
|
return;
|
|
5291
6384
|
}
|
|
5292
6385
|
}
|
|
@@ -5305,7 +6398,7 @@ function registerDashboardCommand(program) {
|
|
|
5305
6398
|
const consoleHandled = await handleWebConsoleApiRequest(req, res, requestUrl, consoleGateway, operator, workflows, triggers, github, engineRequest ? void 0 : { auth: teamAuth, collaboration });
|
|
5306
6399
|
if (consoleHandled) return;
|
|
5307
6400
|
const handled = await handleApiRequest(req, res, requestUrl.pathname, opts.dbPath, controlToken, engineBearer);
|
|
5308
|
-
if (!handled)
|
|
6401
|
+
if (!handled) json2(res, 404, { error: "Not found" });
|
|
5309
6402
|
return;
|
|
5310
6403
|
}
|
|
5311
6404
|
if (devUrl) {
|
|
@@ -5326,12 +6419,12 @@ function registerDashboardCommand(program) {
|
|
|
5326
6419
|
return;
|
|
5327
6420
|
}
|
|
5328
6421
|
if (extname(requestUrl.pathname)) {
|
|
5329
|
-
|
|
6422
|
+
json2(res, 404, { error: "Asset not found" });
|
|
5330
6423
|
return;
|
|
5331
6424
|
}
|
|
5332
6425
|
sendFile(res, join7(assetDir, "index.html"), controlToken);
|
|
5333
6426
|
} catch (err) {
|
|
5334
|
-
|
|
6427
|
+
json2(res, errorStatusCode(err), { error: err instanceof Error ? err.message : "Web application request failed." });
|
|
5335
6428
|
}
|
|
5336
6429
|
});
|
|
5337
6430
|
let dashboardAssetsCleaned = false;
|
|
@@ -5379,19 +6472,31 @@ function registerDashboardCommand(program) {
|
|
|
5379
6472
|
const shutdown = () => {
|
|
5380
6473
|
if (shuttingDown) return;
|
|
5381
6474
|
shuttingDown = true;
|
|
5382
|
-
|
|
5383
|
-
|
|
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(() => {
|
|
5384
6483
|
operator.dispose();
|
|
5385
6484
|
github.dispose();
|
|
6485
|
+
for (const response of activeResponses) response.end();
|
|
5386
6486
|
server.close(() => {
|
|
5387
6487
|
cleanupDashboardAssets();
|
|
5388
6488
|
process.exit(0);
|
|
5389
6489
|
});
|
|
6490
|
+
server.closeAllConnections();
|
|
5390
6491
|
});
|
|
5391
6492
|
};
|
|
5392
6493
|
process.once("SIGINT", shutdown);
|
|
5393
6494
|
process.once("SIGTERM", shutdown);
|
|
5394
6495
|
} catch (error2) {
|
|
6496
|
+
stopHostReconnect?.();
|
|
6497
|
+
hostRepairAbort.abort();
|
|
6498
|
+
await localProviderSessions.dispose();
|
|
6499
|
+
modelBackends.dispose();
|
|
5395
6500
|
backends.dispose();
|
|
5396
6501
|
await startupTriggers?.dispose().catch(() => {
|
|
5397
6502
|
});
|