chainlesschain 0.162.148 → 0.162.149

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.
Files changed (231) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DwpCCp64.js → AIOps-BJif14yR.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-C9pBYocA.js → ActionButton-CXek6gJY.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-BWAkeXxK.js → Analytics-BgQhdAJi.js} +2 -2
  7. package/src/assets/web-panel/assets/{AppLayout-CivFGH6B.js → AppLayout-DTYl5NtR.js} +3 -3
  8. package/src/assets/web-panel/assets/{Audit-Cm8hRpZm.js → Audit-BzATQAeR.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DXdFMsE8.js → Backup-BzdPEQhL.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-uAwSTeql.js → BaseInput-BM0KVh8E.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-KU8eeJJE.js → Chat-D7fHXHkz.js} +4 -4
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-Wm34RR-b.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-C6AMDkjd.js → Checkbox-CAiSQ9vO.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-t2moDxcO.js → Codegen-Df40CrEe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DCcsRRhN.js → Col-DIT2fNFU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DtB-8SAC.js → Community-CSTIbW2k.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CZm8THYA.js → Compact-DtqXZZUi.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-LiQgC7g1.js → Compliance-CUtzw6o7.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-CA8SAxht.js → Cowork-CezmlnmU.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-CtRaF1wD.js → Cron-P4BITarg.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-6-br6Hub.js → Crosschain-BRRiYwvq.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Do2EccrL.js → DID-L5ijB9i4.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-wrXcbzGe.js → Dashboard-CFxro4ob.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-p3FsT245.js → Dropdown-BmAFCQ71.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D95A2L8q.js → EmailListRenderer-DJBCEuon.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-cLuJW2zo.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-Brgq_cbN.js → Federation-DsVCpHMF.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DWLCkNgh.js → FormItemContext-DP3bP5th.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BBqUfQd6.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-8F090w4I.js → Git-I3g_bAw9.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CCogEbxb.js → Governance-jHS5-ioS.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfLD7v7C.js → Inference-CPL7sfx9.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DDuHJewd.js → KnowledgeGraph-Jf236h4C.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CE8KSEVC.js → Logs-Cm2tcSKg.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-DmwpZmhT.js → Marketplace-CZK82rhi.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Bf7AazU8.js → McpTools-CZCeji7a.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D0QU_00S.js → Memory-3l2KVS9k.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-yXS9xdhx.js → MobileBridge-VYdpoLh6.js} +1 -1
  39. package/src/assets/web-panel/assets/{MobileProjects-BuDUoGUP.js → MobileProjects-CiB9cmm1.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BYyHy28p.js → Mtc-wCFZbBuL.js} +2 -2
  41. package/src/assets/web-panel/assets/{MtcAudit-CK0aqpiZ.js → MtcAudit-1a3THIyG.js} +4 -4
  42. package/src/assets/web-panel/assets/{Multisig-mNimKYeb.js → Multisig-DMzLB2hG.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-AioAJ0YA.js → NLProgramming-D9wZbf6l.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-CVVNCLHE.js → Notes-hrFe2ZM-.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-C4ABj-TK.js → NotificationSettings-CHGX5Jwm.js} +1 -1
  46. package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
  47. package/src/assets/web-panel/assets/{Organization-uozl_gWK.js → Organization-By7FkhtY.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BCZOM2hK.js → Overflow--khGSzJn.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-CArcaiaj.js → P2P-CAG9dH35.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-Sgq2Srr8.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
  51. package/src/assets/web-panel/assets/{Permissions-BYrQrXnH.js → Permissions-D35ec6JA.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-BzHStAbz.js → PersonalDataHub-CxBF7hW_.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-k7KqxX1M.js → Pipeline-s6EtOO1s.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-IumTUWqx.js → Privacy-BbJYmWmO.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-BRyImYTf.js → ProjectInit-VGQq1jMT.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-BFIqTBFk.js → ProjectSettings-B1ew3Teh.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-CyyFnUPs.js → Providers-mrO47FIK.js} +1 -1
  59. package/src/assets/web-panel/assets/{QrScannerModal-C82cRx-X.js → QrScannerModal-IJF2mHyw.js} +1 -1
  60. package/src/assets/web-panel/assets/{QuickAsk-DGxDF521.js → QuickAsk-CU0wN_Z0.js} +1 -1
  61. package/src/assets/web-panel/assets/{Recommend-Cns-Am1y.js → Recommend-DhN37R6G.js} +1 -1
  62. package/src/assets/web-panel/assets/{RemoteSession-R7OqAIlg.js → RemoteSession-D_xOX_hu.js} +2 -2
  63. package/src/assets/web-panel/assets/{Reputation-D7mgPIYV.js → Reputation-D_Ssv7uH.js} +1 -1
  64. package/src/assets/web-panel/assets/{Row-B3lEURyd.js → Row-Kx5dKxX7.js} +1 -1
  65. package/src/assets/web-panel/assets/{RssFeed-9_UVrpBt.js → RssFeed-BlGwqZkY.js} +2 -2
  66. package/src/assets/web-panel/assets/{Search-ClZeO80q.js → Search-hLFQzxpb.js} +1 -1
  67. package/src/assets/web-panel/assets/{Security-BNNJczEp.js → Security-DMlvsVt4.js} +3 -3
  68. package/src/assets/web-panel/assets/{Services-C5SjrWUj.js → Services-Bd0Qsj2m.js} +2 -2
  69. package/src/assets/web-panel/assets/{Skeleton-Ci_obQ-g.js → Skeleton-DFclq9X3.js} +1 -1
  70. package/src/assets/web-panel/assets/{Skills-DdebN15P.js → Skills-BNtln2qY.js} +1 -1
  71. package/src/assets/web-panel/assets/{Sla-OinZP2vi.js → Sla-Dch5H19G.js} +1 -1
  72. package/src/assets/web-panel/assets/{SpeechSettings-CxzbNF51.js → SpeechSettings-2UfQcU1g.js} +1 -1
  73. package/src/assets/web-panel/assets/{SyncSettings-D06N_66b.js → SyncSettings-CD9YQr4i.js} +2 -2
  74. package/src/assets/web-panel/assets/{Tasks-BdEdW0AZ.js → Tasks-BNu-7MzI.js} +1 -1
  75. package/src/assets/web-panel/assets/{Templates-F1ctKmPa.js → Templates-D1n9CaIR.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tenant-CVz1Urot.js → Tenant-DenL3iBW.js} +1 -1
  77. package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
  78. package/src/assets/web-panel/assets/{TimelineRenderer-Doitzjmf.js → TimelineRenderer-D87IfukO.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tokens-BpyVRpVA.js → Tokens-BkiZc8E3.js} +1 -1
  80. package/src/assets/web-panel/assets/{Trigger-nWhWYTbU.js → Trigger-BNRSytGN.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trust-DK_G-wLx.js → Trust-Bwbd4X2m.js} +1 -1
  82. package/src/assets/web-panel/assets/{UkeySign-B5yBUojO.js → UkeySign-B8O2bs0o.js} +1 -1
  83. package/src/assets/web-panel/assets/{VideoEditing-C5D51qAe.js → VideoEditing-C2fL8zmH.js} +1 -1
  84. package/src/assets/web-panel/assets/{Wallet-CLbL9K7v.js → Wallet-BORYDdE1.js} +4 -4
  85. package/src/assets/web-panel/assets/{WebAuthn-DZUelI3V.js → WebAuthn-C0V5S2FM.js} +4 -4
  86. package/src/assets/web-panel/assets/{WorkflowEditor-8RPxt4KW.js → WorkflowEditor-2dR0fcHL.js} +1 -1
  87. package/src/assets/web-panel/assets/{chat-4N4tOA3d.js → chat-BmuAFAn8.js} +1 -1
  88. package/src/assets/web-panel/assets/{colors-zbbGVrua.js → colors-CN3smMcK.js} +1 -1
  89. package/src/assets/web-panel/assets/{compact-item-D7iMkbnE.js → compact-item-DCnxb4kW.js} +1 -1
  90. package/src/assets/web-panel/assets/{createContext-CBzPJv-w.js → createContext-vlR43pHn.js} +1 -1
  91. package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
  92. package/src/assets/web-panel/assets/{hasIn-bHdrQSqZ.js → hasIn-D04tHYFd.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-DbE5D0WL.js → index-0DnaZGkC.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-308gCGh7.js → index-430S68MN.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-kmh1TxRa.js → index-B78xL3s2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-QFObYeo1.js → index-BTPEZTk1.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BZb8F8YG.js → index-BXRg8GFQ.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DE9j5YgT.js → index-BasvsWDM.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CiG1IZao.js → index-BeGDEXFs.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bqmx24kh.js → index-BqhASEL4.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BCr0geV9.js → index-BudvKQfG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-S74FpAIn.js → index-C-6NO5il.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DyoNgbXl.js → index-C5Sxb1sE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BtbbdLnf.js → index-CP9m9Ggr.js} +3 -3
  105. package/src/assets/web-panel/assets/{index-CkxK86vf.js → index-CTOCnIQ7.js} +1 -1
  106. package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
  107. package/src/assets/web-panel/assets/{index-BTIjjXry.js → index-ClhAHDK3.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-B7b1hGry.js → index-CnXebZLL.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CPxkoKgA.js → index-CngCC8hC.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Ci009ijp.js → index-Cr-A0FYJ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CdmMoYN1.js → index-CsAsCPJ6.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BBNr77E1.js → index-D28DeAAB.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-B1s0Bz9O.js → index-D5LZVZ0L.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-oDKNgCsP.js → index-D65_XseV.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-QNAG4JtR.js → index-D8vwp4qp.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CQfu1U78.js → index-DDwLgQY9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BnBOpqv3.js → index-DIpY0qM5.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CQ6NcfWz.js → index-DTWg-18U.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D4XKpj6c.js → index-DTtRscUa.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-D1YDh7Wr.js → index-DYAtmqiv.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CvRMA9uT.js → index-D_n8_vfI.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CahryTX0.js → index-DbADHwhr.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BJ4ZGyW0.js → index-DbVsQBOH.js} +1 -1
  124. package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
  125. package/src/assets/web-panel/assets/{index-xYCm6W2h.js → index-HhNjnCfe.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BS4_Mwk6.js → index-ToUh2b1-.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DISWAYce.js → index-TslMTwNy.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DmqlJed-.js → index-_FIkN3jd.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CqdPyWm5.js → index-hpvvtAZ3.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BsKehmmS.js → index-jzR7Ujuw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DUyjUkY7.js → index-ljuBiQW9.js} +1 -1
  132. package/src/assets/web-panel/assets/{initDefaultProps-BD55yNBt.js → initDefaultProps-C3wUb4je.js} +1 -1
  133. package/src/assets/web-panel/assets/{motion-B5Bbe77U.js → motion-D-jYFUNA.js} +1 -1
  134. package/src/assets/web-panel/assets/{move-CTvIqHEH.js → move-tqb8nwJ2.js} +1 -1
  135. package/src/assets/web-panel/assets/{mtc-parser-BkNyPQKB.js → mtc-parser-DSOjMJMz.js} +1 -1
  136. package/src/assets/web-panel/assets/{omit-BaXJXgHA.js → omit-CJ7VimIW.js} +1 -1
  137. package/src/assets/web-panel/assets/{pickAttrs-Bw6-YTxH.js → pickAttrs-qXiG1iT3.js} +1 -1
  138. package/src/assets/web-panel/assets/{placementArrow-BLdbkLqJ.js → placementArrow-CkGBcy1Z.js} +1 -1
  139. package/src/assets/web-panel/assets/{responsiveObserve-IVYkzntU.js → responsiveObserve-D3WvTlLO.js} +1 -1
  140. package/src/assets/web-panel/assets/{slide-DFMFwwtY.js → slide-BEE2ftge.js} +1 -1
  141. package/src/assets/web-panel/assets/{statusUtils-CURYRtZ1.js → statusUtils-CsfBpxiG.js} +1 -1
  142. package/src/assets/web-panel/assets/{styleChecker-Cfz63s1r.js → styleChecker-xPrIiJPI.js} +1 -1
  143. package/src/assets/web-panel/assets/{useFlexGapSupport-COJL0KE3.js → useFlexGapSupport-C5Yd-SCR.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFs-YLLojQQf.js → useFs-D_MDS8HI.js} +1 -1
  145. package/src/assets/web-panel/assets/{usePersonalDataHub-BF_21qUC.js → usePersonalDataHub-BqJh-usA.js} +1 -1
  146. package/src/assets/web-panel/assets/{vnode-9ZpkA7bb.js → vnode-chm_p-gz.js} +1 -1
  147. package/src/assets/web-panel/assets/{zoom-DhtAfhl8.js → zoom-3-YDNz0Z.js} +1 -1
  148. package/src/assets/web-panel/index.html +1 -1
  149. package/src/assets/web-panel/remote-session-sw.js +43 -0
  150. package/src/command-manifest.json +22 -1
  151. package/src/commands/agent.js +44 -5
  152. package/src/commands/codeintel.js +252 -0
  153. package/src/commands/eval.js +248 -0
  154. package/src/commands/plugin.js +354 -0
  155. package/src/commands/team.js +456 -0
  156. package/src/gateways/ws/message-dispatcher.js +45 -2
  157. package/src/gateways/ws/remote-session-protocol.js +37 -0
  158. package/src/gateways/ws/ws-server.js +1 -1
  159. package/src/harness/prompt-compressor.js +27 -1
  160. package/src/harness/remote-command-ledger.js +158 -0
  161. package/src/harness/remote-session-push-apns.js +246 -0
  162. package/src/harness/remote-session-push-errors.js +15 -0
  163. package/src/harness/remote-session-push-fcm.js +9 -25
  164. package/src/harness/remote-session-push-huawei.js +174 -0
  165. package/src/harness/remote-session-push-oppo.js +171 -0
  166. package/src/harness/remote-session-push-senders.js +42 -0
  167. package/src/harness/remote-session-push-vivo.js +179 -0
  168. package/src/harness/remote-session-push-web.js +299 -0
  169. package/src/harness/remote-session-push-xiaomi.js +99 -0
  170. package/src/index.js +6 -0
  171. package/src/lib/agent-core.js +2 -0
  172. package/src/lib/agent-sandbox.js +161 -5
  173. package/src/lib/agent-team/task-lease.js +434 -0
  174. package/src/lib/agent-team/team-budget.js +165 -0
  175. package/src/lib/agent-team/team-mailbox.js +106 -0
  176. package/src/lib/agent-team/team-runner.js +266 -0
  177. package/src/lib/agent-team/team-worktree.js +204 -0
  178. package/src/lib/agents.js +18 -0
  179. package/src/lib/async-hook-supervisor.cjs +305 -0
  180. package/src/lib/eval/runner.js +163 -0
  181. package/src/lib/eval/tasks.js +456 -0
  182. package/src/lib/eval/trend.js +185 -0
  183. package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
  184. package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
  185. package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
  186. package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
  187. package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
  188. package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
  189. package/src/lib/lsp/code-intelligence.js +356 -0
  190. package/src/lib/lsp/jsonrpc-stream.js +139 -0
  191. package/src/lib/lsp/lsp-client.js +318 -0
  192. package/src/lib/lsp/lsp-manager.js +323 -0
  193. package/src/lib/lsp/lsp-server-registry.js +196 -0
  194. package/src/lib/plugin-monitor-supervisor.js +238 -0
  195. package/src/lib/plugin-runtime/agents.js +51 -0
  196. package/src/lib/plugin-runtime/bin.js +100 -0
  197. package/src/lib/plugin-runtime/hooks.js +100 -0
  198. package/src/lib/plugin-runtime/install.js +388 -0
  199. package/src/lib/plugin-runtime/lsp.js +75 -0
  200. package/src/lib/plugin-runtime/manifest.js +458 -0
  201. package/src/lib/plugin-runtime/mcp.js +89 -0
  202. package/src/lib/plugin-runtime/monitors.js +100 -0
  203. package/src/lib/plugin-runtime/policy.js +144 -0
  204. package/src/lib/plugin-runtime/reload.js +79 -0
  205. package/src/lib/plugin-runtime/scopes.js +197 -0
  206. package/src/lib/plugin-runtime/settings.js +119 -0
  207. package/src/lib/plugin-runtime/signature.js +107 -0
  208. package/src/lib/plugin-runtime/skills.js +43 -0
  209. package/src/lib/plugin-runtime/trust.js +140 -0
  210. package/src/lib/sandbox-egress-proxy.js +162 -0
  211. package/src/lib/sandbox-network-policy.js +134 -0
  212. package/src/lib/sandbox-v2.js +10 -4
  213. package/src/lib/settings-hook-events.cjs +78 -6
  214. package/src/lib/settings-hooks.cjs +29 -3
  215. package/src/lib/settings-loader.cjs +35 -1
  216. package/src/lib/skill-loader.js +18 -0
  217. package/src/lib/telemetry/span-recorder.js +237 -0
  218. package/src/repl/agent-repl.js +400 -4
  219. package/src/runtime/agent-core.js +546 -52
  220. package/src/runtime/coding-agent-contract-shared.cjs +82 -8
  221. package/src/runtime/coding-agent-policy.cjs +32 -7
  222. package/src/runtime/fallback-model.js +134 -7
  223. package/src/runtime/headless-runner.js +353 -108
  224. package/src/runtime/mcp-config.js +46 -0
  225. package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +0 -1
  226. package/src/assets/web-panel/assets/OrderTableRenderer-DIp8Xcbd.js +0 -1
  227. package/src/assets/web-panel/assets/Projects-Dpid9CDg.js +0 -1
  228. package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +0 -3
  229. package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +0 -1
  230. package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +0 -1
  231. package/src/assets/web-panel/assets/index-BXiw9dQf.js +0 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Plugin trust gating (Phase 3.3f) — decide whether a plugin's CODE-BEARING
3
+ * components (hooks that run shell, LSP servers that spawn binaries) are allowed
4
+ * to execute.
5
+ *
6
+ * Threat model: `cc plugin add owner/repo` and, worse, a cloned repo that ships
7
+ * a project-scope plugin under `.chainlesschain/plugins/`, could run arbitrary
8
+ * commands the moment the agent starts. So:
9
+ *
10
+ * - user / local scope → TRUSTED. The developer installed it on their own
11
+ * machine (same consent model as an npm dependency's lifecycle scripts).
12
+ * - project scope → UNTRUSTED until the user explicitly trusts it, since
13
+ * it can arrive with a git clone. Trust is pinned to the exact version, so a
14
+ * later version bump re-requires consent.
15
+ *
16
+ * Trust is recorded in the USER data dir (never in the repo), keyed by
17
+ * `<scope>:<name>` → { version, trustedAt }.
18
+ *
19
+ * Only code execution is gated here. Declarative components (skills are prompts,
20
+ * mcp/settings are config) load regardless; this is specifically about not
21
+ * running untrusted SHELL/binaries behind the user's back.
22
+ */
23
+
24
+ import fs from "fs";
25
+ import path from "path";
26
+ import { getElectronUserDataDir } from "../paths.js";
27
+
28
+ export const _deps = {
29
+ existsSync: fs.existsSync,
30
+ readFileSync: fs.readFileSync,
31
+ writeFileSync: fs.writeFileSync,
32
+ mkdirSync: fs.mkdirSync,
33
+ now: () => new Date().toISOString(),
34
+ // Path of the trust store; injectable so unit tests never touch the real
35
+ // user-data dir.
36
+ storePath: () => path.join(getElectronUserDataDir(), "plugin-trust.json"),
37
+ };
38
+
39
+ const AUTO_TRUSTED_SCOPES = new Set(["user", "local"]);
40
+
41
+ export function loadTrustStore() {
42
+ try {
43
+ const p = _deps.storePath();
44
+ if (!_deps.existsSync(p)) return {};
45
+ const data = JSON.parse(_deps.readFileSync(p, "utf8"));
46
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ function saveTrustStore(store) {
53
+ const p = _deps.storePath();
54
+ _deps.mkdirSync(path.dirname(p), { recursive: true });
55
+ _deps.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
56
+ }
57
+
58
+ function trustKey(scope, name) {
59
+ return `${scope}:${name}`;
60
+ }
61
+
62
+ /**
63
+ * May this plugin's code-bearing components run?
64
+ * @param {{ scope, name, version }} plugin
65
+ */
66
+ export function isPluginTrusted(plugin) {
67
+ if (!plugin || !plugin.name) return false;
68
+ if (AUTO_TRUSTED_SCOPES.has(plugin.scope)) return true;
69
+ const entry = loadTrustStore()[trustKey(plugin.scope, plugin.name)];
70
+ return Boolean(entry && entry.version === plugin.version);
71
+ }
72
+
73
+ /** Trust a plugin (records the exact version). */
74
+ export function trustPlugin(name, { scope = "project", version } = {}) {
75
+ if (!version) throw new Error("trustPlugin requires a version");
76
+ const store = loadTrustStore();
77
+ store[trustKey(scope, name)] = { version, trustedAt: _deps.now() };
78
+ saveTrustStore(store);
79
+ return { name, scope, version };
80
+ }
81
+
82
+ /** Revoke trust for a plugin at a scope. */
83
+ export function untrustPlugin(name, { scope = "project" } = {}) {
84
+ const store = loadTrustStore();
85
+ const key = trustKey(scope, name);
86
+ const existed = Object.prototype.hasOwnProperty.call(store, key);
87
+ delete store[key];
88
+ saveTrustStore(store);
89
+ return { name, scope, removed: existed };
90
+ }
91
+
92
+ /** All trust entries (for `cc plugin trust --list`). */
93
+ export function listTrust() {
94
+ return Object.entries(loadTrustStore()).map(([key, v]) => {
95
+ const idx = key.indexOf(":");
96
+ return {
97
+ scope: key.slice(0, idx),
98
+ name: key.slice(idx + 1),
99
+ version: v?.version || null,
100
+ trustedAt: v?.trustedAt || null,
101
+ };
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Split discovered plugins into those whose code may run and those gated out.
107
+ * @returns {{ trusted: any[], skipped: any[] }}
108
+ */
109
+ export function partitionByTrust(plugins) {
110
+ const trusted = [];
111
+ const skipped = [];
112
+ for (const p of plugins || []) {
113
+ (isPluginTrusted(p) ? trusted : skipped).push(p);
114
+ }
115
+ return { trusted, skipped };
116
+ }
117
+
118
+ // One-time stderr notice so a user isn't mystified when a project plugin's
119
+ // hooks/servers silently don't run. Guarded so it prints at most once per
120
+ // (component-kind) per process.
121
+ const _warned = new Set();
122
+ export function warnUntrustedOnce(names, kind) {
123
+ if (!names || names.length === 0) return;
124
+ if (_warned.has(kind)) return;
125
+ _warned.add(kind);
126
+ const list = [...new Set(names)].join(", ");
127
+ try {
128
+ process.stderr.write(
129
+ `[plugins] skipped ${kind} from untrusted project plugin(s): ${list}\n` +
130
+ ` run \`cc plugin trust <name>\` to enable them.\n`,
131
+ );
132
+ } catch {
133
+ /* stderr notice is best-effort */
134
+ }
135
+ }
136
+
137
+ /** Test hook: reset the one-time warning guard. */
138
+ export function _resetTrustWarnings() {
139
+ _warned.clear();
140
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Sandbox egress-filtering proxy (Phase 1 enforcement).
3
+ *
4
+ * The domain policy in `sandbox-network-policy.js` DECIDES whether a host is
5
+ * reachable; this turns that decision into ENFORCEMENT against real
6
+ * subprocesses. A local forward proxy consults `evaluateNetworkAccess` for
7
+ * every request (plain HTTP) and CONNECT tunnel (HTTPS) and refuses the blocked
8
+ * ones. Point a sandboxed process's `HTTP_PROXY`/`HTTPS_PROXY` at it and its
9
+ * `curl`/`npm`/`pip`/`git`/`wget`/`fetch` traffic is filtered.
10
+ *
11
+ * This is a proxy-level control, not a kernel one: a tool that deliberately
12
+ * ignores proxy env can still open a raw socket, so a hard guarantee also needs
13
+ * OS network isolation (Docker `--network`, bwrap netns). But it is fully
14
+ * cross-platform (works on Windows, no root) and closes the common egress path
15
+ * that the domain allow/deny list is actually about.
16
+ */
17
+
18
+ import http from "node:http";
19
+ import net from "node:net";
20
+ import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
21
+
22
+ /**
23
+ * Build the proxy env vars a child process needs to route through the proxy.
24
+ * `NO_PROXY` is intentionally empty so nothing bypasses the filter.
25
+ * @param {number} port
26
+ * @param {string} host address the CHILD uses to reach the proxy (for a Docker
27
+ * container this is `host.docker.internal`, not localhost)
28
+ */
29
+ export function proxyEnv(port, host = "127.0.0.1") {
30
+ const url = `http://${host}:${port}`;
31
+ return {
32
+ HTTP_PROXY: url,
33
+ HTTPS_PROXY: url,
34
+ ALL_PROXY: url,
35
+ http_proxy: url,
36
+ https_proxy: url,
37
+ all_proxy: url,
38
+ NO_PROXY: "",
39
+ no_proxy: "",
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Create (but do not yet listen on) an egress-filtering proxy for a policy.
45
+ * @param {object} policy { allowedDomains?, deniedDomains?, allowPrivate? }
46
+ * @param {object} [opts] { onDecision?(info), bindHost? }
47
+ * @returns {{ listen():Promise<{port,env,server}>, close():Promise<void>,
48
+ * server:import('http').Server, blocked:number, allowed:number }}
49
+ */
50
+ export function createEgressProxy(policy = {}, opts = {}) {
51
+ const bindHost = opts.bindHost || "127.0.0.1";
52
+ const state = { blocked: 0, allowed: 0 };
53
+ const decide = (target, kind) => {
54
+ const verdict = evaluateNetworkAccess(target, policy);
55
+ if (verdict.allowed) state.allowed += 1;
56
+ else state.blocked += 1;
57
+ if (typeof opts.onDecision === "function") {
58
+ try {
59
+ opts.onDecision({ kind, target, ...verdict });
60
+ } catch {
61
+ /* observation is best-effort */
62
+ }
63
+ }
64
+ return verdict;
65
+ };
66
+
67
+ const server = http.createServer((req, res) => {
68
+ // A forward-proxy request carries the absolute URL in the request line.
69
+ const verdict = decide(req.url, "http");
70
+ if (!verdict.allowed) {
71
+ res.writeHead(403, { "content-type": "text/plain" });
72
+ res.end(`egress blocked: ${verdict.host} — ${verdict.reason}\n`);
73
+ return;
74
+ }
75
+ let u;
76
+ try {
77
+ u = new URL(req.url);
78
+ } catch {
79
+ res.writeHead(400, { "content-type": "text/plain" });
80
+ res.end("bad request target\n");
81
+ return;
82
+ }
83
+ const upstream = http.request(
84
+ {
85
+ hostname: u.hostname,
86
+ port: u.port || 80,
87
+ path: `${u.pathname}${u.search}`,
88
+ method: req.method,
89
+ headers: req.headers,
90
+ },
91
+ (upRes) => {
92
+ res.writeHead(upRes.statusCode || 502, upRes.headers);
93
+ upRes.pipe(res);
94
+ },
95
+ );
96
+ upstream.on("error", () => {
97
+ if (!res.headersSent)
98
+ res.writeHead(502, { "content-type": "text/plain" });
99
+ res.end("upstream error\n");
100
+ });
101
+ req.pipe(upstream);
102
+ });
103
+
104
+ // HTTPS (and any TLS) goes through CONNECT — tunnel only allowed hosts.
105
+ server.on("connect", (req, clientSocket, head) => {
106
+ const verdict = decide(req.url, "connect"); // req.url = "host:port"
107
+ if (!verdict.allowed) {
108
+ clientSocket.write(
109
+ "HTTP/1.1 403 Forbidden\r\n" +
110
+ "Content-Type: text/plain\r\n\r\n" +
111
+ `egress blocked: ${verdict.host} — ${verdict.reason}\n`,
112
+ );
113
+ clientSocket.end();
114
+ return;
115
+ }
116
+ const [host, portStr] = req.url.split(":");
117
+ const port = parseInt(portStr, 10) || 443;
118
+ const upstream = net.connect(port, host, () => {
119
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
120
+ if (head && head.length) upstream.write(head);
121
+ upstream.pipe(clientSocket);
122
+ clientSocket.pipe(upstream);
123
+ });
124
+ upstream.on("error", () => {
125
+ try {
126
+ clientSocket.end();
127
+ } catch {
128
+ /* already closed */
129
+ }
130
+ });
131
+ clientSocket.on("error", () => {
132
+ try {
133
+ upstream.destroy();
134
+ } catch {
135
+ /* already gone */
136
+ }
137
+ });
138
+ });
139
+
140
+ return {
141
+ server,
142
+ get blocked() {
143
+ return state.blocked;
144
+ },
145
+ get allowed() {
146
+ return state.allowed;
147
+ },
148
+ listen() {
149
+ return new Promise((resolve, reject) => {
150
+ server.once("error", reject);
151
+ server.listen(0, bindHost, () => {
152
+ server.removeListener("error", reject);
153
+ const port = server.address().port;
154
+ resolve({ port, env: proxyEnv(port), server });
155
+ });
156
+ });
157
+ },
158
+ close() {
159
+ return new Promise((resolve) => server.close(() => resolve()));
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Sandbox network domain policy (Phase 1) — decide whether a Shell subprocess is
3
+ * allowed to reach a given URL/host under `sandbox.network.allowedDomains` /
4
+ * `deniedDomains`. OS-level ENFORCEMENT is platform-specific (a proxy / seatbelt
5
+ * / bwrap egress filter), but the POLICY decision is pure, shared, and must get
6
+ * the tricky cases right — so it lives here, fully unit-tested:
7
+ *
8
+ * - wildcard subdomains (`*.example.com` matches `api.example.com` AND the
9
+ * apex `example.com`), exact hosts, and `*` (all);
10
+ * - deny takes precedence over allow;
11
+ * - default-DENY when an allowlist is set and the host isn't on it
12
+ * (Phase 1 acceptance "未授权域名无法通过 curl/npm/pip 访问");
13
+ * - private / loopback / link-local / metadata targets are blocked unless
14
+ * explicitly allowed — an SSRF / DNS-rebinding guard, and it fixes the
15
+ * `new URL()` IPv6-bracket gotcha where `[::1]` leaks brackets into the
16
+ * hostname and defeats a naive string check.
17
+ */
18
+
19
+ /** Extract a normalized lowercase host from a URL or bare host:port. */
20
+ export function extractHost(target) {
21
+ if (target == null) return null;
22
+ let s = String(target).trim();
23
+ if (!s) return null;
24
+ // Try URL parse first (covers scheme://host:port/path, userinfo, etc.).
25
+ try {
26
+ // Add a scheme if it looks scheme-less so URL() can parse host:port.
27
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `http://${s}`;
28
+ const u = new URL(withScheme);
29
+ s = u.hostname; // may be "[::1]" for IPv6 — normalize below
30
+ } catch {
31
+ // Not a URL — treat as a bare host, strip any :port.
32
+ s = s.replace(/:\d+$/, "");
33
+ }
34
+ // Strip IPv6 brackets ("[::1]" → "::1") — the documented SSRF gotcha.
35
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
36
+ return s.toLowerCase() || null;
37
+ }
38
+
39
+ /** Does `host` match a domain `pattern` (`*`, `*.example.com`, or exact)? */
40
+ export function matchesDomain(host, pattern) {
41
+ if (!host || !pattern) return false;
42
+ const p = String(pattern).trim().toLowerCase();
43
+ if (p === "*") return true;
44
+ if (p.startsWith("*.")) {
45
+ const base = p.slice(2);
46
+ // `*.example.com` matches subdomains AND the apex `example.com`.
47
+ return host === base || host.endsWith(`.${base}`);
48
+ }
49
+ return host === p;
50
+ }
51
+
52
+ function anyMatch(host, patterns) {
53
+ return (patterns || []).some((p) => matchesDomain(host, p));
54
+ }
55
+
56
+ /** Private / loopback / link-local / cloud-metadata target? (SSRF guard) */
57
+ export function isPrivateHost(host) {
58
+ if (!host) return false;
59
+ const h = host.toLowerCase();
60
+ if (h === "localhost" || h.endsWith(".localhost")) return true;
61
+ // Cloud metadata endpoint.
62
+ if (h === "169.254.169.254" || h === "metadata.google.internal") return true;
63
+ // IPv6 loopback / unique-local / link-local.
64
+ if (h === "::1" || h === "::") return true;
65
+ if (/^f[cd][0-9a-f]{2}:/i.test(h)) return true; // fc00::/7 unique-local
66
+ if (/^fe80:/i.test(h)) return true; // link-local
67
+ // IPv4-mapped IPv6 (::ffff:127.0.0.1) — check the tail.
68
+ const mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
69
+ const ipv4 = mapped ? mapped[1] : h;
70
+ const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
71
+ if (m) {
72
+ const [a, b] = [Number(m[1]), Number(m[2])];
73
+ if (a === 127) return true; // 127.0.0.0/8 loopback
74
+ if (a === 10) return true; // 10.0.0.0/8
75
+ if (a === 192 && b === 168) return true; // 192.168.0.0/16
76
+ if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
77
+ if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local
78
+ if (a === 0) return true; // 0.0.0.0/8
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Evaluate network access for a target under a policy.
85
+ *
86
+ * @param {string} target URL or host (optionally with :port)
87
+ * @param {object} policy { allowedDomains?:string[], deniedDomains?:string[],
88
+ * allowPrivate?:boolean }
89
+ * @returns {{ allowed:boolean, host:string|null, reason:string }}
90
+ */
91
+ export function evaluateNetworkAccess(target, policy = {}) {
92
+ const allowed = policy.allowedDomains || [];
93
+ const denied = policy.deniedDomains || [];
94
+ const host = extractHost(target);
95
+ if (!host) {
96
+ return { allowed: false, host: null, reason: "unparseable target" };
97
+ }
98
+ // 1) Deny wins over everything.
99
+ if (anyMatch(host, denied)) {
100
+ return { allowed: false, host, reason: "denied by deniedDomains" };
101
+ }
102
+ // 2) A SPECIFIC (non-`*`) allow-list match overrides the private-host guard,
103
+ // so an intentionally-allowed localhost/dev host still works. A bare `*`
104
+ // does NOT — it means "all public domains", never internal/metadata.
105
+ const explicitNonStar = anyMatch(
106
+ host,
107
+ allowed.filter((p) => String(p).trim() !== "*"),
108
+ );
109
+ if (explicitNonStar) {
110
+ return { allowed: true, host, reason: "matched allowedDomains" };
111
+ }
112
+ // 3) Private / loopback / metadata targets are blocked unless allowed above.
113
+ if (isPrivateHost(host) && !policy.allowPrivate) {
114
+ return {
115
+ allowed: false,
116
+ host,
117
+ reason: "private/loopback target blocked (SSRF guard)",
118
+ };
119
+ }
120
+ // 4) A `*` (or any remaining) allow-list match → allow.
121
+ if (anyMatch(host, allowed)) {
122
+ return { allowed: true, host, reason: "matched allowedDomains" };
123
+ }
124
+ // 5) With an allow-list present, anything not on it is denied (default-deny).
125
+ if (allowed.length > 0) {
126
+ return {
127
+ allowed: false,
128
+ host,
129
+ reason: "not in allowedDomains (default-deny)",
130
+ };
131
+ }
132
+ // 6) No allow-list → permissive (deny-list + private guard still applied).
133
+ return { allowed: true, host, reason: "no allowlist (permissive)" };
134
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import crypto from "crypto";
7
7
  import { safeJsonParse } from "./safe-json.js";
8
+ import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
8
9
  import { createRequire } from "module";
9
10
 
10
11
  const _require = createRequire(import.meta.url);
@@ -158,11 +159,16 @@ function checkFilePermission(permissions, filePath, mode) {
158
159
  }
159
160
 
160
161
  function checkNetworkPermission(permissions, host) {
162
+ // Delegate to the shared domain policy: wildcard subdomains, deny-precedence,
163
+ // default-deny under an allowlist, URL/host normalization, and the SSRF /
164
+ // loopback / metadata guard (incl. the IPv6-bracket gotcha). The old check was
165
+ // exact-string only and let e.g. a URL or `[::1]` slip past.
161
166
  const net = permissions.network || {};
162
- const denied = net.denied || [];
163
- if (denied.includes(host)) return false;
164
- const allowed = net.allowed || [];
165
- return allowed.includes(host) || allowed.includes("*");
167
+ return evaluateNetworkAccess(host, {
168
+ allowedDomains: net.allowed || [],
169
+ deniedDomains: net.denied || [],
170
+ allowPrivate: net.allowPrivate === true,
171
+ }).allowed;
166
172
  }
167
173
 
168
174
  function checkSystemCallPermission(permissions, call) {
@@ -19,6 +19,24 @@
19
19
  const { collectHooks } = require("./settings-hooks.cjs");
20
20
  const { runHooks } = require("./hook-runner.cjs");
21
21
 
22
+ /**
23
+ * Split collected hooks into the blocking (sync) set and the fire-and-forget
24
+ * (`async: true`) set. Only NON-decision events should dispatch async hooks —
25
+ * an async hook can't gate a turn it no longer blocks, so a UserPromptSubmit
26
+ * hook that must be able to `block` should stay sync. The caller decides which
27
+ * events are eligible; this is just the partition.
28
+ * @returns {{ sync: Array, async: Array }}
29
+ */
30
+ function partitionAsyncHooks(hooks) {
31
+ const sync = [];
32
+ const asyncHooks = [];
33
+ for (const h of hooks || []) {
34
+ if (h && h.async === true) asyncHooks.push(h);
35
+ else sync.push(h);
36
+ }
37
+ return { sync, async: asyncHooks };
38
+ }
39
+
22
40
  /** Join the context emitted by the hooks that ran (additionalContext / plain stdout). */
23
41
  function aggregateContext(results) {
24
42
  const parts = [];
@@ -36,19 +54,32 @@ function aggregateContext(results) {
36
54
  /**
37
55
  * UserPromptSubmit settings hooks. A `block`/`ask` decision aborts the turn;
38
56
  * otherwise any emitted context is returned for the caller to inject.
57
+ *
58
+ * `async: true` hooks are EXCLUDED from this blocking run — they can't gate a
59
+ * turn they no longer block, so a fire-and-forget hook is dispatched separately
60
+ * (see dispatchAsyncHooks). This keeps the sync/decision path unchanged while a
61
+ * long-running check runs alongside the turn.
39
62
  * @returns {{ blocked:boolean, reason?:string, hook?:string, additionalContext:string|null }}
40
63
  */
41
- function runUserPromptSubmitHooks(settingsHooks, { prompt, cwd, sessionId } = {}) {
64
+ function runUserPromptSubmitHooks(
65
+ settingsHooks,
66
+ { prompt, cwd, sessionId } = {},
67
+ ) {
42
68
  if (!settingsHooks) return { blocked: false, additionalContext: null };
43
69
  const matched = collectHooks(settingsHooks, "UserPromptSubmit", "");
44
70
  if (matched.length === 0) return { blocked: false, additionalContext: null };
71
+ const { sync } = partitionAsyncHooks(matched);
72
+ if (sync.length === 0) return { blocked: false, additionalContext: null };
45
73
  const payload = {
46
74
  hook_event_name: "UserPromptSubmit",
47
75
  prompt: String(prompt || ""),
48
76
  cwd,
49
77
  session_id: sessionId || null,
50
78
  };
51
- const outcome = runHooks(matched, payload, { cwd, event: "UserPromptSubmit" });
79
+ const outcome = runHooks(sync, payload, {
80
+ cwd,
81
+ event: "UserPromptSubmit",
82
+ });
52
83
  if (outcome.decision === "block" || outcome.decision === "ask") {
53
84
  return {
54
85
  blocked: true,
@@ -57,7 +88,36 @@ function runUserPromptSubmitHooks(settingsHooks, { prompt, cwd, sessionId } = {}
57
88
  additionalContext: null,
58
89
  };
59
90
  }
60
- return { blocked: false, additionalContext: aggregateContext(outcome.results) };
91
+ return {
92
+ blocked: false,
93
+ additionalContext: aggregateContext(outcome.results),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Dispatch the `async: true` hooks for an event fire-and-forget onto the given
99
+ * supervisor (see async-hook-supervisor). Sync hooks are ignored here — they run
100
+ * on the blocking path. No-op (returns []) without a supervisor or async hooks,
101
+ * so callers can wire it unconditionally.
102
+ *
103
+ * @param {object} settingsHooks merged hooks block
104
+ * @param {string} event UserPromptSubmit / PostToolUse / Stop / …
105
+ * @param {object} payload extra fields merged into the hook stdin JSON
106
+ * @param {object} opts { cwd, matchTarget, supervisor }
107
+ * @returns {Array<{command:string, dispatched:boolean, reason?:string}>}
108
+ */
109
+ function dispatchAsyncHooks(settingsHooks, event, payload = {}, opts = {}) {
110
+ const supervisor = opts.supervisor;
111
+ if (!settingsHooks || !supervisor) return [];
112
+ const matched = collectHooks(settingsHooks, event, opts.matchTarget || "");
113
+ if (matched.length === 0) return [];
114
+ const { async: asyncHooks } = partitionAsyncHooks(matched);
115
+ if (asyncHooks.length === 0) return [];
116
+ return supervisor.dispatch(
117
+ asyncHooks,
118
+ { hook_event_name: event, cwd: opts.cwd, ...payload },
119
+ { cwd: opts.cwd },
120
+ );
61
121
  }
62
122
 
63
123
  /**
@@ -82,13 +142,23 @@ function runSessionStartHooks(settingsHooks, { source, cwd, sessionId } = {}) {
82
142
  /**
83
143
  * Generic observe-only fire (SessionEnd / Stop / PreCompact). Returns the raw
84
144
  * runHooks outcome so callers can read a block reason; never gates flow here.
145
+ *
146
+ * `async: true` hooks are EXCLUDED from this synchronous run — they are always
147
+ * fire-and-forget and are dispatched separately (dispatchAsyncHooks), so running
148
+ * them here too would double-execute them.
85
149
  */
86
- function runObserveHooks(settingsHooks, event, payload = {}, { cwd, matchTarget } = {}) {
150
+ function runObserveHooks(
151
+ settingsHooks,
152
+ event,
153
+ payload = {},
154
+ { cwd, matchTarget } = {},
155
+ ) {
87
156
  if (!settingsHooks) return { decision: "continue", results: [] };
88
157
  const matched = collectHooks(settingsHooks, event, matchTarget || "");
89
- if (matched.length === 0) return { decision: "continue", results: [] };
158
+ const { sync } = partitionAsyncHooks(matched);
159
+ if (sync.length === 0) return { decision: "continue", results: [] };
90
160
  return runHooks(
91
- matched,
161
+ sync,
92
162
  { hook_event_name: event, cwd, ...payload },
93
163
  { cwd, event },
94
164
  );
@@ -99,4 +169,6 @@ module.exports = {
99
169
  runSessionStartHooks,
100
170
  runObserveHooks,
101
171
  aggregateContext,
172
+ partitionAsyncHooks,
173
+ dispatchAsyncHooks,
102
174
  };
@@ -39,9 +39,13 @@ const HOOK_EVENTS = Object.freeze([
39
39
  "PostToolUse",
40
40
  "UserPromptSubmit",
41
41
  "Stop",
42
+ "SubagentStart",
42
43
  "SubagentStop",
43
44
  "SessionStart",
45
+ "SessionResume",
46
+ "SessionPause",
44
47
  "SessionEnd",
48
+ "ConfigChange",
45
49
  "PreCompact",
46
50
  "Notification",
47
51
  ]);
@@ -210,18 +214,40 @@ function umbrellaFor(tool) {
210
214
  * matcher is tested against BOTH the CC umbrella (`Bash`) and the raw tool
211
215
  * name (`run_shell`), so settings written either way fire.
212
216
  *
213
- * @returns {Array<{command:string, timeout?:number}>}
217
+ * `async`/`asyncRewake` are carried through (default false) so the caller can
218
+ * split fire-and-forget hooks off the blocking path (see async-hook-supervisor).
219
+ * `event` is stamped on each hook so a supervisor can key runs by (event+cmd).
220
+ *
221
+ * @returns {Array<{command:string, timeout?:number, event:string,
222
+ * async:boolean, asyncRewake:boolean}>}
214
223
  */
215
224
  function collectHooks(hooksBlock, event, toolName) {
216
225
  const groups = (hooksBlock && hooksBlock[event]) || [];
217
226
  const umbrella = umbrellaFor(toolName);
218
227
  const raw = String(toolName || "");
228
+ // Claude-Code MCP parity: a bare server-prefix matcher (`mcp__github`) fires
229
+ // for EVERY tool from that server (`mcp__github__create_issue`, …). Our
230
+ // matcher is anchored (`^…$`), so the server prefix alone would never match a
231
+ // full `mcp__server__tool` name without also testing the prefix explicitly.
232
+ // (Users can still target one tool with the full name, or all of a server's
233
+ // tools with a `mcp__server*` wildcard / `/mcp__server__/` regex.)
234
+ let mcpServer = null;
235
+ if (raw.startsWith("mcp__")) {
236
+ const parts = raw.split("__");
237
+ if (parts.length >= 3) mcpServer = `${parts[0]}__${parts[1]}`;
238
+ }
219
239
  const out = [];
220
240
  for (const g of groups) {
221
241
  const fn = compileMatcher(g.matcher);
222
- if (fn(umbrella) || (raw && fn(raw))) {
242
+ if (fn(umbrella) || (raw && fn(raw)) || (mcpServer && fn(mcpServer))) {
223
243
  for (const h of g.hooks)
224
- out.push({ command: h.command, timeout: h.timeout });
244
+ out.push({
245
+ command: h.command,
246
+ timeout: h.timeout,
247
+ event,
248
+ async: h.async === true,
249
+ asyncRewake: h.asyncRewake === true,
250
+ });
225
251
  }
226
252
  }
227
253
  return out;