specrails-desktop 2.29.1 → 2.29.2
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/client/dist/assets/ActivityFeedPage-ClpD5CFj.js +1 -0
- package/client/dist/assets/AgentBrowserCapture-CSPs9Ofc.js +1 -0
- package/client/dist/assets/AgentModeCodePane-DwcyklA5.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-C69bqlMa.js +1 -0
- package/client/dist/assets/AgentsPage-pRiRr9-W.js +86 -0
- package/client/dist/assets/AnalyticsPage-C14exKsC.js +1 -0
- package/client/dist/assets/BarChart-nBOMNfiy.js +33 -0
- package/client/dist/assets/CodePage-CK76JKm9.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-Cnv0f3XQ.js +1 -0
- package/client/dist/assets/DocsDialog-CHUm_Ym0.js +11 -0
- package/client/dist/assets/DocsPage-COI-oDok.js +11 -0
- package/client/dist/assets/ExportDropdown-BrtfWzEG.js +1 -0
- package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-CypM7R1F.js +19 -0
- package/client/dist/assets/JobDetailModal-B_W0H9dw.js +1 -0
- package/client/dist/assets/JobDetailPage-CeeXOoKS.js +1 -0
- package/client/dist/assets/JobsPage-CwhJG9jw.js +1 -0
- package/client/dist/assets/LoopBuilderPage-C51t1WOM.js +7 -0
- package/client/dist/assets/LoopPreviewModal-Y2HgtIJu.js +1 -0
- package/client/dist/assets/LoopsPage-Bvn1Kw5V.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-DiFvZDPW.js +1 -0
- package/client/dist/assets/PluginsPage-BGPRJpdf.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-D_GTyGLc.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-szwBhd7h.js +1 -0
- package/client/dist/assets/TicketDetailModal-9UEyK974.js +14 -0
- package/client/dist/assets/Trans-B_vKZ-Hb.js +1 -0
- package/client/dist/assets/activity-DiyjsgQV.js +1 -0
- package/client/dist/assets/addon-image-BRmmRHgQ.js +1 -0
- package/client/dist/assets/addon-ligatures-qIZUPvRV.js +2 -0
- package/client/dist/assets/{addon-webgl-DMbGeI_u.js → addon-webgl-C2d7lriQ.js} +3 -3
- package/client/dist/assets/addspec-Rw8uVBC1.js +1 -0
- package/client/dist/assets/agent-4rmXoo_p.js +1 -0
- package/client/dist/assets/agent-BxZuNGih.js +1 -0
- package/client/dist/assets/agent-C3uXOYv_.js +1 -0
- package/client/dist/assets/agent-CA8Y4wbw.js +1 -0
- package/client/dist/assets/agent-CASKrhG12.js +1 -0
- package/client/dist/assets/agent-Cv-jud5n2.js +1 -0
- package/client/dist/assets/agent-DitUxYh2.js +1 -0
- package/client/dist/assets/agent-Doow4X7o.js +1 -0
- package/client/dist/assets/agents-DhG7mg6-.js +1 -0
- package/client/dist/assets/agentstudio-BIUe1Im2.js +1 -0
- package/client/dist/assets/aiedit-iIjz2_Qk.js +1 -0
- package/client/dist/assets/analytics-DZ8Aoq3I.js +1 -0
- package/client/dist/assets/api-B2M7CtDw.js +1 -0
- package/client/dist/assets/arrow-left-CpIR4j7G.js +1 -0
- package/client/dist/assets/arrow-up-right-9DoMn7fD.js +1 -0
- package/client/dist/assets/attachments-B53PG733.js +1 -0
- package/client/dist/assets/brain-DI9lHkKY.js +1 -0
- package/client/dist/assets/browser-CXUcnTMI.js +1 -0
- package/client/dist/assets/button-BUIIBQPT.js +1 -0
- package/client/dist/assets/chat-BGD7MXAy.js +1 -0
- package/client/dist/assets/chevron-right-CViMxucY.js +1 -0
- package/client/dist/assets/chunk-KS7C4IRE-Cx5sqopX.js +3 -0
- package/client/dist/assets/circle-x-CMYsN4ZH.js +1 -0
- package/client/dist/assets/code-BDXEM4ml.js +1 -0
- package/client/dist/assets/commands-DyUZQ1FI.js +1 -0
- package/client/dist/assets/common-CD1QHriA2.js +1 -0
- package/client/dist/assets/common-CwwPaRxx.js +1 -0
- package/client/dist/assets/common-DVQuhs-m.js +1 -0
- package/client/dist/assets/common-Dg2jaX7d.js +1 -0
- package/client/dist/assets/common-DuBwj6tK.js +1 -0
- package/client/dist/assets/common-MRj9MHcc.js +1 -0
- package/client/dist/assets/common-O4ZpnMLp.js +1 -0
- package/client/dist/assets/common-fyeF0v7C2.js +1 -0
- package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +1 -0
- package/client/dist/assets/{css.worker-Wv5dxAWO.js → css.worker-yKcq13f2.js} +13 -13
- package/client/dist/assets/{cssMode-DBHbkBvI.js → cssMode-BM4z9OX0.js} +1 -1
- package/client/dist/assets/dashboard-B_NkSHYm.js +1 -0
- package/client/dist/assets/dashboard-ClBb_3pq.js +1 -0
- package/client/dist/assets/dashboard-CpCmJvMB.js +1 -0
- package/client/dist/assets/dashboard-Czohu35O2.js +1 -0
- package/client/dist/assets/dashboard-DcjbD0UK.js +1 -0
- package/client/dist/assets/dashboard-Dvx5F3we.js +1 -0
- package/client/dist/assets/dashboard-R1bOPjBh2.js +1 -0
- package/client/dist/assets/dashboard-owcDucDi.js +1 -0
- package/client/dist/assets/dialog-CzIekCw0.js +45 -0
- package/client/dist/assets/dist-Cez5CRkX.js +1 -0
- package/client/dist/assets/dist-bVqfYmby.js +1 -0
- package/client/dist/assets/{dist-js-CgpP6tKI.js → dist-js-CcBCbb32.js} +1 -1
- package/client/dist/assets/{dist-js-DHiN-A_E.js → dist-js-VwztRkGj.js} +1 -1
- package/client/dist/assets/download-CFHyyHcw.js +1 -0
- package/client/dist/assets/editor.api2-4ndOupAA.js +872 -0
- package/client/dist/assets/{editor.main-C9J2ikJK.js → editor.main-DxrmGfoq.js} +2 -2
- package/client/dist/assets/editor.worker-tytyn86m.js +26 -0
- package/client/dist/assets/explore-DgBACn39.js +1 -0
- package/client/dist/assets/external-link-BcU5EWZp.js +1 -0
- package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
- package/client/dist/assets/formatDistanceToNow-ByHIauTQ.js +1 -0
- package/client/dist/assets/{freemarker2-J5iIIIK-.js → freemarker2-CELlTDJ2.js} +1 -1
- package/client/dist/assets/getTimezoneOffsetInMilliseconds-BxHAksaU.js +1 -0
- package/client/dist/assets/git-branch-BRM0F5GN.js +1 -0
- package/client/dist/assets/{handlebars-DSw1l_tw.js → handlebars-NEjHJ7o_.js} +1 -1
- package/client/dist/assets/{html-h2-wE1Lj.js → html-CA10zN5A.js} +1 -1
- package/client/dist/assets/{html.worker-CQP8QQsS.js → html.worker-DSJ9zmd7.js} +12 -12
- package/client/dist/assets/{htmlMode-Cc_ij7lW.js → htmlMode-89rPYiyq.js} +1 -1
- package/client/dist/assets/i18n-CgaRxpII.js +2 -0
- package/client/dist/assets/index-BflYQpWm.css +2 -0
- package/client/dist/assets/index-C1_-BTXy.js +72 -0
- package/client/dist/assets/integrations-mB-7zoyT.js +1 -0
- package/client/dist/assets/{javascript-C1q3xv6N.js → javascript-B1gDmwzS.js} +1 -1
- package/client/dist/assets/jira-CmKWERE0.js +1 -0
- package/client/dist/assets/jira-api-BgFIr8eD.js +1 -0
- package/client/dist/assets/job-time-BCL7wee9.js +1 -0
- package/client/dist/assets/jobs-B-sDHEay2.js +1 -0
- package/client/dist/assets/jobs-BCtGfgUA.js +1 -0
- package/client/dist/assets/jobs-BHR_EnDI.js +1 -0
- package/client/dist/assets/jobs-BMbZJjJJ.js +1 -0
- package/client/dist/assets/jobs-CVFD8rar.js +1 -0
- package/client/dist/assets/jobs-DoSaFzAa.js +1 -0
- package/client/dist/assets/jobs-DzSIR8V62.js +1 -0
- package/client/dist/assets/jobs-lk1_Dq1U.js +1 -0
- package/client/dist/assets/json.worker-Oa9UrkKE.js +58 -0
- package/client/dist/assets/{jsonMode-CeaCdEhr.js → jsonMode-xHLMfzhF.js} +1 -1
- package/client/dist/assets/jsx-runtime-zu2_FqZY.js +1 -0
- package/client/dist/assets/lib-C7Mhm-gT.js +7 -0
- package/client/dist/assets/lib-CGdyxIWg.js +29 -0
- package/client/dist/assets/{liquid-eFsVK0V-.js → liquid-BY6hCUXG.js} +1 -1
- package/client/dist/assets/loader-circle-DcygOQpC.js +1 -0
- package/client/dist/assets/loops-BzfXWEwO.js +1 -0
- package/client/dist/assets/{lspLanguageFeatures-DT3-bbhn.js → lspLanguageFeatures-ira2aEJh.js} +1 -1
- package/client/dist/assets/mcp-uDkXYu4L.js +1 -0
- package/client/dist/assets/{mdx-Fuwnh1cX.js → mdx-BNx0xr3N.js} +1 -1
- package/client/dist/assets/monaco.contribution-Bt92R3Vw.js +2 -0
- package/client/dist/assets/nav-myFaXRHD.js +1 -0
- package/client/dist/assets/pencil-C9RkrIPZ.js +1 -0
- package/client/dist/assets/play-DNlrawbS.js +1 -0
- package/client/dist/assets/plus-CsnjfkmX.js +1 -0
- package/client/dist/assets/preload-helper-Czpn1I53.js +1 -0
- package/client/dist/assets/{python-BgoEs6Cj.js → python-DLdbtmNw.js} +1 -1
- package/client/dist/assets/{razor-CM1sPMMq.js → razor-B9gdhAbR.js} +1 -1
- package/client/dist/assets/react-dom-DiWzBDP9.js +1 -0
- package/client/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
- package/client/dist/assets/save-28sIlCdr.js +1 -0
- package/client/dist/assets/search-Cq_gE3Ik.js +1 -0
- package/client/dist/assets/settings-B0ypw9uw.js +1 -0
- package/client/dist/assets/setup-CmK87LuS.js +1 -0
- package/client/dist/assets/sparkles-DrY8hb0X.js +1 -0
- package/client/dist/assets/specs-DII0C8gw.js +1 -0
- package/client/dist/assets/spending-C4p06nen.js +1 -0
- package/client/dist/assets/terminal-BFRZnMPP.js +1 -0
- package/client/dist/assets/tickets-B8BuAaj5.js +1 -0
- package/client/dist/assets/tickets-B_yh4qQk.js +1 -0
- package/client/dist/assets/tickets-Bs37scwY2.js +1 -0
- package/client/dist/assets/tickets-CFk2n2Or.js +1 -0
- package/client/dist/assets/tickets-DQp7JS2R2.js +1 -0
- package/client/dist/assets/tickets-D_yDquEw.js +1 -0
- package/client/dist/assets/tickets-DpmdE8tv2.js +1 -0
- package/client/dist/assets/tickets-pAUJkDTu.js +1 -0
- package/client/dist/assets/trash-2-BOU3WdZZ.js +1 -0
- package/client/dist/assets/triangle-alert-H9T4jAmh.js +1 -0
- package/client/dist/assets/{ts.worker-METxwbDZ.js → ts.worker-BtjBFNHX.js} +48 -33
- package/client/dist/assets/{tsMode-BmT8WogI.js → tsMode-DEjSThU9.js} +1 -1
- package/client/dist/assets/{typescript-CQrQPsoW.js → typescript-CRRnD2Ts.js} +1 -1
- package/client/dist/assets/upload-DYMjAQ6M.js +1 -0
- package/client/dist/assets/useBrowserCaptureSession-2OqX0vap.js +1 -0
- package/client/dist/assets/useDesktop-X8YB9xzv.js +1 -0
- package/client/dist/assets/useSharedWebSocket-DaPh7QBL.js +1 -0
- package/client/dist/assets/utils-BC4DHlz2.js +1 -0
- package/client/dist/assets/value-s-gzBeyF.js +1 -0
- package/client/dist/assets/{workers-U0UXyLUf.js → workers-uOo7WLaa.js} +1 -1
- package/client/dist/assets/x-CBMI9ejn.js +1 -0
- package/client/dist/assets/{xml-WRxJs8_I.js → xml-EDREH2X7.js} +1 -1
- package/client/dist/assets/{yaml-CjpSTjJU.js → yaml-BjaDVX1E.js} +1 -1
- package/client/dist/assets/zap-CHo3aCD1.js +1 -0
- package/client/dist/index.html +73 -32
- package/docs/guide/en/integrations/5-mcp-server.md +2 -2
- package/docs/internals/safe-pr-review-flow.md +190 -132
- package/docs/mcp.md +15 -5
- package/package.json +11 -7
- package/server/dist/active-pr-continuation.js +16 -1
- package/server/dist/agent-chat-manager.js +360 -181
- package/server/dist/agent-cwd-manager.js +52 -8
- package/server/dist/agent-generator.js +90 -48
- package/server/dist/agent-mcp-config.js +69 -36
- package/server/dist/agent-operator-prompt.js +3 -2
- package/server/dist/agent-refine-manager.js +47 -6
- package/server/dist/agent-tier.js +13 -11
- package/server/dist/artifact-registry.js +33 -2
- package/server/dist/auth.js +5 -4
- package/server/dist/browser-capture-manager.js +92 -29
- package/server/dist/chat-manager.js +302 -46
- package/server/dist/contract-refine-runner.js +19 -6
- package/server/dist/db.js +665 -8
- package/server/dist/desktop-router.js +1 -2
- package/server/dist/headroom-manager.js +1310 -121
- package/server/dist/headroom-routing.js +120 -2
- package/server/dist/index.js +48 -13
- package/server/dist/interactive-job-session.js +237 -82
- package/server/dist/jira/jira-adf.js +10 -0
- package/server/dist/jira/jira-db.js +15 -3
- package/server/dist/jira/jira-sync-manager.js +182 -13
- package/server/dist/job-listing.js +140 -0
- package/server/dist/job-spawn-idempotency.js +67 -0
- package/server/dist/loop-run-manager.js +508 -98
- package/server/dist/loop-runs-store.js +266 -13
- package/server/dist/mcp/agent-capability.js +71 -0
- package/server/dist/mcp/guide.js +5 -2
- package/server/dist/mcp/mcp-server.js +5 -10
- package/server/dist/mcp/mcp-token.js +5 -5
- package/server/dist/mcp/tools/jobs.js +20 -13
- package/server/dist/mcp/tools/types.js +20 -42
- package/server/dist/mobile/mobile-router.js +1 -1
- package/server/dist/pr-lifecycle.js +77 -0
- package/server/dist/pr-publisher.js +94 -27
- package/server/dist/process-admission.js +82 -0
- package/server/dist/project-registry.js +572 -174
- package/server/dist/project-router-git.js +13 -2
- package/server/dist/project-router-jobs.js +111 -55
- package/server/dist/project-router-tickets.js +32 -2
- package/server/dist/providers/claude-adapter.js +21 -13
- package/server/dist/queue-manager.js +2332 -623
- package/server/dist/rail-isolated-launch.js +1137 -290
- package/server/dist/rail-isolation.js +4 -2
- package/server/dist/rail-pr-decision.js +740 -266
- package/server/dist/rail-pr-delivery.js +2 -1
- package/server/dist/rail-pr-store.js +317 -18
- package/server/dist/rail-pr-ticket-effects.js +328 -0
- package/server/dist/rail-worktree-release.js +18 -2
- package/server/dist/rails-router.js +149 -119
- package/server/dist/rails-store.js +106 -3
- package/server/dist/ticket-store.js +23 -1
- package/server/dist/transient-children.js +13 -0
- package/server/dist/util/cli-prompt.js +42 -8
- package/server/dist/util/secure-token-file.js +84 -0
- package/server/dist/worktree-manager.js +6 -5
- package/client/dist/assets/ActivityFeedPage-D8RKRRoM.js +0 -1
- package/client/dist/assets/AgentBrowserCapture-BUHPHNSN.js +0 -1
- package/client/dist/assets/AgentModeCodePane-GZc8bNGT.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-DWtQqj3k.js +0 -1
- package/client/dist/assets/AgentsPage-CKPtHkRP.js +0 -86
- package/client/dist/assets/AnalyticsPage-CZW8c77k.js +0 -1
- package/client/dist/assets/BarChart-DugEoe2q.js +0 -33
- package/client/dist/assets/CodePage-CQ-uKETG.js +0 -2
- package/client/dist/assets/DesktopAnalyticsPage-Bgo4SyCH.js +0 -1
- package/client/dist/assets/DocsDialog-DxpWw_La.js +0 -11
- package/client/dist/assets/DocsPage-CmcOkK19.js +0 -11
- package/client/dist/assets/ExportDropdown-DvTS2wc7.js +0 -1
- package/client/dist/assets/IntegrationsPage-B72WxJ3Z.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-B5gh4Qoy.js +0 -19
- package/client/dist/assets/JobDetailModal-D-BeqsJ6.js +0 -1
- package/client/dist/assets/JobDetailPage-DwdozwLO.js +0 -1
- package/client/dist/assets/JobsPage-BDkinklt.js +0 -1
- package/client/dist/assets/LoopBuilderPage-CclJ3VZA.js +0 -7
- package/client/dist/assets/LoopPreviewModal-Dw4UgoLu.js +0 -1
- package/client/dist/assets/LoopsPage-D7ac4M24.js +0 -1
- package/client/dist/assets/PluginsPage-wdC-qa40.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-BlTlRtGI.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-CeMx2Red.js +0 -1
- package/client/dist/assets/activity-Dbl71S-E.js +0 -1
- package/client/dist/assets/addon-image-CwoZj17F.js +0 -1
- package/client/dist/assets/addon-ligatures-CWDrED80.js +0 -2
- package/client/dist/assets/addspec-DrXqP8ip.js +0 -1
- package/client/dist/assets/agent-B8BXmlUQ2.js +0 -1
- package/client/dist/assets/agent-BEXYxDbs.js +0 -1
- package/client/dist/assets/agent-BYc3-6Ln2.js +0 -1
- package/client/dist/assets/agent-BbJorPdT.js +0 -1
- package/client/dist/assets/agent-CewFImgL.js +0 -1
- package/client/dist/assets/agent-DfhYoUmp.js +0 -1
- package/client/dist/assets/agent-TK_rrK4d.js +0 -1
- package/client/dist/assets/agent-bHjA9R_j2.js +0 -1
- package/client/dist/assets/agents-CGDn_3kB.js +0 -1
- package/client/dist/assets/agentstudio-Dxe_94PK.js +0 -1
- package/client/dist/assets/aiedit-DrfHQnlz.js +0 -1
- package/client/dist/assets/analytics-CxDYcBl9.js +0 -1
- package/client/dist/assets/attachments-DvxQbBTC.js +0 -1
- package/client/dist/assets/brain-C92rjjYP.js +0 -1
- package/client/dist/assets/browser-DM8G151q.js +0 -1
- package/client/dist/assets/chat-0dg5hOo_.js +0 -1
- package/client/dist/assets/chunk-BNv3lrIs.js +0 -1
- package/client/dist/assets/code-B62fClk-.js +0 -1
- package/client/dist/assets/commands-DCJ-R9E4.js +0 -1
- package/client/dist/assets/common-BM3LFLCE2.js +0 -1
- package/client/dist/assets/common-Bwafujnc2.js +0 -1
- package/client/dist/assets/common-C-vU-j-I.js +0 -1
- package/client/dist/assets/common-C5HKSbbz.js +0 -1
- package/client/dist/assets/common-DXrmj6WD.js +0 -1
- package/client/dist/assets/common-DhWTkocg.js +0 -1
- package/client/dist/assets/common-DiPTT7V_2.js +0 -1
- package/client/dist/assets/common-DmqBPwZG.js +0 -1
- package/client/dist/assets/dashboard-B5ang-ZS.js +0 -1
- package/client/dist/assets/dashboard-CHHcOWVL2.js +0 -1
- package/client/dist/assets/dashboard-DBD4wPGY2.js +0 -1
- package/client/dist/assets/dashboard-DBuLfWo9.js +0 -1
- package/client/dist/assets/dashboard-DSnztwWH.js +0 -1
- package/client/dist/assets/dashboard-D_aP_InU.js +0 -1
- package/client/dist/assets/dashboard-DmBcdl1B.js +0 -1
- package/client/dist/assets/dashboard-RIcUKY1Y2.js +0 -1
- package/client/dist/assets/editor.api2-C3B0tp2x.js +0 -872
- package/client/dist/assets/editor.worker-Bd9IXS8d.js +0 -26
- package/client/dist/assets/explore-_XyU5e7R.js +0 -1
- package/client/dist/assets/index-BVbgKXUu.css +0 -2
- package/client/dist/assets/index-DyY1QyIn.js +0 -158
- package/client/dist/assets/integrations-By80EBkf.js +0 -1
- package/client/dist/assets/jira-DeIjwCK3.js +0 -1
- package/client/dist/assets/jobs-1GV2-4zd2.js +0 -1
- package/client/dist/assets/jobs-26F9gGcE.js +0 -1
- package/client/dist/assets/jobs-BgQzB0Sx2.js +0 -1
- package/client/dist/assets/jobs-BqWui9_f.js +0 -1
- package/client/dist/assets/jobs-CE8qeIYj.js +0 -1
- package/client/dist/assets/jobs-DrYmsKy5.js +0 -1
- package/client/dist/assets/jobs-DzeuASRf.js +0 -1
- package/client/dist/assets/jobs-Wh7dn28q2.js +0 -1
- package/client/dist/assets/json.worker-DzV-CpCQ.js +0 -58
- package/client/dist/assets/lib-0PFDyMxJ.js +0 -7
- package/client/dist/assets/loops-CO6q75W8.js +0 -1
- package/client/dist/assets/mcp-HwBbQDk0.js +0 -1
- package/client/dist/assets/monaco.contribution-Desq7oq4.js +0 -2
- package/client/dist/assets/nav-BTg8s-cD.js +0 -1
- package/client/dist/assets/preload-helper-DSXbuxSR.js +0 -1
- package/client/dist/assets/settings-cJNynGIZ.js +0 -1
- package/client/dist/assets/setup-D2CmgAro.js +0 -1
- package/client/dist/assets/specs-PGIOzMtc.js +0 -1
- package/client/dist/assets/terminal-CFt56A5f.js +0 -1
- package/client/dist/assets/tickets-B7zT4IRf.js +0 -1
- package/client/dist/assets/tickets-BHv3ewvq.js +0 -1
- package/client/dist/assets/tickets-BoXom7l8.js +0 -1
- package/client/dist/assets/tickets-C3b8cHhd2.js +0 -1
- package/client/dist/assets/tickets-CFXnJsYF.js +0 -1
- package/client/dist/assets/tickets-CWYFvx1W2.js +0 -1
- package/client/dist/assets/tickets-DTHo9c3C.js +0 -1
- package/client/dist/assets/tickets-SuX_HkK4.js +0 -1
- package/client/dist/assets/upload-CmR4h9H1.js +0 -1
- package/client/dist/assets/value-JA3bf47v.js +0 -1
- /package/client/dist/assets/{abap-D0E4FfVW.js → abap-08VXUWAP.js} +0 -0
- /package/client/dist/assets/{activity--iN4Ad2R2.js → activity-B-rp4h_v.js} +0 -0
- /package/client/dist/assets/{activity-6hM_ZSg9.js → activity-C6kJpm9q2.js} +0 -0
- /package/client/dist/assets/{activity-CIOLxGvh.js → activity-CGx4kb_j.js} +0 -0
- /package/client/dist/assets/{activity-BZNfOdiS.js → activity-CnoX2bH7.js} +0 -0
- /package/client/dist/assets/{activity-BuVo5sPB.js → activity-DAyekdlt2.js} +0 -0
- /package/client/dist/assets/{activity-D2r2h3SE2.js → activity-DCxtpT7j.js} +0 -0
- /package/client/dist/assets/{activity-DtxrAUMo2.js → activity-JzwRaWPD.js} +0 -0
- /package/client/dist/assets/{addspec-BMtLZLW62.js → addspec-BXjvkh5q.js} +0 -0
- /package/client/dist/assets/{addspec-CRuvWn8d.js → addspec-C4bwrmpi.js} +0 -0
- /package/client/dist/assets/{addspec-D-JLDsUV.js → addspec-CArHXUDM.js} +0 -0
- /package/client/dist/assets/{addspec-mgPOt5q02.js → addspec-DTKJcXiR.js} +0 -0
- /package/client/dist/assets/{addspec-DfhDbg5c.js → addspec-DbNBbrha2.js} +0 -0
- /package/client/dist/assets/{addspec-CP0HRuxY.js → addspec-htHGZHOe2.js} +0 -0
- /package/client/dist/assets/{addspec-bsNDdqRm2.js → addspec-lr_LP6jk.js} +0 -0
- /package/client/dist/assets/{agents-78bTZGZh.js → agents-6ZOJXWVk2.js} +0 -0
- /package/client/dist/assets/{agents-DDtCJ5M02.js → agents-BZ0XQKjg.js} +0 -0
- /package/client/dist/assets/{agents-CWo-KyAB.js → agents-BqWklnHR.js} +0 -0
- /package/client/dist/assets/{agents-BUdEi3-N.js → agents-Bz-thHkB2.js} +0 -0
- /package/client/dist/assets/{agents-bvyRLczL.js → agents-CJclElx6.js} +0 -0
- /package/client/dist/assets/{agents-BpoUaoW92.js → agents-D_l9L0VA.js} +0 -0
- /package/client/dist/assets/{agents-BLhWCmDS2.js → agents-u_C0jPB5.js} +0 -0
- /package/client/dist/assets/{agentstudio-BMQxc_bz.js → agentstudio-BF19hbXj.js} +0 -0
- /package/client/dist/assets/{agentstudio-C_Jq8Tl4.js → agentstudio-BLGvxWBx2.js} +0 -0
- /package/client/dist/assets/{agentstudio-Dtq3dGYk.js → agentstudio-D9UKiWTj2.js} +0 -0
- /package/client/dist/assets/{agentstudio-CWgXG5W52.js → agentstudio-Drs37nIM.js} +0 -0
- /package/client/dist/assets/{agentstudio-sI27TpBu2.js → agentstudio-nEnKkcXM.js} +0 -0
- /package/client/dist/assets/{agentstudio-jKMTdT70.js → agentstudio-pyFcSBKy.js} +0 -0
- /package/client/dist/assets/{agentstudio-COZriimI2.js → agentstudio-zfTzQh3U.js} +0 -0
- /package/client/dist/assets/{aiedit-hknZlly82.js → aiedit-B0KUbpB6.js} +0 -0
- /package/client/dist/assets/{aiedit-eMN4eQPF.js → aiedit-BoG24nlg2.js} +0 -0
- /package/client/dist/assets/{aiedit-pc5JDUq1.js → aiedit-CjlSRQVP.js} +0 -0
- /package/client/dist/assets/{aiedit-B03pD_Na.js → aiedit-DCjA7jLJ.js} +0 -0
- /package/client/dist/assets/{aiedit-6CHdb_iv2.js → aiedit-DNXxq_Dr.js} +0 -0
- /package/client/dist/assets/{aiedit-igmw-ZoZ2.js → aiedit-DQ-0_pvE.js} +0 -0
- /package/client/dist/assets/{aiedit-BtRqRxML.js → aiedit-olnv3QSt2.js} +0 -0
- /package/client/dist/assets/{analytics-CCprAqk3.js → analytics-BC7mwR4s.js} +0 -0
- /package/client/dist/assets/{analytics-CR1TJqUi2.js → analytics-BiNg8edP.js} +0 -0
- /package/client/dist/assets/{analytics-Bj-SmKHa.js → analytics-C0s_T2GN.js} +0 -0
- /package/client/dist/assets/{analytics-C5dpiS0o.js → analytics-CdsAPAwZ2.js} +0 -0
- /package/client/dist/assets/{analytics-BWMUZ2Pq2.js → analytics-Cr1_q-lC.js} +0 -0
- /package/client/dist/assets/{analytics-DfNdXR0y2.js → analytics-IEsKZvpW2.js} +0 -0
- /package/client/dist/assets/{analytics-rmHuJv0V.js → analytics-SRPf84OT.js} +0 -0
- /package/client/dist/assets/{apex-D_X_8J4d.js → apex-BWPQTe0t.js} +0 -0
- /package/client/dist/assets/{atom-one-dark-BMDPFwRZ.js → atom-one-dark-qiGhVQxg.js} +0 -0
- /package/client/dist/assets/{attachments-BgBNSqQd.js → attachments-D0wXXt9T2.js} +0 -0
- /package/client/dist/assets/{attachments-C9GTTJsx2.js → attachments-DenIjuVI.js} +0 -0
- /package/client/dist/assets/{attachments-BjyMmrwE.js → attachments-DhO7LPfy.js} +0 -0
- /package/client/dist/assets/{attachments-DPCmkFZu2.js → attachments-DoH5w0x4.js} +0 -0
- /package/client/dist/assets/{attachments-DCZOL1RU.js → attachments-Q9yNtiEM.js} +0 -0
- /package/client/dist/assets/{attachments-rNn1ekRy2.js → attachments-V_WZAYXx2.js} +0 -0
- /package/client/dist/assets/{attachments-DL-bPy9E.js → attachments-eaeScpjM.js} +0 -0
- /package/client/dist/assets/{auth-DpZq5p1e.js → auth-D_5bQv5W.js} +0 -0
- /package/client/dist/assets/{azcli-PZLnZ3jH.js → azcli-Bc_sGQ0U.js} +0 -0
- /package/client/dist/assets/{bat-KlBOxpO4.js → bat-i0X4ZdIN.js} +0 -0
- /package/client/dist/assets/{bicep-CgG9oF3V.js → bicep-B5-_aFwp.js} +0 -0
- /package/client/dist/assets/{browser-BPkhILSJ.js → browser-C25DkQiO.js} +0 -0
- /package/client/dist/assets/{browser-BqLqJVVT.js → browser-CiJ7-L32.js} +0 -0
- /package/client/dist/assets/{browser-rWKj-RR_2.js → browser-CvgDySd9.js} +0 -0
- /package/client/dist/assets/{browser-Cvg-QHvd2.js → browser-CyR1Kp3m2.js} +0 -0
- /package/client/dist/assets/{browser-FRv8P02E.js → browser-DwZljcRh2.js} +0 -0
- /package/client/dist/assets/{browser-Cc0iDqUi.js → browser-DxP-alBK.js} +0 -0
- /package/client/dist/assets/{browser-BijVYG0Y2.js → browser-rRCwi7Y7.js} +0 -0
- /package/client/dist/assets/{cameligo-6R45j6Ph.js → cameligo-DMUM7wLl.js} +0 -0
- /package/client/dist/assets/{chat-Bom6bqK3.js → chat-2rIB8HhT.js} +0 -0
- /package/client/dist/assets/{chat-vylxqR0P2.js → chat-BR8oe53x2.js} +0 -0
- /package/client/dist/assets/{chat-Dnwg-NpQ.js → chat-Bua03tty.js} +0 -0
- /package/client/dist/assets/{chat-BgL3zv6x2.js → chat-CZpAQan1.js} +0 -0
- /package/client/dist/assets/{chat-DsbwJhus.js → chat-CdY5SGvS.js} +0 -0
- /package/client/dist/assets/{chat-Bh3NdgJs.js → chat-DZyu3ImY2.js} +0 -0
- /package/client/dist/assets/{chat-DdwZjT8K2.js → chat-D_I7nU5K.js} +0 -0
- /package/client/dist/assets/{clojure-CLAAMO7J.js → clojure-Cm7r79vr.js} +0 -0
- /package/client/dist/assets/{clsx-CNmW0Nip.js → clsx-CjueKrWZ.js} +0 -0
- /package/client/dist/assets/{code-UPVvmAKM2.js → code-76gS0IdL.js} +0 -0
- /package/client/dist/assets/{code-BRcDlze52.js → code-C1VwxlML.js} +0 -0
- /package/client/dist/assets/{code-CeQ0FZpC.js → code-CnMZyJpr.js} +0 -0
- /package/client/dist/assets/{code-DgeHz9CB.js → code-D20ef9Cf2.js} +0 -0
- /package/client/dist/assets/{code-DzC1rMk12.js → code-DdOXaWRY.js} +0 -0
- /package/client/dist/assets/{code-CwnBPPyA.js → code-Duohuf9l.js} +0 -0
- /package/client/dist/assets/{code-BdMy3Ffq.js → code-geHmknB92.js} +0 -0
- /package/client/dist/assets/{coffee-AruSygo-.js → coffee-Ba7i2nA0.js} +0 -0
- /package/client/dist/assets/{commands-DkU0qliq.js → commands-B9RZO4qS.js} +0 -0
- /package/client/dist/assets/{commands-u-4eEHCN.js → commands-BEwbAjtD2.js} +0 -0
- /package/client/dist/assets/{commands-JWMfGDmr.js → commands-Bbxu8kLU.js} +0 -0
- /package/client/dist/assets/{commands-C4Y3C-PF2.js → commands-CDU3qv68.js} +0 -0
- /package/client/dist/assets/{commands-xHDDGfcP2.js → commands-CzezgtQj.js} +0 -0
- /package/client/dist/assets/{commands-CSDs9q_2.js → commands-DTQBF_q52.js} +0 -0
- /package/client/dist/assets/{commands-RHOchN4n2.js → commands-hOtKuOXc.js} +0 -0
- /package/client/dist/assets/{cpp-DEE5r4Pw.js → cpp-C7h46wYY.js} +0 -0
- /package/client/dist/assets/{csharp-BVXJUvbK.js → csharp-BKxtCVv1.js} +0 -0
- /package/client/dist/assets/{csp-cXOF0JoV.js → csp-bTuwJoIa.js} +0 -0
- /package/client/dist/assets/{css-C_zQuAoy.js → css-DIMkf-bt.js} +0 -0
- /package/client/dist/assets/{cypher-t95m_Q-O.js → cypher-CVaqCwHa.js} +0 -0
- /package/client/dist/assets/{dart-B4r1Tg-e.js → dart-onAF5SnQ.js} +0 -0
- /package/client/dist/assets/{dockerfile-6mt7zHQw.js → dockerfile-DZFCIeNp.js} +0 -0
- /package/client/dist/assets/{ecl-DEY_Fz_Q.js → ecl-D05T4iGw.js} +0 -0
- /package/client/dist/assets/{elixir-R6-q1c5t.js → elixir-6RTg0lbw.js} +0 -0
- /package/client/dist/assets/{explore-B7fblXBb2.js → explore-BG99VGpL.js} +0 -0
- /package/client/dist/assets/{explore-CNBzrEPS.js → explore-C0TjprJW2.js} +0 -0
- /package/client/dist/assets/{explore-C0k8HrkN.js → explore-C2dgEo7q.js} +0 -0
- /package/client/dist/assets/{explore-BHtEpoNN2.js → explore-C8NDHbob.js} +0 -0
- /package/client/dist/assets/{explore-Dahc1lOx.js → explore-CWWgzQGR.js} +0 -0
- /package/client/dist/assets/{explore-D_mWBYCS2.js → explore-CcQXpdtS.js} +0 -0
- /package/client/dist/assets/{explore-UvW1jEMa.js → explore-DsXAgnmJ2.js} +0 -0
- /package/client/dist/assets/{flow9-CNQvHBOt.js → flow9-C5_-GSwl.js} +0 -0
- /package/client/dist/assets/{format-command-CuWuRvka.js → format-command-Dfa1o2d-.js} +0 -0
- /package/client/dist/assets/{fsharp-vPZkFXwv.js → fsharp-C8Ef5oNN.js} +0 -0
- /package/client/dist/assets/{go-KUzBMYJ_.js → go-C-y9NEjX.js} +0 -0
- /package/client/dist/assets/{graphql-C3W0rCsD.js → graphql-fmXr3nnJ.js} +0 -0
- /package/client/dist/assets/{hcl-BOOMFSNX.js → hcl-CpzslTdj.js} +0 -0
- /package/client/dist/assets/{ini-CFat1WJr.js → ini-sBoK_t0W.js} +0 -0
- /package/client/dist/assets/{integrations-TAcYNh0b.js → integrations-CH0zy1Mw2.js} +0 -0
- /package/client/dist/assets/{integrations-C1ThkTC52.js → integrations-CIAjhd7L.js} +0 -0
- /package/client/dist/assets/{integrations-2YcNyAl3.js → integrations-CZdwOe8A.js} +0 -0
- /package/client/dist/assets/{integrations-BXCvXUlG2.js → integrations-Ct7omMld.js} +0 -0
- /package/client/dist/assets/{integrations-Crk2rFhL2.js → integrations-D6MSxy3-.js} +0 -0
- /package/client/dist/assets/{integrations-Dn4JiGtW.js → integrations-DbZa-Zyc.js} +0 -0
- /package/client/dist/assets/{integrations-D9YrJ_Ep.js → integrations-b8pH-vfl2.js} +0 -0
- /package/client/dist/assets/{java-Dee-CNAM.js → java-BEtHBSE6.js} +0 -0
- /package/client/dist/assets/{jira-D5efgwp_.js → jira-BySfUuQZ.js} +0 -0
- /package/client/dist/assets/{jira-bDs7Im2C.js → jira-Byw2C0F8.js} +0 -0
- /package/client/dist/assets/{jira-BHAKtc4B.js → jira-CNeYi06w2.js} +0 -0
- /package/client/dist/assets/{jira-Be369RIR2.js → jira-D11gPhxz.js} +0 -0
- /package/client/dist/assets/{jira-CPpgA-5z2.js → jira-D38Gj7AT.js} +0 -0
- /package/client/dist/assets/{jira-uNH1gETL2.js → jira-gAvOyije.js} +0 -0
- /package/client/dist/assets/{jira-DS7ownDy.js → jira-pRaL7JQo2.js} +0 -0
- /package/client/dist/assets/{julia-CGNlLYuU.js → julia-Bri6UV-V.js} +0 -0
- /package/client/dist/assets/{kotlin-CY7lhknJ.js → kotlin-BOotOW0E.js} +0 -0
- /package/client/dist/assets/{less-D0KN7hfJ.js → less-B9JPFI3C.js} +0 -0
- /package/client/dist/assets/{lexon-CuRJufPs.js → lexon-CfSJPG6W.js} +0 -0
- /package/client/dist/assets/{loops-LIgO_-eM.js → loops-CPpgMapU.js} +0 -0
- /package/client/dist/assets/{loops-DJNhp17p2.js → loops-CWWbKuVL.js} +0 -0
- /package/client/dist/assets/{loops-D6D3XHjj.js → loops-Crc9It8o2.js} +0 -0
- /package/client/dist/assets/{loops-CvOWsi5r.js → loops-DH2hZcbK2.js} +0 -0
- /package/client/dist/assets/{loops-vhJdbcz92.js → loops-DS5TmvIU2.js} +0 -0
- /package/client/dist/assets/{loops-K5_HXfJa.js → loops-P2f0cN8-.js} +0 -0
- /package/client/dist/assets/{loops-BvqQ-bHR2.js → loops-fJV-9hH7.js} +0 -0
- /package/client/dist/assets/{lua-DHvH62Zf.js → lua-CsQS60Ue.js} +0 -0
- /package/client/dist/assets/{m3-tlthQ8Fo.js → m3-D-oSqn_W.js} +0 -0
- /package/client/dist/assets/{markdown-Dbx1LgUZ.js → markdown-Cimd5fb3.js} +0 -0
- /package/client/dist/assets/{mcp-Dcl-XxkR.js → mcp-Czd_iyp02.js} +0 -0
- /package/client/dist/assets/{mcp-XJNRfxVp2.js → mcp-D2pc5GGx.js} +0 -0
- /package/client/dist/assets/{mcp-sRmWE8WJ.js → mcp-D4ZjtIx32.js} +0 -0
- /package/client/dist/assets/{mcp-DtbxuH3V2.js → mcp-DIfBUv2D2.js} +0 -0
- /package/client/dist/assets/{mcp-H-gnQUdN.js → mcp-DsTwmjST.js} +0 -0
- /package/client/dist/assets/{mcp-N1nzp8oL.js → mcp-SMkn8boG.js} +0 -0
- /package/client/dist/assets/{mcp-CjVKXAhD2.js → mcp-fSfiVYAc.js} +0 -0
- /package/client/dist/assets/{mips-D_O3IUz_.js → mips-CIPQ_RoX.js} +0 -0
- /package/client/dist/assets/{msdax-C_ZmCnRt.js → msdax-DauUninz.js} +0 -0
- /package/client/dist/assets/{mysql-C-dCEYSX.js → mysql-SOo6toE5.js} +0 -0
- /package/client/dist/assets/{nav-BElxljIF2.js → nav-BRJBxaGt2.js} +0 -0
- /package/client/dist/assets/{nav-COu2E_-L.js → nav-CAGKv3_F.js} +0 -0
- /package/client/dist/assets/{nav-BXO7PJJG2.js → nav-CNskRJKQ2.js} +0 -0
- /package/client/dist/assets/{nav-CW9pxWfC2.js → nav-DY93irBL.js} +0 -0
- /package/client/dist/assets/{nav-CXHUA4uf.js → nav-DhjvYgqt.js} +0 -0
- /package/client/dist/assets/{nav-DdHF5VWJ.js → nav-Gb-r4wop.js} +0 -0
- /package/client/dist/assets/{nav-C8kOuGh8.js → nav-TvumAFD22.js} +0 -0
- /package/client/dist/assets/{objective-c-DZY9Aqxd.js → objective-c-FvmIjYaQ.js} +0 -0
- /package/client/dist/assets/{pascal-BVHLXoor.js → pascal-DrH0SRf2.js} +0 -0
- /package/client/dist/assets/{pascaligo-XdG_6u3k.js → pascaligo-D-ptJ9y-.js} +0 -0
- /package/client/dist/assets/{perl-b9QV0BcD.js → perl-oz_6vUea.js} +0 -0
- /package/client/dist/assets/{pgsql-Du5FDOgT.js → pgsql-DTj74zXo.js} +0 -0
- /package/client/dist/assets/{php-WJD0z2Z_.js → php-nr791fC2.js} +0 -0
- /package/client/dist/assets/{pla-CbciTf2w.js → pla-CopQ2nXW.js} +0 -0
- /package/client/dist/assets/{postiats-C1QxmvVq.js → postiats-43DmfD33.js} +0 -0
- /package/client/dist/assets/{powerquery-BEYDhPLZ.js → powerquery-D3hlyOfw.js} +0 -0
- /package/client/dist/assets/{powershell-CfaYw4XL.js → powershell-DmHpPYUd.js} +0 -0
- /package/client/dist/assets/{protobuf-pwZEDOYa.js → protobuf-C531GsRP.js} +0 -0
- /package/client/dist/assets/{pug-5ftqSPVe.js → pug-Z5eAx3Zn.js} +0 -0
- /package/client/dist/assets/{qsharp-uI6qZMq4.js → qsharp-DkqhCAOL.js} +0 -0
- /package/client/dist/assets/{r-BK54P1EG.js → r-BwWrilGY.js} +0 -0
- /package/client/dist/assets/{redis-DI0yd7kN.js → redis-ClamHrr6.js} +0 -0
- /package/client/dist/assets/{redshift-Dov26eyI.js → redshift-DT7zqm-g.js} +0 -0
- /package/client/dist/assets/{restructuredtext-QqJ_1dC-.js → restructuredtext-BYgofb2h.js} +0 -0
- /package/client/dist/assets/{ruby-5WjveeVb.js → ruby-DezsRK8O.js} +0 -0
- /package/client/dist/assets/{rust-BzVSfqFH.js → rust-DdL9SqIa.js} +0 -0
- /package/client/dist/assets/{sb-DMIUfT-3.js → sb-CcwsVR0C.js} +0 -0
- /package/client/dist/assets/{scala-CSKA7lqt.js → scala-DHpiXF5c.js} +0 -0
- /package/client/dist/assets/{scheme-CojOP2Qc.js → scheme-BeGwcela.js} +0 -0
- /package/client/dist/assets/{scss-DoBZGJG6.js → scss-gp-XZpBa.js} +0 -0
- /package/client/dist/assets/{settings-1w1O5Og82.js → settings-B9ilOSYO.js} +0 -0
- /package/client/dist/assets/{settings-Jc46vUhd.js → settings-BqlWHRHA.js} +0 -0
- /package/client/dist/assets/{settings-BSihkr0e.js → settings-D8dlqt4d.js} +0 -0
- /package/client/dist/assets/{settings-BiDOB3hV2.js → settings-DAZqjX2a2.js} +0 -0
- /package/client/dist/assets/{settings-DNL68KTE.js → settings-DRh39v1S2.js} +0 -0
- /package/client/dist/assets/{settings-BoubJhDP2.js → settings-Mj1OJbfd2.js} +0 -0
- /package/client/dist/assets/{settings-CB-EZ_W8.js → settings-lRReaSYm.js} +0 -0
- /package/client/dist/assets/{setup-D84y3wx-.js → setup-CGZjy2IF2.js} +0 -0
- /package/client/dist/assets/{setup-BuQNKztt2.js → setup-CXa0Sx16.js} +0 -0
- /package/client/dist/assets/{setup-BwMovM9z2.js → setup-D2ktKKMF2.js} +0 -0
- /package/client/dist/assets/{setup-DU0ULX8q2.js → setup-DG2H7ha5.js} +0 -0
- /package/client/dist/assets/{setup-BXpw07r-.js → setup-DK5bHDUq2.js} +0 -0
- /package/client/dist/assets/{setup-7J8TjG4O.js → setup-DrWlIQTZ.js} +0 -0
- /package/client/dist/assets/{setup-BiduSX9d.js → setup-rzHUVgAu.js} +0 -0
- /package/client/dist/assets/{shell-YOaE4udl.js → shell-CC2rA5mh.js} +0 -0
- /package/client/dist/assets/{solidity-l3jSyVLu.js → solidity-BEEn4gHE.js} +0 -0
- /package/client/dist/assets/{sophia-BhSGTaQJ.js → sophia-CRfGWb83.js} +0 -0
- /package/client/dist/assets/{sparql-qhk6QD9w.js → sparql-D_Lu-MrJ.js} +0 -0
- /package/client/dist/assets/{specs-CWtKcVDC.js → specs-BcyxaCwf2.js} +0 -0
- /package/client/dist/assets/{specs-DTplt2XM.js → specs-C1q9geAi.js} +0 -0
- /package/client/dist/assets/{specs-PozUNhqb.js → specs-CLZKysq-2.js} +0 -0
- /package/client/dist/assets/{specs-C9Sd7BhH2.js → specs-DZMS9o54.js} +0 -0
- /package/client/dist/assets/{specs-D2clrt2c2.js → specs-Dm6zUI-R.js} +0 -0
- /package/client/dist/assets/{specs-CBDvEYKv.js → specs-V3TvhKLX.js} +0 -0
- /package/client/dist/assets/{specs-CIPr9reA2.js → specs-dV3Mq5Mu2.js} +0 -0
- /package/client/dist/assets/{sql-DoO3y21-.js → sql-NEE52Syq.js} +0 -0
- /package/client/dist/assets/{st-ChZAY8oG.js → st-DbInun42.js} +0 -0
- /package/client/dist/assets/{swift-D290Tr7_.js → swift-Bxkupp3x.js} +0 -0
- /package/client/dist/assets/{systemverilog-DsxzgJ-o.js → systemverilog-Bz4Y3fRF.js} +0 -0
- /package/client/dist/assets/{tcl-BKDUHAAR.js → tcl-DISqw1ZD.js} +0 -0
- /package/client/dist/assets/{terminal-De2Brd23.js → terminal-BZiIu3rf.js} +0 -0
- /package/client/dist/assets/{terminal-cqfd3EDV.js → terminal-C2NAd7BW2.js} +0 -0
- /package/client/dist/assets/{terminal-WSAvHXoU.js → terminal-IahE4LW92.js} +0 -0
- /package/client/dist/assets/{terminal-Z7KsDM_n.js → terminal-QpXZU2Bn2.js} +0 -0
- /package/client/dist/assets/{terminal-xGbCZJHg.js → terminal-ScjyXNxQ.js} +0 -0
- /package/client/dist/assets/{terminal-BRm03iFA2.js → terminal-d7GadXbN.js} +0 -0
- /package/client/dist/assets/{terminal-DxQWCqSD2.js → terminal-tITa81iz.js} +0 -0
- /package/client/dist/assets/{twig-UpZQaM8G.js → twig-De2hgUGE.js} +0 -0
- /package/client/dist/assets/{typespec-DzaKvN8i.js → typespec-B8J7ngcE.js} +0 -0
- /package/client/dist/assets/{vb-CZpbb7tk.js → vb-DV3o63ZY.js} +0 -0
- /package/client/dist/assets/{wgsl-CNt3OIvl.js → wgsl-DpFanUEy.js} +0 -0
|
@@ -3,17 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.HeadroomManager = exports.HEADROOM_MANAGED_PYTHON_VERSION = void 0;
|
|
6
|
+
exports.HeadroomManager = exports.HEADROOM_ACTIVATION_SOURCE = exports.HEADROOM_ACTIVATION_SCHEMA_VERSION = exports.HEADROOM_MANAGED_PYTHON_VERSION = exports.HEADROOM_RELAY_PATH = void 0;
|
|
7
7
|
exports.getHeadroomManagedInstallPlan = getHeadroomManagedInstallPlan;
|
|
8
|
+
exports.parseWindowsOwnershipSnapshot = parseWindowsOwnershipSnapshot;
|
|
9
|
+
exports.windowsSnapshotHasOwnedListener = windowsSnapshotHasOwnedListener;
|
|
10
|
+
exports.parseHeadroomDoctorRoutes = parseHeadroomDoctorRoutes;
|
|
8
11
|
const child_process_1 = require("child_process");
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
9
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const http_1 = __importDefault(require("http"));
|
|
15
|
+
const net_1 = __importDefault(require("net"));
|
|
10
16
|
const os_1 = __importDefault(require("os"));
|
|
11
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const auth_1 = require("./auth");
|
|
12
19
|
const desktop_db_1 = require("./desktop-db");
|
|
13
20
|
const headroom_routing_1 = require("./headroom-routing");
|
|
21
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
14
22
|
const STATE_KEY = 'plugins.headroom.state';
|
|
15
23
|
const DEFAULT_PORT = 8787;
|
|
24
|
+
const PROXY_HEALTH_REQUEST_TIMEOUT_MS = 750;
|
|
25
|
+
const RELAY_CONNECT_TIMEOUT_MS = 3_000;
|
|
26
|
+
exports.HEADROOM_RELAY_PATH = '/_specrails/headroom';
|
|
16
27
|
exports.HEADROOM_MANAGED_PYTHON_VERSION = '3.12';
|
|
28
|
+
exports.HEADROOM_ACTIVATION_SCHEMA_VERSION = 1;
|
|
29
|
+
exports.HEADROOM_ACTIVATION_SOURCE = 'explicit-user-action';
|
|
17
30
|
function nowIso() {
|
|
18
31
|
return new Date().toISOString();
|
|
19
32
|
}
|
|
@@ -209,6 +222,290 @@ function classifyProxyFailure(output, command) {
|
|
|
209
222
|
}
|
|
210
223
|
return issue('proxy_unhealthy', output, command);
|
|
211
224
|
}
|
|
225
|
+
function positiveInteger(value) {
|
|
226
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
227
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
228
|
+
}
|
|
229
|
+
/** Parse the language-independent JSON emitted by the Windows ownership probe. */
|
|
230
|
+
function parseWindowsOwnershipSnapshot(raw) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, '').trim());
|
|
233
|
+
const listeningValues = Array.isArray(parsed.listeningPids)
|
|
234
|
+
? parsed.listeningPids
|
|
235
|
+
: parsed.listeningPids == null
|
|
236
|
+
? []
|
|
237
|
+
: [parsed.listeningPids];
|
|
238
|
+
const processValues = Array.isArray(parsed.processes)
|
|
239
|
+
? parsed.processes
|
|
240
|
+
: parsed.processes && typeof parsed.processes === 'object'
|
|
241
|
+
? [parsed.processes]
|
|
242
|
+
: [];
|
|
243
|
+
const listeningPids = new Set();
|
|
244
|
+
for (const value of listeningValues) {
|
|
245
|
+
const pid = positiveInteger(value);
|
|
246
|
+
if (pid)
|
|
247
|
+
listeningPids.add(pid);
|
|
248
|
+
}
|
|
249
|
+
const parentByPid = new Map();
|
|
250
|
+
for (const value of processValues) {
|
|
251
|
+
if (!value || typeof value !== 'object')
|
|
252
|
+
continue;
|
|
253
|
+
const row = value;
|
|
254
|
+
const pid = positiveInteger(row.ProcessId);
|
|
255
|
+
const parentPid = positiveInteger(row.ParentProcessId);
|
|
256
|
+
if (pid && parentPid)
|
|
257
|
+
parentByPid.set(pid, parentPid);
|
|
258
|
+
}
|
|
259
|
+
return { listeningPids, parentByPid };
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** True only when a listening PID is the spawned process or one of its descendants. */
|
|
266
|
+
function windowsSnapshotHasOwnedListener(snapshot, rootPid) {
|
|
267
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0)
|
|
268
|
+
return false;
|
|
269
|
+
for (const listenerPid of snapshot.listeningPids) {
|
|
270
|
+
let current = listenerPid;
|
|
271
|
+
const visited = new Set();
|
|
272
|
+
while (current > 0 && !visited.has(current)) {
|
|
273
|
+
if (current === rootPid)
|
|
274
|
+
return true;
|
|
275
|
+
visited.add(current);
|
|
276
|
+
current = snapshot.parentByPid.get(current) ?? 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
function descendantPids(rootPid, parentByPid) {
|
|
282
|
+
const descendants = new Set([rootPid]);
|
|
283
|
+
let changed = true;
|
|
284
|
+
while (changed) {
|
|
285
|
+
changed = false;
|
|
286
|
+
for (const [pid, parentPid] of parentByPid) {
|
|
287
|
+
if (descendants.has(pid) || !descendants.has(parentPid))
|
|
288
|
+
continue;
|
|
289
|
+
descendants.add(pid);
|
|
290
|
+
changed = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return descendants;
|
|
294
|
+
}
|
|
295
|
+
async function captureCommandOutput(command, args, options) {
|
|
296
|
+
return await new Promise((resolve) => {
|
|
297
|
+
let child;
|
|
298
|
+
try {
|
|
299
|
+
child = (0, child_process_1.spawn)(command, args, {
|
|
300
|
+
env: options.env ?? process.env,
|
|
301
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
302
|
+
windowsHide: options.windowsHide,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
resolve(null);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let output = '';
|
|
310
|
+
let size = 0;
|
|
311
|
+
let settled = false;
|
|
312
|
+
const maxBuffer = options.maxBuffer ?? 2 * 1024 * 1024;
|
|
313
|
+
const settle = (value) => {
|
|
314
|
+
if (settled)
|
|
315
|
+
return;
|
|
316
|
+
settled = true;
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
resolve(value);
|
|
319
|
+
};
|
|
320
|
+
const timer = setTimeout(() => {
|
|
321
|
+
try {
|
|
322
|
+
child.kill('SIGKILL');
|
|
323
|
+
}
|
|
324
|
+
catch { /* already gone */ }
|
|
325
|
+
settle(null);
|
|
326
|
+
}, options.timeoutMs);
|
|
327
|
+
timer.unref?.();
|
|
328
|
+
child.stdout?.on('data', (chunk) => {
|
|
329
|
+
if (settled)
|
|
330
|
+
return;
|
|
331
|
+
const value = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
332
|
+
size += Buffer.byteLength(value);
|
|
333
|
+
if (size > maxBuffer) {
|
|
334
|
+
try {
|
|
335
|
+
child.kill('SIGKILL');
|
|
336
|
+
}
|
|
337
|
+
catch { /* already gone */ }
|
|
338
|
+
settle(null);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
output += value;
|
|
342
|
+
});
|
|
343
|
+
child.once('error', () => settle(null));
|
|
344
|
+
child.once('close', (code) => settle(code === 0 ? output : null));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async function readLinuxDescendantPids(rootPid) {
|
|
348
|
+
const descendants = new Set([rootPid]);
|
|
349
|
+
const pending = [rootPid];
|
|
350
|
+
while (pending.length > 0) {
|
|
351
|
+
const current = pending.shift();
|
|
352
|
+
try {
|
|
353
|
+
const taskIds = await fs_1.default.promises.readdir(`/proc/${current}/task`);
|
|
354
|
+
for (const taskId of taskIds) {
|
|
355
|
+
let raw;
|
|
356
|
+
try {
|
|
357
|
+
raw = await fs_1.default.promises.readFile(`/proc/${current}/task/${taskId}/children`, 'utf8');
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
for (const value of raw.trim().split(/\s+/)) {
|
|
363
|
+
const childPid = positiveInteger(value);
|
|
364
|
+
if (!childPid || descendants.has(childPid))
|
|
365
|
+
continue;
|
|
366
|
+
descendants.add(childPid);
|
|
367
|
+
pending.push(childPid);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// A process can exit while its descendants are enumerated. Omitting that
|
|
373
|
+
// branch is fail-closed for listener ownership.
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return descendants;
|
|
377
|
+
}
|
|
378
|
+
async function readPosixParentMap() {
|
|
379
|
+
const args = ['-axo', 'pid=,ppid='];
|
|
380
|
+
const raw = await captureCommandOutput('/bin/ps', args, { timeoutMs: 2_000 })
|
|
381
|
+
?? await captureCommandOutput('/usr/bin/ps', args, { timeoutMs: 2_000 });
|
|
382
|
+
const parentByPid = new Map();
|
|
383
|
+
if (raw === null)
|
|
384
|
+
return parentByPid;
|
|
385
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
386
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
|
|
387
|
+
const pid = positiveInteger(match?.[1]);
|
|
388
|
+
const parentPid = positiveInteger(match?.[2]);
|
|
389
|
+
if (pid && parentPid)
|
|
390
|
+
parentByPid.set(pid, parentPid);
|
|
391
|
+
}
|
|
392
|
+
return parentByPid;
|
|
393
|
+
}
|
|
394
|
+
async function windowsProcessOwnsListeningPort(pid, port) {
|
|
395
|
+
const env = (0, win_spawn_1.windowsSpawnEnv)();
|
|
396
|
+
const systemRoot = (env.SystemRoot || env.windir || 'C:\\Windows').replace(/[\\/]$/, '');
|
|
397
|
+
const powershell = path_1.default.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
398
|
+
// uv-managed console entry points are native trampoline processes on
|
|
399
|
+
// Windows. The Python process that owns the socket is their child, so an
|
|
400
|
+
// exact-PID netstat comparison rejects every legitimate managed install.
|
|
401
|
+
// Query both the listener owner and the process tree in one fail-closed
|
|
402
|
+
// snapshot, then accept only a transitive descendant of our spawned PID.
|
|
403
|
+
const command = [
|
|
404
|
+
"$ErrorActionPreference = 'Stop'",
|
|
405
|
+
`$listeningPids = @(Get-NetTCPConnection -State Listen -LocalPort ${port} | ForEach-Object { [int]$_.OwningProcess })`,
|
|
406
|
+
'$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId)',
|
|
407
|
+
'[pscustomobject]@{ listeningPids = $listeningPids; processes = $processes } | ConvertTo-Json -Compress -Depth 4',
|
|
408
|
+
].join('; ');
|
|
409
|
+
const raw = await captureCommandOutput(powershell, [
|
|
410
|
+
'-NoLogo',
|
|
411
|
+
'-NoProfile',
|
|
412
|
+
'-NonInteractive',
|
|
413
|
+
'-Command',
|
|
414
|
+
command,
|
|
415
|
+
], {
|
|
416
|
+
// Keep this below the relay's connection deadline. A slow/ambiguous OS
|
|
417
|
+
// probe rejects the connection without ever releasing its queued headers.
|
|
418
|
+
timeoutMs: 2_500,
|
|
419
|
+
windowsHide: true,
|
|
420
|
+
env,
|
|
421
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
422
|
+
});
|
|
423
|
+
if (raw === null)
|
|
424
|
+
return false;
|
|
425
|
+
const snapshot = parseWindowsOwnershipSnapshot(raw);
|
|
426
|
+
return !!snapshot && windowsSnapshotHasOwnedListener(snapshot, pid);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* A successful HTTP probe is not proof that the process we spawned owns the
|
|
430
|
+
* endpoint: another local process can bind between the preflight probe and the
|
|
431
|
+
* child calling bind(2). Verify the listening socket belongs to the owned PID
|
|
432
|
+
* before provider credentials are routed to it.
|
|
433
|
+
*/
|
|
434
|
+
async function processOwnsListeningPort(pid, port) {
|
|
435
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
436
|
+
return false;
|
|
437
|
+
if (process.platform === 'linux') {
|
|
438
|
+
try {
|
|
439
|
+
const listeningInodes = new Set();
|
|
440
|
+
const tables = await Promise.all(['/proc/net/tcp', '/proc/net/tcp6'].map(async (table) => {
|
|
441
|
+
try {
|
|
442
|
+
return await fs_1.default.promises.readFile(table, 'utf8');
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
}));
|
|
448
|
+
for (const raw of tables) {
|
|
449
|
+
for (const line of raw.split(/\r?\n/).slice(1)) {
|
|
450
|
+
const columns = line.trim().split(/\s+/);
|
|
451
|
+
if (columns.length < 10 || columns[3] !== '0A')
|
|
452
|
+
continue;
|
|
453
|
+
const portHex = columns[1]?.split(':').pop();
|
|
454
|
+
if (!portHex || Number.parseInt(portHex, 16) !== port)
|
|
455
|
+
continue;
|
|
456
|
+
if (columns[9])
|
|
457
|
+
listeningInodes.add(columns[9]);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (listeningInodes.size === 0)
|
|
461
|
+
return false;
|
|
462
|
+
const ownedPids = await readLinuxDescendantPids(pid);
|
|
463
|
+
for (const ownedPid of ownedPids) {
|
|
464
|
+
let fds;
|
|
465
|
+
try {
|
|
466
|
+
fds = await fs_1.default.promises.readdir(`/proc/${ownedPid}/fd`);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
for (const fd of fds) {
|
|
472
|
+
let target;
|
|
473
|
+
try {
|
|
474
|
+
target = await fs_1.default.promises.readlink(`/proc/${ownedPid}/fd/${fd}`);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const match = target.match(/^socket:\[(\d+)\]$/);
|
|
480
|
+
if (match?.[1] && listeningInodes.has(match[1]))
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (process.platform === 'win32') {
|
|
491
|
+
return await windowsProcessOwnsListeningPort(pid, port);
|
|
492
|
+
}
|
|
493
|
+
const args = [
|
|
494
|
+
'-nP',
|
|
495
|
+
`-iTCP:${port}`,
|
|
496
|
+
'-sTCP:LISTEN',
|
|
497
|
+
'-Fp',
|
|
498
|
+
];
|
|
499
|
+
const output = await captureCommandOutput('/usr/sbin/lsof', args, { timeoutMs: 2_000 })
|
|
500
|
+
?? await captureCommandOutput('lsof', args, { timeoutMs: 2_000 });
|
|
501
|
+
if (output === null)
|
|
502
|
+
return false;
|
|
503
|
+
const ownedPids = descendantPids(pid, await readPosixParentMap());
|
|
504
|
+
return output.split(/\r?\n/).some((line) => {
|
|
505
|
+
const listenerPid = positiveInteger(line.match(/^p(\d+)$/)?.[1]);
|
|
506
|
+
return !!listenerPid && ownedPids.has(listenerPid);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
212
509
|
function providerLabel(provider) {
|
|
213
510
|
return provider === 'codex' ? 'Codex' : 'Claude';
|
|
214
511
|
}
|
|
@@ -260,6 +557,33 @@ function normalizeProviderKey(value) {
|
|
|
260
557
|
return 'claude';
|
|
261
558
|
return null;
|
|
262
559
|
}
|
|
560
|
+
/** Parse only the documented pass/fail signal; human prose is never authority. */
|
|
561
|
+
function parseHeadroomDoctorRoutes(raw) {
|
|
562
|
+
const routes = { codex: false, claude: false };
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(raw);
|
|
565
|
+
if (!Array.isArray(parsed.checks))
|
|
566
|
+
return routes;
|
|
567
|
+
for (const value of parsed.checks) {
|
|
568
|
+
if (!value || typeof value !== 'object')
|
|
569
|
+
continue;
|
|
570
|
+
const check = value;
|
|
571
|
+
const provider = normalizeProviderKey(check.name);
|
|
572
|
+
if (!provider || check.status !== 'pass')
|
|
573
|
+
continue;
|
|
574
|
+
const summary = typeof check.summary === 'string' ? check.summary.trim().toLowerCase() : '';
|
|
575
|
+
// Some Headroom versions have emitted a successful diagnostic check with
|
|
576
|
+
// text such as "not routed". A contradictory result must fail closed.
|
|
577
|
+
if (/\bnot(?:\s+\w+){0,3}\s+routed\b|\bunrouted\b|\brouting\s+(?:is\s+)?(?:disabled|inactive|off)\b/.test(summary))
|
|
578
|
+
continue;
|
|
579
|
+
routes[provider] = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Malformed/unknown doctor schemas are not evidence of a configured route.
|
|
584
|
+
}
|
|
585
|
+
return routes;
|
|
586
|
+
}
|
|
263
587
|
function num(value) {
|
|
264
588
|
const parsed = Number(value);
|
|
265
589
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
@@ -295,18 +619,44 @@ class HeadroomManager {
|
|
|
295
619
|
db;
|
|
296
620
|
broadcast;
|
|
297
621
|
availableProvidersSupplier;
|
|
622
|
+
processControl;
|
|
298
623
|
proxy = null;
|
|
624
|
+
proxyTrustedPort = null;
|
|
625
|
+
proxyStart = null;
|
|
626
|
+
proxyStop = null;
|
|
627
|
+
proxyGeneration = 0;
|
|
628
|
+
shuttingDown = false;
|
|
629
|
+
shutdownPromise = null;
|
|
630
|
+
shutdownState = null;
|
|
631
|
+
lifecycleTail = Promise.resolve();
|
|
632
|
+
lifecycleCommands = new Map();
|
|
299
633
|
proxyTail = '';
|
|
634
|
+
relayToken = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
635
|
+
relayAgent;
|
|
636
|
+
relayUpgradeTeardowns = new Set();
|
|
300
637
|
routeDetectionCache = null;
|
|
301
638
|
metricsCache = emptyMetricsState();
|
|
302
639
|
metricsRefresh = null;
|
|
303
|
-
constructor(db, broadcast, availableProvidersSupplier = () => ['claude', 'codex']) {
|
|
640
|
+
constructor(db, broadcast, availableProvidersSupplier = () => ['claude', 'codex'], processControl = {}) {
|
|
304
641
|
this.db = db;
|
|
305
642
|
this.broadcast = broadcast;
|
|
306
643
|
this.availableProvidersSupplier = availableProvidersSupplier;
|
|
644
|
+
this.processControl = processControl;
|
|
645
|
+
this.relayAgent = new http_1.default.Agent({ keepAlive: true, maxSockets: 16, maxFreeSockets: 4 });
|
|
646
|
+
// Agent#createConnection supports an asynchronous callback. Returning no
|
|
647
|
+
// socket prevents ClientRequest from writing queued headers/body on TCP
|
|
648
|
+
// connect; the socket is handed over only after the post-connect PID check.
|
|
649
|
+
this.relayAgent.createConnection = ((options, callback) => {
|
|
650
|
+
const expectedPort = Number(options.port);
|
|
651
|
+
void this.connectVerifiedBackend(expectedPort).then(({ socket }) => callback?.(null, socket), (err) => callback?.(err, undefined));
|
|
652
|
+
return undefined;
|
|
653
|
+
});
|
|
307
654
|
this.syncRouting();
|
|
308
655
|
}
|
|
309
656
|
getState() {
|
|
657
|
+
return this.buildState(true);
|
|
658
|
+
}
|
|
659
|
+
buildState(refreshMetrics) {
|
|
310
660
|
const persisted = this.readPersisted();
|
|
311
661
|
const uvPath = this.resolveUvPath();
|
|
312
662
|
const install = this.resolveHeadroomInstall(persisted.executablePath ?? null, !!persisted.ignoreSystemInstall);
|
|
@@ -315,10 +665,7 @@ class HeadroomManager {
|
|
|
315
665
|
const installed = !!exe && !!version;
|
|
316
666
|
const availableProviders = this.availableProviderRecord();
|
|
317
667
|
const detectedRoutes = installed && exe ? this.detectProviderRoutes(exe) : providerRecord(persisted.detectedRoutes);
|
|
318
|
-
const activeProviders =
|
|
319
|
-
codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
|
|
320
|
-
claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
|
|
321
|
-
};
|
|
668
|
+
const activeProviders = this.explicitActiveProviders(persisted);
|
|
322
669
|
if (installed) {
|
|
323
670
|
const shouldPersistInstall = persisted.installed !== true ||
|
|
324
671
|
persisted.executablePath !== exe ||
|
|
@@ -326,9 +673,7 @@ class HeadroomManager {
|
|
|
326
673
|
persisted.installSource !== install?.source ||
|
|
327
674
|
persisted.ignoreSystemInstall === true;
|
|
328
675
|
const shouldPersistRoutes = persisted.detectedRoutes?.codex !== detectedRoutes.codex ||
|
|
329
|
-
persisted.detectedRoutes?.claude !== detectedRoutes.claude
|
|
330
|
-
persisted.activeProviders?.codex !== activeProviders.codex ||
|
|
331
|
-
persisted.activeProviders?.claude !== activeProviders.claude;
|
|
676
|
+
persisted.detectedRoutes?.claude !== detectedRoutes.claude;
|
|
332
677
|
if (shouldPersistInstall || shouldPersistRoutes) {
|
|
333
678
|
this.updatePersisted({
|
|
334
679
|
installed: true,
|
|
@@ -337,11 +682,11 @@ class HeadroomManager {
|
|
|
337
682
|
installSource: install?.source ?? null,
|
|
338
683
|
ignoreSystemInstall: false,
|
|
339
684
|
detectedRoutes,
|
|
340
|
-
activeProviders,
|
|
341
685
|
});
|
|
342
686
|
}
|
|
343
687
|
}
|
|
344
|
-
|
|
688
|
+
if (refreshMetrics)
|
|
689
|
+
this.refreshMetricsInBackground();
|
|
345
690
|
const learning = {
|
|
346
691
|
enabled: persisted.learningEnabled ?? installed,
|
|
347
692
|
...this.readLearningBaseline(),
|
|
@@ -369,10 +714,357 @@ class HeadroomManager {
|
|
|
369
714
|
};
|
|
370
715
|
}
|
|
371
716
|
async getFreshState() {
|
|
717
|
+
if (this.shuttingDown)
|
|
718
|
+
return this.shutdownState ?? this.buildState(false);
|
|
372
719
|
await this.refreshMetrics();
|
|
373
720
|
return this.getState();
|
|
374
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Stream a provider request through the stable desktop listener. A TCP
|
|
724
|
+
* connection to the backend is established first, then ownership is verified,
|
|
725
|
+
* and only then are headers/body (including provider credentials) written.
|
|
726
|
+
* A process that rebinds the backend port can therefore receive a TCP handshake
|
|
727
|
+
* but never a secret-bearing byte.
|
|
728
|
+
*/
|
|
729
|
+
async handleRelayRequest(req, res) {
|
|
730
|
+
const upstreamPath = this.relayUpstreamPath(req.url);
|
|
731
|
+
if (!upstreamPath) {
|
|
732
|
+
this.writeRelayError(res, 404, 'Not Found');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const backendPort = this.trustedBackendPort();
|
|
736
|
+
if (!backendPort) {
|
|
737
|
+
this.writeRelayError(res, 503, 'Headroom proxy is unavailable');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const headers = this.filterRelayHttpHeaders(req.headers);
|
|
741
|
+
headers.host = `127.0.0.1:${backendPort}`;
|
|
742
|
+
let upstreamResponse = null;
|
|
743
|
+
let relaySettled = false;
|
|
744
|
+
const upstream = http_1.default.request({
|
|
745
|
+
host: '127.0.0.1',
|
|
746
|
+
port: backendPort,
|
|
747
|
+
method: req.method,
|
|
748
|
+
path: upstreamPath,
|
|
749
|
+
headers,
|
|
750
|
+
agent: this.relayAgent,
|
|
751
|
+
}, (response) => {
|
|
752
|
+
upstreamResponse = response;
|
|
753
|
+
if (res.destroyed) {
|
|
754
|
+
abortRelay(false);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
response.once('aborted', () => abortRelay(true));
|
|
758
|
+
response.once('error', () => abortRelay(true));
|
|
759
|
+
response.once('close', () => {
|
|
760
|
+
// IncomingMessage#close is also emitted after a fully-consumed response.
|
|
761
|
+
// Only an incomplete HTTP message is a broken backend stream.
|
|
762
|
+
if (!response.complete)
|
|
763
|
+
abortRelay(true);
|
|
764
|
+
});
|
|
765
|
+
response.once('end', settleCleanRelay);
|
|
766
|
+
res.writeHead(response.statusCode ?? 502, this.filterRelayHttpHeaders(response.headers));
|
|
767
|
+
response.pipe(res);
|
|
768
|
+
});
|
|
769
|
+
const abortRelay = (notifyClient) => {
|
|
770
|
+
if (relaySettled)
|
|
771
|
+
return;
|
|
772
|
+
relaySettled = true;
|
|
773
|
+
req.unpipe(upstream);
|
|
774
|
+
upstreamResponse?.unpipe(res);
|
|
775
|
+
upstreamResponse?.destroy();
|
|
776
|
+
upstream.destroy();
|
|
777
|
+
if (!notifyClient || res.destroyed || res.writableFinished)
|
|
778
|
+
return;
|
|
779
|
+
if (!res.headersSent)
|
|
780
|
+
this.writeRelayError(res, 503, 'Headroom proxy request failed');
|
|
781
|
+
else
|
|
782
|
+
res.destroy();
|
|
783
|
+
};
|
|
784
|
+
const settleCleanRelay = () => {
|
|
785
|
+
if (req.complete &&
|
|
786
|
+
upstreamResponse?.complete &&
|
|
787
|
+
res.writableFinished)
|
|
788
|
+
relaySettled = true;
|
|
789
|
+
};
|
|
790
|
+
upstream.once('error', () => abortRelay(true));
|
|
791
|
+
upstream.once('close', () => {
|
|
792
|
+
if (!upstreamResponse)
|
|
793
|
+
abortRelay(true);
|
|
794
|
+
});
|
|
795
|
+
req.once('aborted', () => abortRelay(false));
|
|
796
|
+
req.once('error', () => abortRelay(false));
|
|
797
|
+
req.once('close', () => {
|
|
798
|
+
// A normal IncomingMessage closes after the complete request body. Do not
|
|
799
|
+
// confuse that with a client disconnect while a valid response streams.
|
|
800
|
+
if (!req.complete)
|
|
801
|
+
abortRelay(false);
|
|
802
|
+
});
|
|
803
|
+
req.once('end', settleCleanRelay);
|
|
804
|
+
res.once('error', () => abortRelay(false));
|
|
805
|
+
res.once('close', () => {
|
|
806
|
+
if (!res.writableFinished)
|
|
807
|
+
abortRelay(false);
|
|
808
|
+
});
|
|
809
|
+
res.once('finish', settleCleanRelay);
|
|
810
|
+
req.pipe(upstream);
|
|
811
|
+
}
|
|
812
|
+
/** WebSocket equivalent of handleRelayRequest (used by Codex streaming). */
|
|
813
|
+
async handleRelayUpgrade(req, clientSocket, head) {
|
|
814
|
+
const upstreamPath = this.relayUpstreamPath(req.url);
|
|
815
|
+
if (!upstreamPath) {
|
|
816
|
+
this.rejectRelayUpgrade(clientSocket, 404, 'Not Found');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
let backend;
|
|
820
|
+
try {
|
|
821
|
+
backend = await this.connectVerifiedBackend();
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
this.rejectRelayUpgrade(clientSocket, 503, 'Headroom proxy unavailable');
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (clientSocket.destroyed) {
|
|
828
|
+
backend.socket.destroy();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const raw = [`${req.method ?? 'GET'} ${upstreamPath} HTTP/${req.httpVersion}`];
|
|
832
|
+
const connectionTokens = this.connectionHeaderTokens(req.headers);
|
|
833
|
+
let wroteHost = false;
|
|
834
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
835
|
+
const name = req.rawHeaders[i];
|
|
836
|
+
const value = req.rawHeaders[i + 1];
|
|
837
|
+
if (!name || value === undefined)
|
|
838
|
+
continue;
|
|
839
|
+
const lower = name.toLowerCase();
|
|
840
|
+
if (lower === 'host') {
|
|
841
|
+
if (!wroteHost)
|
|
842
|
+
raw.push(`Host: 127.0.0.1:${backend.port}`);
|
|
843
|
+
wroteHost = true;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (lower === 'connection' ||
|
|
847
|
+
lower === 'proxy-connection' ||
|
|
848
|
+
lower === 'keep-alive' ||
|
|
849
|
+
lower === 'transfer-encoding' ||
|
|
850
|
+
lower === 'te' ||
|
|
851
|
+
lower === 'trailer' ||
|
|
852
|
+
lower === 'proxy-authenticate' ||
|
|
853
|
+
lower === 'proxy-authorization' ||
|
|
854
|
+
(connectionTokens.has(lower) && lower !== 'upgrade'))
|
|
855
|
+
continue;
|
|
856
|
+
raw.push(`${name}: ${value}`);
|
|
857
|
+
}
|
|
858
|
+
if (!wroteHost)
|
|
859
|
+
raw.push(`Host: 127.0.0.1:${backend.port}`);
|
|
860
|
+
raw.push('Connection: Upgrade');
|
|
861
|
+
raw.push('', '');
|
|
862
|
+
const backendSocket = backend.socket;
|
|
863
|
+
let clientEnded = false;
|
|
864
|
+
let backendEnded = false;
|
|
865
|
+
let clientClosed = false;
|
|
866
|
+
let backendClosed = false;
|
|
867
|
+
let tearingDown = false;
|
|
868
|
+
const cleanup = () => {
|
|
869
|
+
this.relayUpgradeTeardowns.delete(destroyBoth);
|
|
870
|
+
clientSocket.unpipe(backendSocket);
|
|
871
|
+
backendSocket.unpipe(clientSocket);
|
|
872
|
+
clientSocket.removeListener('error', destroyBoth);
|
|
873
|
+
backendSocket.removeListener('error', destroyBoth);
|
|
874
|
+
clientSocket.removeListener('end', onClientEnd);
|
|
875
|
+
backendSocket.removeListener('end', onBackendEnd);
|
|
876
|
+
clientSocket.removeListener('close', onClientClose);
|
|
877
|
+
backendSocket.removeListener('close', onBackendClose);
|
|
878
|
+
};
|
|
879
|
+
const destroyBoth = () => {
|
|
880
|
+
if (tearingDown)
|
|
881
|
+
return;
|
|
882
|
+
tearingDown = true;
|
|
883
|
+
cleanup();
|
|
884
|
+
if (!clientSocket.destroyed)
|
|
885
|
+
clientSocket.destroy();
|
|
886
|
+
if (!backendSocket.destroyed)
|
|
887
|
+
backendSocket.destroy();
|
|
888
|
+
};
|
|
889
|
+
const finishIfClosed = () => {
|
|
890
|
+
if (clientClosed && backendClosed)
|
|
891
|
+
cleanup();
|
|
892
|
+
};
|
|
893
|
+
const onClientEnd = () => {
|
|
894
|
+
clientEnded = true;
|
|
895
|
+
if (!backendSocket.destroyed && !backendSocket.writableEnded)
|
|
896
|
+
backendSocket.end();
|
|
897
|
+
};
|
|
898
|
+
const onBackendEnd = () => {
|
|
899
|
+
backendEnded = true;
|
|
900
|
+
if (!clientSocket.destroyed && !clientSocket.writableEnded)
|
|
901
|
+
clientSocket.end();
|
|
902
|
+
};
|
|
903
|
+
const onClientClose = () => {
|
|
904
|
+
clientClosed = true;
|
|
905
|
+
if (!clientEnded)
|
|
906
|
+
destroyBoth();
|
|
907
|
+
else
|
|
908
|
+
finishIfClosed();
|
|
909
|
+
};
|
|
910
|
+
const onBackendClose = () => {
|
|
911
|
+
backendClosed = true;
|
|
912
|
+
if (!backendEnded)
|
|
913
|
+
destroyBoth();
|
|
914
|
+
else
|
|
915
|
+
finishIfClosed();
|
|
916
|
+
};
|
|
917
|
+
this.relayUpgradeTeardowns.add(destroyBoth);
|
|
918
|
+
clientSocket.once('error', destroyBoth);
|
|
919
|
+
backendSocket.once('error', destroyBoth);
|
|
920
|
+
clientSocket.once('end', onClientEnd);
|
|
921
|
+
backendSocket.once('end', onBackendEnd);
|
|
922
|
+
clientSocket.once('close', onClientClose);
|
|
923
|
+
backendSocket.once('close', onBackendClose);
|
|
924
|
+
if (clientSocket.destroyed || backendSocket.destroyed) {
|
|
925
|
+
destroyBoth();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
backendSocket.write(raw.join('\r\n'));
|
|
930
|
+
if (head.length > 0)
|
|
931
|
+
backendSocket.write(head);
|
|
932
|
+
// Preserve a clean half-close so a peer that has finished sending can
|
|
933
|
+
// still receive the other direction's final frames. Premature close or
|
|
934
|
+
// error takes the idempotent destroyBoth path above.
|
|
935
|
+
clientSocket.pipe(backendSocket, { end: false });
|
|
936
|
+
backendSocket.pipe(clientSocket, { end: false });
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
destroyBoth();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
relayUpstreamPath(rawUrl) {
|
|
943
|
+
try {
|
|
944
|
+
const parsed = new URL(rawUrl || '/', 'http://127.0.0.1');
|
|
945
|
+
const fullPrefix = `${exports.HEADROOM_RELAY_PATH}/`;
|
|
946
|
+
let relative = parsed.pathname.startsWith(fullPrefix)
|
|
947
|
+
? parsed.pathname.slice(exports.HEADROOM_RELAY_PATH.length)
|
|
948
|
+
: parsed.pathname;
|
|
949
|
+
const separator = relative.indexOf('/', 1);
|
|
950
|
+
const providedToken = separator === -1 ? relative.slice(1) : relative.slice(1, separator);
|
|
951
|
+
if (!providedToken || !(0, auth_1.safeEqual)(providedToken, this.relayToken))
|
|
952
|
+
return null;
|
|
953
|
+
relative = separator === -1 ? '/' : relative.slice(separator);
|
|
954
|
+
return `${relative}${parsed.search}`;
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
trustedBackendPort(expectedPort) {
|
|
961
|
+
const child = this.proxy;
|
|
962
|
+
const port = this.proxyTrustedPort;
|
|
963
|
+
if (!child || !port || !child.pid ||
|
|
964
|
+
(expectedPort !== undefined && port !== expectedPort) ||
|
|
965
|
+
child.exitCode != null || child.signalCode != null)
|
|
966
|
+
return null;
|
|
967
|
+
return port;
|
|
968
|
+
}
|
|
969
|
+
connectVerifiedBackend(expectedPort) {
|
|
970
|
+
const child = this.proxy;
|
|
971
|
+
const port = this.trustedBackendPort(expectedPort);
|
|
972
|
+
if (!child || !port || !child.pid)
|
|
973
|
+
return Promise.reject(new Error('Headroom proxy is not trusted'));
|
|
974
|
+
return new Promise((resolve, reject) => {
|
|
975
|
+
const socket = net_1.default.connect({ host: '127.0.0.1', port });
|
|
976
|
+
let settled = false;
|
|
977
|
+
const timer = setTimeout(() => fail(new Error('Headroom relay connect timed out')), RELAY_CONNECT_TIMEOUT_MS);
|
|
978
|
+
timer.unref?.();
|
|
979
|
+
const fail = (err) => {
|
|
980
|
+
if (settled)
|
|
981
|
+
return;
|
|
982
|
+
settled = true;
|
|
983
|
+
clearTimeout(timer);
|
|
984
|
+
socket.destroy();
|
|
985
|
+
reject(err);
|
|
986
|
+
};
|
|
987
|
+
socket.once('error', fail);
|
|
988
|
+
socket.once('connect', () => {
|
|
989
|
+
void (async () => {
|
|
990
|
+
const ownsProxyPort = this.processControl.ownsProxyPort ?? processOwnsListeningPort;
|
|
991
|
+
let listenerOwned = false;
|
|
992
|
+
try {
|
|
993
|
+
listenerOwned = await ownsProxyPort(child.pid, port);
|
|
994
|
+
}
|
|
995
|
+
catch { /* fail closed */ }
|
|
996
|
+
if (settled)
|
|
997
|
+
return;
|
|
998
|
+
const owned = this.proxy === child &&
|
|
999
|
+
this.proxyTrustedPort === port &&
|
|
1000
|
+
child.exitCode == null &&
|
|
1001
|
+
child.signalCode == null &&
|
|
1002
|
+
!!child.pid &&
|
|
1003
|
+
listenerOwned &&
|
|
1004
|
+
this.proxy === child &&
|
|
1005
|
+
this.proxyTrustedPort === port &&
|
|
1006
|
+
child.exitCode == null &&
|
|
1007
|
+
child.signalCode == null;
|
|
1008
|
+
if (!owned) {
|
|
1009
|
+
fail(new Error('Headroom backend ownership changed'));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
settled = true;
|
|
1013
|
+
clearTimeout(timer);
|
|
1014
|
+
socket.removeListener('error', fail);
|
|
1015
|
+
resolve({ socket, port });
|
|
1016
|
+
})().catch((err) => fail(err instanceof Error ? err : new Error(String(err))));
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
writeRelayError(res, status, message) {
|
|
1021
|
+
if (res.headersSent || res.destroyed)
|
|
1022
|
+
return;
|
|
1023
|
+
res.writeHead(status, { 'content-type': 'application/json', connection: 'close' });
|
|
1024
|
+
res.end(JSON.stringify({ error: message }));
|
|
1025
|
+
}
|
|
1026
|
+
connectionHeaderTokens(headers) {
|
|
1027
|
+
const raw = headers.connection;
|
|
1028
|
+
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
1029
|
+
return new Set(values.flatMap((value) => value.split(',')).map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
1030
|
+
}
|
|
1031
|
+
filterRelayHttpHeaders(headers) {
|
|
1032
|
+
const connectionTokens = this.connectionHeaderTokens(headers);
|
|
1033
|
+
const filtered = {};
|
|
1034
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1035
|
+
const lower = name.toLowerCase();
|
|
1036
|
+
if (value === undefined ||
|
|
1037
|
+
lower === 'connection' ||
|
|
1038
|
+
lower === 'proxy-connection' ||
|
|
1039
|
+
lower === 'keep-alive' ||
|
|
1040
|
+
lower === 'transfer-encoding' ||
|
|
1041
|
+
lower === 'upgrade' ||
|
|
1042
|
+
lower === 'te' ||
|
|
1043
|
+
lower === 'trailer' ||
|
|
1044
|
+
lower === 'proxy-authenticate' ||
|
|
1045
|
+
lower === 'proxy-authorization' ||
|
|
1046
|
+
connectionTokens.has(lower))
|
|
1047
|
+
continue;
|
|
1048
|
+
filtered[name] = value;
|
|
1049
|
+
}
|
|
1050
|
+
return filtered;
|
|
1051
|
+
}
|
|
1052
|
+
rejectRelayUpgrade(socket, status, message) {
|
|
1053
|
+
if (socket.destroyed)
|
|
1054
|
+
return;
|
|
1055
|
+
socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
1056
|
+
}
|
|
1057
|
+
destroyRelayTransports() {
|
|
1058
|
+
this.relayAgent.destroy();
|
|
1059
|
+
for (const teardown of [...this.relayUpgradeTeardowns])
|
|
1060
|
+
teardown();
|
|
1061
|
+
}
|
|
375
1062
|
async install() {
|
|
1063
|
+
return this.serializeLifecycle(() => this.installLocked());
|
|
1064
|
+
}
|
|
1065
|
+
async installLocked() {
|
|
1066
|
+
if (this.shuttingDown)
|
|
1067
|
+
return this.cancelledProxyStart();
|
|
376
1068
|
const uv = this.resolveUvPath();
|
|
377
1069
|
if (!uv) {
|
|
378
1070
|
const failure = issue('uv_missing_or_corrupt');
|
|
@@ -395,6 +1087,8 @@ class HeadroomManager {
|
|
|
395
1087
|
this.emit('installing', logs[0]);
|
|
396
1088
|
this.emit('installing', logs[1]);
|
|
397
1089
|
const pythonResult = await this.runCommand(uv, plan.pythonInstallArgs, plan.env, logs);
|
|
1090
|
+
if (this.shuttingDown)
|
|
1091
|
+
return this.cancelledProxyStart();
|
|
398
1092
|
if (pythonResult.code !== 0) {
|
|
399
1093
|
const failure = classifyInstallFailure(logs.join('\n'), pythonCommand);
|
|
400
1094
|
this.updatePersisted({ lastIssue: failure });
|
|
@@ -406,6 +1100,8 @@ class HeadroomManager {
|
|
|
406
1100
|
this.emit('installing', logs[logs.length - 2]);
|
|
407
1101
|
this.emit('installing', logs[logs.length - 1]);
|
|
408
1102
|
const result = await this.runCommand(uv, plan.toolInstallArgs, plan.env, logs);
|
|
1103
|
+
if (this.shuttingDown)
|
|
1104
|
+
return this.cancelledProxyStart();
|
|
409
1105
|
if (result.code !== 0) {
|
|
410
1106
|
const failure = classifyInstallFailure(logs.join('\n'), installCommand);
|
|
411
1107
|
this.updatePersisted({ lastIssue: failure });
|
|
@@ -429,11 +1125,20 @@ class HeadroomManager {
|
|
|
429
1125
|
lastIssue: null,
|
|
430
1126
|
});
|
|
431
1127
|
await this.enableOutputLearning(exe, logs);
|
|
1128
|
+
if (this.shuttingDown)
|
|
1129
|
+
return this.cancelledProxyStart();
|
|
432
1130
|
this.emit('installed', `Headroom ${version} installed`);
|
|
433
1131
|
await this.refreshMetrics();
|
|
1132
|
+
if (this.shuttingDown)
|
|
1133
|
+
return this.cancelledProxyStart();
|
|
434
1134
|
return { ok: true, state: this.getState(), logs };
|
|
435
1135
|
}
|
|
436
1136
|
async activate(provider) {
|
|
1137
|
+
return this.serializeLifecycle(() => this.activateLocked(provider));
|
|
1138
|
+
}
|
|
1139
|
+
async activateLocked(provider) {
|
|
1140
|
+
if (this.shuttingDown)
|
|
1141
|
+
return this.cancelledProxyStart();
|
|
437
1142
|
const state = this.getState();
|
|
438
1143
|
if (!state.installed || !state.executablePath) {
|
|
439
1144
|
const failure = issue('not_installed');
|
|
@@ -443,13 +1148,18 @@ class HeadroomManager {
|
|
|
443
1148
|
const proxy = await this.ensureProxy();
|
|
444
1149
|
if (!proxy.ok)
|
|
445
1150
|
return proxy;
|
|
1151
|
+
// The start promise can settle just before shutdown/deactivation invalidates
|
|
1152
|
+
// its generation. Re-check the owned runtime synchronously before persisting
|
|
1153
|
+
// a provider route so a resolved promise cannot resurrect activation.
|
|
1154
|
+
if (this.shuttingDown || !this.isProxyAvailable())
|
|
1155
|
+
return this.cancelledProxyStart();
|
|
446
1156
|
const current = this.readPersisted();
|
|
447
|
-
const activeProviders = { ...current
|
|
448
|
-
this.updatePersisted({ activeProviders, lastIssue: null });
|
|
1157
|
+
const activeProviders = { ...this.explicitActiveProviders(current), [provider]: true };
|
|
1158
|
+
this.updatePersisted({ ...this.activationPatch(activeProviders), lastIssue: null });
|
|
449
1159
|
this.syncRouting();
|
|
450
1160
|
if (!this.verifyProviderRoute(provider)) {
|
|
451
1161
|
const rolledBack = { ...activeProviders, [provider]: false };
|
|
452
|
-
this.updatePersisted({
|
|
1162
|
+
this.updatePersisted({ ...this.activationPatch(rolledBack), lastIssue: issue('provider_route_failed') });
|
|
453
1163
|
this.syncRouting();
|
|
454
1164
|
return { ok: false, state: this.getState(), issue: issue('provider_route_failed') };
|
|
455
1165
|
}
|
|
@@ -457,22 +1167,34 @@ class HeadroomManager {
|
|
|
457
1167
|
return { ok: true, state: this.getState() };
|
|
458
1168
|
}
|
|
459
1169
|
async deactivate(provider) {
|
|
1170
|
+
return this.serializeLifecycle(() => this.deactivateLocked(provider));
|
|
1171
|
+
}
|
|
1172
|
+
async deactivateLocked(provider) {
|
|
1173
|
+
if (this.shuttingDown)
|
|
1174
|
+
return this.cancelledProxyStart();
|
|
460
1175
|
const current = this.readPersisted();
|
|
461
|
-
const activeProviders = { ...current
|
|
462
|
-
this.updatePersisted({ activeProviders, lastIssue: null });
|
|
1176
|
+
const activeProviders = { ...this.explicitActiveProviders(current), [provider]: false };
|
|
1177
|
+
this.updatePersisted({ ...this.activationPatch(activeProviders), lastIssue: null });
|
|
463
1178
|
this.syncRouting();
|
|
464
1179
|
if (!activeProviders.codex && !activeProviders.claude)
|
|
465
|
-
this.stopProxy();
|
|
1180
|
+
await this.stopProxy();
|
|
466
1181
|
this.emit('installed', `${provider} Headroom route disabled`);
|
|
467
1182
|
return { ok: true, state: this.getState() };
|
|
468
1183
|
}
|
|
469
1184
|
async uninstall() {
|
|
1185
|
+
return this.serializeLifecycle(() => this.uninstallLocked());
|
|
1186
|
+
}
|
|
1187
|
+
async uninstallLocked() {
|
|
1188
|
+
if (this.shuttingDown)
|
|
1189
|
+
return this.cancelledProxyStart();
|
|
470
1190
|
const before = this.getState();
|
|
471
1191
|
this.updatePersisted({
|
|
472
|
-
|
|
1192
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
473
1193
|
lastIssue: null,
|
|
474
1194
|
});
|
|
475
|
-
this.stopProxy();
|
|
1195
|
+
await this.stopProxy();
|
|
1196
|
+
if (this.shuttingDown)
|
|
1197
|
+
return this.cancelledProxyStart();
|
|
476
1198
|
this.syncRouting();
|
|
477
1199
|
if (!before.installed && !before.executablePath) {
|
|
478
1200
|
this.updatePersisted({
|
|
@@ -480,7 +1202,7 @@ class HeadroomManager {
|
|
|
480
1202
|
version: null,
|
|
481
1203
|
executablePath: null,
|
|
482
1204
|
installSource: null,
|
|
483
|
-
|
|
1205
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
484
1206
|
detectedRoutes: { codex: false, claude: false },
|
|
485
1207
|
lastIssue: null,
|
|
486
1208
|
});
|
|
@@ -493,7 +1215,7 @@ class HeadroomManager {
|
|
|
493
1215
|
executablePath: null,
|
|
494
1216
|
installSource: null,
|
|
495
1217
|
ignoreSystemInstall: true,
|
|
496
|
-
|
|
1218
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
497
1219
|
detectedRoutes: { codex: false, claude: false },
|
|
498
1220
|
lastIssue: null,
|
|
499
1221
|
});
|
|
@@ -516,6 +1238,8 @@ class HeadroomManager {
|
|
|
516
1238
|
UV_CACHE_DIR: uvCacheDir(),
|
|
517
1239
|
UV_NO_PROGRESS: '1',
|
|
518
1240
|
}, logs);
|
|
1241
|
+
if (this.shuttingDown)
|
|
1242
|
+
return this.cancelledProxyStart();
|
|
519
1243
|
const stillInstalled = fileExists(path_1.default.join(uvBinDir(), headroomExeName()));
|
|
520
1244
|
if (result.code !== 0 || stillInstalled) {
|
|
521
1245
|
const failure = issue('uninstall_failed', logs.join('\n'), command);
|
|
@@ -528,7 +1252,7 @@ class HeadroomManager {
|
|
|
528
1252
|
version: null,
|
|
529
1253
|
executablePath: null,
|
|
530
1254
|
installSource: null,
|
|
531
|
-
|
|
1255
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
532
1256
|
detectedRoutes: { codex: false, claude: false },
|
|
533
1257
|
lastIssue: null,
|
|
534
1258
|
});
|
|
@@ -536,18 +1260,65 @@ class HeadroomManager {
|
|
|
536
1260
|
return { ok: true, state: this.getState(), logs };
|
|
537
1261
|
}
|
|
538
1262
|
async setPort(port) {
|
|
1263
|
+
return this.serializeLifecycle(() => this.setPortLocked(port));
|
|
1264
|
+
}
|
|
1265
|
+
async setPortLocked(port) {
|
|
1266
|
+
if (this.shuttingDown)
|
|
1267
|
+
return this.cancelledProxyStart();
|
|
539
1268
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
540
1269
|
const failure = issue('proxy_port_busy', 'Port must be an integer between 1 and 65535.');
|
|
541
1270
|
return { ok: false, state: this.getState(), issue: failure };
|
|
542
1271
|
}
|
|
1272
|
+
const previous = this.readPersisted();
|
|
1273
|
+
const previousPort = this.validPort(previous.port);
|
|
543
1274
|
const active = this.getState().activeProviders;
|
|
544
|
-
this.stopProxy();
|
|
545
|
-
this.
|
|
546
|
-
|
|
547
|
-
if (active.codex
|
|
548
|
-
|
|
1275
|
+
await this.stopProxy();
|
|
1276
|
+
if (this.shuttingDown)
|
|
1277
|
+
return this.cancelledProxyStart();
|
|
1278
|
+
if (!active.codex && !active.claude) {
|
|
1279
|
+
this.updatePersisted({ port, lastIssue: null });
|
|
1280
|
+
return { ok: true, state: this.getState() };
|
|
549
1281
|
}
|
|
550
|
-
|
|
1282
|
+
// Keep routing disabled while the replacement is only a candidate. No job
|
|
1283
|
+
// may send provider credentials to it until our owned child is healthy.
|
|
1284
|
+
this.updatePersisted({
|
|
1285
|
+
port,
|
|
1286
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
1287
|
+
lastIssue: null,
|
|
1288
|
+
});
|
|
1289
|
+
const candidate = await this.ensureProxy();
|
|
1290
|
+
if (this.shuttingDown)
|
|
1291
|
+
return this.cancelledProxyStart();
|
|
1292
|
+
if (candidate.ok) {
|
|
1293
|
+
this.updatePersisted({ ...this.activationPatch(active), lastIssue: null });
|
|
1294
|
+
return { ok: true, state: this.getState() };
|
|
1295
|
+
}
|
|
1296
|
+
// Roll back the entire transition, including the old owned proxy. If even
|
|
1297
|
+
// that cannot be restored, remain fail-closed instead of persisting an
|
|
1298
|
+
// active route with no healthy service behind it.
|
|
1299
|
+
this.updatePersisted({
|
|
1300
|
+
port: previousPort,
|
|
1301
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
1302
|
+
lastIssue: candidate.issue ?? issue('proxy_unhealthy'),
|
|
1303
|
+
});
|
|
1304
|
+
const restored = await this.ensureProxy();
|
|
1305
|
+
if (restored.ok) {
|
|
1306
|
+
this.updatePersisted({
|
|
1307
|
+
...this.activationPatch(active),
|
|
1308
|
+
lastIssue: candidate.issue ?? issue('proxy_unhealthy'),
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
this.updatePersisted({
|
|
1313
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
1314
|
+
lastIssue: restored.issue ?? candidate.issue ?? issue('proxy_unhealthy'),
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
ok: false,
|
|
1319
|
+
state: this.getState(),
|
|
1320
|
+
issue: candidate.issue ?? issue('proxy_unhealthy'),
|
|
1321
|
+
};
|
|
551
1322
|
}
|
|
552
1323
|
diagnostics() {
|
|
553
1324
|
const state = this.getState();
|
|
@@ -566,66 +1337,214 @@ class HeadroomManager {
|
|
|
566
1337
|
learning: state.learning,
|
|
567
1338
|
metrics: state.metrics,
|
|
568
1339
|
routing: {
|
|
569
|
-
codex: state.activeProviders.codex
|
|
570
|
-
|
|
1340
|
+
codex: state.activeProviders.codex
|
|
1341
|
+
? `OPENAI_BASE_URL=${this.relayDiagnosticBaseUrl() ?? `http://127.0.0.1:${state.port}`}/v1`
|
|
1342
|
+
: null,
|
|
1343
|
+
claude: state.activeProviders.claude
|
|
1344
|
+
? `ANTHROPIC_BASE_URL=${this.relayDiagnosticBaseUrl() ?? `http://127.0.0.1:${state.port}`}`
|
|
1345
|
+
: null,
|
|
571
1346
|
},
|
|
572
1347
|
};
|
|
573
1348
|
}
|
|
574
1349
|
async startActiveProxyOnBoot() {
|
|
1350
|
+
return this.serializeLifecycle(() => this.startActiveProxyOnBootLocked());
|
|
1351
|
+
}
|
|
1352
|
+
async startActiveProxyOnBootLocked() {
|
|
1353
|
+
if (this.shuttingDown)
|
|
1354
|
+
return;
|
|
575
1355
|
const active = this.getState().activeProviders;
|
|
576
1356
|
this.syncRouting();
|
|
577
1357
|
if (active.codex || active.claude) {
|
|
578
1358
|
const result = await this.ensureProxy();
|
|
1359
|
+
if (this.shuttingDown)
|
|
1360
|
+
return;
|
|
579
1361
|
if (!result.ok) {
|
|
1362
|
+
// Never keep provider credentials/prompts routed to a process we did not
|
|
1363
|
+
// spawn and therefore cannot authenticate. The user can retry activation
|
|
1364
|
+
// after resolving the port conflict.
|
|
1365
|
+
this.updatePersisted({
|
|
1366
|
+
...this.activationPatch({ codex: false, claude: false }),
|
|
1367
|
+
lastIssue: result.issue ?? issue('proxy_unhealthy'),
|
|
1368
|
+
});
|
|
580
1369
|
console.warn('[headroom] boot proxy start failed:', result.issue);
|
|
581
1370
|
}
|
|
582
1371
|
}
|
|
583
1372
|
}
|
|
1373
|
+
serializeLifecycle(operation) {
|
|
1374
|
+
const run = this.lifecycleTail.then(operation, operation);
|
|
1375
|
+
this.lifecycleTail = run.then(() => undefined, () => undefined);
|
|
1376
|
+
return run;
|
|
1377
|
+
}
|
|
584
1378
|
async ensureProxy() {
|
|
1379
|
+
if (this.proxyStop)
|
|
1380
|
+
await this.proxyStop;
|
|
585
1381
|
const state = this.getState();
|
|
586
1382
|
if (!state.installed || !state.executablePath) {
|
|
587
1383
|
const failure = issue('not_installed');
|
|
588
1384
|
return { ok: false, state, issue: failure };
|
|
589
1385
|
}
|
|
590
|
-
if (this.
|
|
1386
|
+
if (this.shuttingDown) {
|
|
1387
|
+
const failure = issue('proxy_unhealthy', 'Headroom proxy startup was cancelled during shutdown.');
|
|
1388
|
+
return { ok: false, state, issue: failure };
|
|
1389
|
+
}
|
|
1390
|
+
if (this.proxyStart) {
|
|
1391
|
+
if (this.proxyStart.port === state.port)
|
|
1392
|
+
return this.proxyStart.promise;
|
|
1393
|
+
await this.stopProxy();
|
|
1394
|
+
if (this.shuttingDown)
|
|
1395
|
+
return this.cancelledProxyStart();
|
|
1396
|
+
}
|
|
1397
|
+
if (this.isProxyAvailable(state.port))
|
|
591
1398
|
return { ok: true, state };
|
|
592
|
-
if (
|
|
593
|
-
|
|
1399
|
+
if (this.isProxyRunning())
|
|
1400
|
+
await this.stopProxy();
|
|
1401
|
+
const generation = ++this.proxyGeneration;
|
|
1402
|
+
const promise = this.startProxy(state, state.executablePath, generation);
|
|
1403
|
+
this.proxyStart = { generation, port: state.port, promise };
|
|
1404
|
+
const clear = () => {
|
|
1405
|
+
if (this.proxyStart?.promise === promise)
|
|
1406
|
+
this.proxyStart = null;
|
|
1407
|
+
};
|
|
1408
|
+
void promise.then(clear, clear);
|
|
1409
|
+
return promise;
|
|
1410
|
+
}
|
|
1411
|
+
async startProxy(state, executablePath, generation) {
|
|
1412
|
+
// A healthy endpoint is still not proof that the process belongs to this
|
|
1413
|
+
// Specrails instance. Adopting it would route provider auth and prompts to an
|
|
1414
|
+
// unauthenticated port occupant. Treat every pre-existing listener as a hard
|
|
1415
|
+
// conflict and only trust the child created below.
|
|
1416
|
+
if (await this.probeProxyHealthy(state.port)) {
|
|
1417
|
+
if (!this.isProxyGenerationCurrent(generation))
|
|
1418
|
+
return this.cancelledProxyStart();
|
|
1419
|
+
const failure = issue('proxy_port_busy');
|
|
1420
|
+
this.updatePersisted({ lastIssue: failure });
|
|
1421
|
+
this.emit('failed', failure.title);
|
|
1422
|
+
return { ok: false, state: this.getState(), issue: failure };
|
|
594
1423
|
}
|
|
1424
|
+
if (!this.isProxyGenerationCurrent(generation))
|
|
1425
|
+
return this.cancelledProxyStart();
|
|
595
1426
|
const args = ['proxy', '--host', '127.0.0.1', '--port', String(state.port)];
|
|
596
|
-
const command = `${
|
|
1427
|
+
const command = `${executablePath} ${args.join(' ')}`;
|
|
597
1428
|
this.emit('starting-proxy', `Starting proxy on ${state.port}`);
|
|
598
1429
|
this.proxyTail = '';
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1430
|
+
let child;
|
|
1431
|
+
try {
|
|
1432
|
+
child = (this.processControl.spawnProxy ?? child_process_1.spawn)(executablePath, args, {
|
|
1433
|
+
env: {
|
|
1434
|
+
...process.env,
|
|
1435
|
+
HEADROOM_PORT: String(state.port),
|
|
1436
|
+
HEADROOM_OUTPUT_SHAPER: '1',
|
|
1437
|
+
HEADROOM_LEARN: state.learning.enabled ? '1' : '0',
|
|
1438
|
+
HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
|
|
1439
|
+
HEADROOM_MODE: process.env.HEADROOM_MODE ?? 'token',
|
|
1440
|
+
},
|
|
1441
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
catch (err) {
|
|
1445
|
+
const failure = classifyProxyFailure(err instanceof Error ? err.message : String(err), command);
|
|
1446
|
+
this.updatePersisted({ lastIssue: failure });
|
|
1447
|
+
this.emit('failed', failure.title);
|
|
1448
|
+
return { ok: false, state: this.getState(), issue: failure };
|
|
1449
|
+
}
|
|
610
1450
|
this.proxy = child;
|
|
1451
|
+
this.proxyTrustedPort = null;
|
|
1452
|
+
this.syncRouting();
|
|
611
1453
|
child.stdout?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
|
|
612
1454
|
child.stderr?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
|
|
613
|
-
|
|
614
|
-
if (this.proxy
|
|
615
|
-
|
|
1455
|
+
const invalidateOwnedProxy = () => {
|
|
1456
|
+
if (this.proxy !== child)
|
|
1457
|
+
return;
|
|
1458
|
+
this.destroyRelayTransports();
|
|
1459
|
+
this.proxy = null;
|
|
1460
|
+
this.proxyTrustedPort = null;
|
|
1461
|
+
this.syncRouting();
|
|
1462
|
+
};
|
|
1463
|
+
// `spawn()` reports ENOENT/EACCES asynchronously. Without this listener the
|
|
1464
|
+
// EventEmitter contract turns a recoverable activation failure into an
|
|
1465
|
+
// uncaught exception that terminates the desktop sidecar.
|
|
1466
|
+
child.on('error', (err) => {
|
|
1467
|
+
this.proxyTail += `${err.message}\n`;
|
|
1468
|
+
this.trimProxyTail();
|
|
1469
|
+
invalidateOwnedProxy();
|
|
616
1470
|
});
|
|
617
|
-
|
|
1471
|
+
// `exit` invalidates routing immediately; `close` is retained as the final
|
|
1472
|
+
// fallback for unusual ChildProcess implementations and stdio teardown.
|
|
1473
|
+
child.on('exit', invalidateOwnedProxy);
|
|
1474
|
+
child.on('close', invalidateOwnedProxy);
|
|
1475
|
+
const healthy = await this.waitForProxyHealthy(state.port, 6000, () => this.isProxyGenerationCurrent(generation));
|
|
1476
|
+
if (!this.isProxyGenerationCurrent(generation)) {
|
|
1477
|
+
await this.discardProxyChild(child);
|
|
1478
|
+
return this.cancelledProxyStart();
|
|
1479
|
+
}
|
|
618
1480
|
if (!healthy) {
|
|
619
1481
|
const failure = classifyProxyFailure(this.proxyTail, command);
|
|
620
|
-
this.
|
|
1482
|
+
await this.discardProxyChild(child);
|
|
1483
|
+
this.updatePersisted({ lastIssue: failure });
|
|
1484
|
+
this.emit('failed', failure.title);
|
|
1485
|
+
return { ok: false, state: this.getState(), issue: failure };
|
|
1486
|
+
}
|
|
1487
|
+
const ownsProxyPort = this.processControl.ownsProxyPort ?? processOwnsListeningPort;
|
|
1488
|
+
const ownedEndpointIsCurrent = async () => {
|
|
1489
|
+
if (!this.isOwnedProxyCurrent(child, generation) || !child.pid)
|
|
1490
|
+
return false;
|
|
1491
|
+
let listenerOwned = false;
|
|
1492
|
+
try {
|
|
1493
|
+
listenerOwned = await ownsProxyPort(child.pid, state.port);
|
|
1494
|
+
}
|
|
1495
|
+
catch { /* fail closed */ }
|
|
1496
|
+
if (!listenerOwned)
|
|
1497
|
+
return false;
|
|
1498
|
+
return this.isOwnedProxyCurrent(child, generation);
|
|
1499
|
+
};
|
|
1500
|
+
if (!(await ownedEndpointIsCurrent())) {
|
|
1501
|
+
const failure = issue('proxy_port_busy', 'A healthy endpoint answered, but the Specrails-owned Headroom process does not own the listening socket.', command);
|
|
1502
|
+
await this.discardProxyChild(child);
|
|
621
1503
|
this.updatePersisted({ lastIssue: failure });
|
|
622
1504
|
this.emit('failed', failure.title);
|
|
623
1505
|
return { ok: false, state: this.getState(), issue: failure };
|
|
624
1506
|
}
|
|
625
|
-
this.updatePersisted({ lastIssue: null });
|
|
626
1507
|
await this.refreshMetrics();
|
|
1508
|
+
if (!this.isProxyGenerationCurrent(generation)) {
|
|
1509
|
+
await this.discardProxyChild(child);
|
|
1510
|
+
return this.cancelledProxyStart();
|
|
1511
|
+
}
|
|
1512
|
+
if (!(await ownedEndpointIsCurrent())) {
|
|
1513
|
+
const failure = issue('proxy_unhealthy', 'The owned Headroom proxy exited during startup.', command);
|
|
1514
|
+
await this.discardProxyChild(child);
|
|
1515
|
+
this.updatePersisted({ lastIssue: failure });
|
|
1516
|
+
this.emit('failed', failure.title);
|
|
1517
|
+
return { ok: false, state: this.getState(), issue: failure };
|
|
1518
|
+
}
|
|
1519
|
+
this.proxyTrustedPort = state.port;
|
|
1520
|
+
this.updatePersisted({ lastIssue: null });
|
|
627
1521
|
return { ok: true, state: this.getState() };
|
|
628
1522
|
}
|
|
1523
|
+
cancelledProxyStart() {
|
|
1524
|
+
const failure = issue('proxy_unhealthy', 'Headroom proxy startup was superseded by a newer lifecycle operation.');
|
|
1525
|
+
const state = this.shuttingDown && this.shutdownState
|
|
1526
|
+
? this.shutdownState
|
|
1527
|
+
: this.getState();
|
|
1528
|
+
return { ok: false, state, issue: failure };
|
|
1529
|
+
}
|
|
1530
|
+
isProxyGenerationCurrent(generation) {
|
|
1531
|
+
return !this.shuttingDown && this.proxyGeneration === generation;
|
|
1532
|
+
}
|
|
1533
|
+
isOwnedProxyCurrent(child, generation) {
|
|
1534
|
+
return this.isProxyGenerationCurrent(generation) &&
|
|
1535
|
+
this.proxy === child &&
|
|
1536
|
+
child.exitCode == null &&
|
|
1537
|
+
child.signalCode == null;
|
|
1538
|
+
}
|
|
1539
|
+
async discardProxyChild(child) {
|
|
1540
|
+
if (this.proxy === child) {
|
|
1541
|
+
this.destroyRelayTransports();
|
|
1542
|
+
this.proxy = null;
|
|
1543
|
+
this.proxyTrustedPort = null;
|
|
1544
|
+
this.syncRouting();
|
|
1545
|
+
}
|
|
1546
|
+
await this.terminateProxyChild(child);
|
|
1547
|
+
}
|
|
629
1548
|
resolveUvPath() {
|
|
630
1549
|
for (const p of bundledUvCandidates()) {
|
|
631
1550
|
if (fileExists(p))
|
|
@@ -657,7 +1576,7 @@ class HeadroomManager {
|
|
|
657
1576
|
if (this.routeDetectionCache?.executablePath === exe && this.routeDetectionCache.expiresAt > now) {
|
|
658
1577
|
return this.routeDetectionCache.routes;
|
|
659
1578
|
}
|
|
660
|
-
|
|
1579
|
+
let routes = { codex: false, claude: false };
|
|
661
1580
|
try {
|
|
662
1581
|
const state = this.readPersisted();
|
|
663
1582
|
const probe = (0, child_process_1.spawnSync)(exe, ['doctor', '--json', '--port', String(this.validPort(state.port))], {
|
|
@@ -666,16 +1585,8 @@ class HeadroomManager {
|
|
|
666
1585
|
env: process.env,
|
|
667
1586
|
});
|
|
668
1587
|
const raw = `${probe.stdout ?? ''}`.trim();
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const provider = normalizeProviderKey(check.name);
|
|
672
|
-
if (!provider)
|
|
673
|
-
continue;
|
|
674
|
-
const routed = check.status === 'pass' || String(check.summary ?? '').toLowerCase().includes('routed');
|
|
675
|
-
if (routed)
|
|
676
|
-
routes[provider] = true;
|
|
677
|
-
}
|
|
678
|
-
this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: parsed };
|
|
1588
|
+
routes = probe.status === 0 && raw ? parseHeadroomDoctorRoutes(raw) : routes;
|
|
1589
|
+
this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw };
|
|
679
1590
|
}
|
|
680
1591
|
catch {
|
|
681
1592
|
this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: null };
|
|
@@ -718,6 +1629,8 @@ class HeadroomManager {
|
|
|
718
1629
|
HEADROOM_OUTPUT_SHAPER: '1',
|
|
719
1630
|
HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
|
|
720
1631
|
}, logs);
|
|
1632
|
+
if (this.shuttingDown)
|
|
1633
|
+
return;
|
|
721
1634
|
if (result.code === 0) {
|
|
722
1635
|
this.updatePersisted({
|
|
723
1636
|
learningEnabled: true,
|
|
@@ -734,6 +1647,8 @@ class HeadroomManager {
|
|
|
734
1647
|
});
|
|
735
1648
|
}
|
|
736
1649
|
refreshMetricsInBackground() {
|
|
1650
|
+
if (this.shuttingDown)
|
|
1651
|
+
return;
|
|
737
1652
|
const state = this.getMetricsPrereqState();
|
|
738
1653
|
if (!state.installed || !state.executablePath)
|
|
739
1654
|
return;
|
|
@@ -757,10 +1672,7 @@ class HeadroomManager {
|
|
|
757
1672
|
port: this.validPort(persisted.port),
|
|
758
1673
|
availableProviders,
|
|
759
1674
|
detectedRoutes,
|
|
760
|
-
activeProviders:
|
|
761
|
-
codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
|
|
762
|
-
claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
|
|
763
|
-
},
|
|
1675
|
+
activeProviders: this.explicitActiveProviders(persisted),
|
|
764
1676
|
};
|
|
765
1677
|
}
|
|
766
1678
|
async refreshMetrics() {
|
|
@@ -845,17 +1757,60 @@ class HeadroomManager {
|
|
|
845
1757
|
return JSON.parse(result.output);
|
|
846
1758
|
}
|
|
847
1759
|
async readProxyStats(port) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1760
|
+
if (!this.isProxyAvailable(port))
|
|
1761
|
+
return null;
|
|
1762
|
+
return await new Promise((resolve, reject) => {
|
|
1763
|
+
let settled = false;
|
|
1764
|
+
const finish = (value, err) => {
|
|
1765
|
+
if (settled)
|
|
1766
|
+
return;
|
|
1767
|
+
settled = true;
|
|
1768
|
+
clearTimeout(timer);
|
|
1769
|
+
if (err)
|
|
1770
|
+
reject(err);
|
|
1771
|
+
else
|
|
1772
|
+
resolve(value);
|
|
1773
|
+
};
|
|
1774
|
+
const request = http_1.default.request({
|
|
1775
|
+
host: '127.0.0.1',
|
|
1776
|
+
port,
|
|
1777
|
+
method: 'GET',
|
|
1778
|
+
path: '/stats?cached=1',
|
|
1779
|
+
headers: { host: `127.0.0.1:${port}` },
|
|
1780
|
+
agent: this.relayAgent,
|
|
1781
|
+
}, (response) => {
|
|
1782
|
+
if ((response.statusCode ?? 500) < 200 || (response.statusCode ?? 500) >= 300) {
|
|
1783
|
+
response.resume();
|
|
1784
|
+
finish(null);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const chunks = [];
|
|
1788
|
+
let size = 0;
|
|
1789
|
+
response.on('data', (chunk) => {
|
|
1790
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1791
|
+
size += value.length;
|
|
1792
|
+
if (size > 2 * 1024 * 1024) {
|
|
1793
|
+
request.destroy(new Error('Headroom stats response exceeded 2 MB'));
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
chunks.push(value);
|
|
1797
|
+
});
|
|
1798
|
+
response.on('end', () => {
|
|
1799
|
+
try {
|
|
1800
|
+
finish(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
1801
|
+
}
|
|
1802
|
+
catch (err) {
|
|
1803
|
+
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
});
|
|
1807
|
+
const timer = setTimeout(() => {
|
|
1808
|
+
request.destroy(new Error('Headroom stats request timed out'));
|
|
1809
|
+
}, 2_500);
|
|
1810
|
+
timer.unref?.();
|
|
1811
|
+
request.once('error', (err) => finish(null, err));
|
|
1812
|
+
request.end();
|
|
1813
|
+
});
|
|
859
1814
|
}
|
|
860
1815
|
decorateMetrics(metrics, activeProviders, availableProviders, detectedRoutes) {
|
|
861
1816
|
return {
|
|
@@ -882,11 +1837,29 @@ class HeadroomManager {
|
|
|
882
1837
|
};
|
|
883
1838
|
}
|
|
884
1839
|
async runCommand(command, args, extraEnv, logs) {
|
|
1840
|
+
if (this.shuttingDown)
|
|
1841
|
+
return { code: null };
|
|
885
1842
|
return new Promise((resolve) => {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1843
|
+
let child;
|
|
1844
|
+
try {
|
|
1845
|
+
child = (0, child_process_1.spawn)(command, args, {
|
|
1846
|
+
env: { ...process.env, ...extraEnv },
|
|
1847
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
catch (err) {
|
|
1851
|
+
logs.push(err instanceof Error ? err.message : String(err));
|
|
1852
|
+
resolve({ code: null });
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
let settled = false;
|
|
1856
|
+
const settle = (code) => {
|
|
1857
|
+
if (settled)
|
|
1858
|
+
return;
|
|
1859
|
+
settled = true;
|
|
1860
|
+
resolve({ code });
|
|
1861
|
+
};
|
|
1862
|
+
this.trackLifecycleCommand(child, () => settle(null));
|
|
890
1863
|
child.stdout?.on('data', (chunk) => {
|
|
891
1864
|
for (const line of chunk.toString().split(/\r?\n/)) {
|
|
892
1865
|
if (!line)
|
|
@@ -905,32 +1878,45 @@ class HeadroomManager {
|
|
|
905
1878
|
});
|
|
906
1879
|
child.on('error', (err) => {
|
|
907
1880
|
logs.push(err.message);
|
|
908
|
-
|
|
1881
|
+
settle(null);
|
|
909
1882
|
});
|
|
910
|
-
child.on('close', (code) =>
|
|
1883
|
+
child.on('close', (code) => settle(code));
|
|
1884
|
+
if (this.shuttingDown) {
|
|
1885
|
+
settle(null);
|
|
1886
|
+
void this.terminateProxyChild(child);
|
|
1887
|
+
}
|
|
911
1888
|
});
|
|
912
1889
|
}
|
|
913
1890
|
async runCommandCapture(command, args, extraEnv, timeoutMs) {
|
|
1891
|
+
if (this.shuttingDown)
|
|
1892
|
+
return { code: null, output: '' };
|
|
914
1893
|
return new Promise((resolve) => {
|
|
915
1894
|
let output = '';
|
|
916
1895
|
let settled = false;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1896
|
+
let child;
|
|
1897
|
+
try {
|
|
1898
|
+
child = (0, child_process_1.spawn)(command, args, {
|
|
1899
|
+
env: { ...process.env, ...extraEnv },
|
|
1900
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
resolve({ code: null, output: err instanceof Error ? err.message : String(err) });
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
let timer = null;
|
|
921
1908
|
const settle = (code) => {
|
|
922
1909
|
if (settled)
|
|
923
1910
|
return;
|
|
924
1911
|
settled = true;
|
|
925
|
-
|
|
1912
|
+
if (timer)
|
|
1913
|
+
clearTimeout(timer);
|
|
926
1914
|
resolve({ code, output });
|
|
927
1915
|
};
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
child.kill('SIGTERM');
|
|
931
|
-
}
|
|
932
|
-
catch { /* ignore */ }
|
|
1916
|
+
this.trackLifecycleCommand(child, () => settle(null));
|
|
1917
|
+
timer = setTimeout(() => {
|
|
933
1918
|
settle(null);
|
|
1919
|
+
void this.terminateProxyChild(child);
|
|
934
1920
|
}, timeoutMs);
|
|
935
1921
|
child.stdout?.on('data', (chunk) => { output += chunk.toString(); });
|
|
936
1922
|
child.stderr?.on('data', (chunk) => { output += chunk.toString(); });
|
|
@@ -939,23 +1925,68 @@ class HeadroomManager {
|
|
|
939
1925
|
settle(null);
|
|
940
1926
|
});
|
|
941
1927
|
child.on('close', (code) => settle(code));
|
|
1928
|
+
if (this.shuttingDown) {
|
|
1929
|
+
settle(null);
|
|
1930
|
+
void this.terminateProxyChild(child);
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
trackLifecycleCommand(child, cancel) {
|
|
1935
|
+
this.lifecycleCommands.set(child, { cancel });
|
|
1936
|
+
child.once('close', () => {
|
|
1937
|
+
this.lifecycleCommands.delete(child);
|
|
942
1938
|
});
|
|
943
1939
|
}
|
|
944
|
-
|
|
1940
|
+
/** Cancel/terminate every command, including children added while draining. */
|
|
1941
|
+
async drainLifecycleCommands() {
|
|
1942
|
+
const attempted = new Set();
|
|
1943
|
+
while (true) {
|
|
1944
|
+
const pending = [...this.lifecycleCommands.entries()]
|
|
1945
|
+
.filter(([child]) => !attempted.has(child));
|
|
1946
|
+
if (pending.length === 0)
|
|
1947
|
+
return;
|
|
1948
|
+
for (const [child, control] of pending) {
|
|
1949
|
+
attempted.add(child);
|
|
1950
|
+
control.cancel();
|
|
1951
|
+
}
|
|
1952
|
+
await Promise.all(pending.map(([child]) => this.terminateProxyChild(child)));
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async waitForProxyHealthy(port, timeoutMs, isCurrent = () => true) {
|
|
945
1956
|
const deadline = Date.now() + timeoutMs;
|
|
946
1957
|
while (Date.now() < deadline) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1958
|
+
if (!isCurrent())
|
|
1959
|
+
return false;
|
|
1960
|
+
const requestTimeoutMs = Math.max(1, Math.min(PROXY_HEALTH_REQUEST_TIMEOUT_MS, deadline - Date.now()));
|
|
1961
|
+
const healthy = await this.fetchProxyHealth(port, requestTimeoutMs);
|
|
1962
|
+
if (!isCurrent())
|
|
1963
|
+
return false;
|
|
1964
|
+
if (healthy)
|
|
1965
|
+
return true;
|
|
1966
|
+
if (!isCurrent())
|
|
1967
|
+
return false;
|
|
955
1968
|
await new Promise((r) => setTimeout(r, 250));
|
|
956
1969
|
}
|
|
957
1970
|
return false;
|
|
958
1971
|
}
|
|
1972
|
+
async probeProxyHealthy(port) {
|
|
1973
|
+
return this.fetchProxyHealth(port, PROXY_HEALTH_REQUEST_TIMEOUT_MS);
|
|
1974
|
+
}
|
|
1975
|
+
async fetchProxyHealth(port, timeoutMs) {
|
|
1976
|
+
const controller = new AbortController();
|
|
1977
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1978
|
+
timeout.unref?.();
|
|
1979
|
+
try {
|
|
1980
|
+
const res = await fetch(`http://127.0.0.1:${port}/livez`, { signal: controller.signal });
|
|
1981
|
+
return res.ok;
|
|
1982
|
+
}
|
|
1983
|
+
catch {
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
finally {
|
|
1987
|
+
clearTimeout(timeout);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
959
1990
|
verifyProviderRoute(provider) {
|
|
960
1991
|
const state = this.getState();
|
|
961
1992
|
return provider === 'codex'
|
|
@@ -963,27 +1994,124 @@ class HeadroomManager {
|
|
|
963
1994
|
: !!state.activeProviders.claude && state.port > 0;
|
|
964
1995
|
}
|
|
965
1996
|
isProxyRunning() {
|
|
966
|
-
return !!this.proxy &&
|
|
967
|
-
}
|
|
968
|
-
async adoptHealthyProxy(port) {
|
|
969
|
-
if (!await this.waitForProxyHealthy(port, 750))
|
|
970
|
-
return false;
|
|
971
|
-
await this.refreshMetrics();
|
|
972
|
-
this.updatePersisted({ lastIssue: null });
|
|
973
|
-
return true;
|
|
1997
|
+
return !!this.proxy && this.proxy.exitCode == null && this.proxy.signalCode == null;
|
|
974
1998
|
}
|
|
975
|
-
isProxyAvailable() {
|
|
976
|
-
return this.
|
|
1999
|
+
isProxyAvailable(port = this.validPort(this.readPersisted().port)) {
|
|
2000
|
+
return this.proxyTrustedPort === port && this.isProxyRunning();
|
|
977
2001
|
}
|
|
978
|
-
stopProxy() {
|
|
2002
|
+
async stopProxy() {
|
|
2003
|
+
this.proxyGeneration += 1;
|
|
2004
|
+
this.proxyStart = null;
|
|
2005
|
+
this.destroyRelayTransports();
|
|
979
2006
|
const child = this.proxy;
|
|
980
2007
|
this.proxy = null;
|
|
981
|
-
|
|
2008
|
+
this.proxyTrustedPort = null;
|
|
2009
|
+
this.syncRouting();
|
|
2010
|
+
const previousStop = this.proxyStop;
|
|
2011
|
+
const stop = (async () => {
|
|
2012
|
+
if (previousStop)
|
|
2013
|
+
await previousStop;
|
|
2014
|
+
if (child)
|
|
2015
|
+
await this.terminateProxyChild(child);
|
|
2016
|
+
})();
|
|
2017
|
+
this.proxyStop = stop;
|
|
2018
|
+
try {
|
|
2019
|
+
await stop;
|
|
2020
|
+
}
|
|
2021
|
+
finally {
|
|
2022
|
+
if (this.proxyStop === stop)
|
|
2023
|
+
this.proxyStop = null;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Synchronously close lifecycle admission while other process owners drain.
|
|
2028
|
+
* The proxy remains alive until shutdown() so already-routed children can be
|
|
2029
|
+
* terminated before their transport disappears.
|
|
2030
|
+
*/
|
|
2031
|
+
beginShutdown() {
|
|
2032
|
+
if (this.shuttingDown)
|
|
2033
|
+
return;
|
|
2034
|
+
this.shuttingDown = true;
|
|
2035
|
+
this.syncRouting();
|
|
2036
|
+
const state = this.buildState(false);
|
|
2037
|
+
this.shutdownState = { ...state, proxyRunning: false, proxyPid: null };
|
|
2038
|
+
}
|
|
2039
|
+
/** Stop the owned proxy before the app tears down its DB and HTTP server. */
|
|
2040
|
+
shutdown() {
|
|
2041
|
+
if (this.shutdownPromise)
|
|
2042
|
+
return this.shutdownPromise;
|
|
2043
|
+
this.beginShutdown();
|
|
2044
|
+
const shutdown = this.finishShutdown();
|
|
2045
|
+
this.shutdownPromise = shutdown;
|
|
2046
|
+
return shutdown;
|
|
2047
|
+
}
|
|
2048
|
+
async finishShutdown() {
|
|
2049
|
+
// Runtime-only fail closed; persisted activation remains so a clean restart
|
|
2050
|
+
// can start a fresh owned proxy.
|
|
2051
|
+
const persistedPort = this.validPort(this.readPersisted().port);
|
|
2052
|
+
(0, headroom_routing_1.setHeadroomRoutingState)({
|
|
2053
|
+
port: this.routingClientPort(persistedPort),
|
|
2054
|
+
relayBaseUrl: this.relayBaseUrl(),
|
|
2055
|
+
activeProviders: { codex: false, claude: false },
|
|
2056
|
+
});
|
|
2057
|
+
// Routed provider children inherited the stable desktop endpoint. Stop and
|
|
2058
|
+
// await them before the app can release that listener; until then the relay
|
|
2059
|
+
// remains bound and rejects any untrusted backend instead of exposing a
|
|
2060
|
+
// reusable Headroom port.
|
|
2061
|
+
const stoppingProxy = this.stopProxy();
|
|
2062
|
+
await Promise.all([
|
|
2063
|
+
(0, headroom_routing_1.terminateHeadroomRoutedChildren)(),
|
|
2064
|
+
stoppingProxy,
|
|
2065
|
+
this.drainLifecycleCommands(),
|
|
2066
|
+
]);
|
|
2067
|
+
// A lifecycle action may still be unwinding an aborted health/metrics
|
|
2068
|
+
// request. Keep the desktop DB alive until it has observed `shuttingDown`
|
|
2069
|
+
// and completed; queued actions fail at their locked-method guard.
|
|
2070
|
+
await this.lifecycleTail;
|
|
2071
|
+
// Defense in depth for future lifecycle operations: no child may survive a
|
|
2072
|
+
// completed shutdown even if a new await is added after an initial guard.
|
|
2073
|
+
await this.drainLifecycleCommands();
|
|
2074
|
+
await this.stopProxy();
|
|
2075
|
+
}
|
|
2076
|
+
terminateProxyChild(child) {
|
|
2077
|
+
if (!child.pid || child.exitCode != null || child.signalCode != null)
|
|
2078
|
+
return Promise.resolve();
|
|
2079
|
+
const pid = child.pid;
|
|
2080
|
+
const killTree = this.processControl.killTree ?? win_spawn_1.treeKillSafe;
|
|
2081
|
+
return new Promise((resolve) => {
|
|
2082
|
+
let closed = false;
|
|
2083
|
+
let killTimer = null;
|
|
2084
|
+
let deadlineTimer = null;
|
|
2085
|
+
const finish = () => {
|
|
2086
|
+
if (closed)
|
|
2087
|
+
return;
|
|
2088
|
+
closed = true;
|
|
2089
|
+
if (killTimer)
|
|
2090
|
+
clearTimeout(killTimer);
|
|
2091
|
+
if (deadlineTimer)
|
|
2092
|
+
clearTimeout(deadlineTimer);
|
|
2093
|
+
child.removeListener('close', finish);
|
|
2094
|
+
resolve();
|
|
2095
|
+
};
|
|
2096
|
+
child.once('close', finish);
|
|
982
2097
|
try {
|
|
983
|
-
|
|
2098
|
+
killTree(pid, 'SIGTERM', () => { });
|
|
984
2099
|
}
|
|
985
|
-
catch { /*
|
|
986
|
-
|
|
2100
|
+
catch { /* gone */ }
|
|
2101
|
+
killTimer = setTimeout(() => {
|
|
2102
|
+
if (closed)
|
|
2103
|
+
return;
|
|
2104
|
+
try {
|
|
2105
|
+
killTree(pid, 'SIGKILL', () => { });
|
|
2106
|
+
}
|
|
2107
|
+
catch { /* gone */ }
|
|
2108
|
+
}, 2_000);
|
|
2109
|
+
killTimer.unref?.();
|
|
2110
|
+
// A D-state process may never emit close. Do not wedge app shutdown after
|
|
2111
|
+
// escalation; the OS will reclaim it when possible.
|
|
2112
|
+
deadlineTimer = setTimeout(finish, 2_500);
|
|
2113
|
+
deadlineTimer.unref?.();
|
|
2114
|
+
});
|
|
987
2115
|
}
|
|
988
2116
|
trimProxyTail() {
|
|
989
2117
|
if (this.proxyTail.length > 32_000)
|
|
@@ -992,6 +2120,40 @@ class HeadroomManager {
|
|
|
992
2120
|
validPort(value) {
|
|
993
2121
|
return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535 ? Number(value) : DEFAULT_PORT;
|
|
994
2122
|
}
|
|
2123
|
+
relayBaseUrl() {
|
|
2124
|
+
const origin = this.processControl.relayOrigin;
|
|
2125
|
+
if (!origin)
|
|
2126
|
+
return undefined;
|
|
2127
|
+
try {
|
|
2128
|
+
const parsed = new URL(origin);
|
|
2129
|
+
if (parsed.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)) {
|
|
2130
|
+
return undefined;
|
|
2131
|
+
}
|
|
2132
|
+
return `${parsed.origin}${exports.HEADROOM_RELAY_PATH}/${this.relayToken}`;
|
|
2133
|
+
}
|
|
2134
|
+
catch {
|
|
2135
|
+
return undefined;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
relayDiagnosticBaseUrl() {
|
|
2139
|
+
const relay = this.relayBaseUrl();
|
|
2140
|
+
if (!relay)
|
|
2141
|
+
return undefined;
|
|
2142
|
+
return `${new URL(relay).origin}${exports.HEADROOM_RELAY_PATH}/<redacted>`;
|
|
2143
|
+
}
|
|
2144
|
+
routingClientPort(backendPort) {
|
|
2145
|
+
const relay = this.relayBaseUrl();
|
|
2146
|
+
if (!relay)
|
|
2147
|
+
return backendPort;
|
|
2148
|
+
try {
|
|
2149
|
+
const parsed = new URL(relay);
|
|
2150
|
+
const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
|
|
2151
|
+
return this.validPort(port);
|
|
2152
|
+
}
|
|
2153
|
+
catch {
|
|
2154
|
+
return backendPort;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
995
2157
|
readPersisted() {
|
|
996
2158
|
const raw = (0, desktop_db_1.getDesktopSetting)(this.db, STATE_KEY);
|
|
997
2159
|
if (!raw)
|
|
@@ -1013,13 +2175,40 @@ class HeadroomManager {
|
|
|
1013
2175
|
(0, desktop_db_1.setDesktopSetting)(this.db, STATE_KEY, JSON.stringify(next));
|
|
1014
2176
|
this.syncRouting();
|
|
1015
2177
|
}
|
|
2178
|
+
explicitActiveProviders(persisted) {
|
|
2179
|
+
const activation = persisted.activation;
|
|
2180
|
+
if (activation?.version !== exports.HEADROOM_ACTIVATION_SCHEMA_VERSION ||
|
|
2181
|
+
activation.source !== exports.HEADROOM_ACTIVATION_SOURCE)
|
|
2182
|
+
return { codex: false, claude: false };
|
|
2183
|
+
return {
|
|
2184
|
+
codex: activation.activeProviders?.codex === true,
|
|
2185
|
+
claude: activation.activeProviders?.claude === true,
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
activationPatch(activeProviders) {
|
|
2189
|
+
const normalized = providerRecord(activeProviders);
|
|
2190
|
+
return {
|
|
2191
|
+
// Retain the old field for diagnostics/downgrades, but the versioned
|
|
2192
|
+
// envelope below is the sole routing authority.
|
|
2193
|
+
activeProviders: normalized,
|
|
2194
|
+
activation: {
|
|
2195
|
+
version: exports.HEADROOM_ACTIVATION_SCHEMA_VERSION,
|
|
2196
|
+
source: exports.HEADROOM_ACTIVATION_SOURCE,
|
|
2197
|
+
activeProviders: normalized,
|
|
2198
|
+
},
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
1016
2201
|
syncRouting() {
|
|
1017
2202
|
const persisted = this.readPersisted();
|
|
2203
|
+
const port = this.validPort(persisted.port);
|
|
2204
|
+
const trustedRuntimeAvailable = this.proxyTrustedPort === port && this.isProxyRunning();
|
|
2205
|
+
const explicitActiveProviders = this.explicitActiveProviders(persisted);
|
|
1018
2206
|
(0, headroom_routing_1.setHeadroomRoutingState)({
|
|
1019
|
-
port: this.
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
2207
|
+
port: this.routingClientPort(port),
|
|
2208
|
+
relayBaseUrl: this.relayBaseUrl(),
|
|
2209
|
+
activeProviders: this.shuttingDown || !trustedRuntimeAvailable ? { codex: false, claude: false } : {
|
|
2210
|
+
codex: explicitActiveProviders.codex,
|
|
2211
|
+
claude: explicitActiveProviders.claude,
|
|
1023
2212
|
},
|
|
1024
2213
|
});
|
|
1025
2214
|
}
|