chainlesschain 0.162.123 → 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-C5CSd8pY.js → AIOps-BsDJlD_l.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BugJtiJc.js → ActionButton-COjZZKWG.js} +1 -1
- package/src/assets/web-panel/assets/Analytics-4ZDZE7lc.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-Cb0eHetM.js → AppLayout-CYH5k2Rp.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-qjq04wh8.js → Audit-BKlFbLdl.js} +1 -1
- package/src/assets/web-panel/assets/Backup-CiX61Qc2.js +1 -0
- package/src/assets/web-panel/assets/{BaseInput-CZ0c7QHk.js → BaseInput-CDxIQokd.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CaKDT3wV.js → Chat-CaKgdRab.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6-Iu3bG.js → Checkbox-CjiWfu4s.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-TGLFk6P1.js → Codegen-tKWGzajw.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DVBrO7AN.js → Col-ZKWREyNs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-tg6-Rik1.js → Community-CmLuPBUU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DM4ZgFSE.js → Compact-DqknM98n.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-CJwiWkUY.js → Cowork-Dp29kHsC.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-dsdXHvrO.js → Cron-zmEJAetz.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-p7_5Gq-7.js → Crosschain-Sb-uM7Ty.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BB_LBLYT.js → DID-D_UFNzJ5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Bh_HkNSD.js → Dashboard-Btag698v.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-dTFtVnKk.js → Dropdown-CH7l3KCs.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-yqdttzXV.js → EmailListRenderer-Bud2M3XX.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DfX3YpUU.js → FamilyGuardDashboard-Bd2_8uME.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BJApfPV4.js → Federation-1jhstF5P.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DNMwIh7X.js → FormItemContext-CpSH464Y.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C-T8UQf0.js → GenericCardRenderer-CANo8O-y.js} +1 -1
- package/src/assets/web-panel/assets/{Git-SURFhkLi.js → Git-hvuZVoD3.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-B4u_KsfE.js → Governance-BQrWksKt.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-2kr_rKQd.js → Inference-jEBTp12v.js} +1 -1
- 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-W9Iz8b2m.js → Marketplace-9Yi32avt.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D0waMomb.js → McpTools-Qx78XyOT.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-Ci768GQ3.js → Memory-CXYDO1oT.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-RpFuAiNz.js → MobileProjects-DEDbWNIk.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DJdw8btT.js → Mtc-Dwa_vlR8.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Dc596XgH.js → MtcAudit-DAVkxhJv.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig--Qs7eGsH.js → Multisig-C3upmgPN.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D3y71FDX.js → NLProgramming-DO3CZ0_z.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-uMxjj66_.js → Notes-B1Oy3FbC.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-C4ikBOsm.js → NotificationSettings-Bs4-Fc6b.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BpmdunAz.js → OrderTableRenderer-CEy1pIeq.js} +1 -1
- package/src/assets/web-panel/assets/Organization-D4cJ1UNo.js +4 -0
- package/src/assets/web-panel/assets/{Overflow-B-4_IpbB.js → Overflow-fdzvlF7x.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DdOf9BZs.js → P2P-Y13PwXBA.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DDU2Zc9Q.js → PdhVaultBrowser-Ck1zCLe6.js} +3 -3
- package/src/assets/web-panel/assets/Permissions-CvEXP6LC.js +4 -0
- package/src/assets/web-panel/assets/{PersonalDataHub-B3WcvKni.js → PersonalDataHub-JeP7IWMW.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DASuPDc3.js → Pipeline-BZ7wRzG1.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DnixwmhJ.js → Privacy-BJ6qj9Hv.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BQjMDkML.js → ProjectInit-DeWd9UcS.js} +2 -2
- package/src/assets/web-panel/assets/ProjectSettings-DUpVuU9F.js +2 -0
- package/src/assets/web-panel/assets/{Projects-BGWEJIVT.js → Projects-y1WbI337.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Bl0dw6cI.js → Providers-DECW00ek.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BDrk7l3t.js → QuickAsk-Dl-pK9Io.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BPKof_CA.js → Recommend-Bju04wTd.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-6MNJLklK.js → Reputation-BUPa3nEk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DmGJwpfd.js → Row-WYqqaq_5.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-9-Fxh3_w.js → RssFeed-iyQT5q9l.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CCHjey_D.js → Search-CrfwJF0R.js} +1 -1
- package/src/assets/web-panel/assets/{Security-D6CySV7r.js → Security-c4Jxf5Ih.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Co8Mk8qE.js → Services-CRuisolj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BdON8M-1.js → Skeleton-CCnzJkb-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CEJE1Go1.js → Skills-CZURCwZg.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BSxFvFX9.js → Sla-CrMzaEi2.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-52NXsfyj.js → SyncSettings-CJWVtd87.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-He7CweW6.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-Dd5XNbYF.js → Tenant-DrDv1FFc.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-CP15hcNS.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-BdfqBQmM.js → TimelineRenderer-Sl5uXrkN.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CsgNsrdQ.js → Tokens-yRIZTVsR.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dt_N_rvo.js → Trigger-BLCQEOF2.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B1lJzsPn.js → Trust-D5xq8z6s.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CW8YdFxg.js → UkeySign-Dwp0zXQ6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CU6o7qvp.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-BcCA2U8H.js → WorkflowEditor-B5bHRwUG.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Cxl5DGsc.js → chat-DsheLKkG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-FS_7Xggs.js → colors-CST2UkgL.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BzXjd3wJ.js → compact-item-yqEoy1L7.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DhF9bi_u.js → createContext-Jwp78HFY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Y1a0aV9N.js → hasIn-CVS5mFEP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfaSKEVD.js → index-AnW25bEq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmjxpu0L.js → index-BCHAUdel.js} +1 -1
- package/src/assets/web-panel/assets/{index-B48PZ-xM.js → index-BLTUVd0V.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRSkKj9M.js → index-BeblNJDH.js} +1 -1
- package/src/assets/web-panel/assets/{index-uLoKviUT.js → index-BuI27WSx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTltEtoG.js → index-BzetOasL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CETvk0NN.js → index-C0FZScMe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZJMsVI1.js → index-C130LLPq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cgo1J_Kf.js → index-C8rq85gu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBtULjKY.js → index-CDAPnZUs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BepInmSi.js → index-CIE2n8k_.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3IVAG9l.js → index-CRBbVS43.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci7PSoQn.js → index-CSBPc-sI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxYHV8nb.js → index-CTd3lDxp.js} +1 -1
- package/src/assets/web-panel/assets/index-CUWj5L2s.js +1 -0
- package/src/assets/web-panel/assets/{index-CzlCP4jP.js → index-ChRL6rgA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVTex5M8.js → index-CjfN2CpJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-1iacQgo5.js → index-Cjig5i3a.js} +3 -3
- package/src/assets/web-panel/assets/{index-JUukFpxi.js → index-Crk4KWLW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BllceSdr.js → index-CrlRa054.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn9lbqaS.js → index-CtyEOGuj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bt_0ygjA.js → index-Cu6Ql64n.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_Z0PHC0.js → index-Cx_t5rWw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CELVZPNt.js → index-Cy0dNzkp.js} +1 -1
- package/src/assets/web-panel/assets/{index-GCd5hs58.js → index-CzsKK0tQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnvaUocg.js → index-DDAigsel.js} +1 -1
- package/src/assets/web-panel/assets/index-DZfnYE6e.js +1 -0
- package/src/assets/web-panel/assets/{index-C2S8McM1.js → index-D_1b7uh6.js} +1 -1
- package/src/assets/web-panel/assets/{index--fcP34eT.js → index-D_e9gwfn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgHWZVjv.js → index-DbxAlpPC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNmWkPqm.js → index-De600Jjm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxGVbuqr.js → index-Dgyn8-wN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzQkdgQW.js → index-DiK4vSZa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuTKOay7.js → index-DpYn5v2k.js} +1 -1
- package/src/assets/web-panel/assets/{index-Sl-sLj3T.js → index-DxaU_lza.js} +1 -1
- package/src/assets/web-panel/assets/{index-Na733kBa.js → index-Dxljh9Fu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLyIxnjc.js → index-R-VGFpi6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUcVF8e6.js → index-SjxCCf5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-1khPb4ZS.js → index-diWkx2Wq.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D6QmlnQ-.js → initDefaultProps-ClAjrsQ-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BHrTGY1_.js → motion-BalXv_60.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bpph6B_N.js → move-DMelzIW-.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Dx0YMbWn.js → omit-oxecfU3b.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D-a8Fm6C.js → pickAttrs-DJ5F5M6x.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DXFSVgxv.js → placementArrow-DdbKsant.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CxhQ1EaK.js → responsiveObserve-BUuM5cmS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-IlBI0XU8.js → slide-DSV0ccvR.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-COoXVzje.js → statusUtils-B-6oLAXf.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCuq89M_.js → styleChecker-DdCAHtsZ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-tfNE1ALO.js → useFlexGapSupport-ZORkcoZd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DD4bzBkU.js → useFs-BFp46LoP.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DVkH1hbT.js → usePersonalDataHub-H7rxgbdW.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D1ZmXyxM.js → vnode-eIq4Tk0J.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DblBUHfi.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 +57 -0
- 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 +7 -0
- package/src/lib/git-integration.js +57 -1
- 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-oauth.js +208 -20
- 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/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/runtime/agent-core.js +248 -26
- package/src/runtime/headless-stream.js +124 -17
- package/src/assets/web-panel/assets/Analytics-CzxpcV9E.js +0 -3
- 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/Compliance-DFeWryxt.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-h-nL7J6x.js +0 -1
- package/src/assets/web-panel/assets/Logs-DjuWREBJ.js +0 -2
- package/src/assets/web-panel/assets/MobileBridge-4jAehfcz.js +0 -3
- package/src/assets/web-panel/assets/Organization-BBEmFNGM.js +0 -4
- package/src/assets/web-panel/assets/Permissions-B1ebgevF.js +0 -4
- package/src/assets/web-panel/assets/ProjectSettings-dHyZ3SxW.js +0 -2
- 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/Terminal-pFyh42cu.js +0 -3
- 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/devWarning-B1j78JeH.js +0 -1
- package/src/assets/web-panel/assets/index-BaN6wKWx.js +0 -1
- package/src/assets/web-panel/assets/index-N7JKRLCC.js +0 -1
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(
|
|
@@ -209,6 +246,7 @@ export async function registerClient(
|
|
|
209
246
|
"server has no registration_endpoint and no --client-id was given",
|
|
210
247
|
);
|
|
211
248
|
}
|
|
249
|
+
assertSecureEndpoint(metadata.registration_endpoint, "registration_endpoint");
|
|
212
250
|
const reg = await fetchJson(metadata.registration_endpoint, {
|
|
213
251
|
method: "POST",
|
|
214
252
|
headers: { "content-type": "application/json" },
|
|
@@ -230,6 +268,10 @@ export function buildAuthorizeUrl(
|
|
|
230
268
|
metadata,
|
|
231
269
|
{ clientId, redirectUri, scope, codeChallenge, state, resource },
|
|
232
270
|
) {
|
|
271
|
+
assertSecureEndpoint(
|
|
272
|
+
metadata.authorization_endpoint,
|
|
273
|
+
"authorization_endpoint",
|
|
274
|
+
);
|
|
233
275
|
const u = new URL(metadata.authorization_endpoint);
|
|
234
276
|
u.searchParams.set("response_type", "code");
|
|
235
277
|
u.searchParams.set("client_id", clientId);
|
|
@@ -261,6 +303,7 @@ export async function exchangeCodeForToken(
|
|
|
261
303
|
});
|
|
262
304
|
if (clientSecret) body.set("client_secret", clientSecret);
|
|
263
305
|
if (resource) body.set("resource", resource);
|
|
306
|
+
assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
|
|
264
307
|
const tok = await fetchJson(metadata.token_endpoint, {
|
|
265
308
|
method: "POST",
|
|
266
309
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -287,6 +330,7 @@ export async function refreshAccessToken(
|
|
|
287
330
|
});
|
|
288
331
|
if (clientSecret) body.set("client_secret", clientSecret);
|
|
289
332
|
if (resource) body.set("resource", resource);
|
|
333
|
+
assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
|
|
290
334
|
const tok = await fetchJson(metadata.token_endpoint, {
|
|
291
335
|
method: "POST",
|
|
292
336
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -372,26 +416,126 @@ function _writeStoreSecure(file, store) {
|
|
|
372
416
|
}
|
|
373
417
|
}
|
|
374
418
|
|
|
419
|
+
/** Bounded synchronous sleep (the store lock is held only for a few file ops). */
|
|
420
|
+
function _sleepSyncMs(ms) {
|
|
421
|
+
try {
|
|
422
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
423
|
+
} catch {
|
|
424
|
+
const end = Date.now() + ms;
|
|
425
|
+
while (Date.now() < end) {
|
|
426
|
+
/* spin — Atomics.wait unavailable (should not happen on modern Node) */
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Run `fn` while holding a cross-process advisory lock on the token store.
|
|
433
|
+
*
|
|
434
|
+
* saveStoredToken / deleteStoredToken are read-modify-write (load → mutate →
|
|
435
|
+
* write). Two concurrent `cc mcp login` PROCESSES for different servers would
|
|
436
|
+
* each load the old store and the second writer would clobber the first's new
|
|
437
|
+
* entry (lost update) — the atomic temp+rename only prevents a torn file, not a
|
|
438
|
+
* lost update. An O_EXCL lockfile makes the whole RMW mutually exclusive across
|
|
439
|
+
* processes, so each writer re-reads the latest store inside the lock.
|
|
440
|
+
*
|
|
441
|
+
* Robustness: a stale lock (holder crashed) older than STALE_MS is stolen; if
|
|
442
|
+
* the lock can't be acquired within MAX_WAIT_MS we proceed anyway (degrading to
|
|
443
|
+
* the pre-lock last-writer-wins rather than failing the login). When the
|
|
444
|
+
* injected fs lacks the lock primitives (test fs / exotic fs) we run `fn`
|
|
445
|
+
* directly — single-process correctness is unchanged, only cross-process mutual
|
|
446
|
+
* exclusion is lost. Lock timing uses the wall clock (not the `now` test seam).
|
|
447
|
+
*/
|
|
448
|
+
function withStoreLock(file, fn) {
|
|
449
|
+
const fs = _deps.fs;
|
|
450
|
+
if (
|
|
451
|
+
typeof fs.openSync !== "function" ||
|
|
452
|
+
typeof fs.closeSync !== "function" ||
|
|
453
|
+
typeof fs.unlinkSync !== "function"
|
|
454
|
+
) {
|
|
455
|
+
return fn(); // no lock primitives → run unlocked (back-compat)
|
|
456
|
+
}
|
|
457
|
+
const STALE_MS = 10_000;
|
|
458
|
+
const MAX_WAIT_MS = 5_000;
|
|
459
|
+
const lockPath = `${file}.lock`;
|
|
460
|
+
try {
|
|
461
|
+
fs.mkdirSync(pathDefault.dirname(file), { recursive: true, mode: 0o700 });
|
|
462
|
+
} catch {
|
|
463
|
+
/* dir may already exist — openSync below reports any real problem */
|
|
464
|
+
}
|
|
465
|
+
const start = Date.now();
|
|
466
|
+
let fd = null;
|
|
467
|
+
for (;;) {
|
|
468
|
+
try {
|
|
469
|
+
fd = fs.openSync(lockPath, "wx"); // O_CREAT|O_EXCL — fails if held
|
|
470
|
+
break;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
if (!err || err.code !== "EEXIST") {
|
|
473
|
+
// Unexpected (e.g. ENOENT/EACCES): don't fail the login over the lock —
|
|
474
|
+
// proceed unlocked, same as a missing-primitives fs.
|
|
475
|
+
return fn();
|
|
476
|
+
}
|
|
477
|
+
let stale = false;
|
|
478
|
+
try {
|
|
479
|
+
const st = fs.statSync(lockPath);
|
|
480
|
+
const mtime = st.mtimeMs != null ? st.mtimeMs : 0;
|
|
481
|
+
stale = Date.now() - mtime > STALE_MS;
|
|
482
|
+
} catch {
|
|
483
|
+
/* lock vanished between open and stat → retry immediately */
|
|
484
|
+
}
|
|
485
|
+
if (stale) {
|
|
486
|
+
try {
|
|
487
|
+
fs.unlinkSync(lockPath); // steal a lock from a crashed holder
|
|
488
|
+
} catch {
|
|
489
|
+
/* someone else stole it first — retry */
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (Date.now() - start > MAX_WAIT_MS) break; // give up waiting; proceed
|
|
494
|
+
_sleepSyncMs(50);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
return fn();
|
|
499
|
+
} finally {
|
|
500
|
+
if (fd !== null) {
|
|
501
|
+
try {
|
|
502
|
+
fs.closeSync(fd);
|
|
503
|
+
} catch {
|
|
504
|
+
/* ignore */
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
fs.unlinkSync(lockPath);
|
|
508
|
+
} catch {
|
|
509
|
+
/* ignore — a stale-steal by another process may have removed it */
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
375
515
|
export function saveStoredToken(serverUrl, record) {
|
|
376
516
|
const file = tokenStorePath();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
517
|
+
return withStoreLock(file, () => {
|
|
518
|
+
const store = loadTokenStore(); // re-read inside the lock (latest state)
|
|
519
|
+
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
520
|
+
_writeStoreSecure(file, store);
|
|
521
|
+
return store[serverKey(serverUrl)];
|
|
522
|
+
});
|
|
381
523
|
}
|
|
382
524
|
|
|
383
525
|
export function deleteStoredToken(serverUrl) {
|
|
384
526
|
const file = tokenStorePath();
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
527
|
+
return withStoreLock(file, () => {
|
|
528
|
+
const store = loadTokenStore(); // re-read inside the lock (latest state)
|
|
529
|
+
const key = serverKey(serverUrl);
|
|
530
|
+
if (!(key in store)) return false;
|
|
531
|
+
delete store[key];
|
|
532
|
+
try {
|
|
533
|
+
_writeStoreSecure(file, store);
|
|
534
|
+
} catch {
|
|
535
|
+
/* best-effort */
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
});
|
|
395
539
|
}
|
|
396
540
|
|
|
397
541
|
/** True when a record's access token is missing or within `skewMs` of expiry. */
|
|
@@ -440,12 +584,25 @@ function defaultOpenBrowser(url) {
|
|
|
440
584
|
}
|
|
441
585
|
}
|
|
442
586
|
|
|
443
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* Wait for the OAuth redirect on a localhost callback server; resolve
|
|
589
|
+
* {code,state}.
|
|
590
|
+
*
|
|
591
|
+
* When `expectedState` is given, a SUCCESS callback (`code`) is only accepted if
|
|
592
|
+
* its `state` matches: the callback port is fixed and guessable, so a duplicate
|
|
593
|
+
* browser request, a probe, or a drive-by request from a malicious local page
|
|
594
|
+
* must not abort the real login or inject a forged code — those are answered
|
|
595
|
+
* politely but the server keeps waiting for the genuine redirect (until the
|
|
596
|
+
* backstop timeout). A provider `error` is always terminal (so denying consent
|
|
597
|
+
* fails fast even if the provider omits state on error). With no `expectedState`
|
|
598
|
+
* the legacy contract is preserved (first `/callback` hit is terminal).
|
|
599
|
+
*/
|
|
444
600
|
export function waitForCallback({
|
|
445
601
|
port,
|
|
446
602
|
host = "127.0.0.1",
|
|
447
603
|
path = "/callback",
|
|
448
604
|
timeout = 300_000,
|
|
605
|
+
expectedState = null,
|
|
449
606
|
}) {
|
|
450
607
|
return new Promise((resolve, reject) => {
|
|
451
608
|
const server = _deps.createServer((req, res) => {
|
|
@@ -465,13 +622,41 @@ export function waitForCallback({
|
|
|
465
622
|
const code = u.searchParams.get("code");
|
|
466
623
|
const state = u.searchParams.get("state");
|
|
467
624
|
const error = u.searchParams.get("error");
|
|
468
|
-
|
|
469
|
-
|
|
625
|
+
|
|
626
|
+
// Provider error (e.g. user denied) — always terminal so we fail fast.
|
|
627
|
+
if (error) {
|
|
628
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
629
|
+
res.end(renderCallbackPage(error));
|
|
630
|
+
server.close();
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
reject(new Error(`authorization error: ${error}`));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const stateOk = expectedState == null || state === expectedState;
|
|
637
|
+
if (code && stateOk) {
|
|
638
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
639
|
+
res.end(renderCallbackPage(null));
|
|
640
|
+
server.close();
|
|
641
|
+
clearTimeout(timer);
|
|
642
|
+
resolve({ code, state });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// A mismatched/forged/duplicate request to our fixed port must not abort
|
|
647
|
+
// the real login — answer it but keep listening for the genuine redirect.
|
|
648
|
+
if (expectedState != null) {
|
|
649
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
650
|
+
res.end(renderCallbackPage("state mismatch"));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Legacy contract (no expectedState): a /callback hit with no code ends it.
|
|
655
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
656
|
+
res.end(renderCallbackPage("missing code"));
|
|
470
657
|
server.close();
|
|
471
658
|
clearTimeout(timer);
|
|
472
|
-
|
|
473
|
-
else if (!code) reject(new Error("no authorization code in callback"));
|
|
474
|
-
else resolve({ code, state });
|
|
659
|
+
reject(new Error("no authorization code in callback"));
|
|
475
660
|
});
|
|
476
661
|
const timer = setTimeout(() => {
|
|
477
662
|
server.close();
|
|
@@ -540,6 +725,9 @@ export async function authorizeInteractive(serverUrl, opts = {}) {
|
|
|
540
725
|
host,
|
|
541
726
|
path: redirectPath,
|
|
542
727
|
timeout,
|
|
728
|
+
// Ignore callbacks that don't carry the state we issued (forged / drive-by /
|
|
729
|
+
// duplicate hits to the fixed localhost port) instead of aborting the login.
|
|
730
|
+
expectedState: state,
|
|
543
731
|
});
|
|
544
732
|
const opened = _deps.openBrowser(authorizeUrl);
|
|
545
733
|
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
|
}
|
|
@@ -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/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({
|