chainlesschain 0.162.123 → 0.162.125
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 +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/AIOps-DcqNhZhA.js +1 -0
- package/src/assets/web-panel/assets/{ActionButton-BugJtiJc.js → ActionButton-BigtmPoq.js} +1 -1
- package/src/assets/web-panel/assets/Analytics-1J_X_HF4.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-Cb0eHetM.js → AppLayout-DlcDG_a_.js} +5 -5
- package/src/assets/web-panel/assets/Audit-Do-y8_3w.js +1 -0
- package/src/assets/web-panel/assets/Backup-S2Df-50Y.js +1 -0
- package/src/assets/web-panel/assets/{BaseInput-CZ0c7QHk.js → BaseInput-D5kgWJfk.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CaKDT3wV.js → Chat-aUcp3kN1.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6-Iu3bG.js → Checkbox-DTg1U7Xs.js} +1 -1
- package/src/assets/web-panel/assets/Codegen-YO9ZZ4Ky.js +1 -0
- package/src/assets/web-panel/assets/{Col-DVBrO7AN.js → Col-BHonE9fU.js} +1 -1
- package/src/assets/web-panel/assets/Community-DA2AlEFh.js +1 -0
- package/src/assets/web-panel/assets/{Compact-DM4ZgFSE.js → Compact-DULxLRNg.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-BZqfuuZL.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-CJwiWkUY.js → Cowork-CJe3vyMS.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-dsdXHvrO.js → Cron-CeV8AtRu.js} +2 -2
- package/src/assets/web-panel/assets/Crosschain-CAg66zS5.js +1 -0
- package/src/assets/web-panel/assets/{DID-BB_LBLYT.js → DID-Dtup5JZm.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-DAR_8PNy.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-dTFtVnKk.js → Dropdown-K5bTaCYN.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-yqdttzXV.js → EmailListRenderer-C890uErx.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DfX3YpUU.js → FamilyGuardDashboard-B-Tj_8yM.js} +1 -1
- package/src/assets/web-panel/assets/Federation-C6WZ98ni.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-DNMwIh7X.js → FormItemContext-D-LTfGWd.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C-T8UQf0.js → GenericCardRenderer-C80DK4W7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-SURFhkLi.js → Git-Cjvvv3zW.js} +2 -2
- package/src/assets/web-panel/assets/Governance-C0BND77H.js +1 -0
- package/src/assets/web-panel/assets/Inference-BL9kTtSu.js +1 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-CnVTBmJf.js +1 -0
- package/src/assets/web-panel/assets/{Logs-DjuWREBJ.js → Logs-CbCbg7K6.js} +2 -2
- package/src/assets/web-panel/assets/Marketplace-CG5On-fH.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-D0waMomb.js → McpTools-AYYDZZK7.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-Ci768GQ3.js → Memory-tMWbMDQq.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-BAOsf4wB.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-RpFuAiNz.js → MobileProjects-BT-2komQ.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DJdw8btT.js → Mtc-BFwfC63n.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dc596XgH.js → MtcAudit-DYG3CWdH.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig--Qs7eGsH.js → Multisig-KJwd0yWT.js} +3 -3
- package/src/assets/web-panel/assets/NLProgramming-BmL5dNWm.js +1 -0
- package/src/assets/web-panel/assets/{Notes-uMxjj66_.js → Notes-DmkaVaMc.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C4ikBOsm.js → NotificationSettings-tmVQszDZ.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BpmdunAz.js → OrderTableRenderer-BtrR7anf.js} +1 -1
- package/src/assets/web-panel/assets/Organization-pppktQyW.js +4 -0
- package/src/assets/web-panel/assets/{Overflow-B-4_IpbB.js → Overflow-D3lBoQDA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DdOf9BZs.js → P2P-BgE2qGlZ.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-D26Bz5gS.js +7 -0
- package/src/assets/web-panel/assets/Permissions-BR8no2Xe.js +4 -0
- package/src/assets/web-panel/assets/{PersonalDataHub-B3WcvKni.js → PersonalDataHub-DabbyQEF.js} +2 -2
- package/src/assets/web-panel/assets/Pipeline-n4sNpEz8.js +1 -0
- package/src/assets/web-panel/assets/Privacy-CGNp4n8M.js +1 -0
- package/src/assets/web-panel/assets/{ProjectInit-BQjMDkML.js → ProjectInit-CAVA5VsX.js} +2 -2
- package/src/assets/web-panel/assets/ProjectSettings-GCgkfM7A.js +2 -0
- package/src/assets/web-panel/assets/Projects-BYK17p52.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Bl0dw6cI.js → Providers-DpUT5W-d.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BDrk7l3t.js → QuickAsk-ZJxTb7vi.js} +1 -1
- package/src/assets/web-panel/assets/Recommend-CIzFRDk1.js +1 -0
- package/src/assets/web-panel/assets/Reputation-lQ_WDNZn.js +1 -0
- package/src/assets/web-panel/assets/Row-1MEtrgtF.js +1 -0
- package/src/assets/web-panel/assets/{RssFeed-9-Fxh3_w.js → RssFeed-SSgcPPl4.js} +2 -2
- package/src/assets/web-panel/assets/Search-CcTMOGNY.js +1 -0
- package/src/assets/web-panel/assets/{Security-D6CySV7r.js → Security-BA1KMaDx.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Co8Mk8qE.js → Services-_aj7czZn.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BdON8M-1.js → Skeleton-BWgg_0Zr.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CEJE1Go1.js → Skills-C2ZIziYM.js} +1 -1
- package/src/assets/web-panel/assets/Sla-CYxp4axY.js +1 -0
- package/src/assets/web-panel/assets/SpeechSettings-C7Csp1jV.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-52NXsfyj.js → SyncSettings-svGeswiw.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-He7CweW6.js → Tasks-ClISj6oK.js} +1 -1
- package/src/assets/web-panel/assets/Templates-CmO-Fp7r.js +1 -0
- package/src/assets/web-panel/assets/Tenant-a3Ac3lxz.js +1 -0
- package/src/assets/web-panel/assets/Terminal-DyJIRh81.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-BdfqBQmM.js → TimelineRenderer-acXS5N5h.js} +1 -1
- package/src/assets/web-panel/assets/Tokens-DJW0KqV4.js +1 -0
- package/src/assets/web-panel/assets/{Trigger-Dt_N_rvo.js → Trigger-0HeTi4r6.js} +1 -1
- package/src/assets/web-panel/assets/Trust-aFym34eo.js +1 -0
- package/src/assets/web-panel/assets/{UkeySign-CW8YdFxg.js → UkeySign-CYhszHJv.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CU6o7qvp.js → VideoEditing-BbJxPF9X.js} +1 -1
- package/src/assets/web-panel/assets/Wallet-DDthEwiu.js +4 -0
- package/src/assets/web-panel/assets/WebAuthn-DQ6McYPg.js +5 -0
- package/src/assets/web-panel/assets/{WorkflowEditor-BcCA2U8H.js → WorkflowEditor-CnF8zRsi.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Cxl5DGsc.js → chat-Dzkx2gTP.js} +1 -1
- package/src/assets/web-panel/assets/{colors-FS_7Xggs.js → colors-35T51Oo5.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BzXjd3wJ.js → compact-item-TtzNrlu1.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DhF9bi_u.js → createContext-Y1LkWrYb.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Y1a0aV9N.js → hasIn-BOYw1BgS.js} +1 -1
- package/src/assets/web-panel/assets/{index-JUukFpxi.js → index-8jffdb6b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBtULjKY.js → index-B7aKAZzS.js} +1 -1
- package/src/assets/web-panel/assets/index-BGPEaeB1.js +1 -0
- package/src/assets/web-panel/assets/index-BKRlWHUp.js +1 -0
- package/src/assets/web-panel/assets/{index-Dmjxpu0L.js → index-BSpFe6j5.js} +1 -1
- package/src/assets/web-panel/assets/{index-1iacQgo5.js → index-BWgNn9As.js} +28 -26
- package/src/assets/web-panel/assets/{index-Na733kBa.js → index-Bs69SI96.js} +2 -2
- package/src/assets/web-panel/assets/{index-B48PZ-xM.js → index-BtlJWaSP.js} +2 -2
- package/src/assets/web-panel/assets/index-Bw1iOGhM.js +1 -0
- package/src/assets/web-panel/assets/{index-DVTex5M8.js → index-BxWUEFKy.js} +1 -1
- package/src/assets/web-panel/assets/index-C7Wt5feN.js +1 -0
- package/src/assets/web-panel/assets/index-CEwuCM44.js +1 -0
- package/src/assets/web-panel/assets/index-CLqC2sRT.js +3 -0
- package/src/assets/web-panel/assets/{index-DxGVbuqr.js → index-CPGNc2tF.js} +2 -2
- package/src/assets/web-panel/assets/{index-DfaSKEVD.js → index-CVrUs6rf.js} +1 -1
- package/src/assets/web-panel/assets/index-CZ16GlOs.js +1 -0
- package/src/assets/web-panel/assets/{index-CELVZPNt.js → index-CdyFg4FI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CETvk0NN.js → index-CqhwG1ox.js} +4 -4
- package/src/assets/web-panel/assets/index-Cs82BHAj.js +1 -0
- package/src/assets/web-panel/assets/{index-D3IVAG9l.js → index-D2od7Bgx.js} +1 -1
- package/src/assets/web-panel/assets/{index-Sl-sLj3T.js → index-D47robkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci7PSoQn.js → index-D4BgCBa3.js} +2 -2
- package/src/assets/web-panel/assets/index-D6tG4B25.js +55 -0
- package/src/assets/web-panel/assets/index-D7dCBw_M.js +1 -0
- package/src/assets/web-panel/assets/index-DAizH273.js +21 -0
- package/src/assets/web-panel/assets/{index-GCd5hs58.js → index-DFhlelzN.js} +2 -2
- package/src/assets/web-panel/assets/index-DhMSOd_2.js +1 -0
- package/src/assets/web-panel/assets/index-Dhfw-BXs.js +12 -0
- package/src/assets/web-panel/assets/index-DjeUZxE5.js +1 -0
- package/src/assets/web-panel/assets/{index-BnvaUocg.js → index-DmkG479D.js} +2 -2
- package/src/assets/web-panel/assets/index-Dof7lWA_.js +13 -0
- package/src/assets/web-panel/assets/{index-BNmWkPqm.js → index-Dx0hIfC-.js} +1 -1
- package/src/assets/web-panel/assets/index-DzzgU4IU.js +1 -0
- package/src/assets/web-panel/assets/index-G4ClSmJc.js +1 -0
- package/src/assets/web-panel/assets/{index-Cn9lbqaS.js → index-KgXrlfNF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUcVF8e6.js → index-NM9Oke2k.js} +2 -2
- package/src/assets/web-panel/assets/{index-DLyIxnjc.js → index-XIuZV9by.js} +3 -3
- package/src/assets/web-panel/assets/{index-BTltEtoG.js → index-hyqEKkBT.js} +1 -1
- package/src/assets/web-panel/assets/{index-uLoKviUT.js → index-zF77jnI2.js} +2 -2
- package/src/assets/web-panel/assets/{initDefaultProps-D6QmlnQ-.js → initDefaultProps-DD5y5msp.js} +1 -1
- package/src/assets/web-panel/assets/motion-BuoutMia.js +11 -0
- package/src/assets/web-panel/assets/{move-Bpph6B_N.js → move-DkpjqOXg.js} +1 -1
- package/src/assets/web-panel/assets/mtc-parser-BmGJD-v-.js +1 -0
- package/src/assets/web-panel/assets/{omit-Dx0YMbWn.js → omit-DftUE0Sz.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D-a8Fm6C.js → pickAttrs-DDRb9FIu.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DXFSVgxv.js → placementArrow-DPgtxOOD.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CxhQ1EaK.js → responsiveObserve-BQFRzFeZ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-IlBI0XU8.js → slide-DQ4lmUSz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-COoXVzje.js → statusUtils-C1TR4ZiQ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCuq89M_.js → styleChecker-B-Suj978.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-tfNE1ALO.js → useFlexGapSupport-Q7bDZ3EI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DD4bzBkU.js → useFs-DHjRLyQH.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DVkH1hbT.js → usePersonalDataHub-lQOvCvCr.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D1ZmXyxM.js → vnode-Ie0g4-p3.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DblBUHfi.js → zoom-CHP0YEoH.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +10 -0
- package/src/commands/ask.js +24 -1
- package/src/commands/ide.js +60 -5
- package/src/commands/incentive.js +22 -22
- package/src/commands/init.js +5 -4
- package/src/commands/instinct.js +18 -5
- package/src/commands/mcp.js +30 -3
- package/src/commands/memory.js +18 -5
- package/src/commands/mtc.js +5 -1
- package/src/gateways/http/envelope-http-server.js +17 -2
- package/src/gateways/terminal/PtyManager.js +15 -2
- package/src/gateways/ws/message-dispatcher.js +4 -1
- package/src/gateways/ws/ws-server.js +20 -0
- package/src/gateways/ws/ws-session-gateway.js +72 -4
- package/src/harness/background-task-manager.js +5 -1
- package/src/harness/jsonl-session-store.js +51 -0
- package/src/harness/mcp-client.js +119 -21
- package/src/harness/prompt-compressor.js +24 -5
- package/src/harness/worktree-isolator.js +25 -10
- package/src/lib/agent-core.js +4 -0
- package/src/lib/autonomous-agent.js +36 -6
- package/src/lib/chat-intent-service.js +18 -4
- package/src/lib/checkpoint-store.js +54 -4
- package/src/lib/claude-code-bridge.js +48 -3
- package/src/lib/cost-budget.js +13 -2
- package/src/lib/credential-guard.js +45 -0
- package/src/lib/git-integration.js +68 -3
- package/src/lib/goal-store.js +11 -0
- package/src/lib/hook-manager.js +4 -0
- package/src/lib/hook-runner.cjs +11 -1
- package/src/lib/interactive-planner.js +4 -3
- package/src/lib/jetbrains-bridge.js +147 -0
- package/src/lib/json-schema-output.js +47 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/learning/reflection-engine.js +4 -3
- package/src/lib/learning/skill-improver.js +4 -3
- package/src/lib/learning/skill-synthesizer.js +4 -3
- package/src/lib/llm-pricing.js +11 -4
- package/src/lib/llm-providers.js +11 -1
- package/src/lib/mcp-oauth.js +220 -20
- package/src/lib/mcp-serve.js +85 -6
- package/src/lib/orchestrator.js +42 -4
- package/src/lib/process-manager.js +31 -3
- package/src/lib/repl-bang-memorize.js +9 -1
- package/src/lib/repl-completer.js +29 -0
- package/src/lib/runnable-provider.js +7 -1
- package/src/lib/session-insights.js +13 -6
- package/src/lib/session-usage.js +21 -3
- package/src/lib/slot-filler.js +4 -3
- package/src/lib/status-line.cjs +4 -1
- package/src/lib/sub-agent-context.js +15 -0
- package/src/lib/web-fetch.js +54 -3
- package/src/lib/workflow-engine.js +35 -11
- package/src/repl/agent-repl.js +29 -0
- package/src/runtime/agent-core.js +252 -28
- package/src/runtime/headless-runner.js +13 -0
- package/src/runtime/headless-stream.js +133 -17
- package/src/runtime/mcp-config.js +72 -3
- package/src/runtime/pipe-safety.js +51 -0
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/AIOps-C5CSd8pY.js +0 -1
- package/src/assets/web-panel/assets/Analytics-CzxpcV9E.js +0 -3
- package/src/assets/web-panel/assets/Audit-qjq04wh8.js +0 -1
- package/src/assets/web-panel/assets/Backup-C2Y_XNBA.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-TeKnMwPj.js +0 -1
- package/src/assets/web-panel/assets/Codegen-TGLFk6P1.js +0 -1
- package/src/assets/web-panel/assets/Community-tg6-Rik1.js +0 -1
- package/src/assets/web-panel/assets/Compliance-DFeWryxt.js +0 -1
- package/src/assets/web-panel/assets/Crosschain-p7_5Gq-7.js +0 -1
- package/src/assets/web-panel/assets/Dashboard-Bh_HkNSD.js +0 -3
- package/src/assets/web-panel/assets/Federation-BJApfPV4.js +0 -1
- package/src/assets/web-panel/assets/Governance-B4u_KsfE.js +0 -1
- package/src/assets/web-panel/assets/Inference-2kr_rKQd.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-h-nL7J6x.js +0 -1
- package/src/assets/web-panel/assets/Marketplace-W9Iz8b2m.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-4jAehfcz.js +0 -3
- package/src/assets/web-panel/assets/NLProgramming-D3y71FDX.js +0 -1
- package/src/assets/web-panel/assets/Organization-BBEmFNGM.js +0 -4
- package/src/assets/web-panel/assets/PdhVaultBrowser-DDU2Zc9Q.js +0 -7
- package/src/assets/web-panel/assets/Permissions-B1ebgevF.js +0 -4
- package/src/assets/web-panel/assets/Pipeline-DASuPDc3.js +0 -1
- package/src/assets/web-panel/assets/Privacy-DnixwmhJ.js +0 -1
- package/src/assets/web-panel/assets/ProjectSettings-dHyZ3SxW.js +0 -2
- package/src/assets/web-panel/assets/Projects-BGWEJIVT.js +0 -1
- package/src/assets/web-panel/assets/Recommend-BPKof_CA.js +0 -1
- package/src/assets/web-panel/assets/Reputation-6MNJLklK.js +0 -1
- package/src/assets/web-panel/assets/Row-DmGJwpfd.js +0 -1
- package/src/assets/web-panel/assets/Search-CCHjey_D.js +0 -1
- package/src/assets/web-panel/assets/Sla-BSxFvFX9.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-C3rShYlV.js +0 -1
- package/src/assets/web-panel/assets/Templates-UtjBlJIO.js +0 -1
- package/src/assets/web-panel/assets/Tenant-Dd5XNbYF.js +0 -1
- package/src/assets/web-panel/assets/Terminal-pFyh42cu.js +0 -3
- package/src/assets/web-panel/assets/Tokens-CsgNsrdQ.js +0 -1
- package/src/assets/web-panel/assets/Trust-B1lJzsPn.js +0 -1
- package/src/assets/web-panel/assets/Wallet-DPxjEhbN.js +0 -4
- package/src/assets/web-panel/assets/WebAuthn-CuJ29YAF.js +0 -5
- package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +0 -3
- package/src/assets/web-panel/assets/devWarning-B1j78JeH.js +0 -1
- package/src/assets/web-panel/assets/index--fcP34eT.js +0 -1
- package/src/assets/web-panel/assets/index-1khPb4ZS.js +0 -1
- package/src/assets/web-panel/assets/index-B_Z0PHC0.js +0 -12
- package/src/assets/web-panel/assets/index-BaN6wKWx.js +0 -1
- package/src/assets/web-panel/assets/index-BepInmSi.js +0 -1
- package/src/assets/web-panel/assets/index-BllceSdr.js +0 -1
- package/src/assets/web-panel/assets/index-Bt_0ygjA.js +0 -1
- package/src/assets/web-panel/assets/index-BzQkdgQW.js +0 -1
- package/src/assets/web-panel/assets/index-C2S8McM1.js +0 -55
- package/src/assets/web-panel/assets/index-CRSkKj9M.js +0 -1
- package/src/assets/web-panel/assets/index-Cgo1J_Kf.js +0 -1
- package/src/assets/web-panel/assets/index-CuTKOay7.js +0 -1
- package/src/assets/web-panel/assets/index-CxYHV8nb.js +0 -13
- package/src/assets/web-panel/assets/index-CzlCP4jP.js +0 -3
- package/src/assets/web-panel/assets/index-DZJMsVI1.js +0 -1
- package/src/assets/web-panel/assets/index-DgHWZVjv.js +0 -21
- package/src/assets/web-panel/assets/index-N7JKRLCC.js +0 -1
- package/src/assets/web-panel/assets/motion-BHrTGY1_.js +0 -11
- package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +0 -1
package/src/lib/llm-pricing.js
CHANGED
|
@@ -96,9 +96,10 @@ const round = (n, dp = 6) => {
|
|
|
96
96
|
*
|
|
97
97
|
* Per provider, a user entry whose `match` equals a built-in pattern REPLACES
|
|
98
98
|
* it; brand-new patterns are prepended so they win ties; unknown providers are
|
|
99
|
-
* added. Malformed entries (missing match / non-numeric
|
|
100
|
-
* bad config line can't crash cost reporting
|
|
101
|
-
*
|
|
99
|
+
* added. Malformed entries (missing match / non-numeric / negative / Infinity
|
|
100
|
+
* rate) are skipped so a bad config line can't crash cost reporting or produce a
|
|
101
|
+
* negative cost that undercounts spend. Returns a new table; never mutates
|
|
102
|
+
* PRICE_TABLE or the input.
|
|
102
103
|
*
|
|
103
104
|
* @param {object} [overrides]
|
|
104
105
|
* @param {object} [base=PRICE_TABLE]
|
|
@@ -123,8 +124,14 @@ export function mergePricing(overrides, base = PRICE_TABLE) {
|
|
|
123
124
|
e &&
|
|
124
125
|
typeof e.match === "string" &&
|
|
125
126
|
e.match.trim() &&
|
|
127
|
+
// Rates must be finite AND non-negative — a negative price would
|
|
128
|
+
// produce a negative cost that undercounts spend (and could keep a
|
|
129
|
+
// CostBudget under its cap). `>= 0` also rejects NaN (NaN >= 0 is
|
|
130
|
+
// false); isFinite additionally rejects Infinity.
|
|
126
131
|
Number.isFinite(Number(e.in)) &&
|
|
127
|
-
Number
|
|
132
|
+
Number(e.in) >= 0 &&
|
|
133
|
+
Number.isFinite(Number(e.out)) &&
|
|
134
|
+
Number(e.out) >= 0,
|
|
128
135
|
)
|
|
129
136
|
.map((e) => ({
|
|
130
137
|
match: e.match.toLowerCase(),
|
package/src/lib/llm-providers.js
CHANGED
|
@@ -181,12 +181,22 @@ export class LLMProviderRegistry {
|
|
|
181
181
|
.prepare("SELECT * FROM llm_providers WHERE custom = 1")
|
|
182
182
|
.all();
|
|
183
183
|
for (const row of rows) {
|
|
184
|
+
// Tolerate a corrupt `models` TEXT column: `|| "[]"` only covers null,
|
|
185
|
+
// not malformed JSON, and an unguarded throw here would abort the whole
|
|
186
|
+
// custom-provider load (and the registry constructor) over one bad row.
|
|
187
|
+
let models = [];
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(row.models || "[]");
|
|
190
|
+
if (Array.isArray(parsed)) models = parsed;
|
|
191
|
+
} catch {
|
|
192
|
+
/* corrupt models column — fall back to no models for this provider */
|
|
193
|
+
}
|
|
184
194
|
this.providers.set(row.name, {
|
|
185
195
|
name: row.name,
|
|
186
196
|
displayName: row.display_name,
|
|
187
197
|
baseUrl: row.base_url,
|
|
188
198
|
apiKeyEnv: row.api_key_env,
|
|
189
|
-
models
|
|
199
|
+
models,
|
|
190
200
|
custom: true,
|
|
191
201
|
free: false,
|
|
192
202
|
});
|
package/src/lib/mcp-oauth.js
CHANGED
|
@@ -62,6 +62,43 @@ export function randomState(bytes = 16) {
|
|
|
62
62
|
return base64url(_deps.randomBytes(bytes));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Is `hostname` a loopback address? Plain http is tolerated only for these (a
|
|
67
|
+
* self-hosted MCP server on the dev box). Handles the bracketed IPv6 form that
|
|
68
|
+
* `new URL().hostname` yields (e.g. "[::1]") and the whole 127.0.0.0/8 range.
|
|
69
|
+
*/
|
|
70
|
+
export function isLoopbackHost(hostname) {
|
|
71
|
+
const h = String(hostname || "")
|
|
72
|
+
.replace(/^\[|\]$/g, "")
|
|
73
|
+
.toLowerCase();
|
|
74
|
+
if (h === "localhost") return true;
|
|
75
|
+
if (h === "::1") return true;
|
|
76
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Require an OAuth endpoint to be HTTPS (or plain http on loopback for local
|
|
81
|
+
* dev). Authorization codes, PKCE verifiers, client secrets, and bearer/refresh
|
|
82
|
+
* tokens flow through these endpoints, so a discovered (or stored) endpoint that
|
|
83
|
+
* downgrades to cleartext http on a remote host must be refused rather than
|
|
84
|
+
* leaking secrets over the wire (RFC 6749 §3.1.2.1 / RFC 8252). Returns the
|
|
85
|
+
* endpoint unchanged when allowed; throws otherwise.
|
|
86
|
+
*/
|
|
87
|
+
export function assertSecureEndpoint(endpoint, label = "endpoint") {
|
|
88
|
+
let u;
|
|
89
|
+
try {
|
|
90
|
+
u = new URL(endpoint);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`OAuth ${label} is not a valid URL: ${endpoint}`);
|
|
93
|
+
}
|
|
94
|
+
if (u.protocol === "https:") return endpoint;
|
|
95
|
+
if (u.protocol === "http:" && isLoopbackHost(u.hostname)) return endpoint;
|
|
96
|
+
throw new Error(
|
|
97
|
+
`refusing to use insecure OAuth ${label} (${endpoint}); ` +
|
|
98
|
+
`HTTPS is required for non-loopback endpoints`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
/** Minimal HTML-escape for values reflected into the callback page. */
|
|
66
103
|
function escapeHtml(s) {
|
|
67
104
|
return String(s).replace(
|
|
@@ -157,6 +194,12 @@ export async function discoverAuthMetadata(
|
|
|
157
194
|
{ resourceMetadataUrl } = {},
|
|
158
195
|
) {
|
|
159
196
|
const origin = new URL(serverUrl).origin;
|
|
197
|
+
// Discovery itself carries no secrets, but its responses dictate the
|
|
198
|
+
// authorization/token endpoints — so a cleartext-remote discovery is a
|
|
199
|
+
// downgrade vector (a MITM could inject endpoints). Refuse a non-loopback
|
|
200
|
+
// http MCP URL up front (loopback http stays allowed for local dev), matching
|
|
201
|
+
// the endpoint enforcement below.
|
|
202
|
+
assertSecureEndpoint(origin, "MCP server URL");
|
|
160
203
|
// 1. protected-resource metadata (RFC 9728) → authorization_servers[]
|
|
161
204
|
let authServer = origin;
|
|
162
205
|
try {
|
|
@@ -172,6 +215,12 @@ export async function discoverAuthMetadata(
|
|
|
172
215
|
} catch {
|
|
173
216
|
// no protected-resource doc → assume the origin is its own auth server
|
|
174
217
|
}
|
|
218
|
+
// The authorization-server location can come from the REMOTE protected-
|
|
219
|
+
// resource doc, so a malicious/MITM'd doc could point discovery at a
|
|
220
|
+
// cleartext host. Refuse a non-loopback http authServer before fetching its
|
|
221
|
+
// metadata (the discovered endpoints are HTTPS-checked too, but this fails
|
|
222
|
+
// closed earlier and blocks the downgrade).
|
|
223
|
+
assertSecureEndpoint(authServer, "authorization server");
|
|
175
224
|
// 2. authorization-server metadata (RFC 8414)
|
|
176
225
|
const candidates = [
|
|
177
226
|
`${authServer}/.well-known/oauth-authorization-server`,
|
|
@@ -209,6 +258,7 @@ export async function registerClient(
|
|
|
209
258
|
"server has no registration_endpoint and no --client-id was given",
|
|
210
259
|
);
|
|
211
260
|
}
|
|
261
|
+
assertSecureEndpoint(metadata.registration_endpoint, "registration_endpoint");
|
|
212
262
|
const reg = await fetchJson(metadata.registration_endpoint, {
|
|
213
263
|
method: "POST",
|
|
214
264
|
headers: { "content-type": "application/json" },
|
|
@@ -230,6 +280,10 @@ export function buildAuthorizeUrl(
|
|
|
230
280
|
metadata,
|
|
231
281
|
{ clientId, redirectUri, scope, codeChallenge, state, resource },
|
|
232
282
|
) {
|
|
283
|
+
assertSecureEndpoint(
|
|
284
|
+
metadata.authorization_endpoint,
|
|
285
|
+
"authorization_endpoint",
|
|
286
|
+
);
|
|
233
287
|
const u = new URL(metadata.authorization_endpoint);
|
|
234
288
|
u.searchParams.set("response_type", "code");
|
|
235
289
|
u.searchParams.set("client_id", clientId);
|
|
@@ -261,6 +315,7 @@ export async function exchangeCodeForToken(
|
|
|
261
315
|
});
|
|
262
316
|
if (clientSecret) body.set("client_secret", clientSecret);
|
|
263
317
|
if (resource) body.set("resource", resource);
|
|
318
|
+
assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
|
|
264
319
|
const tok = await fetchJson(metadata.token_endpoint, {
|
|
265
320
|
method: "POST",
|
|
266
321
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -287,6 +342,7 @@ export async function refreshAccessToken(
|
|
|
287
342
|
});
|
|
288
343
|
if (clientSecret) body.set("client_secret", clientSecret);
|
|
289
344
|
if (resource) body.set("resource", resource);
|
|
345
|
+
assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
|
|
290
346
|
const tok = await fetchJson(metadata.token_endpoint, {
|
|
291
347
|
method: "POST",
|
|
292
348
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -372,26 +428,126 @@ function _writeStoreSecure(file, store) {
|
|
|
372
428
|
}
|
|
373
429
|
}
|
|
374
430
|
|
|
431
|
+
/** Bounded synchronous sleep (the store lock is held only for a few file ops). */
|
|
432
|
+
function _sleepSyncMs(ms) {
|
|
433
|
+
try {
|
|
434
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
435
|
+
} catch {
|
|
436
|
+
const end = Date.now() + ms;
|
|
437
|
+
while (Date.now() < end) {
|
|
438
|
+
/* spin — Atomics.wait unavailable (should not happen on modern Node) */
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Run `fn` while holding a cross-process advisory lock on the token store.
|
|
445
|
+
*
|
|
446
|
+
* saveStoredToken / deleteStoredToken are read-modify-write (load → mutate →
|
|
447
|
+
* write). Two concurrent `cc mcp login` PROCESSES for different servers would
|
|
448
|
+
* each load the old store and the second writer would clobber the first's new
|
|
449
|
+
* entry (lost update) — the atomic temp+rename only prevents a torn file, not a
|
|
450
|
+
* lost update. An O_EXCL lockfile makes the whole RMW mutually exclusive across
|
|
451
|
+
* processes, so each writer re-reads the latest store inside the lock.
|
|
452
|
+
*
|
|
453
|
+
* Robustness: a stale lock (holder crashed) older than STALE_MS is stolen; if
|
|
454
|
+
* the lock can't be acquired within MAX_WAIT_MS we proceed anyway (degrading to
|
|
455
|
+
* the pre-lock last-writer-wins rather than failing the login). When the
|
|
456
|
+
* injected fs lacks the lock primitives (test fs / exotic fs) we run `fn`
|
|
457
|
+
* directly — single-process correctness is unchanged, only cross-process mutual
|
|
458
|
+
* exclusion is lost. Lock timing uses the wall clock (not the `now` test seam).
|
|
459
|
+
*/
|
|
460
|
+
function withStoreLock(file, fn) {
|
|
461
|
+
const fs = _deps.fs;
|
|
462
|
+
if (
|
|
463
|
+
typeof fs.openSync !== "function" ||
|
|
464
|
+
typeof fs.closeSync !== "function" ||
|
|
465
|
+
typeof fs.unlinkSync !== "function"
|
|
466
|
+
) {
|
|
467
|
+
return fn(); // no lock primitives → run unlocked (back-compat)
|
|
468
|
+
}
|
|
469
|
+
const STALE_MS = 10_000;
|
|
470
|
+
const MAX_WAIT_MS = 5_000;
|
|
471
|
+
const lockPath = `${file}.lock`;
|
|
472
|
+
try {
|
|
473
|
+
fs.mkdirSync(pathDefault.dirname(file), { recursive: true, mode: 0o700 });
|
|
474
|
+
} catch {
|
|
475
|
+
/* dir may already exist — openSync below reports any real problem */
|
|
476
|
+
}
|
|
477
|
+
const start = Date.now();
|
|
478
|
+
let fd = null;
|
|
479
|
+
for (;;) {
|
|
480
|
+
try {
|
|
481
|
+
fd = fs.openSync(lockPath, "wx"); // O_CREAT|O_EXCL — fails if held
|
|
482
|
+
break;
|
|
483
|
+
} catch (err) {
|
|
484
|
+
if (!err || err.code !== "EEXIST") {
|
|
485
|
+
// Unexpected (e.g. ENOENT/EACCES): don't fail the login over the lock —
|
|
486
|
+
// proceed unlocked, same as a missing-primitives fs.
|
|
487
|
+
return fn();
|
|
488
|
+
}
|
|
489
|
+
let stale = false;
|
|
490
|
+
try {
|
|
491
|
+
const st = fs.statSync(lockPath);
|
|
492
|
+
const mtime = st.mtimeMs != null ? st.mtimeMs : 0;
|
|
493
|
+
stale = Date.now() - mtime > STALE_MS;
|
|
494
|
+
} catch {
|
|
495
|
+
/* lock vanished between open and stat → retry immediately */
|
|
496
|
+
}
|
|
497
|
+
if (stale) {
|
|
498
|
+
try {
|
|
499
|
+
fs.unlinkSync(lockPath); // steal a lock from a crashed holder
|
|
500
|
+
} catch {
|
|
501
|
+
/* someone else stole it first — retry */
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (Date.now() - start > MAX_WAIT_MS) break; // give up waiting; proceed
|
|
506
|
+
_sleepSyncMs(50);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
return fn();
|
|
511
|
+
} finally {
|
|
512
|
+
if (fd !== null) {
|
|
513
|
+
try {
|
|
514
|
+
fs.closeSync(fd);
|
|
515
|
+
} catch {
|
|
516
|
+
/* ignore */
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
fs.unlinkSync(lockPath);
|
|
520
|
+
} catch {
|
|
521
|
+
/* ignore — a stale-steal by another process may have removed it */
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
375
527
|
export function saveStoredToken(serverUrl, record) {
|
|
376
528
|
const file = tokenStorePath();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
529
|
+
return withStoreLock(file, () => {
|
|
530
|
+
const store = loadTokenStore(); // re-read inside the lock (latest state)
|
|
531
|
+
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
532
|
+
_writeStoreSecure(file, store);
|
|
533
|
+
return store[serverKey(serverUrl)];
|
|
534
|
+
});
|
|
381
535
|
}
|
|
382
536
|
|
|
383
537
|
export function deleteStoredToken(serverUrl) {
|
|
384
538
|
const file = tokenStorePath();
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
539
|
+
return withStoreLock(file, () => {
|
|
540
|
+
const store = loadTokenStore(); // re-read inside the lock (latest state)
|
|
541
|
+
const key = serverKey(serverUrl);
|
|
542
|
+
if (!(key in store)) return false;
|
|
543
|
+
delete store[key];
|
|
544
|
+
try {
|
|
545
|
+
_writeStoreSecure(file, store);
|
|
546
|
+
} catch {
|
|
547
|
+
/* best-effort */
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
});
|
|
395
551
|
}
|
|
396
552
|
|
|
397
553
|
/** True when a record's access token is missing or within `skewMs` of expiry. */
|
|
@@ -440,12 +596,25 @@ function defaultOpenBrowser(url) {
|
|
|
440
596
|
}
|
|
441
597
|
}
|
|
442
598
|
|
|
443
|
-
/**
|
|
599
|
+
/**
|
|
600
|
+
* Wait for the OAuth redirect on a localhost callback server; resolve
|
|
601
|
+
* {code,state}.
|
|
602
|
+
*
|
|
603
|
+
* When `expectedState` is given, a SUCCESS callback (`code`) is only accepted if
|
|
604
|
+
* its `state` matches: the callback port is fixed and guessable, so a duplicate
|
|
605
|
+
* browser request, a probe, or a drive-by request from a malicious local page
|
|
606
|
+
* must not abort the real login or inject a forged code — those are answered
|
|
607
|
+
* politely but the server keeps waiting for the genuine redirect (until the
|
|
608
|
+
* backstop timeout). A provider `error` is always terminal (so denying consent
|
|
609
|
+
* fails fast even if the provider omits state on error). With no `expectedState`
|
|
610
|
+
* the legacy contract is preserved (first `/callback` hit is terminal).
|
|
611
|
+
*/
|
|
444
612
|
export function waitForCallback({
|
|
445
613
|
port,
|
|
446
614
|
host = "127.0.0.1",
|
|
447
615
|
path = "/callback",
|
|
448
616
|
timeout = 300_000,
|
|
617
|
+
expectedState = null,
|
|
449
618
|
}) {
|
|
450
619
|
return new Promise((resolve, reject) => {
|
|
451
620
|
const server = _deps.createServer((req, res) => {
|
|
@@ -465,13 +634,41 @@ export function waitForCallback({
|
|
|
465
634
|
const code = u.searchParams.get("code");
|
|
466
635
|
const state = u.searchParams.get("state");
|
|
467
636
|
const error = u.searchParams.get("error");
|
|
468
|
-
|
|
469
|
-
|
|
637
|
+
|
|
638
|
+
// Provider error (e.g. user denied) — always terminal so we fail fast.
|
|
639
|
+
if (error) {
|
|
640
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
641
|
+
res.end(renderCallbackPage(error));
|
|
642
|
+
server.close();
|
|
643
|
+
clearTimeout(timer);
|
|
644
|
+
reject(new Error(`authorization error: ${error}`));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const stateOk = expectedState == null || state === expectedState;
|
|
649
|
+
if (code && stateOk) {
|
|
650
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
651
|
+
res.end(renderCallbackPage(null));
|
|
652
|
+
server.close();
|
|
653
|
+
clearTimeout(timer);
|
|
654
|
+
resolve({ code, state });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// A mismatched/forged/duplicate request to our fixed port must not abort
|
|
659
|
+
// the real login — answer it but keep listening for the genuine redirect.
|
|
660
|
+
if (expectedState != null) {
|
|
661
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
662
|
+
res.end(renderCallbackPage("state mismatch"));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Legacy contract (no expectedState): a /callback hit with no code ends it.
|
|
667
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
668
|
+
res.end(renderCallbackPage("missing code"));
|
|
470
669
|
server.close();
|
|
471
670
|
clearTimeout(timer);
|
|
472
|
-
|
|
473
|
-
else if (!code) reject(new Error("no authorization code in callback"));
|
|
474
|
-
else resolve({ code, state });
|
|
671
|
+
reject(new Error("no authorization code in callback"));
|
|
475
672
|
});
|
|
476
673
|
const timer = setTimeout(() => {
|
|
477
674
|
server.close();
|
|
@@ -540,6 +737,9 @@ export async function authorizeInteractive(serverUrl, opts = {}) {
|
|
|
540
737
|
host,
|
|
541
738
|
path: redirectPath,
|
|
542
739
|
timeout,
|
|
740
|
+
// Ignore callbacks that don't carry the state we issued (forged / drive-by /
|
|
741
|
+
// duplicate hits to the fixed localhost port) instead of aborting the login.
|
|
742
|
+
expectedState: state,
|
|
543
743
|
});
|
|
544
744
|
const opened = _deps.openBrowser(authorizeUrl);
|
|
545
745
|
writeOut(
|
package/src/lib/mcp-serve.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import http from "http";
|
|
18
18
|
import fsDefault from "fs";
|
|
19
19
|
import pathDefault from "path";
|
|
20
|
-
import { randomBytes } from "crypto";
|
|
20
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
21
21
|
|
|
22
22
|
export const MAX_READ_BYTES = 200 * 1024;
|
|
23
23
|
export const MAX_LIST_ENTRIES = 500;
|
|
@@ -29,14 +29,65 @@ export const _deps = { fs: fsDefault, path: pathDefault };
|
|
|
29
29
|
|
|
30
30
|
/** Resolve `rel` inside `root`; throws on escape (.. traversal, abs paths out). */
|
|
31
31
|
export function confine(root, rel, deps = _deps) {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
32
|
+
const { path } = deps;
|
|
33
|
+
const fs = deps.fs;
|
|
34
|
+
const normRoot = path.resolve(root);
|
|
35
|
+
const abs = path.resolve(normRoot, rel || ".");
|
|
36
|
+
// Fast string guard: blocks `..` traversal and absolute-path escapes.
|
|
37
|
+
if (abs !== normRoot && !abs.startsWith(normRoot + path.sep)) {
|
|
35
38
|
throw new Error(`path escapes serve root: ${rel}`);
|
|
36
39
|
}
|
|
40
|
+
// Symlink guard: path.resolve does NOT follow symlinks, so a symlink INSIDE
|
|
41
|
+
// root pointing OUTSIDE would pass the string check yet the fs op (read/write/
|
|
42
|
+
// list) would escape the serve boundary — e.g. reading ~/.ssh through a
|
|
43
|
+
// workspace symlink, defeating a `--read-only` exposure. Resolve the real path
|
|
44
|
+
// of the deepest EXISTING ancestor (a not-yet-created file can't be a symlink)
|
|
45
|
+
// and re-check containment against the real root.
|
|
46
|
+
if (fs && typeof fs.realpathSync === "function") {
|
|
47
|
+
let realRoot;
|
|
48
|
+
try {
|
|
49
|
+
realRoot = fs.realpathSync(normRoot);
|
|
50
|
+
} catch {
|
|
51
|
+
return abs; // root itself unresolvable → the string guard is all we have
|
|
52
|
+
}
|
|
53
|
+
let probe = abs;
|
|
54
|
+
for (;;) {
|
|
55
|
+
let real;
|
|
56
|
+
try {
|
|
57
|
+
real = fs.realpathSync(probe);
|
|
58
|
+
} catch {
|
|
59
|
+
const parent = path.dirname(probe);
|
|
60
|
+
if (parent === probe) break; // reached fs root, nothing existed
|
|
61
|
+
probe = parent;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
|
|
65
|
+
throw new Error(`path escapes serve root (symlink): ${rel}`);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
37
70
|
return abs;
|
|
38
71
|
}
|
|
39
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Constant-time check of an `Authorization: Bearer <token>` header against the
|
|
75
|
+
* expected token. A plain `!==` leaks timing that could let a caller recover the
|
|
76
|
+
* token byte-by-byte; hash-length differs are rejected up front (the token is a
|
|
77
|
+
* fixed-length random value, so its length is not secret), then `timingSafeEqual`
|
|
78
|
+
* compares the bytes. Mirrors the ws-server's token check.
|
|
79
|
+
*/
|
|
80
|
+
export function bearerMatches(authHeader, token) {
|
|
81
|
+
const prefix = "Bearer ";
|
|
82
|
+
if (typeof authHeader !== "string" || !authHeader.startsWith(prefix)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const a = Buffer.from(authHeader.slice(prefix.length), "utf-8");
|
|
86
|
+
const b = Buffer.from(String(token), "utf-8");
|
|
87
|
+
if (a.length !== b.length) return false;
|
|
88
|
+
return timingSafeEqual(a, b);
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
function ok(text) {
|
|
41
92
|
return { content: [{ type: "text", text: String(text) }] };
|
|
42
93
|
}
|
|
@@ -57,6 +108,35 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
|
|
|
57
108
|
},
|
|
58
109
|
handler: ({ path: rel }) => {
|
|
59
110
|
const abs = confine(root, rel, deps);
|
|
111
|
+
// Bounded read: pull at most MAX_READ_BYTES+1 bytes into memory. The
|
|
112
|
+
// old `readFileSync(abs)` loaded the WHOLE file before truncating, so
|
|
113
|
+
// MAX_READ_BYTES only capped the RESPONSE — a multi-GB file under the
|
|
114
|
+
// serve root would OOM the process. The +1 byte detects "longer than
|
|
115
|
+
// the cap" without reading the rest. Fall back to readFileSync only
|
|
116
|
+
// when the fd API isn't available on an injected fs.
|
|
117
|
+
if (typeof fs.openSync === "function") {
|
|
118
|
+
const fd = fs.openSync(abs, "r");
|
|
119
|
+
try {
|
|
120
|
+
const buf = Buffer.alloc(MAX_READ_BYTES + 1);
|
|
121
|
+
const n = fs.readSync(fd, buf, 0, MAX_READ_BYTES + 1, 0);
|
|
122
|
+
const truncated = n > MAX_READ_BYTES;
|
|
123
|
+
const text = buf
|
|
124
|
+
.slice(0, truncated ? MAX_READ_BYTES : n)
|
|
125
|
+
.toString("utf-8");
|
|
126
|
+
if (!truncated) return ok(text);
|
|
127
|
+
let total = null;
|
|
128
|
+
try {
|
|
129
|
+
total = fs.statSync(abs).size;
|
|
130
|
+
} catch {
|
|
131
|
+
/* size is best-effort for the message */
|
|
132
|
+
}
|
|
133
|
+
return ok(
|
|
134
|
+
`${text}\n… [truncated${total != null ? ` ${total} bytes` : ""}]`,
|
|
135
|
+
);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
60
140
|
const buf = fs.readFileSync(abs);
|
|
61
141
|
const truncated = buf.length > MAX_READ_BYTES;
|
|
62
142
|
const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
|
|
@@ -194,8 +274,7 @@ export function startMcpServe(opts = {}) {
|
|
|
194
274
|
return send(405, rpcError(null, -32600, "POST only"));
|
|
195
275
|
}
|
|
196
276
|
if (token) {
|
|
197
|
-
|
|
198
|
-
if (auth !== `Bearer ${token}`) {
|
|
277
|
+
if (!bearerMatches(req.headers.authorization, token)) {
|
|
199
278
|
return send(401, rpcError(null, -32001, "unauthorized"));
|
|
200
279
|
}
|
|
201
280
|
}
|
package/src/lib/orchestrator.js
CHANGED
|
@@ -27,6 +27,7 @@ import crypto from "crypto";
|
|
|
27
27
|
import { AgentRouter } from "./agent-router.js";
|
|
28
28
|
import { NotificationManager } from "./notifiers/index.js";
|
|
29
29
|
import { createChatFn } from "./cowork-adapter.js";
|
|
30
|
+
import { firstBalancedJson } from "./json-schema-output.js";
|
|
30
31
|
|
|
31
32
|
/* ---------- _deps injection (Vitest CJS mock pattern) ---------- */
|
|
32
33
|
export const _deps = { execSync };
|
|
@@ -81,6 +82,16 @@ export class Orchestrator extends EventEmitter {
|
|
|
81
82
|
this.ciCommand = options.ciCommand || "npm test";
|
|
82
83
|
this.verbose = options.verbose || false;
|
|
83
84
|
|
|
85
|
+
// Cap retained task history. Every addTask stores a record in _tasks and
|
|
86
|
+
// nothing ever removes it, so a long-running orchestrator (cron-watch /
|
|
87
|
+
// daemon) accumulates tasks without bound. Prune the oldest TERMINAL tasks
|
|
88
|
+
// beyond this cap (in-flight tasks are never dropped). Generous default;
|
|
89
|
+
// 0/invalid → 500.
|
|
90
|
+
this.maxTasks =
|
|
91
|
+
Number.isFinite(options.maxTasks) && options.maxTasks > 0
|
|
92
|
+
? options.maxTasks
|
|
93
|
+
: 500;
|
|
94
|
+
|
|
84
95
|
/** @type {Map<string, object>} taskId → task */
|
|
85
96
|
this._tasks = new Map();
|
|
86
97
|
|
|
@@ -150,6 +161,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
150
161
|
};
|
|
151
162
|
|
|
152
163
|
this._tasks.set(task.id, task);
|
|
164
|
+
this._pruneTasks();
|
|
153
165
|
this.emit("task:added", task);
|
|
154
166
|
|
|
155
167
|
if (this.notifier.isConfigured && task.notify) {
|
|
@@ -182,7 +194,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
182
194
|
return {
|
|
183
195
|
tasks,
|
|
184
196
|
pool: this._router.summary(),
|
|
185
|
-
|
|
197
|
+
ciCommand: this.ciCommand,
|
|
186
198
|
cronActive: this._cronTimer !== null,
|
|
187
199
|
};
|
|
188
200
|
}
|
|
@@ -277,7 +289,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
277
289
|
async _dispatch(task) {
|
|
278
290
|
this.emit("agents:dispatched", { task, count: task.subtasks.length });
|
|
279
291
|
this._log(
|
|
280
|
-
`Dispatching ${task.subtasks.length} subtask(s) to ${this.
|
|
292
|
+
`Dispatching ${task.subtasks.length} subtask(s) to ${this._router.summary().length} agent backend(s)`,
|
|
281
293
|
);
|
|
282
294
|
|
|
283
295
|
const results = await this._router.dispatch(task.subtasks, {
|
|
@@ -371,6 +383,11 @@ export class Orchestrator extends EventEmitter {
|
|
|
371
383
|
cwd,
|
|
372
384
|
encoding: "utf-8",
|
|
373
385
|
timeout: 180_000,
|
|
386
|
+
// execSync defaults maxBuffer to 1 MB; a verbose-but-PASSING test run
|
|
387
|
+
// that prints more than that throws ENOBUFS, which the catch below
|
|
388
|
+
// would misreport as a CI failure (triggering needless retries +
|
|
389
|
+
// re-dispatch). Give CI output generous headroom.
|
|
390
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
374
391
|
stdio: "pipe",
|
|
375
392
|
});
|
|
376
393
|
return {
|
|
@@ -386,6 +403,26 @@ export class Orchestrator extends EventEmitter {
|
|
|
386
403
|
}
|
|
387
404
|
}
|
|
388
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Bound retained task history to maxTasks. Evicts the oldest TERMINAL tasks
|
|
408
|
+
* first (Map preserves insertion order = FIFO); an in-flight task is never
|
|
409
|
+
* dropped, so a burst of concurrent runs can briefly exceed the cap rather
|
|
410
|
+
* than losing live work. The dropped records are completed/failed history
|
|
411
|
+
* the caller has already seen via task:complete / task:failed events.
|
|
412
|
+
*/
|
|
413
|
+
_pruneTasks() {
|
|
414
|
+
if (this._tasks.size <= this.maxTasks) return;
|
|
415
|
+
for (const [id, t] of this._tasks) {
|
|
416
|
+
if (this._tasks.size <= this.maxTasks) break;
|
|
417
|
+
if (
|
|
418
|
+
t.status === TASK_STATUS.COMPLETED ||
|
|
419
|
+
t.status === TASK_STATUS.FAILED
|
|
420
|
+
) {
|
|
421
|
+
this._tasks.delete(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
389
426
|
async _cronTick() {
|
|
390
427
|
this.emit("cron:tick", { at: new Date().toISOString() });
|
|
391
428
|
// Override this method or listen to the event to add input source polling
|
|
@@ -403,8 +440,9 @@ export class Orchestrator extends EventEmitter {
|
|
|
403
440
|
|
|
404
441
|
/** Extract the first JSON array from an LLM response string. */
|
|
405
442
|
function _extractJson(text) {
|
|
406
|
-
|
|
407
|
-
|
|
443
|
+
// Balanced extraction stops at the first complete array, so trailing prose
|
|
444
|
+
// (or a second array) no longer over-captures into an unparseable string.
|
|
445
|
+
return firstBalancedJson(text, "[") || "[]";
|
|
408
446
|
}
|
|
409
447
|
|
|
410
448
|
/** Parse error lines from CI output (test failures, lint errors). */
|
|
@@ -58,6 +58,17 @@ export function startApp(options = {}) {
|
|
|
58
58
|
return child.pid;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Parse a PID-file's contents into a valid positive integer, or null.
|
|
63
|
+
* A corrupt / empty / partially-written file must not yield NaN — that NaN
|
|
64
|
+
* would otherwise flow into `taskkill /PID NaN` or `process.kill(NaN, …)`,
|
|
65
|
+
* which silently fails to act on the real process.
|
|
66
|
+
*/
|
|
67
|
+
export function parsePid(content) {
|
|
68
|
+
const pid = parseInt(String(content).trim(), 10);
|
|
69
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
export function stopApp() {
|
|
62
73
|
const pidFile = getPidFilePath();
|
|
63
74
|
|
|
@@ -66,7 +77,16 @@ export function stopApp() {
|
|
|
66
77
|
return false;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
const pid =
|
|
80
|
+
const pid = parsePid(readFileSync(pidFile, "utf-8"));
|
|
81
|
+
if (pid === null) {
|
|
82
|
+
logger.warn("PID file is empty or corrupt; removing it.");
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(pidFile);
|
|
85
|
+
} catch {
|
|
86
|
+
/* best-effort */
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
70
90
|
|
|
71
91
|
try {
|
|
72
92
|
if (isWindows()) {
|
|
@@ -90,7 +110,15 @@ export function isAppRunning() {
|
|
|
90
110
|
return false;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
const pid =
|
|
113
|
+
const pid = parsePid(readFileSync(pidFile, "utf-8"));
|
|
114
|
+
if (pid === null) {
|
|
115
|
+
try {
|
|
116
|
+
unlinkSync(pidFile);
|
|
117
|
+
} catch {
|
|
118
|
+
/* best-effort */
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
94
122
|
|
|
95
123
|
try {
|
|
96
124
|
process.kill(pid, 0);
|
|
@@ -104,7 +132,7 @@ export function isAppRunning() {
|
|
|
104
132
|
export function getAppPid() {
|
|
105
133
|
const pidFile = getPidFilePath();
|
|
106
134
|
if (!existsSync(pidFile)) return null;
|
|
107
|
-
return
|
|
135
|
+
return parsePid(readFileSync(pidFile, "utf-8"));
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
function findExecutable(binDir, extension) {
|