chainlesschain 0.165.6 → 0.165.8
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/package.json +2 -1
- package/src/assets/web-panel/assets/{AIOps-vfoDb6HZ.js → AIOps-B3gWmXjo.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Cm5f7p93.js → ActionButton-G9pdT-0e.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-yRuvgWxa.js → Analytics-Ca30J7m2.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BROsC4h1.js → AppLayout-BtVTm8PU.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CimHApb1.js → Artifacts-BhViNa6u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DI8kr8zI.js → Audit-DhKq-FnY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DSLzKy-n.js → BackgroundAgents-BL0SVbnE.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DkH_n2r1.js → Backup-BWkblnFi.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CKSrUoRA.js → BaseInput-ClaRNrF8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ClwfMXUS.js → Chat-sZE2Z77o.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjMaqfL7.js → ChatBubbleRenderer-BAfKOnRe.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BYTehcQG.js → Checkbox-Cvhay5WM.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B8AMcp6J.js → Codegen-BpejL-4N.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BNSogPo3.js → Col-g2Ki6g_1.js} +1 -1
- package/src/assets/web-panel/assets/{Community-JxPvGb8O.js → Community-D2xKbNo9.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DF7bFAgE.js → Compact-DbL1Pds3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Bvhqnwrq.js → Compliance-DGtZCV4c.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BDS0CJvM.js → Cowork-4kQpoNsI.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DqvRfxoQ.js → Cron-BxTYXXAN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CDcIvnqS.js → Crosschain-k0J0LT2N.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D7jaUxU3.js → DID-BaqhoPgN.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B5A18wEV.js → Dashboard-T4RVPIM1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-_6IVkGag.js → Dropdown-DszOToWq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9PeiKCG.js → EmailListRenderer-C9KJU7HF.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DXjuEHrX.js → FamilyGuardDashboard-Cmr0JnsA.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-B4IAR9zv.js → Federation-BI-_2xZX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CxQeSNx1.js → FormItemContext-DsnDEPsl.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B8GS-Y3N.js → GenericCardRenderer-BLncAKKK.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BS8SouUX.js → Git-BPQhGXDn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-Dq9aYb5Q.js → Governance-CyElbUQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BrCEe9AJ.js → Inference-CcSntBQD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DybOpiY2.js → KnowledgeGraph-p6e7HZ2r.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DBpzbRfx.js → Logs-CNEtr-wz.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Bj9xI0YH.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Cc7CCSCu.js → Marketplace-Dd2notAJ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DFaIhOfu.js → McpTools-BBsN2CSW.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-KCqlp9ol.js → Memory-Dg2uTvwx.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bys84ySN.js → MobileBridge-r1YMRiJV.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B-6qU-je.js → MobileProjects-C-89kSm4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BgY-fvCq.js → Mtc-CrPZcVgf.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-B8pCmqQJ.js → MtcAudit-sxi7hqJb.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BbOmYutA.js → Multisig-CAgOMfF1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BocLQz_M.js → NLProgramming-DafaPts3.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CSwbxEop.js → Notes-CJM-xUW_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CqFNSYG3.js → NotificationSettings-L0ZjfAh_.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C6gMfAk7.js → OrderTableRenderer-BB1CvCK0.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-QrDXVdMJ.js → Organization-DeLtgAD8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cxntotjt.js → Overflow-CKC2b10z.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CIdBOwNq.js → P2P-eyf2rzY1.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CT5aP8rD.js → PdhVaultBrowser-5upNIOW8.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DH7OM8XU.js → Permissions-B8p3W6JK.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CnJqbWce.js → PersonalDataHub-BnRMxTq3.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-5C3HG1kH.js → Pipeline-DjCNI1li.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B4yIp7xK.js → Privacy-u6gXLbWT.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BMvt-2M0.js → ProjectInit-BZAw4tkM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CvSTEMRG.js → ProjectSettings-B1Tr72q7.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BKBJxEek.js → Projects-DO7HFuBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BdwUqFWN.js → Providers-BjCztyhj.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DtnCKlTm.js → QrScannerModal-ZWyCUbR1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-sTbgwyNr.js → QuickAsk-DhDjeW4G.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Bzr14gSb.js → Recommend-R_CN2MuU.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C_TF5IaJ.js → RemoteSession-uxFcujeu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-GCAM4H2K.js → Reputation-DX8D7rks.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dir0r6OQ.js → Row-BYahQILD.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CpGnlecX.js → RssFeed-CUSQ2tm-.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DMMamC6u.js → Search-CnN_S8Cs.js} +1 -1
- package/src/assets/web-panel/assets/{Security-yqay5yG-.js → Security-DcZlh-O4.js} +4 -4
- package/src/assets/web-panel/assets/{Services-C5i2wh9G.js → Services-ZhBOjzIX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-vUqk35yj.js → Skeleton-D5Qnq_lI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DzcSkhot.js → Skills-BC9cZ3Be.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CB6qbYNr.js → Sla-DauJ3E8_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DbNeZ_fh.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D-RrsxTJ.js → SyncSettings-DUvJbSct.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DFxdOJiq.js → Tasks-BXa-5noQ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-SKAob_Aq.js → Templates-JxaiUt_3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BPfsfHQ-.js → Tenant-BW4ERQN8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-8iw7FpUf.js → Terminal-DltUbdtv.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C4Gjj5Su.js → TimelineRenderer-CMoZz0Tc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-COYhN18e.js → Tokens-D_VEV60K.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B-JzXcf6.js → Trigger-CCXTCs0R.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BJ5bg0nI.js → Trust-lkGRIPcG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BG_ByRMf.js → UkeySign-BfGeQ9HX.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B3kOnSiU.js → VideoEditing-BEC7pYvc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CcoGFnK7.js → Wallet-BZdG24o0.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DdT8vRC_.js → WebAuthn-yoEddedp.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CZu3M9N8.js → WorkflowEditor-BXZEprOW.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DxJt3Qba.js → chat-gg0bzZal.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DdYfwJaB.js → colors-DjRVRuin.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BG3SUzwj.js → compact-item-su-dieAc.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-By51z2-1.js → createContext-Ci3S8oKF.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CxbFX9uH.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B0apZ3KJ.js → hasIn-D8F10heH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE76D-kA.js → index-11wtYQiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkvcEo4p.js → index-1IgxgALJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bos5_aMV.js → index-1agckWSq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfWYsB9t.js → index-5JqhujnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAhHirVH.js → index-6Tniqk1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYINoRLK.js → index-8Z-8BIlN.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5rLiEo0.js → index-BCI5v0yB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ_0qm4g.js → index-BDJntYmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWEuZWCK.js → index-BGzixOGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLIbGOLX.js → index-BMMOVoLD.js} +1 -1
- package/src/assets/web-panel/assets/{index-bP_VlDb4.js → index-BTFpdBtt.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3rWuPMW.js → index-Ba_HyPnE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Da_WBdLX.js → index-BbF1IHof.js} +1 -1
- package/src/assets/web-panel/assets/{index-RD6O-0C6.js → index-BgT9su_e.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpjZhgNd.js → index-BzPB6kGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q2SCX-MI.js → index-C6ygzplr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIrJfqO4.js → index-CC3ZDjzY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBPf3TpM.js → index-CEmXRhAE.js} +1 -1
- package/src/assets/web-panel/assets/index-CJGOlduY.js +1 -0
- package/src/assets/web-panel/assets/{index-Cp6e-hGw.js → index-CMPqd5Cv.js} +1 -1
- package/src/assets/web-panel/assets/{index-kf57LJ93.js → index-COWd8uGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW93tZDv.js → index-CPIYSp4H.js} +1 -1
- package/src/assets/web-panel/assets/{index-BewKYrX9.js → index-CR0h6seT.js} +1 -1
- package/src/assets/web-panel/assets/{index-p1_8SbI1.js → index-CUmDhC4j.js} +1 -1
- package/src/assets/web-panel/assets/index-CkBCN5yH.js +1 -0
- package/src/assets/web-panel/assets/{index-ZxtP8jG7.js → index-CusgZPtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWF8_mkx.js → index-DBAETXft.js} +1 -1
- package/src/assets/web-panel/assets/{index-BP94CrBV.js → index-DIJ2qFOC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNIxXMmN.js → index-Dbq4NMET.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzmQElZy.js → index-DkMEF5VT.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZFbps9po.js → index-DkfQR3hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dz49VOyQ.js → index-DnLucOn9.js} +1 -1
- package/src/assets/web-panel/assets/{index-oxRrmFO9.js → index-DugB_aKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bissefox.js → index-DyLUarhZ.js} +3 -3
- package/src/assets/web-panel/assets/{index-DaIz0NAW.js → index-T6_6ygJZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-SQqOlR3d.js → index-Tu8eWqh-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DCn8IcxV.js → index-W7XUgdVL.js} +1 -1
- package/src/assets/web-panel/assets/{index-3jWkIu5j.js → index-dHZinfko.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1C9i79A.js → index-jyNgp5FI.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BWJktcg8.js → initDefaultProps-Cv4p8e4w.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BEXBzUFV.js → motion-B8EwmMOw.js} +1 -1
- package/src/assets/web-panel/assets/{move-BUKwn2sx.js → move-B8R9F8TK.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-FTI7btqQ.js → mtc-parser-DoFYz5Bp.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CsLtSPH3.js → omit-CHWYLBjI.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DWZxsT_S.js → pickAttrs-CXuPDO77.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bcfs43CX.js → placementArrow-BaAtUUon.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-a9bcC1p-.js → responsiveObserve-DCc4zcDD.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BDTzgOXN.js → slide-B_lx89oZ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Iq1j_HUg.js → statusUtils-DE8DPmR0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DAZEYxeh.js → styleChecker-1ktXOsL_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DVkjyDrh.js → useFlexGapSupport-xH89-wjh.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-wXTpNTHt.js → useFs-DscvfW5p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CtQM_aZr.js → usePersonalDataHub-RdMKQta7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-hjEoG85v.js → vnode-Z3wTni2T.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BHVpyKg_.js → zoom-B0iChCsm.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +91 -60
- package/src/commands/git.js +39 -0
- package/src/commands/plugin.js +17 -2
- package/src/commands/session-group.js +131 -0
- package/src/commands/session-location.js +48 -0
- package/src/commands/session.js +11 -0
- package/src/commands/sync-providers.js +4 -1
- package/src/commands/update.js +1 -0
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/ws-agent-handler.js +20 -12
- package/src/harness/jsonl-session-store.js +279 -37
- package/src/harness/mcp-client.js +701 -91
- package/src/harness/session-index.js +15 -4
- package/src/lib/agent-worktree.js +8 -1
- package/src/lib/auto-mode-safety-classifier.js +3 -17
- package/src/lib/background-agent-keeper-protocol.js +26 -6
- package/src/lib/background-agent-supervisor.js +17 -1
- package/src/lib/browser-evidence.js +1405 -0
- package/src/lib/chrome-connector.js +465 -79
- package/src/lib/claude-project-auto-memory.js +481 -0
- package/src/lib/claude-project-storage-layout.js +104 -0
- package/src/lib/cloud/cloud-client.js +40 -0
- package/src/lib/execution-location-local-supervisor.mjs +226 -0
- package/src/lib/execution-location-runner-lifecycle.js +991 -0
- package/src/lib/execution-location-target-preflight.js +333 -0
- package/src/lib/execution-location-target.js +783 -13
- package/src/lib/file-lines.js +6 -1
- package/src/lib/gateway-client-resilience.js +571 -0
- package/src/lib/gitlab-mr-worktree.js +176 -0
- package/src/lib/hooks-v2-producers.js +119 -15
- package/src/lib/hooks-v2-workspace-context.js +44 -1
- package/src/lib/host-resource-budget.js +295 -0
- package/src/lib/ide-context.js +15 -2
- package/src/lib/llm-config-defaults.js +43 -0
- package/src/lib/mcp-lifecycle-authority.js +887 -0
- package/src/lib/mcp-oauth.js +101 -20
- package/src/lib/mcp-tls.js +103 -3
- package/src/lib/paths.js +260 -32
- package/src/lib/permission-request.js +48 -0
- package/src/lib/plugin-runtime/bin.js +3 -1
- package/src/lib/plugin-runtime/hooks.js +3 -1
- package/src/lib/plugin-runtime/install.js +117 -1
- package/src/lib/plugin-runtime/lsp.js +4 -4
- package/src/lib/plugin-runtime/marketplace-archive-source.js +40 -5
- package/src/lib/plugin-runtime/marketplace-catalog.js +13 -2
- package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
- package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
- package/src/lib/plugin-runtime/marketplace-source-adapter.js +33 -8
- package/src/lib/plugin-runtime/mcp.js +3 -1
- package/src/lib/plugin-runtime/monitors.js +3 -1
- package/src/lib/plugin-runtime/remote-source.js +151 -6
- package/src/lib/plugin-runtime/settings.js +3 -1
- package/src/lib/plugin-runtime/trust.js +274 -13
- package/src/lib/process-execution-broker/credential-agent.js +210 -41
- package/src/lib/process-execution-broker/index.js +15 -1
- package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
- package/src/lib/project-mcp-trust.js +165 -10
- package/src/lib/resume-session.js +9 -6
- package/src/lib/session-core-singletons.js +26 -11
- package/src/lib/session-host-lease.js +52 -16
- package/src/lib/session-lifecycle-scope.js +153 -0
- package/src/lib/session-message-fabric.js +168 -21
- package/src/lib/session-projection.js +244 -4
- package/src/lib/session-store-guard.js +3 -1
- package/src/lib/session-workbench-store.js +466 -0
- package/src/lib/spellcheck.js +211 -0
- package/src/lib/sub-agent-context.js +9 -9
- package/src/lib/sync-cli-db.js +485 -129
- package/src/lib/sync-credentials.js +31 -20
- package/src/lib/web-fetch.js +112 -40
- package/src/lib/web-search.js +127 -61
- package/src/lib/workspace-trust.js +793 -0
- package/src/repl/agent-repl.js +124 -5
- package/src/repl/permission-prompt.js +10 -12
- package/src/runtime/agent-core.js +311 -18
- package/src/runtime/bootstrap.js +37 -13
- package/src/runtime/coding-agent-contract-shared.cjs +11 -4
- package/src/runtime/headless-runner.js +234 -36
- package/src/runtime/headless-stream.js +181 -6
- package/src/runtime/mcp-config.js +298 -8
- package/src/runtime/policies/agent-policy.js +8 -0
- package/src/runtime/status-diagnostics-lite.js +9 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CFyt8iUd.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BHUI7cox.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DorRZcI9.js +0 -1
- package/src/assets/web-panel/assets/index-BETiL0a3.js +0 -1
- package/src/assets/web-panel/assets/index-BhAVHuW8.js +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-only GitLab merge-request reference parsing and worktree footer UI.
|
|
3
|
+
*
|
|
4
|
+
* This intentionally performs no network I/O and never accepts a token. A
|
|
5
|
+
* caller may render an MR produced by a later `glab`/GitLab integration while
|
|
6
|
+
* preserving a safe, portable contract for local worktree workflows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MAX_REFERENCE_BYTES = 4096;
|
|
10
|
+
const MAX_TITLE_BYTES = 512;
|
|
11
|
+
const SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u;
|
|
12
|
+
const IID_RE = /^[1-9][0-9]{0,9}$/u;
|
|
13
|
+
|
|
14
|
+
function safeText(value, label, maximum) {
|
|
15
|
+
if (
|
|
16
|
+
typeof value !== "string" ||
|
|
17
|
+
!value ||
|
|
18
|
+
Buffer.byteLength(value, "utf8") > maximum ||
|
|
19
|
+
/[\r\n\u0000-\u001f\u007f]/u.test(value)
|
|
20
|
+
) {
|
|
21
|
+
throw new TypeError(`${label} is invalid`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeProjectPath(value) {
|
|
27
|
+
const segments = String(value || "").split("/");
|
|
28
|
+
if (
|
|
29
|
+
segments.length < 2 ||
|
|
30
|
+
segments.some((segment) => !SEGMENT_RE.test(segment))
|
|
31
|
+
) {
|
|
32
|
+
throw new TypeError("GitLab project path is invalid");
|
|
33
|
+
}
|
|
34
|
+
return segments.join("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeHost(value) {
|
|
38
|
+
const host = safeText(String(value || "").toLowerCase(), "GitLab host", 255);
|
|
39
|
+
if (
|
|
40
|
+
!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(host) &&
|
|
41
|
+
host !== "localhost"
|
|
42
|
+
) {
|
|
43
|
+
throw new TypeError("GitLab host is invalid");
|
|
44
|
+
}
|
|
45
|
+
return host;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeIid(value) {
|
|
49
|
+
const iid = String(value || "");
|
|
50
|
+
if (!IID_RE.test(iid)) throw new TypeError("GitLab MR iid is invalid");
|
|
51
|
+
return Number(iid);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildWebUrl(host, projectPath, iid) {
|
|
55
|
+
return `https://${host}/${projectPath}/-/merge_requests/${iid}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse an HTTPS GitLab web URL or `namespace/project!iid` shorthand. Nested
|
|
60
|
+
* subgroup paths are allowed; credentials, query strings and fragments are
|
|
61
|
+
* rejected rather than echoed into terminal output.
|
|
62
|
+
*/
|
|
63
|
+
export function parseGitLabMergeRequestReference(value, options = {}) {
|
|
64
|
+
const input = safeText(
|
|
65
|
+
String(value || "").trim(),
|
|
66
|
+
"GitLab MR reference",
|
|
67
|
+
MAX_REFERENCE_BYTES,
|
|
68
|
+
);
|
|
69
|
+
if (/(?:^|\/)(?:\.|\.\.)(?:\/|$)/u.test(input)) {
|
|
70
|
+
throw new TypeError("GitLab MR reference is invalid");
|
|
71
|
+
}
|
|
72
|
+
const shorthand = input.match(/^(.+)!([1-9][0-9]{0,9})$/u);
|
|
73
|
+
if (shorthand && !input.includes("://")) {
|
|
74
|
+
const host = safeHost(options.defaultHost || "gitlab.com");
|
|
75
|
+
const projectPath = safeProjectPath(shorthand[1]);
|
|
76
|
+
const iid = safeIid(shorthand[2]);
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
host,
|
|
79
|
+
projectPath,
|
|
80
|
+
iid,
|
|
81
|
+
webUrl: buildWebUrl(host, projectPath, iid),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(input);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new TypeError("GitLab MR reference is invalid");
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
url.protocol !== "https:" ||
|
|
93
|
+
url.username ||
|
|
94
|
+
url.password ||
|
|
95
|
+
url.port ||
|
|
96
|
+
url.search ||
|
|
97
|
+
url.hash
|
|
98
|
+
) {
|
|
99
|
+
throw new TypeError("GitLab MR reference is invalid");
|
|
100
|
+
}
|
|
101
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
102
|
+
const marker = parts.lastIndexOf("-");
|
|
103
|
+
if (
|
|
104
|
+
marker < 2 ||
|
|
105
|
+
parts[marker + 1] !== "merge_requests" ||
|
|
106
|
+
parts.length !== marker + 3
|
|
107
|
+
) {
|
|
108
|
+
throw new TypeError("GitLab MR reference is invalid");
|
|
109
|
+
}
|
|
110
|
+
const host = safeHost(url.hostname);
|
|
111
|
+
const projectPath = safeProjectPath(parts.slice(0, marker).join("/"));
|
|
112
|
+
const iid = safeIid(parts[marker + 2]);
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
host,
|
|
115
|
+
projectPath,
|
|
116
|
+
iid,
|
|
117
|
+
webUrl: buildWebUrl(host, projectPath, iid),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function safeOptionalText(value, label, maximum = 255) {
|
|
122
|
+
if (value == null || value === "") return null;
|
|
123
|
+
return safeText(String(value), label, maximum);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Normalize a local GitLab MR display model without retaining API extras. */
|
|
127
|
+
export function normalizeGitLabMergeRequest(value, options = {}) {
|
|
128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
129
|
+
throw new TypeError("GitLab MR is invalid");
|
|
130
|
+
}
|
|
131
|
+
const reference = parseGitLabMergeRequestReference(
|
|
132
|
+
value.webUrl || value.web_url || value.reference,
|
|
133
|
+
options,
|
|
134
|
+
);
|
|
135
|
+
const iid = value.iid == null ? reference.iid : safeIid(value.iid);
|
|
136
|
+
if (iid !== reference.iid)
|
|
137
|
+
throw new TypeError("GitLab MR iid does not match reference");
|
|
138
|
+
const state = safeOptionalText(
|
|
139
|
+
value.state || "opened",
|
|
140
|
+
"GitLab MR state",
|
|
141
|
+
32,
|
|
142
|
+
)?.toLowerCase();
|
|
143
|
+
if (state && !new Set(["opened", "closed", "merged", "locked"]).has(state)) {
|
|
144
|
+
throw new TypeError("GitLab MR state is invalid");
|
|
145
|
+
}
|
|
146
|
+
return Object.freeze({
|
|
147
|
+
...reference,
|
|
148
|
+
title: safeOptionalText(value.title, "GitLab MR title", MAX_TITLE_BYTES),
|
|
149
|
+
state: state || "opened",
|
|
150
|
+
sourceBranch: safeOptionalText(
|
|
151
|
+
value.sourceBranch || value.source_branch,
|
|
152
|
+
"GitLab MR source branch",
|
|
153
|
+
),
|
|
154
|
+
targetBranch: safeOptionalText(
|
|
155
|
+
value.targetBranch || value.target_branch,
|
|
156
|
+
"GitLab MR target branch",
|
|
157
|
+
),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Render a compact, safe footer suitable for terminal and IDE worktree views. */
|
|
162
|
+
export function renderGitLabMergeRequestFooter(value, options = {}) {
|
|
163
|
+
const mr = normalizeGitLabMergeRequest(value, options);
|
|
164
|
+
const lines = [`GitLab MR !${mr.iid} · ${mr.state}`, mr.webUrl];
|
|
165
|
+
if (mr.title) lines.push(mr.title);
|
|
166
|
+
if (mr.sourceBranch || mr.targetBranch) {
|
|
167
|
+
lines.push(`${mr.sourceBranch || "?"} → ${mr.targetBranch || "?"}`);
|
|
168
|
+
}
|
|
169
|
+
const worktreePath = safeOptionalText(
|
|
170
|
+
options.worktreePath,
|
|
171
|
+
"GitLab worktree path",
|
|
172
|
+
4096,
|
|
173
|
+
);
|
|
174
|
+
if (worktreePath) lines.push(`worktree: ${worktreePath}`);
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
@@ -1,21 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Best-effort bridge from real runtime producers to Hooks v2.
|
|
3
3
|
* Hook failures never change the surrounding agent or interaction result.
|
|
4
|
+
*
|
|
5
|
+
* A headless host may subscribe to a particular session to project a small,
|
|
6
|
+
* machine-readable lifecycle stream. The projection deliberately excludes
|
|
7
|
+
* hook command lines, input, output, and error text: hooks often handle
|
|
8
|
+
* credentials, paths, and user content. The observer is diagnostic only and
|
|
9
|
+
* is never allowed to affect a hook decision.
|
|
4
10
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
const sessionObservers = new Map();
|
|
12
|
+
|
|
13
|
+
function safeId(value, max = 160) {
|
|
14
|
+
if (typeof value !== "string") return null;
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed ? trimmed.slice(0, max) : null;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
+
function observerSetFor(context = {}) {
|
|
20
|
+
const sessionId = safeId(context.session_id ?? context.sessionId);
|
|
21
|
+
return sessionId ? sessionObservers.get(sessionId) || null : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function projectLifecycle(eventName, context = {}, phase, outcome = null) {
|
|
25
|
+
const projected = {
|
|
26
|
+
type: phase,
|
|
27
|
+
schema_version: 1,
|
|
28
|
+
hook_event: safeId(eventName, 96) || "unknown",
|
|
29
|
+
};
|
|
30
|
+
const sessionId = safeId(context.session_id ?? context.sessionId);
|
|
31
|
+
const traceId = safeId(context.trace_id ?? context.traceId);
|
|
32
|
+
const parentId = safeId(context.parent_id ?? context.parentId);
|
|
33
|
+
const turnId = safeId(context.turn_id ?? context.turnId);
|
|
34
|
+
const toolUseId = safeId(context.tool_use_id ?? context.toolUseId);
|
|
35
|
+
if (sessionId) projected.session_id = sessionId;
|
|
36
|
+
if (traceId) projected.trace_id = traceId;
|
|
37
|
+
if (parentId) projected.parent_id = parentId;
|
|
38
|
+
if (turnId) projected.turn_id = turnId;
|
|
39
|
+
if (toolUseId) projected.tool_use_id = toolUseId;
|
|
40
|
+
if (!outcome || typeof outcome !== "object") return projected;
|
|
41
|
+
|
|
42
|
+
if (phase === "hook_progress") {
|
|
43
|
+
projected.hook_id = safeId(outcome.hookId, 160) || null;
|
|
44
|
+
projected.status = safeId(outcome.status, 32) || "unknown";
|
|
45
|
+
projected.decision = safeId(outcome.decision, 32) || "continue";
|
|
46
|
+
if (Number.isFinite(outcome.durationMs)) {
|
|
47
|
+
projected.duration_ms = Math.max(0, Math.floor(outcome.durationMs));
|
|
48
|
+
}
|
|
49
|
+
return projected;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
projected.decision = safeId(outcome.decision, 32) || "continue";
|
|
53
|
+
projected.blocked = outcome.blocked === true;
|
|
54
|
+
projected.requires_approval = outcome.requiresApproval === true;
|
|
55
|
+
projected.hook_count = Array.isArray(outcome.results)
|
|
56
|
+
? outcome.results.length
|
|
57
|
+
: 0;
|
|
58
|
+
projected.error = outcome.error ? "hook_runtime_error" : undefined;
|
|
59
|
+
return projected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function notifyObservers(eventName, context, phase, outcome = null) {
|
|
63
|
+
const observers = observerSetFor(context);
|
|
64
|
+
if (!observers || observers.size === 0) return;
|
|
65
|
+
const event = projectLifecycle(eventName, context, phase, outcome);
|
|
66
|
+
for (const observer of observers) {
|
|
67
|
+
try {
|
|
68
|
+
observer(event);
|
|
69
|
+
} catch {
|
|
70
|
+
// Observability must never delay or weaken the hook boundary.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Register a diagnostic observer for a single canonical session id. */
|
|
76
|
+
export function addHooksV2EventObserver(sessionId, observer) {
|
|
77
|
+
const key = safeId(sessionId);
|
|
78
|
+
if (!key || typeof observer !== "function") return () => {};
|
|
79
|
+
let observers = sessionObservers.get(key);
|
|
80
|
+
if (!observers) {
|
|
81
|
+
observers = new Set();
|
|
82
|
+
sessionObservers.set(key, observers);
|
|
83
|
+
}
|
|
84
|
+
observers.add(observer);
|
|
85
|
+
return () => {
|
|
86
|
+
observers.delete(observer);
|
|
87
|
+
if (observers.size === 0) sessionObservers.delete(key);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function dispatchHooksV2Event(eventName, context, options) {
|
|
19
92
|
try {
|
|
20
93
|
const { default: runtime } = await import("./hooks-v2-runtime.js");
|
|
21
94
|
if (!runtime || typeof runtime.executeHooks !== "function") {
|
|
@@ -39,6 +112,39 @@ export async function executeHooksV2Event(
|
|
|
39
112
|
}
|
|
40
113
|
}
|
|
41
114
|
|
|
115
|
+
function reportOutcome(eventName, context, outcome) {
|
|
116
|
+
for (const result of Array.isArray(outcome?.results) ? outcome.results : []) {
|
|
117
|
+
notifyObservers(eventName, context, "hook_progress", result);
|
|
118
|
+
}
|
|
119
|
+
notifyObservers(eventName, context, "hook_response", outcome);
|
|
120
|
+
return outcome;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function emitHooksV2Event(eventName, context = {}, options = {}) {
|
|
124
|
+
notifyObservers(eventName, context, "hook_started");
|
|
125
|
+
void dispatchHooksV2Event(eventName, context, options).then((outcome) =>
|
|
126
|
+
reportOutcome(eventName, context, outcome),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decision-capable producer bridge. Callers at a real gate (PreToolUse,
|
|
132
|
+
* Setup, prompt expansion) await this result; observer producers can keep
|
|
133
|
+
* using emitHooksV2Event().
|
|
134
|
+
*/
|
|
135
|
+
export async function executeHooksV2Event(
|
|
136
|
+
eventName,
|
|
137
|
+
context = {},
|
|
138
|
+
options = {},
|
|
139
|
+
) {
|
|
140
|
+
notifyObservers(eventName, context, "hook_started");
|
|
141
|
+
return reportOutcome(
|
|
142
|
+
eventName,
|
|
143
|
+
context,
|
|
144
|
+
await dispatchHooksV2Event(eventName, context, options),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
42
148
|
export function resolvePromptExpansion(prompt, outcome = {}) {
|
|
43
149
|
const base = String(prompt ?? "");
|
|
44
150
|
const updates = [];
|
|
@@ -69,8 +175,6 @@ export function resolvePromptExpansion(prompt, outcome = {}) {
|
|
|
69
175
|
prompt: resolved,
|
|
70
176
|
blocked: outcome.blocked === true || outcome.decision === "block",
|
|
71
177
|
reason:
|
|
72
|
-
outcome.blockingResult?.reason ||
|
|
73
|
-
outcome.blockingResult?.message ||
|
|
74
|
-
null,
|
|
178
|
+
outcome.blockingResult?.reason || outcome.blockingResult?.message || null,
|
|
75
179
|
};
|
|
76
180
|
}
|
|
@@ -2,6 +2,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
evaluateWorkspaceTrustDecision,
|
|
7
|
+
projectWorkspaceTrustAudit,
|
|
8
|
+
} from "./workspace-trust.js";
|
|
5
9
|
|
|
6
10
|
const trustedWorkspaceStorage = new AsyncLocalStorage();
|
|
7
11
|
const registeredHostWorkspaces = new Map();
|
|
@@ -57,12 +61,37 @@ function captureWorkspaceIdentity(workspaceRoot) {
|
|
|
57
61
|
"Hooks v2 host workspace root must resolve to a directory",
|
|
58
62
|
);
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
const identity = {
|
|
61
65
|
canonicalRoot,
|
|
62
66
|
device: stats.dev,
|
|
63
67
|
inode: stats.ino,
|
|
64
68
|
generation: workspaceGeneration(stats),
|
|
69
|
+
};
|
|
70
|
+
// Hooks have no user-configured path authority: the trusted host bootstrap
|
|
71
|
+
// is their separate evidence/consent channel. It still passes through the
|
|
72
|
+
// shared canonical identity and strict lattice so audit consumers see the
|
|
73
|
+
// same repository/worktree identity as MCP and plugins.
|
|
74
|
+
const decision = evaluateWorkspaceTrustDecision({
|
|
75
|
+
workspaceRoot: canonicalRoot,
|
|
76
|
+
evidence: [
|
|
77
|
+
{
|
|
78
|
+
source: "hooks",
|
|
79
|
+
consent: "host-bound",
|
|
80
|
+
fingerprint: `${stats.dev}\0${stats.ino}\0${identity.generation}`,
|
|
81
|
+
decision: "allow",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
65
84
|
});
|
|
85
|
+
if (decision.decision !== "allow") {
|
|
86
|
+
throw invalidWorkspaceRoot(
|
|
87
|
+
"Hooks v2 host workspace trust identity is unavailable",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
Object.defineProperty(identity, "workspaceTrustAudit", {
|
|
91
|
+
value: projectWorkspaceTrustAudit(decision),
|
|
92
|
+
enumerable: false,
|
|
93
|
+
});
|
|
94
|
+
return Object.freeze(identity);
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
function workspaceIdentityKey(identity) {
|
|
@@ -242,6 +271,20 @@ export function resolveHostHooksV2WorkspaceBinding(binding) {
|
|
|
242
271
|
return verifyWorkspaceRecord(record);
|
|
243
272
|
}
|
|
244
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Return the redacted shared trust projection for one currently valid Hooks
|
|
276
|
+
* binding. It deliberately exposes neither the canonical path nor a host
|
|
277
|
+
* identity token.
|
|
278
|
+
*/
|
|
279
|
+
export function projectHostHooksV2WorkspaceTrustAudit(binding) {
|
|
280
|
+
if (!binding || typeof binding !== "object") return null;
|
|
281
|
+
const record = registeredHostWorkspaces.get(binding.bindingId);
|
|
282
|
+
if (!record || record.binding !== binding || !verifyWorkspaceRecord(record)) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return record.identity.workspaceTrustAudit || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
245
288
|
/**
|
|
246
289
|
* Resolve an opaque durable binding only against roots registered by the
|
|
247
290
|
* current host process. No persisted or event-supplied path is accepted.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-owned, content-free resource accounting for long-running agent hosts.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally separate from a provider/session budget: it applies
|
|
5
|
+
* backpressure to host queues and caches before work is admitted, while an
|
|
6
|
+
* optional SessionResourceBudget remains the shared authority for tool time.
|
|
7
|
+
* Queue slots retain only opaque identifiers, never renderer/event payloads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
export const HOST_BACKLOG_KINDS = Object.freeze(["renderer", "tool", "event"]);
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_HOST_RESOURCE_LIMITS = Object.freeze({
|
|
15
|
+
webFetchTtlMs: 60_000,
|
|
16
|
+
maxWebFetchEntries: 32,
|
|
17
|
+
maxWebFetchBytes: 8 * 1024 * 1024,
|
|
18
|
+
maxWebSearchResults: 20,
|
|
19
|
+
maxRendererBacklog: 64,
|
|
20
|
+
maxToolBacklog: 32,
|
|
21
|
+
maxEventBacklog: 256,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const BACKLOG_LIMIT_FIELDS = Object.freeze({
|
|
25
|
+
renderer: "maxRendererBacklog",
|
|
26
|
+
tool: "maxToolBacklog",
|
|
27
|
+
event: "maxEventBacklog",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function normalizeNonNegativeInteger(field, value) {
|
|
31
|
+
const normalized = Number(value);
|
|
32
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
33
|
+
throw new TypeError(`invalid host resource budget limit: ${field}`);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePositiveInteger(field, value) {
|
|
39
|
+
const normalized = normalizeNonNegativeInteger(field, value);
|
|
40
|
+
if (normalized < 1) {
|
|
41
|
+
throw new TypeError(`invalid host resource budget limit: ${field}`);
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cloneCacheValue(value) {
|
|
47
|
+
// WebFetch produces JSON-compatible values. structuredClone retains arrays
|
|
48
|
+
// and JSON output without allowing callers to mutate the retained cache.
|
|
49
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
50
|
+
return JSON.parse(JSON.stringify(value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cacheValueBytes(value) {
|
|
54
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class HostResourceBudgetError extends Error {
|
|
58
|
+
constructor(reason, message = null) {
|
|
59
|
+
super(message || `Host resource budget exhausted: ${reason}`);
|
|
60
|
+
this.name = "HostResourceBudgetError";
|
|
61
|
+
this.code = "ERR_HOST_RESOURCE_BUDGET";
|
|
62
|
+
this.budgetReason = reason;
|
|
63
|
+
this.retryable = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A host creates one instance per interactive/headless run and passes it to
|
|
69
|
+
* tool adapters. Slots represent work waiting to be rendered, completed, or
|
|
70
|
+
* delivered; callers must release them once their consumer has drained it.
|
|
71
|
+
*/
|
|
72
|
+
export class HostResourceBudget {
|
|
73
|
+
constructor({
|
|
74
|
+
sessionBudget = null,
|
|
75
|
+
webFetchTtlMs = DEFAULT_HOST_RESOURCE_LIMITS.webFetchTtlMs,
|
|
76
|
+
maxWebFetchEntries = DEFAULT_HOST_RESOURCE_LIMITS.maxWebFetchEntries,
|
|
77
|
+
maxWebFetchBytes = DEFAULT_HOST_RESOURCE_LIMITS.maxWebFetchBytes,
|
|
78
|
+
maxWebSearchResults = DEFAULT_HOST_RESOURCE_LIMITS.maxWebSearchResults,
|
|
79
|
+
maxRendererBacklog = DEFAULT_HOST_RESOURCE_LIMITS.maxRendererBacklog,
|
|
80
|
+
maxToolBacklog = DEFAULT_HOST_RESOURCE_LIMITS.maxToolBacklog,
|
|
81
|
+
maxEventBacklog = DEFAULT_HOST_RESOURCE_LIMITS.maxEventBacklog,
|
|
82
|
+
now = () => Date.now(),
|
|
83
|
+
} = {}) {
|
|
84
|
+
if (
|
|
85
|
+
sessionBudget !== null &&
|
|
86
|
+
(typeof sessionBudget !== "object" ||
|
|
87
|
+
typeof sessionBudget.beginTool !== "function")
|
|
88
|
+
) {
|
|
89
|
+
throw new TypeError("sessionBudget must provide beginTool()");
|
|
90
|
+
}
|
|
91
|
+
if (typeof now !== "function") {
|
|
92
|
+
throw new TypeError("host resource budget now must be a function");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this._sessionBudget = sessionBudget;
|
|
96
|
+
this._now = now;
|
|
97
|
+
this.webFetchTtlMs = normalizeNonNegativeInteger(
|
|
98
|
+
"webFetchTtlMs",
|
|
99
|
+
webFetchTtlMs,
|
|
100
|
+
);
|
|
101
|
+
this.maxWebFetchEntries = normalizeNonNegativeInteger(
|
|
102
|
+
"maxWebFetchEntries",
|
|
103
|
+
maxWebFetchEntries,
|
|
104
|
+
);
|
|
105
|
+
this.maxWebFetchBytes = normalizeNonNegativeInteger(
|
|
106
|
+
"maxWebFetchBytes",
|
|
107
|
+
maxWebFetchBytes,
|
|
108
|
+
);
|
|
109
|
+
this.maxWebSearchResults = normalizePositiveInteger(
|
|
110
|
+
"maxWebSearchResults",
|
|
111
|
+
maxWebSearchResults,
|
|
112
|
+
);
|
|
113
|
+
this._backlogs = new Map(
|
|
114
|
+
HOST_BACKLOG_KINDS.map((kind) => [
|
|
115
|
+
kind,
|
|
116
|
+
{
|
|
117
|
+
max: normalizeNonNegativeInteger(
|
|
118
|
+
BACKLOG_LIMIT_FIELDS[kind],
|
|
119
|
+
{
|
|
120
|
+
maxRendererBacklog,
|
|
121
|
+
maxToolBacklog,
|
|
122
|
+
maxEventBacklog,
|
|
123
|
+
}[BACKLOG_LIMIT_FIELDS[kind]],
|
|
124
|
+
),
|
|
125
|
+
slots: new Set(),
|
|
126
|
+
},
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
this._webFetchCache = new Map();
|
|
130
|
+
this._webFetchCacheBytes = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_backlog(kind) {
|
|
134
|
+
const backlog = this._backlogs.get(kind);
|
|
135
|
+
if (!backlog) throw new TypeError(`invalid host backlog kind: ${kind}`);
|
|
136
|
+
return backlog;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
admit(kind) {
|
|
140
|
+
const backlog = this._backlog(kind);
|
|
141
|
+
if (backlog.slots.size >= backlog.max) {
|
|
142
|
+
throw new HostResourceBudgetError(`${kind}-backlog`);
|
|
143
|
+
}
|
|
144
|
+
const id = `host-${kind}-${randomUUID()}`;
|
|
145
|
+
backlog.slots.add(id);
|
|
146
|
+
let released = false;
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
id,
|
|
149
|
+
kind,
|
|
150
|
+
release: () => {
|
|
151
|
+
if (released) return false;
|
|
152
|
+
released = true;
|
|
153
|
+
return backlog.slots.delete(id);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
admitRenderer() {
|
|
159
|
+
return this.admit("renderer");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
admitEvent() {
|
|
163
|
+
return this.admit("event");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
admitTool({ kind = "tool" } = {}) {
|
|
167
|
+
const slot = this.admit("tool");
|
|
168
|
+
let sessionLease = null;
|
|
169
|
+
try {
|
|
170
|
+
if (this._sessionBudget) {
|
|
171
|
+
const admission = this._sessionBudget.beginTool({
|
|
172
|
+
id: `host-tool-${randomUUID()}`,
|
|
173
|
+
kind: String(kind || "tool"),
|
|
174
|
+
});
|
|
175
|
+
if (!admission?.ok) {
|
|
176
|
+
throw new HostResourceBudgetError(
|
|
177
|
+
`session-${admission?.reason || "tool-admission"}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
sessionLease = admission;
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
slot.release();
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let released = false;
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
...slot,
|
|
190
|
+
release: () => {
|
|
191
|
+
if (released) return false;
|
|
192
|
+
released = true;
|
|
193
|
+
try {
|
|
194
|
+
sessionLease?.end();
|
|
195
|
+
} catch {
|
|
196
|
+
// A session may already be aborting when a completed host I/O task
|
|
197
|
+
// releases its slot. The authoritative abort is preserved, while
|
|
198
|
+
// this cleanup must still make the bounded host slot reusable.
|
|
199
|
+
} finally {
|
|
200
|
+
slot.release();
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
capWebSearchResults(value) {
|
|
208
|
+
const requested = normalizePositiveInteger("webSearchResults", value);
|
|
209
|
+
return Math.min(requested, this.maxWebSearchResults);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_expireWebFetch(now = this._now()) {
|
|
213
|
+
for (const [key, entry] of this._webFetchCache) {
|
|
214
|
+
if (entry.expiresAt > now) continue;
|
|
215
|
+
this._webFetchCache.delete(key);
|
|
216
|
+
this._webFetchCacheBytes -= entry.bytes;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getWebFetch(key) {
|
|
221
|
+
if (typeof key !== "string" || key.length === 0) return null;
|
|
222
|
+
this._expireWebFetch();
|
|
223
|
+
const entry = this._webFetchCache.get(key);
|
|
224
|
+
if (!entry) return null;
|
|
225
|
+
// Map insertion order gives us a bounded LRU cache without retaining any
|
|
226
|
+
// user-provided key outside the configured entry/byte ceilings.
|
|
227
|
+
this._webFetchCache.delete(key);
|
|
228
|
+
this._webFetchCache.set(key, entry);
|
|
229
|
+
return cloneCacheValue(entry.value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
putWebFetch(key, value) {
|
|
233
|
+
if (
|
|
234
|
+
this.webFetchTtlMs === 0 ||
|
|
235
|
+
this.maxWebFetchEntries === 0 ||
|
|
236
|
+
this.maxWebFetchBytes === 0 ||
|
|
237
|
+
typeof key !== "string" ||
|
|
238
|
+
key.length === 0
|
|
239
|
+
) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
const copy = cloneCacheValue(value);
|
|
243
|
+
const bytes = cacheValueBytes(copy);
|
|
244
|
+
if (bytes > this.maxWebFetchBytes) return false;
|
|
245
|
+
this._expireWebFetch();
|
|
246
|
+
const previous = this._webFetchCache.get(key);
|
|
247
|
+
if (previous) {
|
|
248
|
+
this._webFetchCache.delete(key);
|
|
249
|
+
this._webFetchCacheBytes -= previous.bytes;
|
|
250
|
+
}
|
|
251
|
+
while (
|
|
252
|
+
this._webFetchCache.size >= this.maxWebFetchEntries ||
|
|
253
|
+
this._webFetchCacheBytes + bytes > this.maxWebFetchBytes
|
|
254
|
+
) {
|
|
255
|
+
const oldest = this._webFetchCache.entries().next().value;
|
|
256
|
+
if (!oldest) break;
|
|
257
|
+
const [oldestKey, oldestEntry] = oldest;
|
|
258
|
+
this._webFetchCache.delete(oldestKey);
|
|
259
|
+
this._webFetchCacheBytes -= oldestEntry.bytes;
|
|
260
|
+
}
|
|
261
|
+
this._webFetchCache.set(key, {
|
|
262
|
+
expiresAt: this._now() + this.webFetchTtlMs,
|
|
263
|
+
bytes,
|
|
264
|
+
value: copy,
|
|
265
|
+
});
|
|
266
|
+
this._webFetchCacheBytes += bytes;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
status() {
|
|
271
|
+
this._expireWebFetch();
|
|
272
|
+
const backlog = Object.fromEntries(
|
|
273
|
+
[...this._backlogs.entries()].map(([kind, state]) => [
|
|
274
|
+
kind,
|
|
275
|
+
{ active: state.slots.size, max: state.max },
|
|
276
|
+
]),
|
|
277
|
+
);
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
limits: Object.freeze({
|
|
280
|
+
webFetchTtlMs: this.webFetchTtlMs,
|
|
281
|
+
maxWebFetchEntries: this.maxWebFetchEntries,
|
|
282
|
+
maxWebFetchBytes: this.maxWebFetchBytes,
|
|
283
|
+
maxWebSearchResults: this.maxWebSearchResults,
|
|
284
|
+
maxRendererBacklog: backlog.renderer.max,
|
|
285
|
+
maxToolBacklog: backlog.tool.max,
|
|
286
|
+
maxEventBacklog: backlog.event.max,
|
|
287
|
+
}),
|
|
288
|
+
backlog: Object.freeze(backlog),
|
|
289
|
+
webFetchCache: Object.freeze({
|
|
290
|
+
entries: this._webFetchCache.size,
|
|
291
|
+
bytes: this._webFetchCacheBytes,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|