chainlesschain 0.162.122 → 0.162.124
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-BsDJlD_l.js +1 -0
- package/src/assets/web-panel/assets/{ActionButton-Bqvaz1Yj.js → ActionButton-COjZZKWG.js} +1 -1
- package/src/assets/web-panel/assets/Analytics-4ZDZE7lc.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-cs0TRZya.js → AppLayout-CYH5k2Rp.js} +5 -5
- package/src/assets/web-panel/assets/Audit-BKlFbLdl.js +1 -0
- package/src/assets/web-panel/assets/Backup-CiX61Qc2.js +1 -0
- package/src/assets/web-panel/assets/{BaseInput-4qhQiJfI.js → BaseInput-CDxIQokd.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DUFX3Mv4.js → Chat-CaKgdRab.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C4eyD_nB.js → Checkbox-CjiWfu4s.js} +1 -1
- package/src/assets/web-panel/assets/Codegen-tKWGzajw.js +1 -0
- package/src/assets/web-panel/assets/{Col-3eHStfvJ.js → Col-ZKWREyNs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DFbKjEgh.js → Community-CmLuPBUU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D5str1h-.js → Compact-DqknM98n.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-Cdliboj6.js → Cowork-Dp29kHsC.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Bfmwl6iZ.js → Cron-zmEJAetz.js} +2 -2
- package/src/assets/web-panel/assets/Crosschain-Sb-uM7Ty.js +1 -0
- package/src/assets/web-panel/assets/{DID-xqi72kjo.js → DID-D_UFNzJ5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-4l68QdSQ.js → Dashboard-Btag698v.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-C2wsZOdn.js → Dropdown-CH7l3KCs.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-Bud2M3XX.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CHK5rapA.js → FamilyGuardDashboard-Bd2_8uME.js} +1 -1
- package/src/assets/web-panel/assets/Federation-1jhstF5P.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-lLOEHvAb.js → FormItemContext-CpSH464Y.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CaDh2XiY.js → GenericCardRenderer-CANo8O-y.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B9SmGPMU.js → Git-hvuZVoD3.js} +2 -2
- package/src/assets/web-panel/assets/Governance-BQrWksKt.js +1 -0
- package/src/assets/web-panel/assets/Inference-jEBTp12v.js +1 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-tDiV2taf.js +1 -0
- package/src/assets/web-panel/assets/Logs-DDkTnedu.js +2 -0
- package/src/assets/web-panel/assets/Marketplace-9Yi32avt.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-Dxlypa0R.js → McpTools-Qx78XyOT.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BxBHR7eW.js → Memory-CXYDO1oT.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-59gyfK6T.js → MobileProjects-DEDbWNIk.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-2YBBawFi.js → Mtc-Dwa_vlR8.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DczAeHfM.js → MtcAudit-DAVkxhJv.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CPxyt7a-.js → Multisig-C3upmgPN.js} +3 -3
- package/src/assets/web-panel/assets/NLProgramming-DO3CZ0_z.js +1 -0
- package/src/assets/web-panel/assets/{Notes-tHRMLOQ3.js → Notes-B1Oy3FbC.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DxIYUWPW.js → NotificationSettings-Bs4-Fc6b.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CEy1pIeq.js +1 -0
- package/src/assets/web-panel/assets/Organization-D4cJ1UNo.js +4 -0
- package/src/assets/web-panel/assets/{Overflow-DrdVw_fC.js → Overflow-fdzvlF7x.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CMBYxejX.js → P2P-Y13PwXBA.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-Ck1zCLe6.js +7 -0
- package/src/assets/web-panel/assets/Permissions-CvEXP6LC.js +4 -0
- package/src/assets/web-panel/assets/{PersonalDataHub-nuqNTK2w.js → PersonalDataHub-JeP7IWMW.js} +3 -3
- package/src/assets/web-panel/assets/Pipeline-BZ7wRzG1.js +1 -0
- package/src/assets/web-panel/assets/Privacy-BJ6qj9Hv.js +1 -0
- package/src/assets/web-panel/assets/{ProjectInit-hCWXNnGJ.js → ProjectInit-DeWd9UcS.js} +2 -2
- package/src/assets/web-panel/assets/ProjectSettings-DUpVuU9F.js +2 -0
- package/src/assets/web-panel/assets/{Projects-C6l6z4Mo.js → Projects-y1WbI337.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-kEifEWLb.js → Providers-DECW00ek.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B6oLUYKR.js → QuickAsk-Dl-pK9Io.js} +1 -1
- package/src/assets/web-panel/assets/Recommend-Bju04wTd.js +1 -0
- package/src/assets/web-panel/assets/Reputation-BUPa3nEk.js +1 -0
- package/src/assets/web-panel/assets/{Row-W6LSmdzh.js → Row-WYqqaq_5.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DJi97lWh.js → RssFeed-iyQT5q9l.js} +3 -3
- package/src/assets/web-panel/assets/Search-CrfwJF0R.js +1 -0
- package/src/assets/web-panel/assets/{Security-BtawKhmm.js → Security-c4Jxf5Ih.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BdCe11of.js → Services-CRuisolj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C_wcDxpN.js → Skeleton-CCnzJkb-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-n2GgEfQ1.js → Skills-CZURCwZg.js} +1 -1
- package/src/assets/web-panel/assets/Sla-CrMzaEi2.js +1 -0
- package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-BBbldyeR.js → SyncSettings-CJWVtd87.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DMkBOCW-.js → Tasks-61lrQ_lS.js} +1 -1
- package/src/assets/web-panel/assets/Templates-Cg-iF2g1.js +1 -0
- package/src/assets/web-panel/assets/Tenant-DrDv1FFc.js +1 -0
- package/src/assets/web-panel/assets/Terminal-CP15hcNS.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Drf_B0al.js → TimelineRenderer-Sl5uXrkN.js} +1 -1
- package/src/assets/web-panel/assets/Tokens-yRIZTVsR.js +1 -0
- package/src/assets/web-panel/assets/{Trigger-CNUB_qRQ.js → Trigger-BLCQEOF2.js} +1 -1
- package/src/assets/web-panel/assets/Trust-D5xq8z6s.js +1 -0
- package/src/assets/web-panel/assets/{UkeySign-7o1O51qV.js → UkeySign-Dwp0zXQ6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ccb1cTxo.js → VideoEditing-CIHA55Sx.js} +1 -1
- package/src/assets/web-panel/assets/Wallet-Hjajvnto.js +4 -0
- package/src/assets/web-panel/assets/WebAuthn-DqSURUvI.js +5 -0
- package/src/assets/web-panel/assets/{WorkflowEditor-BcFvWc2d.js → WorkflowEditor-B5bHRwUG.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CPvCSHY4.js → chat-DsheLKkG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Ca7MoLtb.js → colors-CST2UkgL.js} +1 -1
- package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +3 -0
- package/src/assets/web-panel/assets/{compact-item-B9AUAVCQ.js → compact-item-yqEoy1L7.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CnMJHqHw.js → createContext-Jwp78HFY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bevd8pXd.js → hasIn-CVS5mFEP.js} +1 -1
- package/src/assets/web-panel/assets/{index-gR1xGRvy.js → index-AnW25bEq.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3zlqc60.js → index-BCHAUdel.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzmhHe0m.js → index-BLTUVd0V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM1tI3g5.js → index-BeblNJDH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAKYScOz.js → index-BuI27WSx.js} +1 -1
- package/src/assets/web-panel/assets/{index-PidVLx0u.js → index-BzetOasL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dnk8TOm3.js → index-C0FZScMe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvhLkwFR.js → index-C130LLPq.js} +1 -1
- package/src/assets/web-panel/assets/{index-0DtNS6oi.js → index-C8rq85gu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUlsrbu2.js → index-CDAPnZUs.js} +1 -1
- package/src/assets/web-panel/assets/{index-HdGuA1pS.js → index-CIE2n8k_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ecThTNhE.js → index-CRBbVS43.js} +1 -1
- package/src/assets/web-panel/assets/{index-DW3rTBy_.js → index-CSBPc-sI.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5i4gKvN.js → index-CTd3lDxp.js} +1 -1
- package/src/assets/web-panel/assets/index-CUWj5L2s.js +1 -0
- package/src/assets/web-panel/assets/{index-B619_m_T.js → index-ChRL6rgA.js} +1 -1
- package/src/assets/web-panel/assets/{index-tUD_CWx-.js → index-CjfN2CpJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcRDjaEi.js → index-Cjig5i3a.js} +3 -3
- package/src/assets/web-panel/assets/{index-Ct3wP67S.js → index-Crk4KWLW.js} +1 -1
- package/src/assets/web-panel/assets/{index-p1rI3d66.js → index-CrlRa054.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPk2HXTA.js → index-CtyEOGuj.js} +1 -1
- package/src/assets/web-panel/assets/{index-aB7GLimB.js → index-Cu6Ql64n.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFkrnvs_.js → index-Cx_t5rWw.js} +1 -1
- package/src/assets/web-panel/assets/{index-ienIjr7_.js → index-Cy0dNzkp.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAjYLhik.js → index-CzsKK0tQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGEc6vlv.js → index-DDAigsel.js} +1 -1
- package/src/assets/web-panel/assets/index-DZfnYE6e.js +1 -0
- package/src/assets/web-panel/assets/{index-C3biz3RB.js → index-D_1b7uh6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9VR1tES.js → index-D_e9gwfn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGBbFzIJ.js → index-DbxAlpPC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVxhWXMd.js → index-De600Jjm.js} +1 -1
- package/src/assets/web-panel/assets/{index-2KMpdEzs.js → index-Dgyn8-wN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQKVKyjf.js → index-DiK4vSZa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtIv89mI.js → index-DpYn5v2k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTxmgfz2.js → index-DxaU_lza.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd62bwRd.js → index-Dxljh9Fu.js} +1 -1
- package/src/assets/web-panel/assets/{index-kUeSjyHt.js → index-R-VGFpi6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwDJvHfR.js → index-SjxCCf5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-BV1U7tR6.js → index-diWkx2Wq.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-dsKHYCPH.js → initDefaultProps-ClAjrsQ-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DdPCK2zh.js → motion-BalXv_60.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bi0bmOjg.js → move-DMelzIW-.js} +1 -1
- package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +1 -0
- package/src/assets/web-panel/assets/{omit-DCrBiySU.js → omit-oxecfU3b.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CcJuqfya.js → pickAttrs-DJ5F5M6x.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DFGuzth-.js → placementArrow-DdbKsant.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B_WDlgvI.js → responsiveObserve-BUuM5cmS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BSFabVgO.js → slide-DSV0ccvR.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CebvsGH8.js → statusUtils-B-6oLAXf.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CMuE7NWy.js → styleChecker-DdCAHtsZ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-RWGVgZNu.js → useFlexGapSupport-ZORkcoZd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-srxzzfG_.js → useFs-BFp46LoP.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-B5OwxZuK.js → usePersonalDataHub-H7rxgbdW.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CXOEebAH.js → vnode-eIq4Tk0J.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DKNwqG2v.js → zoom-CTNrv8-H.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/ask.js +24 -1
- 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/gateways/http/envelope-http-server.js +17 -2
- package/src/gateways/terminal/PtyManager.js +15 -2
- package/src/gateways/ws/ws-server.js +20 -0
- package/src/gateways/ws/ws-session-gateway.js +16 -0
- package/src/harness/mcp-client.js +124 -6
- package/src/harness/worktree-isolator.js +20 -9
- package/src/lib/agent-core.js +3 -0
- package/src/lib/autonomous-agent.js +7 -6
- package/src/lib/chat-intent-service.js +4 -2
- package/src/lib/checkpoint-store.js +23 -0
- package/src/lib/credential-guard.js +291 -0
- package/src/lib/ensure-utf8.js +6 -1
- package/src/lib/git-integration.js +57 -1
- package/src/lib/hook-manager.js +15 -6
- package/src/lib/hook-runner.cjs +11 -1
- package/src/lib/interactive-planner.js +4 -3
- package/src/lib/json-schema-output.js +47 -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/mcp-client.js +1 -0
- package/src/lib/mcp-oauth.js +243 -24
- package/src/lib/mcp-serve.js +56 -6
- package/src/lib/orchestrator.js +4 -2
- package/src/lib/process-manager.js +31 -3
- package/src/lib/repl-completer.js +29 -0
- package/src/lib/repl-rewind.js +58 -0
- package/src/lib/repl-vim.js +13 -1
- package/src/lib/settings-hooks.cjs +12 -3
- package/src/lib/slot-filler.js +4 -3
- package/src/lib/sub-agent-context.js +6 -0
- package/src/lib/web-fetch.js +54 -3
- package/src/lib/workflow-engine.js +35 -11
- package/src/repl/agent-repl.js +52 -2
- package/src/runtime/agent-core.js +324 -26
- package/src/runtime/headless-stream.js +124 -17
- package/src/assets/web-panel/assets/AIOps-BmS46GrQ.js +0 -1
- package/src/assets/web-panel/assets/Analytics-BhCnMbKz.js +0 -3
- package/src/assets/web-panel/assets/Audit-AwLNQzvX.js +0 -1
- package/src/assets/web-panel/assets/Backup-BOglo5g6.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-LWgyWUF_.js +0 -1
- package/src/assets/web-panel/assets/Codegen-B9SRqAqh.js +0 -1
- package/src/assets/web-panel/assets/Compliance-DDYPQDiG.js +0 -1
- package/src/assets/web-panel/assets/Crosschain-Bfo6RvSg.js +0 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CWh_fKmI.js +0 -1
- package/src/assets/web-panel/assets/Federation-JpiE2iKR.js +0 -1
- package/src/assets/web-panel/assets/Governance-B3ODoSO2.js +0 -1
- package/src/assets/web-panel/assets/Inference-C_jP_nlz.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-zYwypnwu.js +0 -1
- package/src/assets/web-panel/assets/Logs-D6owai5o.js +0 -2
- package/src/assets/web-panel/assets/Marketplace-umRB80w4.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-Car9kLtE.js +0 -3
- package/src/assets/web-panel/assets/NLProgramming-DvKJ8imu.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BMpVfQkg.js +0 -1
- package/src/assets/web-panel/assets/Organization-BBsie-kN.js +0 -4
- package/src/assets/web-panel/assets/PdhVaultBrowser-BBHzSaeq.js +0 -7
- package/src/assets/web-panel/assets/Permissions-W4kD06Cj.js +0 -4
- package/src/assets/web-panel/assets/Pipeline-DzCX3yHy.js +0 -1
- package/src/assets/web-panel/assets/Privacy-9ENAJQOC.js +0 -1
- package/src/assets/web-panel/assets/ProjectSettings--BzvEaxE.js +0 -2
- package/src/assets/web-panel/assets/Recommend-BIEMGN0q.js +0 -1
- package/src/assets/web-panel/assets/Reputation-D-9AEaRr.js +0 -1
- package/src/assets/web-panel/assets/Search-DOEB2utf.js +0 -1
- package/src/assets/web-panel/assets/Sla-D5WfrvLP.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-D6qCWpan.js +0 -1
- package/src/assets/web-panel/assets/Templates-DbLImR1I.js +0 -1
- package/src/assets/web-panel/assets/Tenant-B0qM4w6Z.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BjKUkiR1.js +0 -3
- package/src/assets/web-panel/assets/Tokens-DgCx2Bfa.js +0 -1
- package/src/assets/web-panel/assets/Trust-JQUTjXXI.js +0 -1
- package/src/assets/web-panel/assets/Wallet-EMf9MlLK.js +0 -4
- package/src/assets/web-panel/assets/WebAuthn-DOYoaCmi.js +0 -5
- package/src/assets/web-panel/assets/community-parser-CO25nZFt.js +0 -3
- package/src/assets/web-panel/assets/devWarning-vFgm_Ssk.js +0 -1
- package/src/assets/web-panel/assets/index-BGUjPh7h.js +0 -1
- package/src/assets/web-panel/assets/index-Cx-wP5XE.js +0 -1
- package/src/assets/web-panel/assets/mtc-parser-BbAbUB-9.js +0 -1
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
|
}
|
|
@@ -194,8 +245,7 @@ export function startMcpServe(opts = {}) {
|
|
|
194
245
|
return send(405, rpcError(null, -32600, "POST only"));
|
|
195
246
|
}
|
|
196
247
|
if (token) {
|
|
197
|
-
|
|
198
|
-
if (auth !== `Bearer ${token}`) {
|
|
248
|
+
if (!bearerMatches(req.headers.authorization, token)) {
|
|
199
249
|
return send(401, rpcError(null, -32001, "unauthorized"));
|
|
200
250
|
}
|
|
201
251
|
}
|
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 };
|
|
@@ -403,8 +404,9 @@ export class Orchestrator extends EventEmitter {
|
|
|
403
404
|
|
|
404
405
|
/** Extract the first JSON array from an LLM response string. */
|
|
405
406
|
function _extractJson(text) {
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
// Balanced extraction stops at the first complete array, so trailing prose
|
|
408
|
+
// (or a second array) no longer over-captures into an unparseable string.
|
|
409
|
+
return firstBalancedJson(text, "[") || "[]";
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
/** 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) {
|
|
@@ -32,6 +32,20 @@ export function extractAtPrefix(line) {
|
|
|
32
32
|
return m ? { prefix: m[2] } : null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* In `!` bash mode (the line starts with `!`), the trailing whitespace-delimited
|
|
37
|
+
* token completes to a filesystem path (Claude-Code 2.1.193 bash-mode file
|
|
38
|
+
* autocomplete). Returns `{ prefix }` — the token with any leading `!` stripped
|
|
39
|
+
* (so `!cat src/fo` → `src/fo`, `!scr` → `scr`) — or null when not in bash mode.
|
|
40
|
+
*/
|
|
41
|
+
export function extractBashPathPrefix(line) {
|
|
42
|
+
if (!/^\s*!/.test(line || "")) return null;
|
|
43
|
+
const m = /(\S*)$/.exec(line || "");
|
|
44
|
+
let tok = m ? m[1] : "";
|
|
45
|
+
if (tok.startsWith("!")) tok = tok.slice(1); // `!src/fo` → `src/fo`
|
|
46
|
+
return { prefix: tok };
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
/** Normalize to forward slashes (what @refs use on every platform). */
|
|
36
50
|
function fwd(p) {
|
|
37
51
|
return String(p).replace(/\\/g, "/");
|
|
@@ -183,6 +197,21 @@ export function makeAtCompleter(opts = {}) {
|
|
|
183
197
|
return [hits, line];
|
|
184
198
|
}
|
|
185
199
|
}
|
|
200
|
+
// `!` bash-mode file-path completion (Claude-Code 2.1.193 parity): the
|
|
201
|
+
// trailing token completes to filesystem paths under cwd — a shell command
|
|
202
|
+
// runs from cwd, so this uses the filesystem candidate set only (not the
|
|
203
|
+
// editor's open-file list). Checked before @refs so a `@` inside a bash
|
|
204
|
+
// command (e.g. `!grep @x file`) still completes the trailing path token.
|
|
205
|
+
const bash = extractBashPathPrefix(line);
|
|
206
|
+
if (bash) {
|
|
207
|
+
const fromFs = fileCandidates(bash.prefix, {
|
|
208
|
+
cwd: getCwd(),
|
|
209
|
+
deps: opts.deps,
|
|
210
|
+
});
|
|
211
|
+
// readline replaces the trailing `prefix` with the chosen path, leaving
|
|
212
|
+
// the `!` and the rest of the command intact.
|
|
213
|
+
return [fromFs.slice(0, MAX_CANDIDATES), bash.prefix];
|
|
214
|
+
}
|
|
186
215
|
const at = extractAtPrefix(line);
|
|
187
216
|
if (!at) return [[], line];
|
|
188
217
|
refreshIde(); // async top-up for the NEXT tab; this one uses the cache
|
package/src/lib/repl-rewind.js
CHANGED
|
@@ -161,6 +161,64 @@ export function buildResumeRecap(messages, { previewChars = 160 } = {}) {
|
|
|
161
161
|
return lines;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Snapshot the live conversation so a later `/rewind clear` can restore it
|
|
166
|
+
* (Claude-Code 2.1.191: "/rewind support for resuming a conversation from
|
|
167
|
+
* before /clear was run"). Drops the system prompt (index 0, re-added on
|
|
168
|
+
* restore) and copies the checkpoint marks. Returns null when there is nothing
|
|
169
|
+
* worth stashing (empty conversation) so a no-op /clear can't clobber an
|
|
170
|
+
* existing restorable snapshot.
|
|
171
|
+
*
|
|
172
|
+
* @param {Array} messages
|
|
173
|
+
* @param {Array} [marks]
|
|
174
|
+
* @returns {{messages:Array, marks:Array}|null}
|
|
175
|
+
*/
|
|
176
|
+
export function snapshotClearedConversation(messages, marks) {
|
|
177
|
+
const body = (messages || []).slice(1);
|
|
178
|
+
if (body.length === 0) return null;
|
|
179
|
+
return {
|
|
180
|
+
messages: body.slice(),
|
|
181
|
+
marks: Array.isArray(marks) ? marks.slice() : [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Restore a conversation stashed by /clear, swapping it IN PLACE with the
|
|
187
|
+
* current (post-clear) conversation so the restore is itself undoable. Keeps
|
|
188
|
+
* `messages[0]` (the system prompt). Returns the new stash (the swapped-out
|
|
189
|
+
* current state, or null when it was empty) plus counts, or null when there is
|
|
190
|
+
* nothing to restore.
|
|
191
|
+
*
|
|
192
|
+
* @param {Array} messages live conversation (index 0 = system prompt, kept)
|
|
193
|
+
* @param {Array} marks live checkpoint marks array (mutated in place)
|
|
194
|
+
* @param {{messages:Array, marks:Array}|null} cleared stash from /clear
|
|
195
|
+
* @returns {{restored:number, stashed:number, newCleared:{messages:Array,marks:Array}|null}|null}
|
|
196
|
+
*/
|
|
197
|
+
export function restoreClearedConversation(messages, marks, cleared) {
|
|
198
|
+
if (
|
|
199
|
+
!cleared ||
|
|
200
|
+
!Array.isArray(cleared.messages) ||
|
|
201
|
+
cleared.messages.length === 0
|
|
202
|
+
) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const curMsgs = (messages || []).slice(1);
|
|
206
|
+
const curMarks = Array.isArray(marks) ? marks.slice() : [];
|
|
207
|
+
// Replace messages[1..] with the stashed conversation (system prompt kept).
|
|
208
|
+
messages.splice(1, messages.length - 1, ...cleared.messages);
|
|
209
|
+
if (Array.isArray(marks)) {
|
|
210
|
+
marks.splice(0, marks.length, ...(cleared.marks || []));
|
|
211
|
+
}
|
|
212
|
+
const newCleared = curMsgs.length
|
|
213
|
+
? { messages: curMsgs, marks: curMarks }
|
|
214
|
+
: null;
|
|
215
|
+
return {
|
|
216
|
+
restored: cleared.messages.length,
|
|
217
|
+
stashed: curMsgs.length,
|
|
218
|
+
newCleared,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
164
222
|
/** Render the picker list (shared by /rewind and double-Esc). */
|
|
165
223
|
export function renderTurnList(turns) {
|
|
166
224
|
if (!turns.length) return " (no user turns yet)";
|
package/src/lib/repl-vim.js
CHANGED
|
@@ -248,7 +248,7 @@ const bell = (state, patch = {}) => ({
|
|
|
248
248
|
* with line/cursor; `submit:true` ⇒ run the line.
|
|
249
249
|
*/
|
|
250
250
|
export function feedNormalKey(state, ch, key = {}) {
|
|
251
|
-
const s = { ...state, message: null };
|
|
251
|
+
const s = { ...state, message: null, notice: null };
|
|
252
252
|
const line = s.line;
|
|
253
253
|
|
|
254
254
|
// Enter → submit the line (parity with insert-mode Enter).
|
|
@@ -420,6 +420,18 @@ export function feedNormalKey(state, ch, key = {}) {
|
|
|
420
420
|
case "t":
|
|
421
421
|
case "T":
|
|
422
422
|
return { ...s, awaitChar: { kind: ch }, count: String(count) };
|
|
423
|
+
case "/":
|
|
424
|
+
case "?":
|
|
425
|
+
// cc's NORMAL mode has no prompt-history search (readline owns history in
|
|
426
|
+
// INSERT mode). A user pressing `/` here most likely wants a slash
|
|
427
|
+
// command, so hint how to reach one instead of an opaque bell
|
|
428
|
+
// (Claude-Code 2.1.191: "hint how to reach slash commands").
|
|
429
|
+
return {
|
|
430
|
+
...s,
|
|
431
|
+
count: "",
|
|
432
|
+
notice:
|
|
433
|
+
"NORMAL /: press i, then type /command (slash commands run in insert mode; history = ↑ or Ctrl-R)",
|
|
434
|
+
};
|
|
423
435
|
default: {
|
|
424
436
|
// Pure motions (h l 0 ^ $ w b e) and Backspace/arrows mapped to h/l.
|
|
425
437
|
let motionCh = ch;
|
|
@@ -132,9 +132,18 @@ function compileMatcher(pattern) {
|
|
|
132
132
|
/* fall through to wildcard */
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
// OR-separated alternatives: pipe `Edit|Write` AND comma `Bash,PowerShell`
|
|
136
|
+
// (Claude-Code 2.1.191: comma-separated matchers silently never fired). Empty
|
|
137
|
+
// segments (trailing `Bash,` / `Edit|`) are dropped so they can't collapse to
|
|
138
|
+
// a match-everything matcher. Regex `/…/` matchers are handled above, so a
|
|
139
|
+
// comma inside `{1,3}` is never split here.
|
|
140
|
+
if (pattern.includes("|") || pattern.includes(",")) {
|
|
141
|
+
const ms = pattern
|
|
142
|
+
.split(/[|,]/)
|
|
143
|
+
.map((p) => p.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((p) => compileMatcher(p));
|
|
146
|
+
return ms.length ? (v) => ms.some((m) => m(v)) : () => false;
|
|
138
147
|
}
|
|
139
148
|
const esc = pattern
|
|
140
149
|
.replace(/[.+^${}()[\]\\]/g, "\\$&")
|
package/src/lib/slot-filler.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { EventEmitter } from "events";
|
|
11
|
+
import { firstBalancedJson } from "./json-schema-output.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Required slots per intent type — must be filled before execution.
|
|
@@ -341,9 +342,9 @@ Keep values concise (single words or short strings).`;
|
|
|
341
342
|
]);
|
|
342
343
|
|
|
343
344
|
const content = response?.message?.content || response?.content || "";
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
const parsed = JSON.parse(
|
|
345
|
+
const jsonText = firstBalancedJson(content, "{");
|
|
346
|
+
if (jsonText) {
|
|
347
|
+
const parsed = JSON.parse(jsonText);
|
|
347
348
|
// Filter out null values
|
|
348
349
|
const result = {};
|
|
349
350
|
for (const [key, value] of Object.entries(parsed)) {
|
|
@@ -74,6 +74,9 @@ export class SubAgentContext {
|
|
|
74
74
|
this.inheritedContext = options.inheritedContext || null;
|
|
75
75
|
this.allowedTools = options.allowedTools || null; // null = all
|
|
76
76
|
this.depth = options.depth || 1; // nesting level (parent main loop = 0)
|
|
77
|
+
// Shared run-wide TOTAL-sub-agent counter (one object across the whole tree)
|
|
78
|
+
// so this sub-agent's own spawns draw from the same breadth pool.
|
|
79
|
+
this.subAgentBudget = options.subAgentBudget || null;
|
|
77
80
|
this.cwd = options.cwd || process.cwd();
|
|
78
81
|
this.status = "active";
|
|
79
82
|
this.result = null;
|
|
@@ -252,6 +255,9 @@ export class SubAgentContext {
|
|
|
252
255
|
cwd: this.cwd,
|
|
253
256
|
// Nesting level: lets a nested spawn_sub_agent see — and cap — its depth.
|
|
254
257
|
subAgentDepth: this.depth,
|
|
258
|
+
// Shared total-sub-agent counter so a nested spawn_sub_agent draws from
|
|
259
|
+
// (and is bounded by) the run's single breadth pool.
|
|
260
|
+
subAgentBudget: this.subAgentBudget,
|
|
255
261
|
...loopOptions,
|
|
256
262
|
};
|
|
257
263
|
if (this.iterationBudget) {
|
package/src/lib/web-fetch.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import http from "http";
|
|
9
9
|
import https from "https";
|
|
10
|
+
import { lookup as dnsLookup } from "dns";
|
|
10
11
|
import { URL } from "url";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_MAX_BYTES = 2_000_000;
|
|
@@ -139,7 +140,46 @@ function stripTags(html) {
|
|
|
139
140
|
return String(html).replace(/<[^>]+>/g, "");
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
/**
|
|
144
|
+
* A `lookup` for http(s).request that resolves the hostname and REJECTS the
|
|
145
|
+
* connection if any resolved IP is private/loopback/link-local. checkAllowed
|
|
146
|
+
* only inspects the URL's literal hostname, so a public-looking domain that
|
|
147
|
+
* DNS-resolves to an internal IP (e.g. 169.254.169.254 cloud metadata) would
|
|
148
|
+
* otherwise sail through — classic DNS-based SSRF. Because Node connects to the
|
|
149
|
+
* exact address this returns, it also closes the DNS-rebinding TOCTOU window.
|
|
150
|
+
* Returns a Node-style lookup fn; `allowPrivateHosts` opts out (same flag as
|
|
151
|
+
* checkAllowed).
|
|
152
|
+
*/
|
|
153
|
+
export function makeSafeLookup(allowPrivateHosts, deps = _deps) {
|
|
154
|
+
const lookup = deps.lookup || dnsLookup;
|
|
155
|
+
return (hostname, options, callback) => {
|
|
156
|
+
const cb = typeof options === "function" ? options : callback;
|
|
157
|
+
const opts = typeof options === "function" ? {} : options || {};
|
|
158
|
+
lookup(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
159
|
+
if (err) return cb(err);
|
|
160
|
+
const list = Array.isArray(addresses) ? addresses : [];
|
|
161
|
+
if (list.length === 0) {
|
|
162
|
+
return cb(new Error(`could not resolve host: ${hostname}`));
|
|
163
|
+
}
|
|
164
|
+
if (!allowPrivateHosts) {
|
|
165
|
+
for (const a of list) {
|
|
166
|
+
if (isPrivateHost(a.address)) {
|
|
167
|
+
return cb(
|
|
168
|
+
new Error(`private/loopback resolved IP blocked: ${a.address}`),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const chosen = list[0];
|
|
174
|
+
cb(null, chosen.address, chosen.family);
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function _doRequest(
|
|
180
|
+
parsed,
|
|
181
|
+
{ maxBytes, timeout, headers, allowPrivateHosts },
|
|
182
|
+
) {
|
|
143
183
|
const lib = parsed.protocol === "https:" ? https : http;
|
|
144
184
|
return new Promise((resolve, reject) => {
|
|
145
185
|
const req = lib.request(
|
|
@@ -149,6 +189,10 @@ async function _doRequest(parsed, { maxBytes, timeout, headers }) {
|
|
|
149
189
|
hostname: parsed.hostname,
|
|
150
190
|
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
151
191
|
path: parsed.pathname + parsed.search,
|
|
192
|
+
// Resolve-and-check: reject the connection if the host resolves to a
|
|
193
|
+
// private IP (DNS-SSRF + rebinding guard). String-host check is in
|
|
194
|
+
// checkAllowed; this covers the resolved address.
|
|
195
|
+
lookup: makeSafeLookup(allowPrivateHosts),
|
|
152
196
|
headers: {
|
|
153
197
|
"User-Agent": "ChainlessChain-Agent/1.0",
|
|
154
198
|
Accept: "*/*",
|
|
@@ -210,7 +254,12 @@ export async function webFetch(url, options = {}) {
|
|
|
210
254
|
let redirects = 0;
|
|
211
255
|
let response;
|
|
212
256
|
while (true) {
|
|
213
|
-
response = await _doRequest(parsed, {
|
|
257
|
+
response = await _doRequest(parsed, {
|
|
258
|
+
maxBytes,
|
|
259
|
+
timeout,
|
|
260
|
+
headers,
|
|
261
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
262
|
+
});
|
|
214
263
|
if (!response.redirect) break;
|
|
215
264
|
if (++redirects > maxRedirects) {
|
|
216
265
|
return { error: "too many redirects" };
|
|
@@ -250,7 +299,9 @@ export async function webFetch(url, options = {}) {
|
|
|
250
299
|
};
|
|
251
300
|
}
|
|
252
301
|
|
|
253
|
-
|
|
302
|
+
// `lookup` is injectable so tests can resolve a domain to a private IP without
|
|
303
|
+
// real network; production uses the system resolver.
|
|
304
|
+
export const _deps = { http, https, lookup: dnsLookup };
|
|
254
305
|
|
|
255
306
|
// ===== V2 Surface: Web Fetch governance overlay (CLI v0.142.0) =====
|
|
256
307
|
export const WFET_TARGET_MATURITY_V2 = Object.freeze({
|
|
@@ -37,6 +37,22 @@ export const TEMPLATE_TYPE = Object.freeze({
|
|
|
37
37
|
TEST_SUITE: "test_suite",
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Parse a JSON column from a DB row without throwing. A truncated write, a
|
|
42
|
+
* manual edit, or a schema-drift row would otherwise make `JSON.parse` throw and
|
|
43
|
+
* crash the whole operation — and for list/map callers, ONE corrupt row would
|
|
44
|
+
* hide every other workflow. On bad/missing JSON we return `fallback` so a
|
|
45
|
+
* single bad row degrades to an empty/null value instead of poisoning the call.
|
|
46
|
+
*/
|
|
47
|
+
export function _safeParse(json, fallback) {
|
|
48
|
+
if (json == null) return fallback;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(json);
|
|
51
|
+
} catch {
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
/**
|
|
41
57
|
* Ensure workflow tables exist in the database.
|
|
42
58
|
*/
|
|
@@ -183,7 +199,7 @@ export function getWorkflow(db, workflowId) {
|
|
|
183
199
|
.prepare("SELECT * FROM workflows WHERE id = ?")
|
|
184
200
|
.get(workflowId);
|
|
185
201
|
if (!row) return null;
|
|
186
|
-
return { ...row, dag:
|
|
202
|
+
return { ...row, dag: _safeParse(row.dag, null) };
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
/**
|
|
@@ -196,7 +212,7 @@ export function listWorkflows(db) {
|
|
|
196
212
|
.all();
|
|
197
213
|
return rows.map((row) => ({
|
|
198
214
|
...row,
|
|
199
|
-
dag:
|
|
215
|
+
dag: _safeParse(row.dag, null),
|
|
200
216
|
}));
|
|
201
217
|
}
|
|
202
218
|
|
|
@@ -222,6 +238,11 @@ export function executeWorkflow(db, workflowId, input = {}) {
|
|
|
222
238
|
if (!workflow) {
|
|
223
239
|
throw new Error(`Workflow not found: ${workflowId}`);
|
|
224
240
|
}
|
|
241
|
+
if (!Array.isArray(workflow.dag) || workflow.dag.length === 0) {
|
|
242
|
+
// _safeParse yields null for a corrupt `dag` column — give a clear error
|
|
243
|
+
// instead of an opaque NPE in topologicalSort.
|
|
244
|
+
throw new Error(`Workflow ${workflowId} has a corrupt or empty DAG`);
|
|
245
|
+
}
|
|
225
246
|
|
|
226
247
|
const execId = `exec-${crypto.randomBytes(8).toString("hex")}`;
|
|
227
248
|
const stages = workflow.dag;
|
|
@@ -321,7 +342,7 @@ export function pauseExecution(db, executionId) {
|
|
|
321
342
|
executionId,
|
|
322
343
|
);
|
|
323
344
|
|
|
324
|
-
const log =
|
|
345
|
+
const log = _safeParse(exec.log, []);
|
|
325
346
|
log.push({
|
|
326
347
|
action: "paused",
|
|
327
348
|
timestamp: new Date().toISOString(),
|
|
@@ -354,7 +375,7 @@ export function resumeExecution(db, executionId) {
|
|
|
354
375
|
executionId,
|
|
355
376
|
);
|
|
356
377
|
|
|
357
|
-
const log =
|
|
378
|
+
const log = _safeParse(exec.log, []);
|
|
358
379
|
log.push({
|
|
359
380
|
action: "resumed",
|
|
360
381
|
timestamp: new Date().toISOString(),
|
|
@@ -387,7 +408,7 @@ export function rollbackExecution(db, executionId) {
|
|
|
387
408
|
executionId,
|
|
388
409
|
);
|
|
389
410
|
|
|
390
|
-
const log =
|
|
411
|
+
const log = _safeParse(exec.log, []);
|
|
391
412
|
log.push({
|
|
392
413
|
action: "rolled_back",
|
|
393
414
|
timestamp: new Date().toISOString(),
|
|
@@ -415,7 +436,7 @@ export function getExecutionLog(db, executionId) {
|
|
|
415
436
|
id: exec.id,
|
|
416
437
|
workflowId: exec.workflow_id,
|
|
417
438
|
status: exec.status,
|
|
418
|
-
log:
|
|
439
|
+
log: _safeParse(exec.log, []),
|
|
419
440
|
startedAt: exec.started_at,
|
|
420
441
|
completedAt: exec.completed_at,
|
|
421
442
|
};
|
|
@@ -688,7 +709,7 @@ export function createCheckpoint(db, executionId) {
|
|
|
688
709
|
currentStage: exec.current_stage,
|
|
689
710
|
capturedAt: new Date().toISOString(),
|
|
690
711
|
};
|
|
691
|
-
const nodeStates =
|
|
712
|
+
const nodeStates = _safeParse(exec.log, []);
|
|
692
713
|
|
|
693
714
|
db.prepare(
|
|
694
715
|
`INSERT INTO workflow_checkpoints (id, execution_id, workflow_id, state_snapshot, node_states, created_at)
|
|
@@ -722,8 +743,8 @@ export function listCheckpoints(db, executionId) {
|
|
|
722
743
|
id: row.id,
|
|
723
744
|
executionId: row.execution_id,
|
|
724
745
|
workflowId: row.workflow_id,
|
|
725
|
-
snapshot:
|
|
726
|
-
nodeStates:
|
|
746
|
+
snapshot: _safeParse(row.state_snapshot, {}),
|
|
747
|
+
nodeStates: _safeParse(row.node_states, []),
|
|
727
748
|
createdAt: row.created_at,
|
|
728
749
|
}));
|
|
729
750
|
}
|
|
@@ -750,8 +771,8 @@ export function rollbackToCheckpoint(db, executionId, checkpointId) {
|
|
|
750
771
|
throw new Error(`Execution not found: ${executionId}`);
|
|
751
772
|
}
|
|
752
773
|
|
|
753
|
-
const snapshot =
|
|
754
|
-
const nodeStates =
|
|
774
|
+
const snapshot = _safeParse(cp.state_snapshot, {});
|
|
775
|
+
const nodeStates = _safeParse(cp.node_states, []);
|
|
755
776
|
|
|
756
777
|
const restoredLog = [
|
|
757
778
|
...nodeStates,
|
|
@@ -792,6 +813,9 @@ export function setBreakpoint(db, workflowId, nodeId, condition = null) {
|
|
|
792
813
|
if (!workflow) {
|
|
793
814
|
throw new Error(`Workflow not found: ${workflowId}`);
|
|
794
815
|
}
|
|
816
|
+
if (!Array.isArray(workflow.dag)) {
|
|
817
|
+
throw new Error(`Workflow ${workflowId} has a corrupt DAG`);
|
|
818
|
+
}
|
|
795
819
|
const nodeExists = workflow.dag.some((s) => s.id === nodeId);
|
|
796
820
|
if (!nodeExists) {
|
|
797
821
|
throw new Error(`Node ${nodeId} not in workflow ${workflowId}`);
|