chainlesschain 0.162.154 → 0.162.155

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 (198) 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-CIpoXyVI.js → AIOps-Cth_zZeA.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Ch_QXIPk.js → ActionButton-zfwA3Oxn.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CvaBYShv.js → Analytics-FeKc4aU2.js} +2 -2
  7. package/src/assets/web-panel/assets/AppLayout-C6WIrDZk.js +9 -0
  8. package/src/assets/web-panel/assets/{Audit-Bng8eHCW.js → Audit-C2gRq5Qi.js} +1 -1
  9. package/src/assets/web-panel/assets/BackgroundAgents-Dq2340Kl.css +1 -0
  10. package/src/assets/web-panel/assets/BackgroundAgents-TeTIFiW-.js +1 -0
  11. package/src/assets/web-panel/assets/{Backup-DvBs8-lm.js → Backup-CLJk3k6i.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-DKwajm0g.js → BaseInput-5ms537NY.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-BoGpZ3uO.js → Chat-4Nr5U9PJ.js} +5 -5
  14. package/src/assets/web-panel/assets/ChatBubbleRenderer-BFEnA8LN.js +1 -0
  15. package/src/assets/web-panel/assets/{Checkbox-BMJEmxFQ.js → Checkbox-B2Bca5ev.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Bsoag5CV.js → Codegen-D6Uysdbv.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-CDjH9-sp.js → Col-B3XQ8xc7.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-Cg2QHPQ6.js → Community-CqwKtoPz.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-BU2BaqP-.js → Compact-BZZINTus.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-D6ZYMCqZ.js → Compliance-Dr3BXkWA.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DaZ8VW-I.js → Cowork-rx_x9Wqc.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-DQY1RH9v.js → Cron-BpZr7rHc.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-BVR28vk9.js → Crosschain-CeV5EYVg.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-N44KvTP-.js → DID-fe2Qaowz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-Bjhlx6wS.js → Dashboard-Fh_EdWVa.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-CpLYxpy1.js → Dropdown-N1PIwWYf.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-srEITSPI.js → EmailListRenderer-B6vK93Ed.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-juOC_fX2.js → FamilyGuardDashboard-e3iUZhAg.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-nRaQtaXW.js → Federation-uInc9YcN.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DflhTj09.js → FormItemContext-DmQZSX0z.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-DulI_3NI.js → GenericCardRenderer-CV_Z8OAo.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-BxOPefXn.js → Git-jXAhNN8d.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-C6G_Wnwi.js → Governance-Blw1esBB.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-BnFYa2BF.js → Inference-CoK8Ia0Z.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-TqQLEabm.js → KnowledgeGraph-5xrglMTK.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BH4AVh7s.js → Logs-BGIG9798.js} +2 -2
  37. package/src/assets/web-panel/assets/{Marketplace-DoWLTaEE.js → Marketplace-DFqhJMuY.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-D6yC7OG_.js → McpTools-7U8tEhwK.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-DLCeHj21.js → Memory-BrvboPLE.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-BZBE0GRl.js → MobileBridge-nzcd7d1H.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DJA15G1J.js → MobileProjects-DETVNKa7.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-CyTKJDHn.js → Mtc-SZcCYec3.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-DZKjgEVc.js → MtcAudit-Cx_d3N17.js} +5 -5
  44. package/src/assets/web-panel/assets/{Multisig-2rMPNhSO.js → Multisig-DtcmSUo-.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-VeMk4Fx-.js → NLProgramming-Dgx2QHl-.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-jr2GHMhh.js → Notes-oHTGEHoJ.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-WN9VZyFb.js → NotificationSettings-qKD-IlOl.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-CjNO-e9T.js → OrderTableRenderer-CiU4Sy1J.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-DdjVHuzT.js → Organization-BBNXnltP.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-l8vBsGGq.js → Overflow-Df2fDbvT.js} +1 -1
  51. package/src/assets/web-panel/assets/{OverrideContext-BlgL9440.js → OverrideContext-C0w0P4A3.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-CVQDHHwq.js → P2P-DUHj6zjO.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-CTsNh8qE.js → PdhVaultBrowser-B2nWgdoV.js} +4 -4
  54. package/src/assets/web-panel/assets/{Permissions-DLW87uyK.js → Permissions-CiW0afny.js} +3 -3
  55. package/src/assets/web-panel/assets/{PersonalDataHub-N8OvASTx.js → PersonalDataHub-cc1mHU_D.js} +2 -2
  56. package/src/assets/web-panel/assets/{Pipeline-BPZmdlz2.js → Pipeline-B93zTapU.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-MFZw_GNh.js → Privacy-Db1jXWfd.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-zW0pEsfU.js → ProjectInit-DFoZ1h9U.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CppVHr_o.js → ProjectSettings-BkkUnmYs.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-BTglkJO2.js → Projects-Xa779XLC.js} +1 -1
  61. package/src/assets/web-panel/assets/{Providers-CPHZ6JKT.js → Providers-DOF01IEN.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-DO4klNZ2.js → QrScannerModal-TohRsKd0.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-B37TTqUa.js → QuickAsk-DjLcr1ZU.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-n7KIfK7y.js → Recommend--Ism6bMx.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-SjVoS9mm.js → RemoteSession-B8CL3YHM.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-Dt6RTeHi.js → Reputation-CaRxB3Ep.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-Dty6jz5V.js → Row-BioxZBBH.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-DDLhqw5j.js → RssFeed-B2LipwH3.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-DrYbXi-W.js → Search-BpjLBV8u.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Fc9Z6fbB.js → Security-TibHuJWP.js} +3 -3
  71. package/src/assets/web-panel/assets/{Services-BAhPgjSM.js → Services-DtkuWseW.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-8JF7EPfE.js → Skeleton-BIcPQae6.js} +1 -1
  73. package/src/assets/web-panel/assets/Skills-DoTWIQOX.js +1 -0
  74. package/src/assets/web-panel/assets/{Sla-CSvjcATc.js → Sla-uUSU185m.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-Demx7Gnx.js → SpeechSettings-B7Bgc9mk.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-CKLnRWJr.js → SyncSettings-K-XrJ2s9.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-mhITRxjv.js → Tasks-B3hEXNcI.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-AjQI5rxD.js → Templates-CaFnyGDK.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-dbtEh1yR.js → Tenant-C2X0PtEC.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-DX-9jjfk.js → Terminal-CKU5yVsT.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C11EDZRt.js → TimelineRenderer-8trZeYDC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-GiA9fiZn.js → Tokens-BUEEe9US.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BPcnclJn.js → Trigger-COU97I7Y.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-C-DVY8nq.js → Trust-DNWQChaP.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DxodSwME.js → UkeySign-DWX_VtCp.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-D_WV0O8O.js → VideoEditing-D9eIDhsp.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-DSeNlVSU.js → Wallet-BOnCCgIL.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-Bwuuvpz1.js → WebAuthn-DN4dD-W8.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-8M96EU1N.js → WorkflowEditor-ClvDb_jX.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-D5S6SmLm.js → chat-DIxQ_Eeb.js} +1 -1
  91. package/src/assets/web-panel/assets/{collapseMotion-DVfhbsdP.js → collapseMotion-CqM1QOwM.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-5fbnGkAP.js → colors-BOX3WKcP.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-DaSuuMlW.js → compact-item-Bny82VDi.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-BNOHcXAF.js → createContext-Cnoy3enS.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-Dr9dzlQ5.js +1 -0
  96. package/src/assets/web-panel/assets/{echarts-CL0F2SnX.js → echarts-C61Vn3mP.js} +1 -1
  97. package/src/assets/web-panel/assets/{hasIn-DONwZUra.js → hasIn-BJ7fkx3F.js} +1 -1
  98. package/src/assets/web-panel/assets/{icons-BsDmGpcT.js → icons-DEmcnxBf.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B_yDD9Lm.js → index--CcGiwAI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-5_fj5gVd.js → index--Xb2lq6E.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-uR5KTNZA.js → index-2N9Y1cLx.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-MOgO5Yvx.js → index-7GLXvG8p.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CLXf-8ek.js → index-B1-h2QrC.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D7gs_3Ck.js → index-B6WoW8DJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CztESwB4.js → index-B7Fah_BX.js} +1 -1
  106. package/src/assets/web-panel/assets/index-B92M3t6B.js +1 -0
  107. package/src/assets/web-panel/assets/{index-BEO5f7Vf.js → index-BDkN6YOI.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-_6S41ikV.js → index-BJ8-oLHI.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CrkCFohi.js → index-BM0mQhVv.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BrAJSu-v.js → index-BP7HFsAK.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BcqOK7Lv.js → index-BWPey0Uf.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BLzMuz38.js → index-BbZynivw.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DbdQBn4n.js → index-BcmXbIwv.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-KwV9g_cf.js → index-Bk5yST66.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-ClGqO-2X.js → index-Bvheiep2.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Dd7xwGcD.js → index-C--PPqwi.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DP2-3A_j.js → index-C1nHEOFE.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CAYmc5Nh.js → index-CEPTiBWy.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-LRIt-AFl.js → index-CEq5Vh0i.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dm5A-MN5.js → index-CN6emwbq.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Cf2OCa8q.js → index-CQ-xLCVb.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DHx2lhHW.js → index-CW6n-y1L.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DM3Ug61e.js → index-CZ6CHiCQ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BJp4kRjw.js → index-CZCTan8Q.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-PsMErAB1.js → index-CnjtAKdw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CEzd5m5a.js → index-D2qNdrNW.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-B4L3gsNA.js → index-D9QEc7Tq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BcWMZqb4.js → index-DBhkJqVF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CSfYJqXt.js → index-DSRIjAWO.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CmUOjyL6.js → index-DxX8hhmL.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-RkInlwV0.js → index-Kio1ekYg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CTZeOnzO.js → index-OaNFXV8N.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BK2N5yVW.js → index-Ty4w18mR.js} +4 -4
  134. package/src/assets/web-panel/assets/{index-Cn5LtyIH.js → index-ceH9DsAi.js} +1 -1
  135. package/src/assets/web-panel/assets/index-d2UcGpK-.js +1 -0
  136. package/src/assets/web-panel/assets/{index-xo2f4DZR.js → index-qOBKrw-g.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-vYtprlNs.js → index-rn6xd12s.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-CxwLynp9.js → initDefaultProps-BmxMBuUZ.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-XbPvGJbW.js → motion-DnXNwo2k.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-D1u5pcgS.js → move-WnIJwmZN.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-BDxhTBMH.js → mtc-parser-DAto-c2w.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-EtIFGg5k.js → omit-BtvW9W0H.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-D33sWb0l.js → pickAttrs-DFqI9-_C.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-BwFRzaGo.js → placementArrow-Dax6wOK3.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-CBk2WqvQ.js → responsiveObserve-BUFWWC83.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-B52HtrKn.js → slide-DyARyuSz.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-D4qxqWnH.js → statusUtils-BZSDugpi.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-BxaCFlOL.js → styleChecker-B7o_K-4R.js} +1 -1
  149. package/src/assets/web-panel/assets/useFlexGapSupport-Dg7Bg8VG.js +1 -0
  150. package/src/assets/web-panel/assets/{useFs-C6VAU1_6.js → useFs-DQYXo5Rk.js} +1 -1
  151. package/src/assets/web-panel/assets/{useMergedState-Dc9sO2d7.js → useMergedState-DXbJa_zE.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-rEG6oXG8.js → usePersonalDataHub-E8NBAnkq.js} +1 -1
  153. package/src/assets/web-panel/assets/{useRefs-CAeFRzPf.js → useRefs-BrCfMLMc.js} +1 -1
  154. package/src/assets/web-panel/assets/{useState-LwExUYDO.js → useState-CnxtbLoC.js} +1 -1
  155. package/src/assets/web-panel/assets/{vendor-BMxUs6UX.js → vendor-Bn1JzzAn.js} +1 -1
  156. package/src/assets/web-panel/assets/{vnode-DOTafkEi.js → vnode-DD71_w1b.js} +1 -1
  157. package/src/assets/web-panel/assets/{zoom-BuwwN5K2.js → zoom-zc9EMd3q.js} +1 -1
  158. package/src/assets/web-panel/index.html +3 -3
  159. package/src/command-manifest.json +36 -1
  160. package/src/commands/agent.js +23 -2
  161. package/src/commands/agents.js +62 -1
  162. package/src/commands/auto-mode.js +87 -0
  163. package/src/commands/background-session.js +499 -0
  164. package/src/commands/command.js +4 -1
  165. package/src/commands/permissions.js +49 -0
  166. package/src/commands/remote-control.js +407 -0
  167. package/src/data/changelog.json +7 -2
  168. package/src/gateways/ws/background-agent-protocol.js +337 -0
  169. package/src/gateways/ws/message-dispatcher.js +10 -0
  170. package/src/gateways/ws/remote-session-protocol.js +102 -1
  171. package/src/gateways/ws/ws-server.js +61 -0
  172. package/src/harness/mcp-client.js +8 -0
  173. package/src/index.js +10 -0
  174. package/src/lib/auto-mode-config.js +396 -0
  175. package/src/lib/background-agent-supervisor.js +202 -17
  176. package/src/lib/background-session-transport.js +315 -0
  177. package/src/lib/permission-denial-store.js +159 -0
  178. package/src/lib/remote-approval-bridge.js +360 -0
  179. package/src/lib/remote-control.js +270 -0
  180. package/src/lib/repl-denials.js +26 -0
  181. package/src/lib/shell-approval.js +4 -0
  182. package/src/lib/ws-rpc-client.js +150 -0
  183. package/src/repl/agent-repl.js +208 -16
  184. package/src/repl/permission-tier.js +31 -0
  185. package/src/runtime/agent-core.js +57 -0
  186. package/src/runtime/headless-runner.js +126 -8
  187. package/src/runtime/headless-stream.js +87 -1
  188. package/src/runtime/mcp-config.js +11 -0
  189. package/src/runtime/mcp-tool-search.js +0 -0
  190. package/src/runtime/policies/agent-policy.js +17 -0
  191. package/src/workers/background-agent-worker.js +218 -37
  192. package/src/assets/web-panel/assets/AppLayout-B6Q6olcr.js +0 -9
  193. package/src/assets/web-panel/assets/ChatBubbleRenderer-mDQl_PXO.js +0 -1
  194. package/src/assets/web-panel/assets/Skills-DL39dY0t.js +0 -1
  195. package/src/assets/web-panel/assets/devWarning-K_woVElh.js +0 -1
  196. package/src/assets/web-panel/assets/index-3ILvaBpo.js +0 -1
  197. package/src/assets/web-panel/assets/index-JpB-FoZq.js +0 -1
  198. package/src/assets/web-panel/assets/useFlexGapSupport-B1lWlqXN.js +0 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Minimal promise-based WebSocket RPC client for the cc WS server protocol
3
+ * (auth + id-correlated request/response + fire-hose events). Used by
4
+ * `cc remote-control` for its in-process loopback HOST connection — the
5
+ * remote-session registry closes a session when its host disconnects, so the
6
+ * unified entry keeps this client open for the server's lifetime.
7
+ *
8
+ * Protocol notes:
9
+ * - every request carries a unique `id`. Flat responses (auth, remote-session-*)
10
+ * echo it back as `id`; envelope responses (session-create & friends,
11
+ * createCodingAgentEvent) repurpose `id` as a random eventId and carry the
12
+ * correlation key in `requestId` — so match `requestId` FIRST, then `id`.
13
+ * - the FIRST frame matching a pending key settles that request (fine for
14
+ * command/one-shot topics; streaming topics need onEvent, not request()).
15
+ * - messages matching no pending key are events → onEvent callbacks.
16
+ */
17
+
18
+ import WebSocket from "ws";
19
+
20
+ const DEFAULT_TIMEOUT_MS = 15_000;
21
+
22
+ export class WsRpcClient {
23
+ constructor({ url, WebSocketImpl = WebSocket, timeoutMs } = {}) {
24
+ if (!url) throw new Error("url is required");
25
+ this.url = url;
26
+ this.WebSocketImpl = WebSocketImpl;
27
+ this.timeoutMs = timeoutMs || DEFAULT_TIMEOUT_MS;
28
+ this.ws = null;
29
+ this._pending = new Map();
30
+ this._counter = 0;
31
+ this._eventHandlers = new Set();
32
+ this._closed = false;
33
+ }
34
+
35
+ onEvent(handler) {
36
+ this._eventHandlers.add(handler);
37
+ return () => this._eventHandlers.delete(handler);
38
+ }
39
+
40
+ connect() {
41
+ return new Promise((resolve, reject) => {
42
+ const ws = new this.WebSocketImpl(this.url);
43
+ this.ws = ws;
44
+ const onOpen = () => {
45
+ ws.off?.("error", onError);
46
+ resolve();
47
+ };
48
+ const onError = (err) => {
49
+ reject(new Error(`WebSocket connect failed: ${err.message || err}`));
50
+ };
51
+ ws.once("open", onOpen);
52
+ ws.once("error", onError);
53
+ ws.on("message", (data) => this._onMessage(data));
54
+ ws.on("close", () => this._onClose());
55
+ });
56
+ }
57
+
58
+ _onMessage(data) {
59
+ let message;
60
+ try {
61
+ message = JSON.parse(data.toString("utf8"));
62
+ } catch {
63
+ return; // non-JSON frames are not part of this protocol
64
+ }
65
+ const key =
66
+ message.requestId && this._pending.has(message.requestId)
67
+ ? message.requestId
68
+ : message.id && this._pending.has(message.id)
69
+ ? message.id
70
+ : null;
71
+ if (key) {
72
+ const pending = this._pending.get(key);
73
+ this._pending.delete(key);
74
+ clearTimeout(pending.timer);
75
+ if (message.type === "error") {
76
+ pending.reject(
77
+ new Error(
78
+ message.message ||
79
+ message.payload?.message ||
80
+ message.code ||
81
+ message.payload?.code ||
82
+ "server error",
83
+ ),
84
+ );
85
+ } else {
86
+ pending.resolve(message);
87
+ }
88
+ return;
89
+ }
90
+ for (const handler of this._eventHandlers) {
91
+ try {
92
+ handler(message);
93
+ } catch {
94
+ // one bad handler must not break the read loop
95
+ }
96
+ }
97
+ }
98
+
99
+ _onClose() {
100
+ this._closed = true;
101
+ for (const [, pending] of this._pending) {
102
+ clearTimeout(pending.timer);
103
+ pending.reject(new Error("connection closed"));
104
+ }
105
+ this._pending.clear();
106
+ }
107
+
108
+ /** Send `{id, type, ...payload}` and await the matching response frame. */
109
+ request(type, payload = {}, { timeoutMs } = {}) {
110
+ if (this._closed || !this.ws || this.ws.readyState !== this.ws.OPEN) {
111
+ return Promise.reject(new Error("not connected"));
112
+ }
113
+ const id = `rpc-${process.pid}-${++this._counter}`;
114
+ const frame = { id, type, ...payload };
115
+ return new Promise((resolve, reject) => {
116
+ const timer = setTimeout(() => {
117
+ this._pending.delete(id);
118
+ reject(new Error(`request "${type}" timed out`));
119
+ }, timeoutMs || this.timeoutMs);
120
+ if (typeof timer.unref === "function") timer.unref();
121
+ this._pending.set(id, { resolve, reject, timer });
122
+ this.ws.send(JSON.stringify(frame), (err) => {
123
+ if (err) {
124
+ this._pending.delete(id);
125
+ clearTimeout(timer);
126
+ reject(err);
127
+ }
128
+ });
129
+ });
130
+ }
131
+
132
+ /** Authenticate against a token-protected server. No-op when token is null. */
133
+ async auth(token) {
134
+ if (!token) return { success: true, skipped: true };
135
+ const result = await this.request("auth", { token });
136
+ if (!result.success) {
137
+ throw new Error(result.message || "authentication failed");
138
+ }
139
+ return result;
140
+ }
141
+
142
+ close() {
143
+ this._closed = true;
144
+ try {
145
+ this.ws?.close();
146
+ } catch {
147
+ // already closing
148
+ }
149
+ }
150
+ }
@@ -34,6 +34,7 @@ import {
34
34
  recordDenial,
35
35
  formatDenials,
36
36
  } from "../lib/repl-denials.js";
37
+ import { appendRecentDenials } from "../lib/permission-denial-store.js";
37
38
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
38
39
  import {
39
40
  createSession,
@@ -112,6 +113,7 @@ import {
112
113
  } from "./permission-prompt.js";
113
114
  import {
114
115
  parsePermissionTier,
116
+ parsePermissionModeArg,
115
117
  describeTier,
116
118
  nextTier,
117
119
  } from "./permission-tier.js";
@@ -154,9 +156,23 @@ let _classifyAllShell = false;
154
156
  let _sandbox = null;
155
157
  // Bounded log of tool calls the agent was BLOCKED from running this session
156
158
  // (shell-policy / ApprovalGate / settings rule / hook). Surfaced by
157
- // `/permissions denials` (Claude-Code 2.1.193 "recent denials"). In-memory only.
159
+ // `/permissions denials` and mirrored to `cc permissions recent`.
158
160
  const _recentDenials = [];
159
161
 
162
+ function persistRecentDenial(record, options = {}) {
163
+ try {
164
+ appendRecentDenials(record, {
165
+ sessionId: options.sessionId,
166
+ permissionMode: options.permissionMode,
167
+ cwd: options.cwd || process.cwd(),
168
+ source: "repl",
169
+ });
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
160
176
  /**
161
177
  * Fire settings.json Notification hooks (observe-only) — the agent needs the
162
178
  * user's attention (e.g. waiting on a permission/risk confirmation). A hook can
@@ -318,7 +334,15 @@ export async function agentLoop(messages, options) {
318
334
  : "",
319
335
  });
320
336
  if (denial) {
321
- recordDenial(options.denialLog, { ...denial, at: Date.now() });
337
+ const record = { ...denial, at: Date.now() };
338
+ recordDenial(options.denialLog, record);
339
+ if (options.persistRecentDenials === true) {
340
+ persistRecentDenial(record, {
341
+ sessionId: options.sessionId,
342
+ permissionMode: options.permissionMode,
343
+ cwd: options.cwd,
344
+ });
345
+ }
322
346
  }
323
347
  }
324
348
  // Parity with Desktop AIChatPage's `Switch to Trusted` button:
@@ -409,10 +433,21 @@ export async function startAgentRepl(options = {}) {
409
433
  // the injection after the turn (or as a backstop at the next submit on throw).
410
434
  let pendingBtw = [];
411
435
  let _btwRestore = null;
412
- // Current ApprovalGate session tier (strict|trusted|autopilot), mirrored here
413
- // so Shift+Tab can cycle it and `/permissions <tier>` can set it. Kept in sync
414
- // with _approvalGate.setSessionPolicy below.
436
+ // Current permission mode (strict|trusted|autopilot|auto), mirrored here so
437
+ // Shift+Tab can cycle it and `/permissions <tier>` can set it. Kept in sync
438
+ // with _approvalGate.setSessionPolicy below. "auto" rides the trusted gate
439
+ // tier but additionally activates the autoMode.decisions classifier wrapper
440
+ // (when settings customize it) — gateTierFor() maps mode → real gate tier.
415
441
  let _sessionTier = "strict";
442
+ const gateTierFor = (mode) =>
443
+ mode === "auto" ? "trusted" : mode === "dontAsk" ? "strict" : mode;
444
+ // dontAsk (headless parity, interactive form): anything that would prompt
445
+ // is denied instead — both confirmers below consult this before prompting.
446
+ const _dontAskActive = () => _sessionTier === "dontAsk";
447
+ // Resolved autoMode.decisions map (loaded once at startup); null until the
448
+ // gate is wired. The wrapper is installed only when settings customize the
449
+ // map, and only bites while _sessionTier === "auto" (isActive predicate).
450
+ let _autoModeResolved = null;
416
451
  const baseUrl = options.baseUrl || "http://localhost:11434";
417
452
  const apiKey = options.apiKey || null;
418
453
  // Configured vision model (config.llm.visionModel) — used when a turn carries
@@ -558,8 +593,39 @@ export async function startAgentRepl(options = {}) {
558
593
  const { getApprovalGate } =
559
594
  await import("../lib/session-core-singletons.js");
560
595
  _approvalGate = await getApprovalGate();
596
+ // autoMode.decisions: wrap the gate with the configurable classifier
597
+ // BEFORE setConfirmer so the wrapper captures the interactive confirmer.
598
+ // Inactive (delegating) unless the session mode is "auto"; not installed
599
+ // at all when settings don't customize the map (byte-identical path).
600
+ try {
601
+ const {
602
+ loadAutoModeConfig,
603
+ resolveAutoModeDecisions,
604
+ createAutoModeApprovalGate,
605
+ } = await import("../lib/auto-mode-config.js");
606
+ _autoModeResolved = resolveAutoModeDecisions(
607
+ loadAutoModeConfig({ cwd: process.cwd() }).effective,
608
+ );
609
+ if (_autoModeResolved.customized) {
610
+ _approvalGate = createAutoModeApprovalGate(
611
+ _approvalGate,
612
+ _autoModeResolved,
613
+ { isActive: () => _sessionTier === "auto" },
614
+ );
615
+ }
616
+ } catch (_err) {
617
+ _autoModeResolved = null; // fail to the plain gate tiers
618
+ }
561
619
  if (typeof _approvalGate.setConfirmer === "function") {
562
620
  _approvalGate.setConfirmer(async ({ tool, args, riskLevel }) => {
621
+ if (_dontAskActive()) {
622
+ process.stdout.write(
623
+ chalk.yellow(
624
+ `\n ✕ denied without asking (dontAsk mode): ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}\n`,
625
+ ),
626
+ );
627
+ return false;
628
+ }
563
629
  await _fireNotification(
564
630
  `Permission needed: ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}`,
565
631
  );
@@ -630,6 +696,14 @@ export async function startAgentRepl(options = {}) {
630
696
  // Confirmer is shared by permission `ask` rules AND hook `ask` decisions,
631
697
  // so define it unconditionally (a `hook:` rule label flows through too).
632
698
  _permissionConfirm = async ({ tool, args, rule, reason }) => {
699
+ if (_dontAskActive()) {
700
+ process.stdout.write(
701
+ chalk.yellow(
702
+ `\n ✕ denied without asking (dontAsk mode): ${tool}${rule ? ` (${rule})` : reason ? ` — ${reason}` : ""}\n`,
703
+ ),
704
+ );
705
+ return false;
706
+ }
633
707
  await _fireNotification(
634
708
  `Permission needed: ${tool}${rule ? " (" + rule + ")" : ""}`,
635
709
  );
@@ -1021,6 +1095,27 @@ export async function startAgentRepl(options = {}) {
1021
1095
  `(mcp__<server>__<tool>)`,
1022
1096
  ),
1023
1097
  );
1098
+ // MCP tool search (context scaling): defer big tool schemas behind the
1099
+ // internal tool_search tool. Below-threshold / disabled → no-op.
1100
+ try {
1101
+ const { maybeApplyToolSearch } =
1102
+ await import("../runtime/mcp-tool-search.js");
1103
+ const ts = maybeApplyToolSearch(_adhocMcp, {
1104
+ model,
1105
+ provider,
1106
+ cwd: process.cwd(),
1107
+ });
1108
+ if (ts) {
1109
+ logger.log(
1110
+ chalk.gray(
1111
+ `MCP: tool search active — ${ts.deferredCount} schema(s) ` +
1112
+ `deferred (~${ts.savedTokens} tok saved)`,
1113
+ ),
1114
+ );
1115
+ }
1116
+ } catch (_err) {
1117
+ // best-effort — full schemas still work without deferral
1118
+ }
1024
1119
  }
1025
1120
  } catch (mcpErr) {
1026
1121
  logger.log(chalk.yellow(`MCP: --mcp-config failed — ${mcpErr.message}`));
@@ -1045,6 +1140,27 @@ export async function startAgentRepl(options = {}) {
1045
1140
  }
1046
1141
  }
1047
1142
 
1143
+ // --permission-mode for the interactive session (headless parity): manual →
1144
+ // strict, acceptEdits → trusted, bypassPermissions → autopilot, and auto →
1145
+ // trusted tier + the autoMode.decisions classifier. An explicit flag wins
1146
+ // over the bundle's approvalPolicy default. Unknown values are ignored here
1147
+ // (headless validates them; interactively we just keep the default tier).
1148
+ if (options.permissionMode && _approvalGate && sessionId) {
1149
+ const parsed = parsePermissionModeArg(options.permissionMode);
1150
+ if (parsed && typeof _approvalGate.setSessionPolicy === "function") {
1151
+ try {
1152
+ _approvalGate.setSessionPolicy(sessionId, parsed.tier);
1153
+ _sessionTier = parsed.auto
1154
+ ? "auto"
1155
+ : parsed.dontAsk
1156
+ ? "dontAsk"
1157
+ : parsed.tier;
1158
+ } catch (_err) {
1159
+ // Non-critical — keep the default tier
1160
+ }
1161
+ }
1162
+ }
1163
+
1048
1164
  // Phase G #5 — inject top-K memory recall into system prompt for new sessions
1049
1165
  // Skip on resume (existing context already reflects prior work) and when
1050
1166
  // --no-recall-memory is passed.
@@ -1394,7 +1510,9 @@ export async function startAgentRepl(options = {}) {
1394
1510
  sessionId &&
1395
1511
  typeof _approvalGate.setSessionPolicy === "function"
1396
1512
  ) {
1397
- const next = nextTier(_sessionTier);
1513
+ // "auto" rides the trusted tier, so it cycles as trusted → autopilot
1514
+ // (cycling is also how you exit auto mode without /permissions).
1515
+ const next = nextTier(gateTierFor(_sessionTier));
1398
1516
  try {
1399
1517
  _approvalGate.setSessionPolicy(sessionId, next);
1400
1518
  _sessionTier = next;
@@ -1741,7 +1859,7 @@ export async function startAgentRepl(options = {}) {
1741
1859
  ` ${chalk.cyan("/init")} Inventory this folder into a cc.md project-memory file (/init [--force])`,
1742
1860
  );
1743
1861
  logger.log(
1744
- ` ${chalk.cyan("/context")} Live context-window usage by role`,
1862
+ ` ${chalk.cyan("/context")} Live context-window usage by role + MCP tool schemas`,
1745
1863
  );
1746
1864
  logger.log(
1747
1865
  ` ${chalk.cyan("/copy")} Copy last response to clipboard (/copy code → last code block)`,
@@ -2580,6 +2698,48 @@ export async function startAgentRepl(options = {}) {
2580
2698
  : ""
2581
2699
  }`,
2582
2700
  );
2701
+ // MCP tool-schema share of the window (gap-analysis 第二阶段 item 3):
2702
+ // the tools parameter rides along on EVERY request but isn't part of
2703
+ // `messages`, so it's invisible to the role buckets above. Show the
2704
+ // per-server schema cost + tool-search state + optimization advice.
2705
+ if (_adhocMcp?.extraToolDefinitions?.length) {
2706
+ const { describeMcpToolContext } =
2707
+ await import("../runtime/mcp-tool-search.js");
2708
+ const info = describeMcpToolContext(_adhocMcp, {
2709
+ estimate: estimateTokens,
2710
+ model,
2711
+ provider,
2712
+ });
2713
+ if (info) {
2714
+ const ts = info.toolSearch;
2715
+ logger.log(chalk.bold("\nMCP tool schemas (sent every request):"));
2716
+ for (const row of info.servers) {
2717
+ const share = window
2718
+ ? Math.round((row.sentTokens / window) * 100)
2719
+ : 0;
2720
+ logger.log(
2721
+ ` ${row.server.padEnd(16)}${String(row.sentTokens).padStart(7)} tok ${String(share).padStart(3)}%` +
2722
+ chalk.gray(
2723
+ ` (${row.tools} tools${
2724
+ row.deferred ? `, ${row.deferred} deferred` : ""
2725
+ })`,
2726
+ ),
2727
+ );
2728
+ }
2729
+ logger.log(
2730
+ ` ${"total".padEnd(16)}${String(info.sentTokens).padStart(7)} tok` +
2731
+ (ts.active
2732
+ ? chalk.gray(
2733
+ ` (tool search: ${ts.deferredCount} deferred, ` +
2734
+ `${ts.loadedCount} loaded, ~${info.savedTokens} tok saved)`,
2735
+ )
2736
+ : ""),
2737
+ );
2738
+ for (const line of info.advice) {
2739
+ logger.log(chalk.yellow(` ⚠ ${line}`));
2740
+ }
2741
+ }
2742
+ }
2583
2743
  } catch (err) {
2584
2744
  logger.error(`/context failed: ${err.message}`);
2585
2745
  }
@@ -3740,17 +3900,22 @@ export async function startAgentRepl(options = {}) {
3740
3900
  // Review what the agent was BLOCKED from running this session
3741
3901
  // (Claude-Code 2.1.193 "recent denials").
3742
3902
  logger.log(formatDenials(_recentDenials));
3903
+ logger.log(
3904
+ chalk.gray(
3905
+ " Cross-session history (headless + REPL): cc permissions recent",
3906
+ ),
3907
+ );
3743
3908
  prompt();
3744
3909
  return;
3745
3910
  }
3746
3911
  if (arg) {
3747
- // Set this session's approval tier mid-session (Claude-Code
3912
+ // Set this session's permission mode mid-session (Claude-Code
3748
3913
  // permission-mode / Shift+Tab parity; mirrors `cc session policy --set`).
3749
- const tier = parsePermissionTier(arg);
3750
- if (!tier) {
3914
+ const parsed = parsePermissionModeArg(arg);
3915
+ if (!parsed) {
3751
3916
  logger.info(
3752
- "Usage: /permissions [strict|trusted|autopilot] " +
3753
- "(aliases: default · accept-edits · bypass). No arg = show rules.",
3917
+ "Usage: /permissions [strict|trusted|autopilot|auto|dontask] " +
3918
+ "(aliases: default · manual · accept-edits · bypass). No arg = show rules.",
3754
3919
  );
3755
3920
  } else if (
3756
3921
  !_approvalGate ||
@@ -3762,11 +3927,31 @@ export async function startAgentRepl(options = {}) {
3762
3927
  );
3763
3928
  } else {
3764
3929
  try {
3765
- _approvalGate.setSessionPolicy(sessionId, tier);
3766
- _sessionTier = tier;
3930
+ _approvalGate.setSessionPolicy(sessionId, parsed.tier);
3931
+ _sessionTier = parsed.auto
3932
+ ? "auto"
3933
+ : parsed.dontAsk
3934
+ ? "dontAsk"
3935
+ : parsed.tier;
3767
3936
  logger.info(
3768
- `Approval policy → ${chalk.cyan(tier)} ${chalk.gray(`(${describeTier(tier)})`)}`,
3937
+ `Approval policy → ${chalk.cyan(_sessionTier)} ${chalk.gray(`(${describeTier(_sessionTier)})`)}`,
3769
3938
  );
3939
+ if (parsed.auto) {
3940
+ if (_autoModeResolved?.customized) {
3941
+ const m = _autoModeResolved.map;
3942
+ logger.info(
3943
+ chalk.gray(
3944
+ ` autoMode.decisions: low→${m.low.decision} · medium→${m.medium.decision} · high→${m.high.decision} (cc auto-mode config for sources)`,
3945
+ ),
3946
+ );
3947
+ } else {
3948
+ logger.info(
3949
+ chalk.gray(
3950
+ " no customized autoMode.decisions in settings — auto behaves like trusted",
3951
+ ),
3952
+ );
3953
+ }
3954
+ }
3770
3955
  } catch (_err) {
3771
3956
  logger.info("Could not set the approval policy.");
3772
3957
  }
@@ -3785,7 +3970,12 @@ export async function startAgentRepl(options = {}) {
3785
3970
  logger.log(renderPermissions(_permissionRules, { files }));
3786
3971
  logger.log(
3787
3972
  chalk.gray(
3788
- " Set tier mid-session: /permissions <strict|trusted|autopilot>",
3973
+ ` Current mode: ${_sessionTier} (${describeTier(_sessionTier)})`,
3974
+ ),
3975
+ );
3976
+ logger.log(
3977
+ chalk.gray(
3978
+ " Set tier mid-session: /permissions <strict|trusted|autopilot|auto|dontask>",
3789
3979
  ),
3790
3980
  );
3791
3981
  if (_recentDenials.length) {
@@ -4371,6 +4561,8 @@ export async function startAgentRepl(options = {}) {
4371
4561
  checkpointSession: sessionId,
4372
4562
  checkpointMarks: _checkpointMarks,
4373
4563
  denialLog: _recentDenials,
4564
+ persistRecentDenials: true,
4565
+ permissionMode: _sessionTier,
4374
4566
  prepareCall,
4375
4567
  approvalGate: _approvalGate,
4376
4568
  permissionRules: _permissionRules,
@@ -10,6 +10,7 @@ const TIER_ALIASES = {
10
10
  // strict — every risky action asks (the default)
11
11
  strict: "strict",
12
12
  default: "strict",
13
+ manual: "strict",
13
14
  normal: "strict",
14
15
  off: "strict",
15
16
  // trusted — low/medium-risk auto-approved, high-risk still asks (acceptEdits)
@@ -45,6 +46,32 @@ export function parsePermissionTier(arg) {
45
46
  : null;
46
47
  }
47
48
 
49
+ const AUTO_ALIASES = new Set(["auto", "auto-mode", "automode"]);
50
+ const DONT_ASK_ALIASES = new Set(["dontask", "dont-ask", "noask", "no-ask"]);
51
+
52
+ /**
53
+ * Parse a permission-mode argument that may also be `auto` (the configurable
54
+ * autoMode.decisions classifier) or `dontAsk` (anything that would prompt is
55
+ * denied instead — headless semantics brought to the interactive session).
56
+ * Auto rides the trusted gate tier; dontAsk rides strict; the REPL activates
57
+ * the matching behavior while the mode is engaged.
58
+ *
59
+ * @param {string} arg
60
+ * @returns {{ tier: "strict"|"trusted"|"autopilot", auto: boolean, dontAsk: boolean } | null}
61
+ */
62
+ export function parsePermissionModeArg(arg) {
63
+ const a = String(arg == null ? "" : arg)
64
+ .trim()
65
+ .toLowerCase();
66
+ if (AUTO_ALIASES.has(a))
67
+ return { tier: "trusted", auto: true, dontAsk: false };
68
+ if (DONT_ASK_ALIASES.has(a)) {
69
+ return { tier: "strict", auto: false, dontAsk: true };
70
+ }
71
+ const tier = parsePermissionTier(a);
72
+ return tier ? { tier, auto: false, dontAsk: false } : null;
73
+ }
74
+
48
75
  /** One-line description of what a tier auto-approves. */
49
76
  export function describeTier(tier) {
50
77
  switch (tier) {
@@ -54,6 +81,10 @@ export function describeTier(tier) {
54
81
  return "low/medium-risk auto-approved; high-risk still asks";
55
82
  case "strict":
56
83
  return "every risky action asks";
84
+ case "auto":
85
+ return "autoMode.decisions classifier (riskLevel → allow/ask/deny); unconfigured = trusted";
86
+ case "dontAsk":
87
+ return "anything that would ask is denied instead (no prompts)";
57
88
  default:
58
89
  return "";
59
90
  }
@@ -64,6 +64,7 @@ import {
64
64
  toOllamaMessages,
65
65
  imageUrlBlockToAnthropic,
66
66
  } from "../lib/image-input.js";
67
+ import { executeToolSearch, gateDeferredMcpCall } from "./mcp-tool-search.js";
67
68
 
68
69
  /**
69
70
  * Names of MCP servers currently mounted by an in-flight run_skill call.
@@ -1439,6 +1440,7 @@ export async function executeTool(name, args, context = {}) {
1439
1440
  additionalDirectories: context.additionalDirectories || null,
1440
1441
  sandbox: context.sandbox || null,
1441
1442
  ruleAllowed,
1443
+ settingsVerdict,
1442
1444
  subAgentDepth: context.subAgentDepth || 0,
1443
1445
  subAgentBudget: context.subAgentBudget || null,
1444
1446
  interactiveApproval: context.interactiveApproval || false,
@@ -2057,6 +2059,7 @@ async function executeToolInner(
2057
2059
  additionalDirectories,
2058
2060
  sandbox,
2059
2061
  ruleAllowed = false,
2062
+ settingsVerdict = null,
2060
2063
  subAgentDepth = 0,
2061
2064
  subAgentBudget = null,
2062
2065
  interactiveApproval = false,
@@ -2338,6 +2341,40 @@ async function executeToolInner(
2338
2341
  : {}),
2339
2342
  ...(classifyAllShell ? { classifyAllShell: true } : {}),
2340
2343
  };
2344
+ // Layer-by-layer explanation chain for a blocked command: which layers
2345
+ // were consulted and what each said (settings rules → shell policy →
2346
+ // approval gate). Attached to denial results so `/permissions denials`
2347
+ // and `cc permissions recent` can explain WHY, not just THAT.
2348
+ const buildPermissionChain = (gated) => {
2349
+ const chain = [
2350
+ {
2351
+ layer: "settings-rules",
2352
+ outcome:
2353
+ settingsVerdict?.decision || (ruleAllowed ? "allow" : "no-match"),
2354
+ rule: settingsVerdict?.rule || null,
2355
+ },
2356
+ {
2357
+ layer: "shell-policy",
2358
+ outcome: shellPolicy?.decision || null,
2359
+ rule: shellPolicy?.ruleId || null,
2360
+ reason: shellPolicy?.reason || null,
2361
+ },
2362
+ ];
2363
+ // A hard shell-policy deny returns before the gate is consulted
2364
+ // (via === "shell-policy") — no approval-gate layer to explain then.
2365
+ if (gated && gated.via !== "shell-policy") {
2366
+ chain.push({
2367
+ layer: "approval-gate",
2368
+ outcome: gated.decision,
2369
+ via: gated.via || null,
2370
+ policy: gated.policy || null,
2371
+ riskLevel: gated.riskLevel || null,
2372
+ rule: gated.gateRule || null,
2373
+ reason: gated.gateReason || null,
2374
+ });
2375
+ }
2376
+ return chain;
2377
+ };
2341
2378
  const override = getRuntimeToolDescriptorByCommand(args.command);
2342
2379
  let shellPolicy;
2343
2380
  let approvalOutcome = null;
@@ -2375,6 +2412,7 @@ async function executeToolInner(
2375
2412
  : `[ApprovalGate] command denied by the ${tierLabel}approval policy (via ${gated.via}). Retrying the same command will not help — it needs user approval. Tell the user (they can run it themselves, approve it, or relax the policy) and continue with other work.`,
2376
2413
  shellCommandPolicy: shellPolicy,
2377
2414
  approval: approvalOutcome,
2415
+ permissionChain: buildPermissionChain(gated),
2378
2416
  },
2379
2417
  override || runtimeDescriptor,
2380
2418
  );
@@ -2386,6 +2424,7 @@ async function executeToolInner(
2386
2424
  {
2387
2425
  error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
2388
2426
  shellCommandPolicy: shellPolicy,
2427
+ permissionChain: buildPermissionChain(null),
2389
2428
  },
2390
2429
  override || runtimeDescriptor,
2391
2430
  );
@@ -3347,6 +3386,15 @@ async function executeToolInner(
3347
3386
  }
3348
3387
  }
3349
3388
 
3389
+ // Internal tool-search tool (MCP context scaling): resolves deferred
3390
+ // MCP tool schemas from the registry attached at setup time. Read-only,
3391
+ // local — same risk class as list_skills, so no approval gate.
3392
+ if (localToolExecutor?.kind === "tool-search") {
3393
+ return attachDescriptor(
3394
+ executeToolSearch(localToolExecutor.registry, args || {}),
3395
+ );
3396
+ }
3397
+
3350
3398
  if (localToolExecutor?.kind === "mcp") {
3351
3399
  if (!mcpClient || typeof mcpClient.callTool !== "function") {
3352
3400
  return attachDescriptor({
@@ -3354,6 +3402,15 @@ async function executeToolInner(
3354
3402
  });
3355
3403
  }
3356
3404
 
3405
+ // Deferred-schema gate: a direct call to a tool whose schema was never
3406
+ // loaded returns a self-healing error embedding the schema (and marks
3407
+ // it loaded), instead of forwarding likely-malformed arguments to the
3408
+ // server. No-op unless tool search deferred this tool.
3409
+ const deferredGate = gateDeferredMcpCall(name, localToolExecutor);
3410
+ if (deferredGate) {
3411
+ return attachDescriptor(deferredGate);
3412
+ }
3413
+
3357
3414
  try {
3358
3415
  const result = await mcpClient.callTool(
3359
3416
  localToolExecutor.serverName,