chainlesschain 0.162.189 → 0.162.193

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 (316) hide show
  1. package/README.md +61 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-DbZft8w0.js → AIOps-DBLkaTje.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DoOZpwfN.js → ActionButton-BAy-Qebo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-B0J0KLTW.js → Analytics-Dfb1Mlau.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-D9eIR0qq.js → AppLayout-BbFkN8m0.js} +5 -5
  12. package/src/assets/web-panel/assets/{Artifacts-BnZXEVeo.js → Artifacts-kM4-cEEH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-X1ce4Bjn.js → Audit-BSQfYWf3.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BvWR-y5v.js → BackgroundAgents-CWDbI_fG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-BublSGEd.js → Backup-BxLPbq_Q.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-CR_77Hso.js → BaseInput-DuIbdrp6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-DixW52tq.js → Chat-NEmjGahU.js} +6 -6
  18. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDHDpBBA.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
  19. package/src/assets/web-panel/assets/{Checkbox-CCv1C87K.js → Checkbox-CaoeG3Ns.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-B03Cc3dd.js → Codegen-BFf_g3xY.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-CA06-6pU.js → Col-CNOd8mdQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-Bj881J52.js → Community-BhpDqz-i.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-UZLTPoaL.js → Compact-CQ42T7Z6.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-kGwzRb_0.js → Compliance-DI5PcLMN.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-KundoYM8.js → Cowork-wYYZe0wR.js} +3 -3
  26. package/src/assets/web-panel/assets/{Cron-BYZyoqDe.js → Cron-Dlq03DD_.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-BhrrppBX.js → Crosschain-BzqS8PP3.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-CdC6EguZ.js → DID-It55Yem7.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CcQx3J4y.js → Dashboard-9HkH6oxn.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-BpijtEzQ.js → Dropdown-00e2YWRB.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-DfZjeZTe.js → EmailListRenderer-By-ugRDL.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BFYSf69C.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-Okdi3pJm.js → Federation-BME5sbZY.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-mH-HebKW.js → FormItemContext-B4TEZCr-.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-xivV69x7.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-1rh_YBwg.js → Git-BrS6WVSr.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-eAXkNxdY.js → Governance-BMdCWnKF.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CS6dMVTN.js → Inference-Bcp3vzcA.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-Bq_TnkvD.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-KchHtiHt.js → Logs-D-W_6x09.js} +2 -2
  41. package/src/assets/web-panel/assets/{MarkdownRenderer-TabqHW9B.js → MarkdownRenderer-DElGPxCZ.js} +1 -1
  42. package/src/assets/web-panel/assets/{Marketplace-CsmGcX5X.js → Marketplace-Dzn2guvI.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-BaPFnnyz.js → McpTools-uPict-KL.js} +6 -6
  44. package/src/assets/web-panel/assets/{Memory-2m-sUUf7.js → Memory-BCw43kOb.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-D5nVZBRu.js → MobileBridge-DmV8Wv7F.js} +1 -1
  46. package/src/assets/web-panel/assets/MobileProjects-C-HaCmSM.js +1 -0
  47. package/src/assets/web-panel/assets/{Mtc-3GDvamGa.js → Mtc-C6jxJ34Y.js} +6 -6
  48. package/src/assets/web-panel/assets/{MtcAudit-D2zGuZCH.js → MtcAudit-Za8ExwhQ.js} +5 -5
  49. package/src/assets/web-panel/assets/{Multisig-BBVQ4t6t.js → Multisig-KKDbuEgb.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-BIOCyOGp.js → NLProgramming-Dj97zbCk.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-DVwBGpzc.js → Notes-DBXvMraU.js} +4 -4
  52. package/src/assets/web-panel/assets/{NotificationSettings-BDnHfIfX.js → NotificationSettings-DOxITLYl.js} +1 -1
  53. package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
  54. package/src/assets/web-panel/assets/{Organization-D3SRKaa7.js → Organization-D1i73mjb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-C2B8ahyn.js → Overflow-kW6Q3rvg.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-2iwQzG65.js → P2P-C-v_8p2v.js} +2 -2
  57. package/src/assets/web-panel/assets/{PdhVaultBrowser-CoPiWqMN.js → PdhVaultBrowser-tUbbY64W.js} +5 -5
  58. package/src/assets/web-panel/assets/{Permissions-yV5XNXo5.js → Permissions-DkHxEZ58.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-uCDJhMUF.js → PersonalDataHub-DWuMPGrK.js} +4 -4
  60. package/src/assets/web-panel/assets/{Pipeline-hN7ucMOA.js → Pipeline-30IYXWWm.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-CgU0ptN4.js → Privacy-DZNQ4RrH.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CfYPiT6j.js → ProjectInit-De2MCElf.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-B0A2C4nw.js → ProjectSettings-5uHk9wsC.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-BE5C0ajn.js → Projects-XjDfrFLb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-CstQPf8W.js → Providers-Bgwj6gcE.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-Co3z_ynk.js → QrScannerModal-gKql9umv.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-DMTS6pQ6.js → QuickAsk-BWvmwkh7.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-ETw4odUa.js → Recommend-Uok4hZPd.js} +1 -1
  69. package/src/assets/web-panel/assets/RemoteSession-DLJzTk9a.js +5 -0
  70. package/src/assets/web-panel/assets/{Reputation-BTVqQgKq.js → Reputation-ZVwIPMSO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-CBKKwwtO.js → Row-CN04HxWR.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-BHUQ438I.js → RssFeed-BdSrEG4g.js} +3 -3
  73. package/src/assets/web-panel/assets/{Search-B4NqeUsF.js → Search-5PwVd0IR.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-BRjoYG5_.js → Security-DdBo4zjN.js} +4 -4
  75. package/src/assets/web-panel/assets/{Services-CxQzu3p3.js → Services-D3letU7M.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-BKzwwmWO.js → Skeleton--Jb83f1v.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-DrsFcgth.js → Skills-Bg-CulHX.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-D21tmXCH.js → Sla-7QOa7S-X.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-CjyjLfp8.js → SpeechSettings-DQwppjRw.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-Cu45-tnx.js → SyncSettings-dNPQGL-s.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-oMcILYOj.js → Tasks-Ggs-Z48q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-Bh-Zcr5R.js → Templates-78TYHgRO.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-68ns63aI.js → Tenant-xHwwv9vO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-BLJkFykE.js → Terminal-BJXSJfm4.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-xcrJLq-O.js → TimelineRenderer-fLbjRhJw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-C6PpFJtf.js → Tokens-oMSBSBmB.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-BaKA7tdM.js → Trigger-CfnY_wX_.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-BzhA57Jc.js → Trust-AdyTgYKO.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-4_U3JkIR.js → UkeySign-C5K_81dS.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-162AANxX.js → VideoEditing-vcGVDqki.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-B1viuql_.js → Wallet-hhOc3hlY.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-ZVWazhCy.js → WebAuthn-CVloC11s.js} +4 -4
  93. package/src/assets/web-panel/assets/{WorkflowEditor-tzdyUk-1.js → WorkflowEditor-CYeE32R5.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-DmrHNuwq.js → chat-CNROR5bN.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DAJ_M-PK.js → colors-Cjx-cnwU.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-uIzmYwLs.js → compact-item-DOIfelvD.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-D_ZmuLwb.js → createContext-DfA7UZ8u.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-CDt_shTL.js → hasIn-BMSCzcfM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CaelyMgF.js → index-5-HXgOX5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-D4n1lGTB.js → index-AWg-0M1S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-saf3VNeP.js → index-B4iYzpkL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Bx4d6c1k.js → index-B5Udeb_6.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DdUMcwRB.js → index-B7gf77CJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-clSX4PEc.js → index-BEMwBBHk.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DBFmcaCQ.js → index-BM1y_9hM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-ByzGvpR9.js → index-BUonmKSE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C16fYKFD.js → index-BbQ2eQ8H.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CzXqvMuP.js → index-BemJ2u_E.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D9OE8B3d.js → index-BolaOQxx.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-OzTxFIJP.js → index-BxOk9vkG.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DqDSF2RW.js → index-By61ojUx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CG0lY4h3.js → index-C2K9SNVX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-QsgfSZrV.js → index-C3L5gdiD.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-GcKOHD_c.js → index-C7xn0Nub.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BRa4kJ_S.js → index-CAnvhwg6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CeUpKjvi.js → index-CEKMcOyU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Cvak6Oe2.js → index-CNC5onm4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CbD_WW_6.js → index-CVL27kXC.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BHvlm0In.js → index-CVm1_i3t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-rS3gl_68.js → index-CdYBye8B.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-FyplHuNt.js → index-ClNMrqEY.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DfSSy9uy.js → index-Cs6l7uRu.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-rXdwUeE3.js → index-CshQbilz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DeGBoLQA.js → index-Cyweh0Hy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-C5WcdJH2.js → index-DBpWXFed.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-CcZ9xJI9.js → index-DHtpHZAQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKyd472S.js → index-DLmn0BoK.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BAvSmJXW.js → index-DPAWBrMf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-ifxmu8SY.js → index-DjEsxO5h.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BDF6EQBs.js → index-FBc_CJBd.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Bbxjfstq.js → index-Icdoqmzt.js} +1 -1
  133. package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
  134. package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
  135. package/src/assets/web-panel/assets/{index-BTCXf1t-.js → index-dCd8cSUq.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-DvFhhe6r.js → index-m2vG9gG_.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-DwfvPe-j.js → index-mTpHoEzr.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-Z8P4BexE.js → index-ti_lnTWl.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-pmfaD-aK.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CaQ2CpYF.js → motion-s3iVK41k.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-CkKTqNbI.js → move-B6B-E5Jz.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-Dd4xsgRI.js → mtc-parser-BiYgTidA.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-BFuA5YEs.js → omit-pU1RdUDA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-DDfxOAJc.js → pickAttrs-Cdoq_SeM.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-Be595JCp.js → placementArrow-DIuvAn2K.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-7byljUM3.js → responsiveObserve-8w16YR2D.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-CTWfOTaY.js → slide-VZ01cZe3.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-CNZOuIxM.js → statusUtils-lh8YMN8Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-DwOcBV78.js → styleChecker-CmDyreec.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-B6MNwAO9.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-CzyQjH9r.js → useFs-Thnu-Iaj.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-Dxh3KiVA.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-DVfZiS5N.js → vnode-Cg6lFY_X.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-BYme7fbY.js → zoom-DVfsR1YO.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1107 -175
  159. package/src/command-surface-policy.js +197 -0
  160. package/src/commands/agent.js +76 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  164. package/src/commands/checkpoint.js +947 -22
  165. package/src/commands/compact.js +101 -25
  166. package/src/commands/config.js +506 -20
  167. package/src/commands/doctor.js +4 -1
  168. package/src/commands/hub.js +1 -1
  169. package/src/commands/mcp.js +688 -24
  170. package/src/commands/pack.js +16 -2
  171. package/src/commands/plugin.js +20 -12
  172. package/src/commands/session-mcp-recovery.js +218 -0
  173. package/src/commands/session.js +470 -110
  174. package/src/commands/setup.js +36 -10
  175. package/src/commands/skill.js +181 -110
  176. package/src/commands/status.js +21 -5
  177. package/src/commands/update.js +68 -6
  178. package/src/constants.js +11 -0
  179. package/src/data/changelog.json +14 -1
  180. package/src/gateways/ws/action-protocol.js +10 -0
  181. package/src/gateways/ws/session-protocol.js +555 -27
  182. package/src/gateways/ws/ws-agent-handler.js +541 -14
  183. package/src/gateways/ws/ws-session-gateway.js +129 -22
  184. package/src/gateways/ws/ws-session-state.js +431 -0
  185. package/src/harness/background-task-manager.js +233 -36
  186. package/src/harness/jsonl-session-store.js +2551 -167
  187. package/src/harness/mcp-client.js +802 -61
  188. package/src/harness/prompt-compressor.js +227 -27
  189. package/src/harness/session-index.js +89 -38
  190. package/src/harness/session-list-index.js +254 -0
  191. package/src/harness/structured-handoff.js +410 -0
  192. package/src/lazy-dispatch.js +747 -84
  193. package/src/lib/__tests__/logger.test.js +50 -2
  194. package/src/lib/advisor-runtime.js +1047 -0
  195. package/src/lib/agent-core.js +1 -0
  196. package/src/lib/agent-sandbox.js +120 -26
  197. package/src/lib/agent-session-export.js +20 -0
  198. package/src/lib/agent-team/team-runner.js +230 -8
  199. package/src/lib/artifact-store.js +131 -8
  200. package/src/lib/background-agent-supervisor.js +185 -13
  201. package/src/lib/background-interaction-journal.js +37 -9
  202. package/src/lib/background-launch-profile.js +1058 -0
  203. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  204. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  205. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  206. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  207. package/src/lib/checkpoint-restore-recovery.js +884 -0
  208. package/src/lib/checkpoint-restore-retention.js +126 -0
  209. package/src/lib/checkpoint-restore-saga.js +5079 -0
  210. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  211. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  212. package/src/lib/checkpoint-store.js +1688 -97
  213. package/src/lib/checkpoint-timeline-authority.js +522 -0
  214. package/src/lib/checkpoint-timeline.js +642 -0
  215. package/src/lib/config-keys.js +18 -119
  216. package/src/lib/config-manager.js +446 -27
  217. package/src/lib/config-redaction.js +57 -0
  218. package/src/lib/config-schema.js +853 -0
  219. package/src/lib/cowork-mcp-tools.js +5 -0
  220. package/src/lib/cowork-task-runner.js +458 -63
  221. package/src/lib/delivery-coordinator.js +1234 -0
  222. package/src/lib/delivery-evidence.js +450 -0
  223. package/src/lib/doctor-checkup.js +199 -14
  224. package/src/lib/executable-path.js +34 -0
  225. package/src/lib/fatal-handler.js +16 -0
  226. package/src/lib/file-checkpoint.js +4909 -186
  227. package/src/lib/file-lines.js +125 -0
  228. package/src/lib/headless-config-command.js +16 -5
  229. package/src/lib/hook-runner.cjs +139 -38
  230. package/src/lib/hooks-v2-workspace-context.js +10 -0
  231. package/src/lib/impacted-gate-selector.js +424 -0
  232. package/src/lib/interaction-adapter.js +62 -0
  233. package/src/lib/jsonl-session-store.js +1 -35
  234. package/src/lib/logger.js +66 -14
  235. package/src/lib/mcp-call-ledger-store.js +1069 -0
  236. package/src/lib/mcp-call-ledger.js +747 -0
  237. package/src/lib/mcp-client.js +5 -0
  238. package/src/lib/mcp-conflict-scheduler.js +468 -0
  239. package/src/lib/mcp-effect-contract.js +102 -0
  240. package/src/lib/mcp-headers-helper-trust.js +145 -0
  241. package/src/lib/mcp-headers-helper.js +616 -0
  242. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  243. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  244. package/src/lib/mcp-recovery-adjudication.js +443 -0
  245. package/src/lib/output-context.js +86 -0
  246. package/src/lib/packer/file-identity.js +170 -0
  247. package/src/lib/packer/index.js +3 -0
  248. package/src/lib/packer/native-update-state.js +277 -0
  249. package/src/lib/packer/pack-update-applier.js +1983 -136
  250. package/src/lib/packer/pack-update-checker.js +34 -0
  251. package/src/lib/packer/pack-update-downloader.js +875 -86
  252. package/src/lib/packer/pack-update-signature.js +86 -0
  253. package/src/lib/packer/pkg-config-generator.js +6 -0
  254. package/src/lib/paths.js +65 -10
  255. package/src/lib/plan-mode.js +831 -14
  256. package/src/lib/plan-persistence.js +540 -0
  257. package/src/lib/plugin-runtime/hooks.js +227 -69
  258. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  259. package/src/lib/pr-automation-policy.js +146 -8
  260. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  261. package/src/lib/process-execution-broker/index.js +53 -8
  262. package/src/lib/process-execution-broker/workspace-transaction.js +976 -73
  263. package/src/lib/project-instructions.js +165 -29
  264. package/src/lib/project-mcp-trust.js +67 -0
  265. package/src/lib/public-ide-capability-manifest.js +272 -0
  266. package/src/lib/public-ide-capability-validation.js +351 -0
  267. package/src/lib/secret-store.js +267 -43
  268. package/src/lib/secure-fs.js +1089 -0
  269. package/src/lib/security-audit.js +58 -0
  270. package/src/lib/service-manager.js +34 -13
  271. package/src/lib/session-budget-runtime.js +1452 -0
  272. package/src/lib/session-host-snapshot.js +388 -0
  273. package/src/lib/session-message-provenance.js +429 -0
  274. package/src/lib/session-projection.js +804 -0
  275. package/src/lib/session-resource-budget.js +1495 -0
  276. package/src/lib/settings-hooks.cjs +138 -22
  277. package/src/lib/settings-loader.cjs +25 -2
  278. package/src/lib/side-effect-ledger-store.js +45 -23
  279. package/src/lib/skill-execution-identity.js +217 -0
  280. package/src/lib/skill-loader.js +299 -18
  281. package/src/lib/sub-agent-context.js +236 -28
  282. package/src/lib/sub-agent-registry.js +5 -0
  283. package/src/lib/subagent-contract.js +94 -0
  284. package/src/lib/todo-manager.js +185 -18
  285. package/src/lib/todo-persistence.js +417 -0
  286. package/src/lib/turn-binding-store.js +41 -23
  287. package/src/lib/with-file-lock.js +25 -1
  288. package/src/lib/ws-chat-handler.js +21 -0
  289. package/src/program-base.js +18 -0
  290. package/src/repl/advisor-command.js +103 -0
  291. package/src/repl/agent-repl.js +2156 -145
  292. package/src/repl/btw-command.js +93 -10
  293. package/src/repl/clipboard-image.js +119 -0
  294. package/src/repl/config-summary.js +64 -108
  295. package/src/repl/prompt-editor.js +233 -0
  296. package/src/repl/prompt-interactions.js +344 -0
  297. package/src/repl/prompt-stash.js +354 -0
  298. package/src/repl/prompt-suggestions.js +312 -0
  299. package/src/repl/repl-keybindings.js +227 -0
  300. package/src/repl/session-recap.js +217 -0
  301. package/src/repl/slash-command-registry.js +22 -0
  302. package/src/repl/terminal-layout.js +88 -0
  303. package/src/runtime/agent-core.js +1264 -325
  304. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  305. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  306. package/src/runtime/diagnostics.js +32 -13
  307. package/src/runtime/headless-runner.js +298 -118
  308. package/src/runtime/headless-stream.js +353 -95
  309. package/src/runtime/mcp-config.js +148 -14
  310. package/src/runtime/status-diagnostics-lite.js +137 -0
  311. package/src/assets/web-panel/assets/MobileProjects-Bo8MLZvT.js +0 -1
  312. package/src/assets/web-panel/assets/OrderTableRenderer-C7Ek20ag.js +0 -1
  313. package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +0 -5
  314. package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +0 -1
  315. package/src/assets/web-panel/assets/index-D-Swwqld.js +0 -1
  316. package/src/assets/web-panel/assets/index-FMZAMKy8.js +0 -1
@@ -18,6 +18,7 @@ import fs from "fs";
18
18
  import path from "path";
19
19
  import broker from "../lib/process-execution-broker/index.js";
20
20
  import os from "os";
21
+ import { isProxy } from "node:util/types";
21
22
  import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
22
23
  import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
23
24
  import sharedPermissionRules from "../lib/permission-rules.cjs";
@@ -59,7 +60,6 @@ import {
59
60
  } from "../lib/idempotency.js";
60
61
  import { buildSearchCommand } from "../lib/search-command.js";
61
62
  import { discoverCommands } from "../lib/slash-commands.js";
62
- import { createSkillProcessBroker } from "../lib/skill-process-broker.js";
63
63
  import {
64
64
  isRetryableStreamError,
65
65
  STREAM_RETRY_BASE_MS,
@@ -70,10 +70,6 @@ import {
70
70
  replaceByHash,
71
71
  snippetAround,
72
72
  } from "../lib/hashline.js";
73
- import {
74
- mountSkillMcpServers,
75
- unmountSkillMcpServers,
76
- } from "../lib/skill-mcp.js";
77
73
  import {
78
74
  hasImageContent,
79
75
  toOllamaMessages,
@@ -99,6 +95,24 @@ import {
99
95
  beginManagedToolCheckpoint,
100
96
  settleManagedToolCheckpoint,
101
97
  } from "../lib/managed-tool-checkpoint.js";
98
+ import {
99
+ createMcpCallLedger,
100
+ McpEffect,
101
+ snapshotMcpJsonRpcInput,
102
+ } from "../lib/mcp-call-ledger.js";
103
+ import {
104
+ MCP_OUTCOME_UNKNOWN_CODE,
105
+ markMcpLedgerOutcomeUnknown,
106
+ } from "../lib/mcp-ledger-recovery-admission.js";
107
+ import {
108
+ createHostOwnedMcpEffectContract,
109
+ createMcpConflictScheduler,
110
+ } from "../lib/mcp-conflict-scheduler.js";
111
+ import {
112
+ buildExtractiveHandoff,
113
+ formatStructuredHandoff,
114
+ } from "../harness/structured-handoff.js";
115
+ import { projectCanonicalResumeMessages } from "../lib/session-message-provenance.js";
102
116
 
103
117
  export { formatProviderHttpError };
104
118
 
@@ -114,13 +128,31 @@ export function getActiveMcpServers() {
114
128
  return new Set(_activeMcpServers);
115
129
  }
116
130
 
131
+ // Direct executeTool callers do not pass agentLoop's run-scoped scheduler.
132
+ // Share a bounded scheduler per MCP client so two concurrent calls cannot each
133
+ // create a private lock and accidentally bypass unknown/write serialization.
134
+ const _directMcpSchedulers = new WeakMap();
135
+ function directMcpConflictScheduler(mcpClient) {
136
+ if (
137
+ (typeof mcpClient !== "object" || mcpClient === null) &&
138
+ typeof mcpClient !== "function"
139
+ ) {
140
+ return createMcpConflictScheduler({ maxActive: 1 });
141
+ }
142
+ let scheduler = _directMcpSchedulers.get(mcpClient);
143
+ if (!scheduler) {
144
+ scheduler = createMcpConflictScheduler();
145
+ _directMcpSchedulers.set(mcpClient, scheduler);
146
+ }
147
+ return scheduler;
148
+ }
149
+
117
150
  const { isDangerousGitCommand, isReadOnlyGitCommand, normalizeGitCommand } =
118
151
  sharedCodingAgentPolicy;
119
152
  const { evaluateShellCommandPolicy } = sharedShellPolicy;
120
153
  const { evaluatePermissionRules } = sharedPermissionRules;
121
154
  const { collectHooks, umbrellaFor } = sharedSettingsHooks;
122
- const { runHooks: runCommandHooks, runHooksParallel: runCommandHooksParallel } =
123
- sharedHookRunner;
155
+ const { runHooks: runCommandHooks } = sharedHookRunner;
124
156
  const {
125
157
  runObserveHooks,
126
158
  aggregateContext,
@@ -635,19 +667,6 @@ function _resolveShellTimeout(raw) {
635
667
  return Math.min(Math.floor(n), MAX_SHELL_TIMEOUT_MS);
636
668
  }
637
669
 
638
- /**
639
- * Opt-in: run ALL matching decision hooks and take the STRICTEST outcome
640
- * (block > ask > allow > continue) instead of the default in-order first-wins
641
- * short-circuit — so an earlier hook's `ask` can no longer mask a later hook's
642
- * `block`. Default off = byte-identical short-circuit behavior. Flipping the
643
- * default is a product decision (all matching hooks then run every time, which
644
- * changes hook-execution volume + side effects).
645
- */
646
- function _hookStrictMergeEnabled() {
647
- const v = process.env.CC_HOOK_STRICT_MERGE;
648
- return v === "1" || v === "true";
649
- }
650
-
651
670
  /**
652
671
  * Run settings.json `PreToolUse` hooks (decision-capable). DB hooks are handled
653
672
  * separately + stay observe-only. A `block` decision stops the tool; an `ask`
@@ -677,18 +696,18 @@ async function runSettingsPreToolUseHooks(name, args, context, cwd) {
677
696
  parentId: context.hookParentId || null,
678
697
  },
679
698
  );
680
- // Strict merge (opt-in) runs ALL matching hooks and takes the strictest
681
- // decision. This path is async so the hooks run in TRUE PARALLEL
682
- // (runHooksParallel) — wall-clock is the slowest hook, not their sum — while
683
- // still yielding the same strictest outcome. Default path is the sync in-order
684
- // short-circuit runner (byte-identical).
685
- const outcome = _hookStrictMergeEnabled()
686
- ? await runCommandHooksParallel(matched, payload, {
687
- cwd,
688
- event: "PreToolUse",
689
- broker,
690
- })
691
- : runCommandHooks(matched, payload, { cwd, event: "PreToolUse", broker });
699
+ // Run every matching authority-bearing hook in configured order, then merge
700
+ // the outcomes with block > ask > allow > continue. Sequential execution
701
+ // preserves the documented hook side-effect order while preventing an early
702
+ // ask/allow from masking a later block. Environment variables cannot weaken
703
+ // this permission boundary.
704
+ const outcome = runCommandHooks(matched, payload, {
705
+ cwd,
706
+ event: "PreToolUse",
707
+ mergeStrict: true,
708
+ failClosed: true,
709
+ broker,
710
+ });
692
711
  if (outcome.decision === "block") {
693
712
  return { blocked: true, reason: outcome.reason, hook: outcome.hook };
694
713
  }
@@ -742,6 +761,10 @@ async function runSettingsPreToolUseHooks(name, args, context, cwd) {
742
761
  * @returns {{decision:("allow"|"deny"|null), reason?:string, hook?:string}}
743
762
  */
744
763
  function runSettingsPermissionRequestHooks(name, args, context, cwd, reason) {
764
+ // Command hooks are arbitrary processes. Until a host-enforced read-only
765
+ // hook sandbox exists, no command hook may execute inside the planning-only
766
+ // capability fence (even when the underlying tool is read-only).
767
+ if (context.planReadOnlyFenceActive === true) return { decision: null };
745
768
  const matched = collectHooks(
746
769
  context.settingsHooks,
747
770
  "PermissionRequest",
@@ -768,7 +791,8 @@ function runSettingsPermissionRequestHooks(name, args, context, cwd, reason) {
768
791
  const outcome = runCommandHooks(matched, payload, {
769
792
  cwd,
770
793
  event: "PermissionRequest",
771
- mergeStrict: _hookStrictMergeEnabled(),
794
+ mergeStrict: true,
795
+ failClosed: true,
772
796
  broker,
773
797
  });
774
798
  // Precedence: deny > ask > allow > defer. The runner normalizes deny/block →
@@ -895,17 +919,21 @@ export function getAgentToolDefinitions({
895
919
  names = null,
896
920
  disabledTools = [],
897
921
  extraTools = [],
922
+ exactToolNames = false,
898
923
  } = {}) {
899
- const allowedNames =
900
- Array.isArray(names) && names.length > 0 ? new Set(names) : null;
901
- const disabledNames = new Set(
902
- Array.isArray(disabledTools) ? disabledTools : [],
903
- );
924
+ // `names` historically selects a built-in tool preset while independently
925
+ // contributed host/MCP tools remain visible. Preserve that top-level behavior
926
+ // for a non-empty preset, but let authority-bearing child contexts opt into an
927
+ // exact all-tool ceiling. An explicit empty array is always deny-all.
928
+ const allowedNames = Array.isArray(names) ? new Set(names) : null;
904
929
  const extraToolNames = new Set(
905
930
  (Array.isArray(extraTools) ? extraTools : [])
906
931
  .map((tool) => tool?.function?.name)
907
932
  .filter(Boolean),
908
933
  );
934
+ const disabledNames = new Set(
935
+ Array.isArray(disabledTools) ? disabledTools : [],
936
+ );
909
937
  const allTools = mergeToolDefinitions(
910
938
  AGENT_TOOLS,
911
939
  Array.isArray(extraTools) ? extraTools : [],
@@ -914,7 +942,11 @@ export function getAgentToolDefinitions({
914
942
  return allTools.filter((tool) => {
915
943
  const name = tool?.function?.name;
916
944
  if (!name) return false;
917
- if (allowedNames && !allowedNames.has(name) && !extraToolNames.has(name)) {
945
+ const unlistedExtraAllowed =
946
+ allowedNames?.size > 0 &&
947
+ exactToolNames !== true &&
948
+ extraToolNames.has(name);
949
+ if (allowedNames && !allowedNames.has(name) && !unlistedExtraAllowed) {
918
950
  return false;
919
951
  }
920
952
  if (disabledNames.has(name)) return false;
@@ -1598,7 +1630,8 @@ async function tryIdeDiffApprovalForEdit(
1598
1630
  ) {
1599
1631
  if (!IDE_DIFF_EDIT_TOOLS.has(name)) return null;
1600
1632
  if (typeof context.permissionConfirm !== "function") return null; // interactive only
1601
- if (!context.mcpClient || !context.externalToolExecutors) return null;
1633
+ const hostMcpClient = context.mcpHostClient || context.mcpClient;
1634
+ if (!hostMcpClient || !context.externalToolExecutors) return null;
1602
1635
  try {
1603
1636
  const {
1604
1637
  ideDiffApprovalEnabled,
@@ -1608,7 +1641,7 @@ async function tryIdeDiffApprovalForEdit(
1608
1641
  summarizeUserAmendments,
1609
1642
  } = await import("../lib/ide-context.js");
1610
1643
  const mcpLike = {
1611
- mcpClient: context.mcpClient,
1644
+ mcpClient: hostMcpClient,
1612
1645
  externalToolExecutors: context.externalToolExecutors,
1613
1646
  };
1614
1647
  if (!ideDiffApprovalEnabled() || !hasIdeOpenDiff(mcpLike)) return null;
@@ -1722,6 +1755,19 @@ export async function executeTool(name, args, context = {}) {
1722
1755
  const skillLoader = context.skillLoader || _defaultSkillLoader;
1723
1756
  const cwd = context.cwd || process.cwd();
1724
1757
  const planManager = context.planManager || getPlanModeManager();
1758
+ // The provider receives a filtered tool schema, but an untrusted/buggy model
1759
+ // can still emit an arbitrary tool_call. Enforce the exact same immutable
1760
+ // capability set again at execution time; null/absent preserves direct-call
1761
+ // compatibility, while [] is an explicit deny-all ceiling.
1762
+ if (
1763
+ Array.isArray(context.effectiveAllowedToolNames) &&
1764
+ !context.effectiveAllowedToolNames.includes(name)
1765
+ ) {
1766
+ return {
1767
+ error: `[Tool Capability] Tool "${name}" is outside this run's effective tool set.`,
1768
+ policy: { decision: "blocked", via: "effective-tool-set" },
1769
+ };
1770
+ }
1725
1771
  const localToolDescriptor =
1726
1772
  context.externalToolDescriptors &&
1727
1773
  typeof context.externalToolDescriptors === "object"
@@ -1789,11 +1835,60 @@ export async function executeTool(name, args, context = {}) {
1789
1835
  const workspacePathDenial = guardAgentFileToolPaths(name, args, context, cwd);
1790
1836
  if (workspacePathDenial) return workspacePathDenial;
1791
1837
 
1838
+ // A malformed settings source may have hidden a deny/ask hook. Runtime
1839
+ // loaders attach these parse failures as host-owned metadata; executing with
1840
+ // a partial authority set would be strictly wider than the configured path.
1841
+ const hookAuthorityErrors = context.settingsHooks?._authorityErrors;
1842
+ if (Array.isArray(hookAuthorityErrors) && hookAuthorityErrors.length > 0) {
1843
+ return {
1844
+ error:
1845
+ "[Hook Authority] Tool execution blocked because one or more hook policy sources could not be parsed.",
1846
+ policy: { decision: "blocked", via: "hook-authority-load" },
1847
+ incidents: hookAuthorityErrors.map((entry) => ({
1848
+ sourceFile: entry?.sourceFile || entry?.file || null,
1849
+ code: entry?.code || "CC_HOOK_AUTHORITY_INVALID",
1850
+ })),
1851
+ };
1852
+ }
1853
+
1792
1854
  const toolContext = createToolContext({
1793
1855
  toolName: runtimeDescriptor?.name || name,
1794
1856
  cwd,
1795
1857
  metadata: { descriptor: runtimeDescriptor },
1796
1858
  });
1859
+ const observeHookIncidents = [];
1860
+ const recordObserveHookIncident = (event, source, error) => {
1861
+ const incident = Object.freeze({
1862
+ code: "CC_HOOK_OBSERVER_DEGRADED",
1863
+ event,
1864
+ source,
1865
+ degraded: true,
1866
+ reason: String(error?.message || error || "hook observer failed")
1867
+ .replace(/\s+/g, " ")
1868
+ .slice(0, 240),
1869
+ });
1870
+ observeHookIncidents.push(incident);
1871
+ try {
1872
+ emitHooksV2Event("HookFailure", {
1873
+ schema_version: 1,
1874
+ session_id: context.sessionId || null,
1875
+ turn_id: context.turnId || null,
1876
+ tool_use_id: context.toolCallId || null,
1877
+ tool_name: name,
1878
+ hook_event_name: event,
1879
+ hook_source: source,
1880
+ incident_code: incident.code,
1881
+ });
1882
+ } catch {
1883
+ // Incident delivery must never turn an observe-only failure into a new
1884
+ // authority decision or hide the original tool outcome.
1885
+ }
1886
+ try {
1887
+ context.onHookIncident?.(incident);
1888
+ } catch {
1889
+ // Optional observer only.
1890
+ }
1891
+ };
1797
1892
 
1798
1893
  // Persona toolsDisabled guard
1799
1894
  const persona = _loadProjectPersona(cwd);
@@ -1815,10 +1910,10 @@ export async function executeTool(name, args, context = {}) {
1815
1910
  // 3. settings `ask` → confirm (headless w/o confirmer falls closed).
1816
1911
  // Reached only after BOTH denies clear, so a denied
1817
1912
  // tool never wastes a confirmation round-trip.
1818
- // 4. settings `allow` → pre-authorize (ruleAllowed): short-circuit the
1819
- // plan-mode block + run_shell ApprovalGate. The hard
1820
- // shell-policy denylist still applies — allow never
1821
- // re-enables an unsafe `rm -rf /`.
1913
+ // 4. settings `allow` → pre-authorize interactive gates OUTSIDE the
1914
+ // plan-mode hard ceiling. It can never add a tool to
1915
+ // the planning/approved capability set. The hard
1916
+ // shell-policy denylist also still applies.
1822
1917
  // No matching rule + no host policy → every existing layer runs unchanged
1823
1918
  // (default behaviour is byte-for-byte).
1824
1919
  const settingsVerdict = context.permissionRules
@@ -1849,18 +1944,10 @@ export async function executeTool(name, args, context = {}) {
1849
1944
  toolPolicies && typeof toolPolicies === "object"
1850
1945
  ? toolPolicies[name]
1851
1946
  : null;
1852
- const isExternalHostTool =
1853
- hostToolPolicy && !STATIC_AGENT_TOOL_NAMES.has(name);
1854
- const isExternalLocalTool =
1855
- localToolDescriptor && !STATIC_AGENT_TOOL_NAMES.has(name);
1856
1947
  const hostPolicyAllowsReadOnlyGit =
1857
1948
  name === "git" &&
1858
1949
  hostToolPolicy?.planModeBehavior === "readonly-conditional" &&
1859
1950
  isReadOnlyGitCommand(args.command);
1860
- const localReadOnlyAllowedInPlanMode =
1861
- isExternalLocalTool &&
1862
- planManager.isActive() &&
1863
- localToolDescriptor?.isReadOnly === true;
1864
1951
 
1865
1952
  // 2. host deny (a settings `allow` does not relax this)
1866
1953
  if (
@@ -1879,6 +1966,51 @@ export async function executeTool(name, args, context = {}) {
1879
1966
  };
1880
1967
  }
1881
1968
 
1969
+ // Plan mode is a capability ceiling, not another prompt/approval source.
1970
+ // Evaluate it before settings `ask`, sensitive-file confirmation, and every
1971
+ // other interactive gate so no `allow` or confirmed `ask` can widen the
1972
+ // planning tool set. Read-only git remains a built-in conditional capability.
1973
+ // External tools remain blocked until a host-owned, per-tool effect authority
1974
+ // exists; server annotations/descriptors cannot self-authorize Plan access.
1975
+ const executionLockActive = planManager.executionLock != null;
1976
+ const planReadOnlyFenceActive =
1977
+ planManager.isActive() && !executionLockActive;
1978
+ const externalToolBlockedDuringPlanning =
1979
+ planReadOnlyFenceActive && !STATIC_AGENT_TOOL_NAMES.has(name);
1980
+ context = { ...context, planReadOnlyFenceActive };
1981
+ if (
1982
+ planManager.isActive() &&
1983
+ !(name === "git" && isReadOnlyGitCommand(args.command)) &&
1984
+ (externalToolBlockedDuringPlanning || !planManager.isToolAllowed(name))
1985
+ ) {
1986
+ if (!executionLockActive) {
1987
+ planManager.addPlanItem({
1988
+ title: `${name}: ${formatToolArgs(name, args)}`,
1989
+ tool: name,
1990
+ params: args,
1991
+ estimatedImpact:
1992
+ name === "run_shell" ||
1993
+ name === "run_code" ||
1994
+ name === "git" ||
1995
+ localToolDescriptor?.riskLevel === "high"
1996
+ ? "high"
1997
+ : GUARDED_FILE_MUTATION_TOOLS.has(name) ||
1998
+ localToolDescriptor?.riskLevel === "medium"
1999
+ ? "medium"
2000
+ : "low",
2001
+ });
2002
+ }
2003
+ return {
2004
+ error: executionLockActive
2005
+ ? `[Plan Execution Lock] Tool "${name}" was not in the approved tool set. Request a plan revision before using it.`
2006
+ : `[Plan Mode] Tool "${name}" is blocked during planning. It has been added to the plan. Use /plan approve to execute.`,
2007
+ policy: {
2008
+ decision: "blocked",
2009
+ via: executionLockActive ? "plan-execution-lock" : "plan-mode",
2010
+ },
2011
+ };
2012
+ }
2013
+
1882
2014
  // 3 + 4. settings ask / allow (only reached when neither layer denied)
1883
2015
  let ruleAllowed = false;
1884
2016
  if (settingsVerdict.decision === "ask") {
@@ -2062,43 +2194,17 @@ export async function executeTool(name, args, context = {}) {
2062
2194
  }
2063
2195
  }
2064
2196
 
2065
- // Plan mode: settings/host allow rules may help exploration, but they cannot
2066
- // widen the immutable tool set issued when the user approved the plan.
2067
- const executionLockActive = planManager.executionLock != null;
2068
- if (
2069
- planManager.isActive() &&
2070
- (!ruleAllowed || executionLockActive) &&
2071
- !(name === "git" && isReadOnlyGitCommand(args.command)) &&
2072
- !planManager.isToolAllowed(name) &&
2073
- !(
2074
- !executionLockActive &&
2075
- isExternalHostTool &&
2076
- hostToolPolicy?.allowed === true
2077
- ) &&
2078
- !(!executionLockActive && localReadOnlyAllowedInPlanMode)
2079
- ) {
2080
- if (!executionLockActive) {
2081
- planManager.addPlanItem({
2082
- title: `${name}: ${formatToolArgs(name, args)}`,
2083
- tool: name,
2084
- params: args,
2085
- estimatedImpact:
2086
- name === "run_shell" ||
2087
- name === "run_code" ||
2088
- name === "git" ||
2089
- localToolDescriptor?.riskLevel === "high"
2090
- ? "high"
2091
- : GUARDED_FILE_MUTATION_TOOLS.has(name) ||
2092
- localToolDescriptor?.riskLevel === "medium"
2093
- ? "medium"
2094
- : "low",
2095
- });
2096
- }
2097
- return {
2098
- error: executionLockActive
2099
- ? `[Plan Execution Lock] Tool "${name}" was not in the approved tool set. Request a plan revision before using it.`
2100
- : `[Plan Mode] Tool "${name}" is blocked during planning. It has been added to the plan. Use /plan approve to execute.`,
2101
- };
2197
+ // Subtree rules are an authority input, so the first file mutation in a
2198
+ // subtree must observe them before any PreToolUse hook or filesystem effect.
2199
+ // Discovery+read commits the complete source/target batch atomically; this
2200
+ // call is intentionally deferred once and succeeds only on an explicit retry.
2201
+ if (GUARDED_FILE_MUTATION_TOOLS.has(name)) {
2202
+ const instructionPreflight = await _preflightMutationSubtreeInstructions(
2203
+ fileMutationPaths(name, args),
2204
+ cwd,
2205
+ context.subtreeInstructionScope || context.sessionId || "__legacy__",
2206
+ );
2207
+ if (instructionPreflight) return instructionPreflight;
2102
2208
  }
2103
2209
 
2104
2210
  // PreToolUse hooks. DB hooks (cc hook add) stay observe-only — a failure
@@ -2106,77 +2212,79 @@ export async function executeTool(name, args, context = {}) {
2106
2212
  // capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
2107
2213
  // `ask` routes to the confirmer. Runs after permission resolution so a
2108
2214
  // settings deny / host deny short-circuits before any hook process spawns.
2109
- const hooksV2Pre = await executeHooksV2Event(
2110
- "PreToolUse",
2111
- {
2112
- schema_version: 1,
2113
- session_id: context.sessionId || null,
2114
- turn_id: context.turnId || null,
2115
- tool_use_id: context.toolCallId || null,
2116
- tool_name: name,
2117
- input_keys:
2118
- args && typeof args === "object" ? Object.keys(args).sort() : [],
2119
- cwd,
2120
- },
2121
- { failClosed: true },
2122
- );
2123
- if (hooksV2Pre.blocked || hooksV2Pre.decision === "block") {
2124
- return {
2125
- error: `[Hook v2] PreToolUse blocked "${name}".`,
2126
- policy: { decision: "block", via: "hooks-v2" },
2127
- };
2128
- }
2129
- if (hooksV2Pre.requiresApproval || hooksV2Pre.decision === "ask") {
2130
- const confirm = context.permissionConfirm || context.shellConfirm || null;
2131
- const approved =
2132
- typeof confirm === "function"
2133
- ? await confirm({
2134
- tool: name,
2135
- args,
2136
- reason: "Hooks v2 PreToolUse requested confirmation",
2137
- source: "hooks-v2",
2138
- })
2139
- : false;
2140
- if (!approved) {
2141
- emitHooksV2Event("PermissionDenied", {
2215
+ if (!planReadOnlyFenceActive) {
2216
+ const hooksV2Pre = await executeHooksV2Event(
2217
+ "PreToolUse",
2218
+ {
2142
2219
  schema_version: 1,
2143
2220
  session_id: context.sessionId || null,
2144
2221
  turn_id: context.turnId || null,
2145
2222
  tool_use_id: context.toolCallId || null,
2146
2223
  tool_name: name,
2147
- source: "hooks-v2",
2148
- });
2224
+ input_keys:
2225
+ args && typeof args === "object" ? Object.keys(args).sort() : [],
2226
+ cwd,
2227
+ },
2228
+ { failClosed: true },
2229
+ );
2230
+ if (hooksV2Pre.blocked || hooksV2Pre.decision === "block") {
2149
2231
  return {
2150
- error: `[Hook v2] PreToolUse confirmation denied for "${name}".`,
2151
- policy: { decision: "deny", via: "hooks-v2" },
2232
+ error: `[Hook v2] PreToolUse blocked "${name}".`,
2233
+ policy: { decision: "block", via: "hooks-v2" },
2152
2234
  };
2153
2235
  }
2154
- }
2155
- if (hookDb) {
2156
- try {
2157
- await executeHooks(hookDb, HookEvents.PreToolUse, {
2158
- tool: name,
2159
- args,
2160
- timestamp: new Date().toISOString(),
2161
- descriptor: runtimeDescriptor,
2162
- context: toolContext,
2163
- });
2164
- } catch (_err) {
2165
- // Hook failure should not block tool execution
2236
+ if (hooksV2Pre.requiresApproval || hooksV2Pre.decision === "ask") {
2237
+ const confirm = context.permissionConfirm || context.shellConfirm || null;
2238
+ const approved =
2239
+ typeof confirm === "function"
2240
+ ? await confirm({
2241
+ tool: name,
2242
+ args,
2243
+ reason: "Hooks v2 PreToolUse requested confirmation",
2244
+ source: "hooks-v2",
2245
+ })
2246
+ : false;
2247
+ if (!approved) {
2248
+ emitHooksV2Event("PermissionDenied", {
2249
+ schema_version: 1,
2250
+ session_id: context.sessionId || null,
2251
+ turn_id: context.turnId || null,
2252
+ tool_use_id: context.toolCallId || null,
2253
+ tool_name: name,
2254
+ source: "hooks-v2",
2255
+ });
2256
+ return {
2257
+ error: `[Hook v2] PreToolUse confirmation denied for "${name}".`,
2258
+ policy: { decision: "deny", via: "hooks-v2" },
2259
+ };
2260
+ }
2166
2261
  }
2167
- }
2168
- if (context.settingsHooks) {
2169
- const pre = await runSettingsPreToolUseHooks(name, args, context, cwd);
2170
- // A hook `ask` resolved by the IDE diff review: accepted → the IDE
2171
- // already wrote the file, return the synthetic result and skip the tool;
2172
- // rejected → the ide-diff deny shape (via:"ide-diff", not via:"hook").
2173
- if (pre.ideApplied) return pre.ideApplied;
2174
- if (pre.blocked) {
2175
- if (pre.ideResult) return pre.ideResult;
2176
- return {
2177
- error: `[Hook] PreToolUse blocked "${name}"${pre.reason ? ": " + pre.reason : ""}`,
2178
- policy: { decision: "block", via: "hook", hook: pre.hook || null },
2179
- };
2262
+ if (hookDb) {
2263
+ try {
2264
+ await executeHooks(hookDb, HookEvents.PreToolUse, {
2265
+ tool: name,
2266
+ args,
2267
+ timestamp: new Date().toISOString(),
2268
+ descriptor: runtimeDescriptor,
2269
+ context: toolContext,
2270
+ });
2271
+ } catch (error) {
2272
+ recordObserveHookIncident("PreToolUse", "database", error);
2273
+ }
2274
+ }
2275
+ if (context.settingsHooks) {
2276
+ const pre = await runSettingsPreToolUseHooks(name, args, context, cwd);
2277
+ // A hook `ask` resolved by the IDE diff review: accepted → the IDE
2278
+ // already wrote the file, return the synthetic result and skip the tool;
2279
+ // rejected → the ide-diff deny shape (via:"ide-diff", not via:"hook").
2280
+ if (pre.ideApplied) return pre.ideApplied;
2281
+ if (pre.blocked) {
2282
+ if (pre.ideResult) return pre.ideResult;
2283
+ return {
2284
+ error: `[Hook] PreToolUse blocked "${name}"${pre.reason ? ": " + pre.reason : ""}`,
2285
+ policy: { decision: "block", via: "hook", hook: pre.hook || null },
2286
+ };
2287
+ }
2180
2288
  }
2181
2289
  }
2182
2290
 
@@ -2201,10 +2309,18 @@ export async function executeTool(name, args, context = {}) {
2201
2309
  // 用量归因: shared per-run sink for child-loop (sub-agent / isolated
2202
2310
  // skill) token usage, drained by agentLoop as attributed events.
2203
2311
  subAgentUsageSink: context.subAgentUsageSink || null,
2312
+ planManager,
2313
+ permissionRules: context.permissionRules || null,
2314
+ effectiveAllowedToolNames: context.effectiveAllowedToolNames ?? null,
2204
2315
  hostManagedToolPolicy: context.hostManagedToolPolicy || null,
2205
2316
  externalToolDescriptors: context.externalToolDescriptors || null,
2206
2317
  externalToolExecutors: context.externalToolExecutors || null,
2207
2318
  mcpClient: context.mcpClient || null,
2319
+ mcpHostClient: context.mcpHostClient || context.mcpClient || null,
2320
+ mcpCallLedger: context.mcpCallLedger || null,
2321
+ mcpConflictScheduler: context.mcpConflictScheduler || null,
2322
+ subtreeInstructionScope:
2323
+ context.subtreeInstructionScope || context.sessionId || "__legacy__",
2208
2324
  shellPolicyOverrides: context.shellPolicyOverrides || null,
2209
2325
  approvalGate: context.approvalGate || null,
2210
2326
  shellConfirm: context.shellConfirm || null,
@@ -2214,6 +2330,7 @@ export async function executeTool(name, args, context = {}) {
2214
2330
  settingsVerdict,
2215
2331
  subAgentDepth: context.subAgentDepth || 0,
2216
2332
  subAgentBudget: context.subAgentBudget || null,
2333
+ sessionBudget: context.sessionBudget || null,
2217
2334
  // Effective contract of THIS loop (parent ceiling for a nested spawn) +
2218
2335
  // the MCP tool definitions this loop exposes (inheritable by a spawn).
2219
2336
  subAgentContract: context.subAgentContract || null,
@@ -2230,15 +2347,15 @@ export async function executeTool(name, args, context = {}) {
2230
2347
  managedCheckpoint: context.managedCheckpoint === true,
2231
2348
  });
2232
2349
  } catch (err) {
2233
- if (hookDb) {
2350
+ if (hookDb && !planReadOnlyFenceActive) {
2234
2351
  try {
2235
2352
  await executeHooks(hookDb, HookEvents.ToolError, {
2236
2353
  tool: name,
2237
2354
  args,
2238
2355
  error: err.message,
2239
2356
  });
2240
- } catch (_err) {
2241
- // Non-critical
2357
+ } catch (error) {
2358
+ recordObserveHookIncident("ToolError", "database", error);
2242
2359
  }
2243
2360
  }
2244
2361
  throw err;
@@ -2261,7 +2378,7 @@ export async function executeTool(name, args, context = {}) {
2261
2378
  }
2262
2379
 
2263
2380
  // PostToolUse hook
2264
- if (hookDb) {
2381
+ if (hookDb && !planReadOnlyFenceActive) {
2265
2382
  try {
2266
2383
  await executeHooks(hookDb, HookEvents.PostToolUse, {
2267
2384
  tool: name,
@@ -2273,13 +2390,18 @@ export async function executeTool(name, args, context = {}) {
2273
2390
  descriptor: runtimeDescriptor,
2274
2391
  context: toolContext,
2275
2392
  });
2276
- } catch (_err) {
2277
- // Non-critical
2393
+ } catch (error) {
2394
+ recordObserveHookIncident("PostToolUse", "database", error);
2278
2395
  }
2279
2396
  }
2280
2397
  // settings.json PostToolUse hooks: can't un-run the tool, but a `block`
2281
2398
  // reason is attached as `hookFeedback` to be surfaced back to the model.
2282
- if (context.settingsHooks && toolResult && typeof toolResult === "object") {
2399
+ if (
2400
+ !planReadOnlyFenceActive &&
2401
+ context.settingsHooks &&
2402
+ toolResult &&
2403
+ typeof toolResult === "object"
2404
+ ) {
2283
2405
  try {
2284
2406
  const matched = collectHooks(context.settingsHooks, "PostToolUse", name);
2285
2407
  if (matched && matched.length > 0) {
@@ -2314,6 +2436,20 @@ export async function executeTool(name, args, context = {}) {
2314
2436
  event: "PostToolUse",
2315
2437
  broker,
2316
2438
  });
2439
+ for (const hookResult of outcome.results || []) {
2440
+ if (
2441
+ hookResult?.nonBlockingError === true ||
2442
+ hookResult?.breakerOpen === true ||
2443
+ hookResult?.malformedDecision === true ||
2444
+ hookResult?.unsupportedDecision === true
2445
+ ) {
2446
+ recordObserveHookIncident(
2447
+ "PostToolUse",
2448
+ "settings-command",
2449
+ hookResult.reason || hookResult.error || "hook command failed",
2450
+ );
2451
+ }
2452
+ }
2317
2453
  if (outcome.decision === "block" && outcome.reason) {
2318
2454
  toolResult.hookFeedback = outcome.reason;
2319
2455
  }
@@ -2324,8 +2460,8 @@ export async function executeTool(name, args, context = {}) {
2324
2460
  context.hookSupervisor.dispatch(asyncHooks, payload, { cwd, broker });
2325
2461
  }
2326
2462
  }
2327
- } catch (_err) {
2328
- // PostToolUse hooks are best-effort
2463
+ } catch (error) {
2464
+ recordObserveHookIncident("PostToolUse", "settings-command", error);
2329
2465
  }
2330
2466
  }
2331
2467
 
@@ -2366,8 +2502,8 @@ export async function executeTool(name, args, context = {}) {
2366
2502
  ? `${toolResult.hookFeedback}\n${outcome.reason}`
2367
2503
  : outcome.reason;
2368
2504
  }
2369
- } catch (_err) {
2370
- // SubagentStop hooks are best-effort
2505
+ } catch (error) {
2506
+ recordObserveHookIncident("SubagentStop", "settings-command", error);
2371
2507
  }
2372
2508
  }
2373
2509
 
@@ -2385,7 +2521,7 @@ export async function executeTool(name, args, context = {}) {
2385
2521
  typeof toolResult === "object" &&
2386
2522
  !toolResult.error &&
2387
2523
  args?.path &&
2388
- context.mcpClient &&
2524
+ (context.mcpHostClient || context.mcpClient) &&
2389
2525
  context.externalToolExecutors
2390
2526
  ) {
2391
2527
  try {
@@ -2393,7 +2529,7 @@ export async function executeTool(name, args, context = {}) {
2393
2529
  await import("../lib/ide-context.js");
2394
2530
  const diags = await collectIdeDiagnostics(
2395
2531
  {
2396
- mcpClient: context.mcpClient,
2532
+ mcpClient: context.mcpHostClient || context.mcpClient,
2397
2533
  externalToolExecutors: context.externalToolExecutors,
2398
2534
  },
2399
2535
  path.resolve(cwd, args.path),
@@ -2405,6 +2541,14 @@ export async function executeTool(name, args, context = {}) {
2405
2541
  }
2406
2542
  }
2407
2543
 
2544
+ if (
2545
+ observeHookIncidents.length > 0 &&
2546
+ toolResult &&
2547
+ typeof toolResult === "object"
2548
+ ) {
2549
+ toolResult.hookIncidents = observeHookIncidents;
2550
+ }
2551
+
2408
2552
  return toolResult;
2409
2553
  }
2410
2554
 
@@ -2898,15 +3042,20 @@ export async function _withPostEditDiagnostics(
2898
3042
  // NOT loaded up front — they cost tokens for subtrees a run may never touch.
2899
3043
  // When a tool first ACCESSES a path inside such a subtree, we inject that
2900
3044
  // subtree's directory instructions into the tool result (the SAME channel as
2901
- // newDiagnostics), exactly once per subtree per process. A stateful
2902
- // SubtreeInstructionLoader per root remembers what it already injected so a
3045
+ // newDiagnostics), exactly once per subtree per agent session. A stateful
3046
+ // SubtreeInstructionLoader per root/session remembers what it already injected so a
2903
3047
  // second access to the same subtree is a no-op. Disable with
2904
3048
  // CC_SUBTREE_INSTRUCTIONS=0. Common case (no cc.md below cwd) → zero cost.
2905
- const _subtreeLoaderPool = new Map(); // root -> SubtreeInstructionLoader
3049
+ const _subtreeLoaderPool = new Map(); // root -> Map<session scope, loader>
2906
3050
 
2907
- async function _getSubtreeLoader(cwd) {
3051
+ async function _getSubtreeLoader(cwd, sessionScope = "__legacy__") {
2908
3052
  const root = path.resolve(cwd || process.cwd());
2909
- let loader = _subtreeLoaderPool.get(root);
3053
+ let scopedLoaders = _subtreeLoaderPool.get(root);
3054
+ if (!scopedLoaders) {
3055
+ scopedLoaders = new Map();
3056
+ _subtreeLoaderPool.set(root, scopedLoaders);
3057
+ }
3058
+ let loader = scopedLoaders.get(sessionScope);
2910
3059
  if (!loader) {
2911
3060
  const { SubtreeInstructionLoader } =
2912
3061
  await import("../lib/project-instructions.js");
@@ -2925,7 +3074,7 @@ async function _getSubtreeLoader(cwd) {
2925
3074
  baseDir: root,
2926
3075
  instructionExcludes,
2927
3076
  });
2928
- _subtreeLoaderPool.set(root, loader);
3077
+ scopedLoaders.set(sessionScope, loader);
2929
3078
  }
2930
3079
  return loader;
2931
3080
  }
@@ -2940,15 +3089,28 @@ export function _resetSubtreeInstructionLoaders() {
2940
3089
  * with their (capped) content read for inline injection — or null when the
2941
3090
  * subtree has no NEW cc.md/CLAUDE.md/AGENTS.md (the common case → zero cost).
2942
3091
  */
2943
- async function _subtreeInstructionsFor(accessedPath, cwd) {
3092
+ async function _prepareSubtreeInstructions(
3093
+ accessedPaths,
3094
+ cwd,
3095
+ sessionScope = "__legacy__",
3096
+ ) {
2944
3097
  if (process.env.CC_SUBTREE_INSTRUCTIONS === "0") return null;
2945
3098
  try {
2946
- const loader = await _getSubtreeLoader(cwd);
2947
- const fresh = loader.onAccess(accessedPath);
3099
+ const loader = await _getSubtreeLoader(cwd, sessionScope);
3100
+ const discovered = new Map();
3101
+ for (const accessedPath of Array.isArray(accessedPaths)
3102
+ ? accessedPaths
3103
+ : [accessedPaths]) {
3104
+ for (const candidate of loader.discover(accessedPath)) {
3105
+ discovered.set(candidate.identity, candidate);
3106
+ }
3107
+ }
3108
+ const fresh = [...discovered.values()];
2948
3109
  if (!fresh || !fresh.length) return null;
2949
3110
  const { DEFAULT_MAX_FILE_BYTES } =
2950
3111
  await import("../lib/project-instructions.js");
2951
3112
  const out = [];
3113
+ const errors = [];
2952
3114
  for (const f of fresh) {
2953
3115
  try {
2954
3116
  const buf = fs.readFileSync(f.path);
@@ -2962,23 +3124,84 @@ async function _subtreeInstructionsFor(accessedPath, cwd) {
2962
3124
  content,
2963
3125
  ...(truncated ? { truncated: true } : {}),
2964
3126
  });
2965
- } catch {
2966
- // discovered file vanished before we could read it — skip it
3127
+ } catch (err) {
3128
+ errors.push({ path: f.path, message: err.message });
2967
3129
  }
2968
3130
  }
2969
- return out.length ? out : null;
2970
- } catch {
2971
- return null; // fail-open: never break a tool because injection errored
3131
+ // Commit only after every candidate in the source/target batch has been
3132
+ // read. Any failure leaves the entire batch discoverable for a retry.
3133
+ if (!errors.length) loader.commit(fresh);
3134
+ return out.length || errors.length ? { instructions: out, errors } : null;
3135
+ } catch (err) {
3136
+ // Paths outside cwd are handled by the normal path/sandbox authority and
3137
+ // have no cwd-subtree rules. Other failures remain visible to mutation
3138
+ // preflight so they cannot silently widen authority.
3139
+ if (err?.code === "ERR_SUBTREE_INSTRUCTION_BOUNDARY") return null;
3140
+ return {
3141
+ instructions: [],
3142
+ errors: [{ path: null, message: err?.message || String(err) }],
3143
+ };
3144
+ }
3145
+ }
3146
+
3147
+ async function _subtreeInstructionsFor(accessedPath, cwd, sessionScope) {
3148
+ const prepared = await _prepareSubtreeInstructions(
3149
+ accessedPath,
3150
+ cwd,
3151
+ sessionScope,
3152
+ );
3153
+ return prepared?.instructions?.length ? prepared.instructions : null;
3154
+ }
3155
+
3156
+ async function _preflightMutationSubtreeInstructions(
3157
+ accessedPaths,
3158
+ cwd,
3159
+ sessionScope,
3160
+ ) {
3161
+ const prepared = await _prepareSubtreeInstructions(
3162
+ accessedPaths,
3163
+ cwd,
3164
+ sessionScope,
3165
+ );
3166
+ if (!prepared) return null;
3167
+ if (prepared.errors?.length) {
3168
+ return {
3169
+ error:
3170
+ "[Subtree Instructions] Mutation blocked because applicable instructions could not be loaded. No mutation was performed.",
3171
+ policy: { decision: "blocked", via: "subtree-instructions" },
3172
+ instructionLoadErrors: prepared.errors,
3173
+ mutationPerformed: false,
3174
+ ...(prepared.instructions?.length
3175
+ ? { subtreeInstructions: prepared.instructions }
3176
+ : {}),
3177
+ };
2972
3178
  }
3179
+ if (!prepared.instructions?.length) return null;
3180
+ return {
3181
+ error:
3182
+ "[Subtree Instructions] Mutation deferred before its first effect. Review these authoritative subtree rules, then retry the tool call.",
3183
+ policy: { decision: "deferred", via: "subtree-instructions" },
3184
+ subtreeInstructions: prepared.instructions,
3185
+ mutationPerformed: false,
3186
+ };
2973
3187
  }
2974
3188
 
2975
3189
  /**
2976
3190
  * Attach freshly-discovered subtree instructions to a SUCCESSFUL tool result
2977
3191
  * (no-op on an error result or when the subtree has nothing new).
2978
3192
  */
2979
- async function _withSubtreeInstructions(result, accessedPath, cwd) {
3193
+ async function _withSubtreeInstructions(
3194
+ result,
3195
+ accessedPath,
3196
+ cwd,
3197
+ sessionScope,
3198
+ ) {
2980
3199
  if (result && result.error) return result;
2981
- const subtreeInstructions = await _subtreeInstructionsFor(accessedPath, cwd);
3200
+ const subtreeInstructions = await _subtreeInstructionsFor(
3201
+ accessedPath,
3202
+ cwd,
3203
+ sessionScope,
3204
+ );
2982
3205
  return subtreeInstructions ? { ...result, subtreeInstructions } : result;
2983
3206
  }
2984
3207
 
@@ -3035,6 +3258,168 @@ async function promoteBrowserScreenshot(
3035
3258
  }
3036
3259
  }
3037
3260
 
3261
+ const MCP_LEDGER_EFFECTS = new Set(["read", "unknown", "write", "destructive"]);
3262
+
3263
+ /**
3264
+ * Reduce descriptor + managed-host metadata to the host-owned contract written
3265
+ * to the MCP ledger. A server's read-only declaration is never promoted to a
3266
+ * read authorization. Server declarations may only make the classification
3267
+ * stricter (write/destructive) when host authority is absent.
3268
+ */
3269
+ function mcpLedgerEffectContract(descriptor, hostPolicy) {
3270
+ const descriptorContract = descriptor?.effectContract || {};
3271
+ const hostContract = hostPolicy?.effectContract || {};
3272
+ const authorizedCandidate =
3273
+ hostContract.authorizedEffect || hostPolicy?.authorizedEffect;
3274
+ const authorizedEffect = MCP_LEDGER_EFFECTS.has(authorizedCandidate)
3275
+ ? authorizedCandidate
3276
+ : null;
3277
+ const declaredEffect = MCP_LEDGER_EFFECTS.has(
3278
+ descriptorContract.declaredEffect,
3279
+ )
3280
+ ? descriptorContract.declaredEffect
3281
+ : "unknown";
3282
+ const effect =
3283
+ authorizedEffect ||
3284
+ (declaredEffect === "destructive" || declaredEffect === "write"
3285
+ ? declaredEffect
3286
+ : "unknown");
3287
+ const annotations = descriptorContract.annotations || {};
3288
+
3289
+ return {
3290
+ effect,
3291
+ destructive:
3292
+ effect === "destructive" || annotations.destructiveHint === true,
3293
+ sideEffecting: effect === "write" || effect === "destructive",
3294
+ idempotent:
3295
+ typeof annotations.idempotentHint === "boolean"
3296
+ ? annotations.idempotentHint
3297
+ : null,
3298
+ openWorld:
3299
+ typeof annotations.openWorldHint === "boolean"
3300
+ ? annotations.openWorldHint
3301
+ : null,
3302
+ trusted:
3303
+ authorizedEffect != null &&
3304
+ (hostContract.trusted === true || hostPolicy?.sourceTrusted === true),
3305
+ source:
3306
+ hostContract.provenance ||
3307
+ descriptorContract.provenance ||
3308
+ descriptor?.source ||
3309
+ "mcp",
3310
+ };
3311
+ }
3312
+
3313
+ function mcpLedgerScopes(args) {
3314
+ const resourceScopes = [];
3315
+ const networkScopes = [];
3316
+ const visit = (value, key = "", depth = 0) => {
3317
+ if (depth > 3 || value == null) return;
3318
+ if (Array.isArray(value)) {
3319
+ value.slice(0, 32).forEach((entry) => visit(entry, key, depth + 1));
3320
+ return;
3321
+ }
3322
+ if (typeof value === "object") {
3323
+ Object.entries(value)
3324
+ .slice(0, 64)
3325
+ .forEach(([childKey, childValue]) =>
3326
+ visit(childValue, childKey, depth + 1),
3327
+ );
3328
+ return;
3329
+ }
3330
+ if (typeof value !== "string") return;
3331
+
3332
+ const normalizedKey = String(key).toLowerCase();
3333
+ const isNetworkScope = /url|uri|endpoint|origin|host/.test(normalizedKey);
3334
+ if (isNetworkScope) {
3335
+ networkScopes.push(value);
3336
+ }
3337
+ // URL/URI values can carry credentials, query secrets and private paths.
3338
+ // The network ledger normalizes them to origin only; never duplicate the
3339
+ // raw value into resourceScopes, whose identifiers are intentionally not
3340
+ // URL parsers. The input digest still binds the exact request at rest.
3341
+ if (
3342
+ !isNetworkScope &&
3343
+ /path|file|resource|repo|project|workspace/.test(normalizedKey)
3344
+ ) {
3345
+ resourceScopes.push(`${normalizedKey}:${value}`);
3346
+ }
3347
+ };
3348
+ visit(args || {});
3349
+ return { resourceScopes, networkScopes };
3350
+ }
3351
+
3352
+ const MCP_TRANSPORT_OUTCOME_UNKNOWN_CODE = "CC_MCP_TRANSPORT_OUTCOME_UNKNOWN";
3353
+ const MCP_PROTOCOL_RESULT_INVALID_CODE = "CC_MCP_PROTOCOL_RESULT_INVALID";
3354
+ const MCP_RESULT_PROJECTION_FAILED_CODE = "CC_MCP_RESULT_PROJECTION_FAILED";
3355
+
3356
+ function safeMcpProperty(value, property) {
3357
+ try {
3358
+ return value?.[property];
3359
+ } catch {
3360
+ return undefined;
3361
+ }
3362
+ }
3363
+
3364
+ function safeMcpErrorMessage(error) {
3365
+ const message = safeMcpProperty(error, "message");
3366
+ if (typeof message === "string" && message) return message;
3367
+ try {
3368
+ return String(error || "unknown error");
3369
+ } catch {
3370
+ return "unknown error";
3371
+ }
3372
+ }
3373
+
3374
+ function safeMcpErrorCode(error, fallback) {
3375
+ const code = safeMcpProperty(error, "code");
3376
+ return typeof code === "string" && code ? code : fallback;
3377
+ }
3378
+
3379
+ function invalidMcpProtocolResult(cause) {
3380
+ const error = new TypeError("MCP result could not be inspected safely", {
3381
+ cause,
3382
+ });
3383
+ error.code = MCP_PROTOCOL_RESULT_INVALID_CODE;
3384
+ return error;
3385
+ }
3386
+
3387
+ function mcpTransportOutcomeIsUnsafe(effectContract) {
3388
+ return !(
3389
+ effectContract?.effect === McpEffect.READ &&
3390
+ effectContract?.trusted === true
3391
+ );
3392
+ }
3393
+
3394
+ function mcpOutcomeUnknownPayload(
3395
+ ledger,
3396
+ ticket,
3397
+ { phase, reasonCode = MCP_OUTCOME_UNKNOWN_CODE } = {},
3398
+ ) {
3399
+ const stableReasonCode =
3400
+ typeof reasonCode === "string" && reasonCode
3401
+ ? reasonCode
3402
+ : MCP_OUTCOME_UNKNOWN_CODE;
3403
+ try {
3404
+ markMcpLedgerOutcomeUnknown(ledger, stableReasonCode);
3405
+ } catch {
3406
+ // The public result must remain deterministic even for a hostile ledger.
3407
+ }
3408
+ return {
3409
+ error:
3410
+ "MCP tool may have completed, but its outcome is unknown; do not retry automatically until durable recovery is adjudicated.",
3411
+ code: MCP_OUTCOME_UNKNOWN_CODE,
3412
+ status: "outcome_unknown",
3413
+ outcomeUnknown: true,
3414
+ retryable: false,
3415
+ mcpLedgerId: safeMcpProperty(ticket, "ledgerId") || null,
3416
+ mcpLedgerIncident: {
3417
+ phase: phase || "settled",
3418
+ code: stableReasonCode,
3419
+ },
3420
+ };
3421
+ }
3422
+
3038
3423
  async function executeToolInner(
3039
3424
  name,
3040
3425
  args,
@@ -3047,11 +3432,17 @@ async function executeToolInner(
3047
3432
  sessionId,
3048
3433
  turnId,
3049
3434
  toolCallId,
3435
+ planManager = null,
3436
+ permissionRules = null,
3437
+ effectiveAllowedToolNames = null,
3050
3438
  hostManagedToolPolicy,
3051
3439
  externalToolDescriptors,
3052
3440
  externalToolExecutors,
3053
3441
  extraToolDefinitions = null,
3054
3442
  mcpClient,
3443
+ mcpCallLedger = null,
3444
+ mcpConflictScheduler = null,
3445
+ subtreeInstructionScope = "__legacy__",
3055
3446
  memoryDb = null,
3056
3447
  permanentMemory = null,
3057
3448
  subAgentContract = null,
@@ -3066,6 +3457,7 @@ async function executeToolInner(
3066
3457
  settingsVerdict = null,
3067
3458
  subAgentDepth = 0,
3068
3459
  subAgentBudget = null,
3460
+ sessionBudget = null,
3069
3461
  interactiveApproval = false,
3070
3462
  settingsHooks = null,
3071
3463
  signal = null,
@@ -3195,6 +3587,7 @@ async function executeToolInner(
3195
3587
  },
3196
3588
  filePath,
3197
3589
  cwd,
3590
+ subtreeInstructionScope,
3198
3591
  ),
3199
3592
  );
3200
3593
  }
@@ -3207,6 +3600,7 @@ async function executeToolInner(
3207
3600
  },
3208
3601
  filePath,
3209
3602
  cwd,
3603
+ subtreeInstructionScope,
3210
3604
  ),
3211
3605
  );
3212
3606
  }
@@ -3237,6 +3631,7 @@ async function executeToolInner(
3237
3631
  ),
3238
3632
  filePath,
3239
3633
  cwd,
3634
+ subtreeInstructionScope,
3240
3635
  ),
3241
3636
  );
3242
3637
  }
@@ -3318,6 +3713,7 @@ async function executeToolInner(
3318
3713
  ),
3319
3714
  targetPath,
3320
3715
  cwd,
3716
+ subtreeInstructionScope,
3321
3717
  ),
3322
3718
  );
3323
3719
  }
@@ -3418,6 +3814,7 @@ async function executeToolInner(
3418
3814
  ),
3419
3815
  filePath,
3420
3816
  cwd,
3817
+ subtreeInstructionScope,
3421
3818
  ),
3422
3819
  );
3423
3820
  }
@@ -3483,6 +3880,7 @@ async function executeToolInner(
3483
3880
  ),
3484
3881
  filePath,
3485
3882
  cwd,
3883
+ subtreeInstructionScope,
3486
3884
  ),
3487
3885
  );
3488
3886
  }
@@ -4444,13 +4842,28 @@ async function executeToolInner(
4444
4842
  llmOptions,
4445
4843
  subAgentDepth,
4446
4844
  subAgentBudget,
4845
+ sessionBudget,
4447
4846
  subAgentContract,
4448
4847
  settingsHooks,
4848
+ // Immutable parent execution authority. The child may only tighten
4849
+ // these boundaries; it never reconstructs a fresh default policy.
4850
+ planManager,
4851
+ permissionRules,
4852
+ effectiveAllowedToolNames,
4853
+ hostManagedToolPolicy,
4854
+ sandbox,
4855
+ additionalDirectories,
4856
+ approvalGate,
4857
+ shellPolicyOverrides,
4858
+ classifyAllShell,
4859
+ unattendedActionPolicy,
4449
4860
  // Parent trace for the child's hook envelopes (parent_id).
4450
4861
  hookTraceId,
4451
4862
  // Parent MCP plumbing — a spawn can inherit these into the child,
4452
4863
  // filtered by the resolved contract's mcpServers allow-list.
4453
4864
  mcpClient,
4865
+ mcpCallLedger,
4866
+ mcpConflictScheduler,
4454
4867
  externalToolDescriptors,
4455
4868
  externalToolExecutors,
4456
4869
  extraToolDefinitions,
@@ -4522,14 +4935,31 @@ async function executeToolInner(
4522
4935
  case "todo_write": {
4523
4936
  try {
4524
4937
  const { writeTodos } = await import("../lib/todo-manager.js");
4525
- const result = writeTodos(sessionId, args.todos);
4938
+ const result = writeTodos(sessionId, args.todos, {
4939
+ ...(Number.isSafeInteger(args.expected_revision)
4940
+ ? { expectedRevision: args.expected_revision }
4941
+ : {}),
4942
+ });
4526
4943
  if (!result.success) {
4527
- return attachDescriptor({ error: result.error });
4944
+ return attachDescriptor({
4945
+ error: result.error,
4946
+ ...(result.code ? { code: result.code } : {}),
4947
+ ...(Number.isSafeInteger(result.expectedRevision)
4948
+ ? { expectedRevision: result.expectedRevision }
4949
+ : {}),
4950
+ ...(Number.isSafeInteger(result.actualRevision)
4951
+ ? { actualRevision: result.actualRevision }
4952
+ : {}),
4953
+ ...(result.recoveryStrategy
4954
+ ? { recoveryStrategy: result.recoveryStrategy }
4955
+ : {}),
4956
+ });
4528
4957
  }
4529
4958
  return attachDescriptor({
4530
4959
  success: true,
4531
4960
  count: result.count,
4532
4961
  summary: result.summary,
4962
+ revision: result.revision,
4533
4963
  });
4534
4964
  } catch (err) {
4535
4965
  return attachDescriptor({ error: `todo_write failed: ${err.message}` });
@@ -5218,6 +5648,7 @@ async function executeToolInner(
5218
5648
  },
5219
5649
  dirPath,
5220
5650
  cwd,
5651
+ subtreeInstructionScope,
5221
5652
  ),
5222
5653
  );
5223
5654
  }
@@ -5243,7 +5674,14 @@ async function executeToolInner(
5243
5674
  });
5244
5675
  }
5245
5676
  try {
5246
- if (typeof skillLoader.materializeSkill === "function") {
5677
+ if (typeof skillLoader.materializeSkillForExecution === "function") {
5678
+ match = await skillLoader.materializeSkillForExecution(match, {
5679
+ sessionId,
5680
+ turnId,
5681
+ loadedBecause: "run_skill",
5682
+ bodyIncluded: false,
5683
+ });
5684
+ } else if (typeof skillLoader.materializeSkill === "function") {
5247
5685
  match = skillLoader.materializeSkill(match, {
5248
5686
  sessionId,
5249
5687
  turnId,
@@ -5254,12 +5692,26 @@ async function executeToolInner(
5254
5692
  } catch (error) {
5255
5693
  return attachDescriptor({
5256
5694
  error: `Skill "${args.skill_name}" body could not be loaded: ${error.message}`,
5695
+ ...(error?.code ? { code: error.code } : {}),
5696
+ policy: {
5697
+ decision: "blocked",
5698
+ via: "skill-execution-boundary",
5699
+ },
5257
5700
  });
5258
5701
  }
5259
5702
 
5260
5703
  // Check if skill requests isolation (via SKILL.md frontmatter)
5261
5704
  const skillIsolation = match.isolation === true;
5262
5705
  if (skillIsolation) {
5706
+ const isolatedSkillTools = [
5707
+ "read_file",
5708
+ "search_files",
5709
+ "list_dir",
5710
+ ].filter(
5711
+ (toolName) =>
5712
+ !Array.isArray(effectiveAllowedToolNames) ||
5713
+ effectiveAllowedToolNames.includes(toolName),
5714
+ );
5263
5715
  // 用量归因: an isolated skill runs as a child loop whose real token
5264
5716
  // usage would otherwise be invisible — forward it into the parent
5265
5717
  // run's sink tagged origin:"skill" so `cc session usage --by skill`
@@ -5271,11 +5723,36 @@ async function executeToolInner(
5271
5723
  // Run skill through isolated sub-agent context
5272
5724
  const subCtx = SubAgentContext.create({
5273
5725
  role: `skill-${args.skill_name}`,
5274
- task: `Execute the "${args.skill_name}" skill with input: ${(args.input || "").substring(0, 200)}`,
5275
- allowedTools: ["read_file", "search_files", "list_dir"],
5726
+ task:
5727
+ `Execute the "${args.skill_name}" skill using only the approved tools.\n\n` +
5728
+ `Authoritative skill instructions:\n${String(match.body || "").substring(0, 48000)}\n\n` +
5729
+ `User input:\n${String(args.input || "").substring(0, 8000)}`,
5730
+ allowedTools: isolatedSkillTools,
5276
5731
  hookParentTraceId: hookTraceId || null,
5277
5732
  toolAdmission,
5278
5733
  cwd,
5734
+ llmOptions: llmOptions || null,
5735
+ ...(sessionBudget ? { sessionBudget } : {}),
5736
+ ...(permissionRules ? { permissionRules } : {}),
5737
+ ...(hostManagedToolPolicy
5738
+ ? {
5739
+ hostManagedToolPolicy: {
5740
+ ...hostManagedToolPolicy,
5741
+ toolDefinitions: [],
5742
+ },
5743
+ }
5744
+ : {}),
5745
+ ...(planManager ? { planManager } : {}),
5746
+ ...(sandbox ? { sandbox } : {}),
5747
+ ...(Array.isArray(additionalDirectories)
5748
+ ? { additionalDirectories: [...additionalDirectories] }
5749
+ : {}),
5750
+ ...(approvalGate ? { approvalGate } : {}),
5751
+ ...(shellPolicyOverrides ? { shellPolicyOverrides } : {}),
5752
+ ...(classifyAllShell ? { classifyAllShell: true } : {}),
5753
+ ...(unattendedActionPolicy ? { unattendedActionPolicy } : {}),
5754
+ ...(mcpCallLedger ? { mcpCallLedger } : {}),
5755
+ ...(mcpConflictScheduler ? { mcpConflictScheduler } : {}),
5279
5756
  onUsage: skillUsageSink
5280
5757
  ? (u) => {
5281
5758
  try {
@@ -5286,6 +5763,7 @@ async function executeToolInner(
5286
5763
  provider: u?.provider ?? null,
5287
5764
  model: u?.model ?? null,
5288
5765
  usage: u?.usage || null,
5766
+ ...(u?.source ? { source: u.source } : {}),
5289
5767
  attribution: {
5290
5768
  origin: "skill",
5291
5769
  skill: args.skill_name,
@@ -5304,6 +5782,25 @@ async function executeToolInner(
5304
5782
  skillSubRef = subCtx;
5305
5783
  try {
5306
5784
  const result = await subCtx.run(args.input);
5785
+ const resolvedFailure =
5786
+ subCtx.status === "failed" ||
5787
+ result?.success === false ||
5788
+ Boolean(result?.error);
5789
+ if (resolvedFailure) {
5790
+ const failureDetail =
5791
+ result?.error?.message ||
5792
+ result?.error ||
5793
+ result?.summary ||
5794
+ `child status ${subCtx.status || "failed"}`;
5795
+ return attachDescriptor({
5796
+ success: false,
5797
+ isolated: true,
5798
+ skill: args.skill_name,
5799
+ code: result?.code || "CC_SKILL_ISOLATED_EXECUTION_FAILED",
5800
+ error: `Isolated skill execution failed: ${failureDetail}`,
5801
+ ...(result?.summary ? { summary: result.summary } : {}),
5802
+ });
5803
+ }
5307
5804
  return attachDescriptor({
5308
5805
  success: true,
5309
5806
  isolated: true,
@@ -5313,79 +5810,24 @@ async function executeToolInner(
5313
5810
  });
5314
5811
  } catch (err) {
5315
5812
  return attachDescriptor({
5813
+ success: false,
5814
+ isolated: true,
5815
+ skill: args.skill_name,
5816
+ code: err?.code || "CC_SKILL_ISOLATED_EXECUTION_FAILED",
5316
5817
  error: `Isolated skill execution failed: ${err.message}`,
5317
5818
  });
5318
5819
  }
5319
5820
  }
5320
5821
 
5321
- // Skill-Embedded MCP: mount the skill's declared MCP servers for
5322
- // the duration of handler.execute, then unmount in finally. The
5323
- // handler may use them via taskContext.mcpClient. If mcpClient is
5324
- // null (no MCP set up for this session), skip silently.
5325
- let mountedMcpServers = [];
5326
- const hasSkillMcps =
5327
- Array.isArray(match.mcpServers) && match.mcpServers.length > 0;
5328
- if (hasSkillMcps && mcpClient) {
5329
- try {
5330
- const mountResult = await mountSkillMcpServers(mcpClient, match, {
5331
- onWarn: (msg) => {
5332
- // Non-fatal — logged as warning, skipped servers captured
5333
- // in mountResult.skipped.
5334
-
5335
- console.warn(msg);
5336
- },
5337
- });
5338
- mountedMcpServers = mountResult.mounted;
5339
- for (const s of mountedMcpServers) {
5340
- _activeMcpServers.add(typeof s === "string" ? s : s.name);
5341
- }
5342
- } catch (err) {
5343
- return attachDescriptor({
5344
- error: `Skill MCP mount failed: ${err.message}`,
5345
- });
5346
- }
5347
- }
5348
-
5349
- try {
5350
- const handlerPath = path.join(match.skillDir, "handler.js");
5351
- const imported = await import(
5352
- `file://${handlerPath.replace(/\\/g, "/")}`
5353
- );
5354
- const handler = imported.default || imported;
5355
- if (handler.init) await handler.init(match);
5356
- const task = {
5357
- params: { input: args.input },
5358
- input: args.input,
5359
- action: args.input,
5360
- };
5361
- const taskContext = {
5362
- projectRoot: cwd,
5363
- workspacePath: cwd,
5364
- processBroker: createSkillProcessBroker(match),
5365
- // Expose the MCP client + mounted servers so the skill handler
5366
- // can call MCP tools directly without going through the agent
5367
- // loop. Handlers that don't need MCP can ignore these.
5368
- mcpClient: mcpClient || null,
5369
- mountedMcpServers,
5370
- };
5371
- const result = await handler.execute(task, taskContext, match);
5372
- return attachDescriptor(result);
5373
- } catch (err) {
5374
- return attachDescriptor({
5375
- error: `Skill execution failed: ${err.message}`,
5376
- });
5377
- } finally {
5378
- if (mountedMcpServers.length > 0 && mcpClient) {
5379
- try {
5380
- await unmountSkillMcpServers(mcpClient, mountedMcpServers);
5381
- } catch (_err) {
5382
- // Non-critical — mount/unmount errors don't fail the skill
5383
- }
5384
- for (const s of mountedMcpServers) {
5385
- _activeMcpServers.delete(typeof s === "string" ? s : s.name);
5386
- }
5387
- }
5388
- }
5822
+ // Defense in depth: materializeSkill currently rejects this path before
5823
+ // reaching here. Keep the runtime fence explicit so a custom/legacy
5824
+ // loader can never re-enable arbitrary handler.js imports in the CLI
5825
+ // process or hand a Skill the raw MCP client/process broker.
5826
+ return attachDescriptor({
5827
+ error: `Skill "${args.skill_name}" cannot execute handler.js directly. Add isolation: true and use the controlled agent-tool path.`,
5828
+ code: "CC_SKILL_DIRECT_HANDLER_BLOCKED",
5829
+ policy: { decision: "blocked", via: "skill-execution-boundary" },
5830
+ });
5389
5831
  }
5390
5832
 
5391
5833
  case "list_skills": {
@@ -5501,21 +5943,268 @@ async function executeToolInner(
5501
5943
  return attachDescriptor(deferredGate);
5502
5944
  }
5503
5945
 
5946
+ let mcpWireInput;
5947
+ try {
5948
+ mcpWireInput = snapshotMcpJsonRpcInput(args || {});
5949
+ } catch (error) {
5950
+ return attachDescriptor({
5951
+ error:
5952
+ "MCP tool blocked because its input is not strict immutable JSON data",
5953
+ policy: {
5954
+ decision: "blocked",
5955
+ via: "mcp-wire-input",
5956
+ code: safeMcpErrorCode(error, "CC_MCP_WIRE_INPUT_INVALID"),
5957
+ },
5958
+ });
5959
+ }
5960
+
5961
+ const schedulerScopes = mcpLedgerScopes(mcpWireInput);
5962
+ const ledgerEffectContract = mcpLedgerEffectContract(
5963
+ localToolDescriptor,
5964
+ hostToolPolicy,
5965
+ );
5966
+ const schedulerEffectContract = ledgerEffectContract.trusted
5967
+ ? createHostOwnedMcpEffectContract(ledgerEffectContract)
5968
+ : ledgerEffectContract;
5969
+ const scheduler =
5970
+ mcpConflictScheduler || directMcpConflictScheduler(mcpClient);
5971
+ let schedulerLease;
5504
5972
  try {
5505
- const result = await mcpClient.callTool(
5506
- localToolExecutor.serverName,
5507
- localToolExecutor.toolName,
5508
- args || {},
5973
+ schedulerLease = await scheduler.acquire(
5974
+ {
5975
+ effectContract: schedulerEffectContract,
5976
+ ...schedulerScopes,
5977
+ },
5978
+ { signal },
5509
5979
  );
5510
- if (result && typeof result === "object") {
5511
- return attachDescriptor(result);
5512
- }
5513
- return attachDescriptor({ result });
5514
5980
  } catch (err) {
5515
5981
  return attachDescriptor({
5516
- error: `MCP tool execution failed: ${err.message}`,
5982
+ error: `MCP tool blocked by the effect conflict scheduler: ${err.message}`,
5983
+ policy: {
5984
+ decision: "blocked",
5985
+ via: "mcp-conflict-scheduler",
5986
+ code: err?.code || null,
5987
+ },
5517
5988
  });
5518
5989
  }
5990
+
5991
+ try {
5992
+ const ledger = mcpCallLedger || createMcpCallLedger();
5993
+ let ledgerTicket;
5994
+ try {
5995
+ ledgerTicket = await ledger.begin({
5996
+ sessionId,
5997
+ turnId,
5998
+ toolName: localToolExecutor.toolName,
5999
+ serverName: localToolExecutor.serverName,
6000
+ input: mcpWireInput,
6001
+ effectContract: ledgerEffectContract,
6002
+ ...schedulerScopes,
6003
+ });
6004
+ } catch (err) {
6005
+ return attachDescriptor({
6006
+ error: `MCP tool blocked because its call ledger prewrite failed: ${err.message}`,
6007
+ policy: {
6008
+ decision: "blocked",
6009
+ via: "mcp-ledger-prewrite",
6010
+ code: err?.code || null,
6011
+ ledgerId: err?.ledgerId || null,
6012
+ effect: err?.effect || null,
6013
+ blockMode: err?.blockMode || null,
6014
+ },
6015
+ });
6016
+ }
6017
+
6018
+ const transportFailure = async (callError) => {
6019
+ if (mcpTransportOutcomeIsUnsafe(ledgerEffectContract)) {
6020
+ return attachDescriptor(
6021
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6022
+ phase: "call",
6023
+ reasonCode: MCP_TRANSPORT_OUTCOME_UNKNOWN_CODE,
6024
+ }),
6025
+ );
6026
+ }
6027
+ try {
6028
+ await ledgerTicket.settle({
6029
+ status: "failed",
6030
+ error: callError,
6031
+ });
6032
+ } catch (ledgerError) {
6033
+ return attachDescriptor(
6034
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6035
+ phase: "settled",
6036
+ reasonCode: safeMcpErrorCode(
6037
+ ledgerError,
6038
+ "CC_MCP_LEDGER_SETTLE_FAILED",
6039
+ ),
6040
+ }),
6041
+ );
6042
+ }
6043
+ return attachDescriptor({
6044
+ error: `MCP tool execution failed: ${safeMcpErrorMessage(callError)}`,
6045
+ mcpLedgerId: safeMcpProperty(ledgerTicket, "ledgerId") || null,
6046
+ });
6047
+ };
6048
+
6049
+ let pendingResult;
6050
+ try {
6051
+ pendingResult = Reflect.apply(mcpClient.callTool, mcpClient, [
6052
+ localToolExecutor.serverName,
6053
+ localToolExecutor.toolName,
6054
+ mcpWireInput,
6055
+ ]);
6056
+ } catch (callError) {
6057
+ return await transportFailure(callError);
6058
+ }
6059
+
6060
+ let result;
6061
+ if (
6062
+ pendingResult !== null &&
6063
+ (typeof pendingResult === "object" ||
6064
+ typeof pendingResult === "function")
6065
+ ) {
6066
+ if (isProxy(pendingResult)) {
6067
+ return attachDescriptor(
6068
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6069
+ phase: "result",
6070
+ reasonCode: MCP_PROTOCOL_RESULT_INVALID_CODE,
6071
+ }),
6072
+ );
6073
+ }
6074
+
6075
+ let nativePromise;
6076
+ try {
6077
+ nativePromise = pendingResult instanceof Promise;
6078
+ } catch (inspectionError) {
6079
+ return attachDescriptor(
6080
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6081
+ phase: "result",
6082
+ reasonCode: safeMcpErrorCode(
6083
+ invalidMcpProtocolResult(inspectionError),
6084
+ MCP_PROTOCOL_RESULT_INVALID_CODE,
6085
+ ),
6086
+ }),
6087
+ );
6088
+ }
6089
+
6090
+ if (nativePromise) {
6091
+ try {
6092
+ result = await pendingResult;
6093
+ } catch (callError) {
6094
+ return await transportFailure(callError);
6095
+ }
6096
+ } else {
6097
+ let then;
6098
+ try {
6099
+ then = Reflect.get(pendingResult, "then", pendingResult);
6100
+ } catch (inspectionError) {
6101
+ return attachDescriptor(
6102
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6103
+ phase: "result",
6104
+ reasonCode: safeMcpErrorCode(
6105
+ invalidMcpProtocolResult(inspectionError),
6106
+ MCP_PROTOCOL_RESULT_INVALID_CODE,
6107
+ ),
6108
+ }),
6109
+ );
6110
+ }
6111
+ if (typeof then === "function") {
6112
+ try {
6113
+ result = await new Promise((resolve, reject) => {
6114
+ try {
6115
+ Reflect.apply(then, pendingResult, [resolve, reject]);
6116
+ } catch (thenError) {
6117
+ reject(thenError);
6118
+ }
6119
+ });
6120
+ } catch (thenError) {
6121
+ return attachDescriptor(
6122
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6123
+ phase: "result",
6124
+ reasonCode: safeMcpErrorCode(
6125
+ invalidMcpProtocolResult(thenError),
6126
+ MCP_PROTOCOL_RESULT_INVALID_CODE,
6127
+ ),
6128
+ }),
6129
+ );
6130
+ }
6131
+ } else {
6132
+ result = pendingResult;
6133
+ }
6134
+ }
6135
+ } else {
6136
+ result = pendingResult;
6137
+ }
6138
+
6139
+ let protocolError;
6140
+ try {
6141
+ if (
6142
+ result !== null &&
6143
+ (typeof result === "object" || typeof result === "function") &&
6144
+ isProxy(result)
6145
+ ) {
6146
+ throw invalidMcpProtocolResult();
6147
+ }
6148
+ protocolError = result?.isError === true;
6149
+ } catch (inspectionError) {
6150
+ return attachDescriptor(
6151
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6152
+ phase: "result",
6153
+ reasonCode: safeMcpErrorCode(
6154
+ invalidMcpProtocolResult(inspectionError),
6155
+ MCP_PROTOCOL_RESULT_INVALID_CODE,
6156
+ ),
6157
+ }),
6158
+ );
6159
+ }
6160
+
6161
+ try {
6162
+ await ledgerTicket.settle(
6163
+ protocolError
6164
+ ? {
6165
+ status: "failed",
6166
+ output: result,
6167
+ error: new Error("MCP server returned isError=true"),
6168
+ }
6169
+ : { status: "completed", output: result },
6170
+ );
6171
+ } catch (ledgerError) {
6172
+ return attachDescriptor(
6173
+ mcpOutcomeUnknownPayload(ledger, ledgerTicket, {
6174
+ phase: "settled",
6175
+ reasonCode: safeMcpErrorCode(
6176
+ ledgerError,
6177
+ "CC_MCP_LEDGER_SETTLE_FAILED",
6178
+ ),
6179
+ }),
6180
+ );
6181
+ }
6182
+
6183
+ const mcpLedgerId = safeMcpProperty(ledgerTicket, "ledgerId") || null;
6184
+ try {
6185
+ if (result && typeof result === "object") {
6186
+ return attachDescriptor({ ...result, mcpLedgerId });
6187
+ }
6188
+ return attachDescriptor({ result, mcpLedgerId });
6189
+ } catch (projectionError) {
6190
+ return attachDescriptor({
6191
+ error:
6192
+ "MCP tool completed, but its result could not be projected safely; do not retry automatically.",
6193
+ code: MCP_RESULT_PROJECTION_FAILED_CODE,
6194
+ retryable: false,
6195
+ mcpLedgerId,
6196
+ mcpLedgerIncident: {
6197
+ phase: "result",
6198
+ code: safeMcpErrorCode(
6199
+ projectionError,
6200
+ MCP_RESULT_PROJECTION_FAILED_CODE,
6201
+ ),
6202
+ },
6203
+ });
6204
+ }
6205
+ } finally {
6206
+ schedulerLease.release();
6207
+ }
5519
6208
  }
5520
6209
 
5521
6210
  if (
@@ -6190,11 +6879,41 @@ function* _drainSubAgentUsage(sink) {
6190
6879
  provider: u?.provider ?? null,
6191
6880
  model: u?.model ?? null,
6192
6881
  usage: u?.usage || {},
6882
+ ...(u?.source ? { source: u.source } : {}),
6193
6883
  attribution: u?.attribution || null,
6194
6884
  };
6195
6885
  }
6196
6886
  }
6197
6887
 
6888
+ /**
6889
+ * Build the bounded, deterministic parent-to-child handoff used when callers
6890
+ * do not supply explicit context. Keeping the same nine-field schema as
6891
+ * semantic compaction prevents decisions, tests, and unresolved side effects
6892
+ * from disappearing merely because work crosses a sub-agent boundary.
6893
+ */
6894
+ export function buildSubAgentHandoffContext(messages) {
6895
+ if (!Array.isArray(messages)) return null;
6896
+ const canonicalMessages = projectCanonicalResumeMessages(messages);
6897
+ const hasContent = canonicalMessages.some((message) => {
6898
+ if (!message || !["user", "assistant", "tool"].includes(message.role)) {
6899
+ return false;
6900
+ }
6901
+ if (typeof message.content === "string") {
6902
+ return message.content.trim().length > 0;
6903
+ }
6904
+ return message.content != null;
6905
+ });
6906
+ if (!hasContent) return null;
6907
+
6908
+ const handoff = buildExtractiveHandoff(canonicalMessages, {
6909
+ maxContentChars: 6000,
6910
+ maxItemsPerField: 6,
6911
+ maxItemChars: 500,
6912
+ maxFallbackSourceChars: 16000,
6913
+ });
6914
+ return `[Structured parent handoff v1]\n${formatStructuredHandoff(handoff)}`;
6915
+ }
6916
+
6198
6917
  async function _executeSpawnSubAgent(args, ctx) {
6199
6918
  // Nesting cap: refuse before any context/registry work.
6200
6919
  const currentDepth = ctx.subAgentDepth || 0;
@@ -6218,8 +6937,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6218
6937
  hardChildrenCap: ctx.subAgentBudget?.max ?? MAX_SUB_AGENTS_PER_RUN,
6219
6938
  });
6220
6939
  if (!recur.ok) return { error: `spawn_sub_agent: ${recur.reason}` };
6221
- } catch {
6222
- /* contract module unavailable — hard caps above still apply */
6940
+ } catch (err) {
6941
+ return {
6942
+ error: `spawn_sub_agent: parent contract enforcement failed closed (${err.message}).`,
6943
+ };
6223
6944
  }
6224
6945
  }
6225
6946
  // Breadth cap: a shared counter (one object for the whole tree) bounds the
@@ -6242,9 +6963,20 @@ async function _executeSpawnSubAgent(args, ctx) {
6242
6963
  role,
6243
6964
  task,
6244
6965
  context: inheritedContext,
6966
+ contextMode,
6245
6967
  tools: explicitTools,
6246
6968
  profile: profileName,
6247
6969
  } = args;
6970
+ // Compatibility bridge for callers that used the original overloaded
6971
+ // `context: fresh|fork` authority spelling. An explicit `contextMode` always
6972
+ // wins and makes `context` unambiguously prompt text.
6973
+ const legacyContextMode =
6974
+ contextMode == null &&
6975
+ (inheritedContext === "fresh" || inheritedContext === "fork")
6976
+ ? inheritedContext
6977
+ : null;
6978
+ if (legacyContextMode) inheritedContext = null;
6979
+ const requestedContextMode = contextMode ?? legacyContextMode;
6248
6980
  // Extended sub-agent contract (gap 2026-07-11 P1): per-spawn deny-list,
6249
6981
  // iteration cap and worktree isolation — spawn args win over the agent
6250
6982
  // file's frontmatter defaults.
@@ -6311,7 +7043,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6311
7043
  // effort, context inheritance); this child's contract becomes the ceiling for
6312
7044
  // ITS own nested spawns (threaded via SubAgentContext.subAgentContract).
6313
7045
  let effectiveContract = null;
6314
- let explicitContext = null;
7046
+ let explicitContextMode = null;
6315
7047
  let skillAllowlist = null;
6316
7048
  // MCP-server + hook allow-lists for child INHERITANCE. Default `[]` = inherit
6317
7049
  // NONE, which equals today's behavior (a spawned child gets zero MCP tools /
@@ -6326,10 +7058,23 @@ async function _executeSpawnSubAgent(args, ctx) {
6326
7058
  // = byte-identical.
6327
7059
  let permModeDriven = false;
6328
7060
  try {
6329
- const { resolveSubagentContract, normalizeSubagentContract } =
6330
- await import("../lib/subagent-contract.js");
6331
- const spawnContract = normalizeSubagentContract(args);
6332
- explicitContext = spawnContract.context ?? mdContract?.context ?? null;
7061
+ const {
7062
+ resolveSubagentContract,
7063
+ normalizeSubagentContract,
7064
+ assertValidSubagentContract,
7065
+ } = await import("../lib/subagent-contract.js");
7066
+ // `context` predates the authority contract and remains arbitrary prompt
7067
+ // text except for the narrow legacy spelling handled above. Map the
7068
+ // separate contextMode field onto the contract's canonical `context` key.
7069
+ const authorityArgs = { ...args };
7070
+ delete authorityArgs.context;
7071
+ delete authorityArgs.contextMode;
7072
+ if (requestedContextMode != null) {
7073
+ authorityArgs.context = requestedContextMode;
7074
+ }
7075
+ assertValidSubagentContract(authorityArgs);
7076
+ const spawnContract = normalizeSubagentContract(authorityArgs);
7077
+ explicitContextMode = spawnContract.context ?? mdContract?.context ?? null;
6333
7078
  effectiveContract = resolveSubagentContract({
6334
7079
  parent: ctx.subAgentContract || {},
6335
7080
  definition: mdContract,
@@ -6349,7 +7094,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6349
7094
  const skillsDriven =
6350
7095
  spawnContract.skills != null ||
6351
7096
  mdContract?.skills != null ||
6352
- explicitContext != null;
7097
+ explicitContextMode != null;
6353
7098
  skillAllowlist = skillsDriven ? (effectiveContract.skills ?? null) : null;
6354
7099
  // MCP/hooks work the OTHER way from skills: their pre-inheritance default is
6355
7100
  // "none", so the silent-`fresh`→[] resolution IS the safe current behavior —
@@ -6357,12 +7102,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6357
7102
  // into inheriting the corresponding parent capabilities.
6358
7103
  mcpAllow = effectiveContract.mcpServers ?? null;
6359
7104
  hookAllow = effectiveContract.hooks ?? null;
6360
- } catch {
6361
- effectiveContract = null; // contract resolution is best-effort
6362
- skillAllowlist = null;
6363
- mcpAllow = []; // inherit no MCP / hooks when resolution fails
6364
- hookAllow = [];
6365
- permModeDriven = false; // no gate when resolution fails
7105
+ } catch (err) {
7106
+ return {
7107
+ error: `spawn_sub_agent: authority contract resolution failed closed (${err.message}).`,
7108
+ };
6366
7109
  }
6367
7110
 
6368
7111
  // Filter the parent loop's live MCP plumbing + settings hooks down to what the
@@ -6383,9 +7126,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6383
7126
  mcpAllow,
6384
7127
  );
6385
7128
  inheritedHooks = filterInheritedHooks(ctx.settingsHooks || null, hookAllow);
6386
- } catch {
6387
- inheritedMcp = null; // inheritance is best-effort; never break the spawn
6388
- inheritedHooks = null;
7129
+ } catch (err) {
7130
+ return {
7131
+ error: `spawn_sub_agent: capability inheritance failed closed (${err.message}).`,
7132
+ };
6389
7133
  }
6390
7134
 
6391
7135
  // Memory INHERITANCE (contract `memory` boolean, tighten-only across depth):
@@ -6412,8 +7156,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6412
7156
  available: isGitRepo(ctx.cwd),
6413
7157
  });
6414
7158
  if (!iso.ok) return { error: `spawn_sub_agent: ${iso.reason}` };
6415
- } catch {
6416
- /* helper unavailable — sub-agent-context.js still fails closed at run() */
7159
+ } catch (err) {
7160
+ return {
7161
+ error: `spawn_sub_agent: isolation enforcement failed closed (${err.message}).`,
7162
+ };
6417
7163
  }
6418
7164
  }
6419
7165
 
@@ -6430,8 +7176,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6430
7176
  error: `Unknown sub-agent profile: "${profileName}". Valid: explorer|executor|design`,
6431
7177
  };
6432
7178
  }
6433
- } catch (_err) {
6434
- // profile module optional — proceed without
7179
+ } catch (err) {
7180
+ return {
7181
+ error: `spawn_sub_agent: requested profile resolution failed closed (${err.message}).`,
7182
+ };
6435
7183
  }
6436
7184
  }
6437
7185
 
@@ -6453,6 +7201,16 @@ async function _executeSpawnSubAgent(args, ctx) {
6453
7201
  allowedTools = base.filter((t) => !deny.has(t));
6454
7202
  }
6455
7203
 
7204
+ // A child can never regain a tool omitted from the parent's effective schema.
7205
+ // `[]` remains deny-all all the way into the child's execution-time fence.
7206
+ if (Array.isArray(ctx.effectiveAllowedToolNames)) {
7207
+ const parentCeiling = new Set(ctx.effectiveAllowedToolNames);
7208
+ const requested = Array.isArray(allowedTools)
7209
+ ? allowedTools
7210
+ : [...parentCeiling];
7211
+ allowedTools = requested.filter((tool) => parentCeiling.has(tool));
7212
+ }
7213
+
6456
7214
  // permissionMode enforcement into the child gate. Reuses the runner's own
6457
7215
  // resolvers (single-sourced, no drift); best-effort so it never breaks a spawn.
6458
7216
  // • `plan` → clamp to the read-only tool set (same rule as resolveEnabledTools),
@@ -6501,27 +7259,23 @@ async function _executeSpawnSubAgent(args, ctx) {
6501
7259
  defaultPolicy: perm.sessionPolicy,
6502
7260
  });
6503
7261
  }
6504
- } catch {
6505
- // Enforcement is best-effort — never break the spawn.
7262
+ } catch (err) {
7263
+ return {
7264
+ error: `spawn_sub_agent: permission-mode enforcement failed closed (${err.message}).`,
7265
+ };
6506
7266
  }
6507
7267
  }
6508
7268
 
6509
- // Auto-condense parent context if caller didn't provide explicit context.
6510
- // An explicit `context: fresh` contract suppresses this inheritance (the
6511
- // child starts clean); `fork` / unset keep the existing auto-condense.
7269
+ // Build a structured parent handoff if the caller did not provide explicit
7270
+ // An explicit `contextMode: fresh` contract suppresses this inheritance (the
7271
+ // child starts clean); `fork` / unset keep deterministic auto-inheritance.
6512
7272
  let resolvedContext = inheritedContext || null;
6513
7273
  if (
6514
7274
  !resolvedContext &&
6515
- explicitContext !== "fresh" &&
7275
+ explicitContextMode !== "fresh" &&
6516
7276
  Array.isArray(ctx.parentMessages)
6517
7277
  ) {
6518
- const recentMsgs = ctx.parentMessages
6519
- .filter((m) => m.role === "assistant" && typeof m.content === "string")
6520
- .slice(-3)
6521
- .map((m) => m.content.substring(0, 200));
6522
- if (recentMsgs.length > 0) {
6523
- resolvedContext = recentMsgs.join("\n---\n");
6524
- }
7278
+ resolvedContext = buildSubAgentHandoffContext(ctx.parentMessages);
6525
7279
  }
6526
7280
 
6527
7281
  // Link child to parent session so registry-scoped queries and
@@ -6557,8 +7311,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6557
7311
  ? `${resolvedContext}\n---\n${injected}`
6558
7312
  : injected;
6559
7313
  }
6560
- } catch (_err) {
6561
- // SubagentStart hooks are best-effort — never break the spawn.
7314
+ } catch (err) {
7315
+ return {
7316
+ error: `spawn_sub_agent: SubagentStart authority hook failed closed (${err.message}).`,
7317
+ };
6562
7318
  }
6563
7319
  }
6564
7320
 
@@ -6592,6 +7348,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6592
7348
  provider: u?.provider ?? null,
6593
7349
  model: u?.model ?? null,
6594
7350
  usage: u?.usage || null,
7351
+ ...(u?.source ? { source: u.source } : {}),
6595
7352
  attribution: {
6596
7353
  origin: "subagent",
6597
7354
  subagentId: subCtxRef?.id || null,
@@ -6626,11 +7383,23 @@ async function _executeSpawnSubAgent(args, ctx) {
6626
7383
  if (sparse) wtOpts.sparsePaths = sparse;
6627
7384
  if (symlink != null) wtOpts.symlinkDirectories = symlink;
6628
7385
  if (Object.keys(wtOpts).length) subWorktreeOptions = wtOpts;
6629
- } catch {
6630
- subWorktreeOptions = null; // best-effort → full checkout on any error
7386
+ } catch (err) {
7387
+ return {
7388
+ error: `spawn_sub_agent: worktree scope resolution failed closed (${err.message}).`,
7389
+ };
6631
7390
  }
6632
7391
  }
6633
7392
 
7393
+ // Preserve host deny/policy metadata, but do not implicitly inherit hosted
7394
+ // external definitions. MCP/host capabilities enter the child only through
7395
+ // the explicit contract-filtered plumbing above.
7396
+ const childHostManagedToolPolicy = ctx.hostManagedToolPolicy
7397
+ ? { ...ctx.hostManagedToolPolicy, toolDefinitions: [] }
7398
+ : null;
7399
+ // A parent gate is already an authority ceiling. Prefer it over a newly
7400
+ // derived child gate so an explicit child mode can never relax the parent.
7401
+ const effectiveChildApprovalGate = ctx.approvalGate || childApprovalGate;
7402
+
6634
7403
  const subCtx = SubAgentContext.create({
6635
7404
  role,
6636
7405
  task,
@@ -6639,7 +7408,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6639
7408
  // and THIS run's id as parent_id on every settings-hook payload it fires.
6640
7409
  hookParentTraceId: ctx.hookTraceId || null,
6641
7410
  inheritedContext: resolvedContext,
6642
- allowedTools: allowedTools || null,
7411
+ allowedTools: allowedTools ?? null,
6643
7412
  cwd: ctx.cwd,
6644
7413
  profile: profile || null,
6645
7414
  llmOptions: subLlmOptions,
@@ -6647,6 +7416,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6647
7416
  // Same shared counter object so the child's own spawns draw from the run's
6648
7417
  // single total-sub-agent pool (breadth cap spans the whole tree).
6649
7418
  subAgentBudget: ctx.subAgentBudget || null,
7419
+ ...(ctx.sessionBudget ? { sessionBudget: ctx.sessionBudget } : {}),
6650
7420
  onUsage,
6651
7421
  // Extended contract (gap 2026-07-11): per-agent iteration cap + opt-in
6652
7422
  // worktree isolation. undefined keeps the profile/flag defaults intact.
@@ -6674,6 +7444,10 @@ async function _executeSpawnSubAgent(args, ctx) {
6674
7444
  externalToolDescriptors: inheritedMcp.externalToolDescriptors,
6675
7445
  externalToolExecutors: inheritedMcp.externalToolExecutors,
6676
7446
  mcpClient: inheritedMcp.mcpClient,
7447
+ ...(ctx.mcpCallLedger ? { mcpCallLedger: ctx.mcpCallLedger } : {}),
7448
+ ...(ctx.mcpConflictScheduler
7449
+ ? { mcpConflictScheduler: ctx.mcpConflictScheduler }
7450
+ : {}),
6677
7451
  }
6678
7452
  : {}),
6679
7453
  ...(inheritedHooks ? { settingsHooks: inheritedHooks } : {}),
@@ -6696,7 +7470,27 @@ async function _executeSpawnSubAgent(args, ctx) {
6696
7470
  // permissionMode ApprovalGate (2026-07-13): a dedicated confirmer-less gate
6697
7471
  // seeded with the mode's tier gates the child's run_shell / browser_act;
6698
7472
  // absent (ungated) for a plain default spawn = byte-identical.
6699
- ...(childApprovalGate ? { approvalGate: childApprovalGate } : {}),
7473
+ ...(effectiveChildApprovalGate
7474
+ ? { approvalGate: effectiveChildApprovalGate }
7475
+ : {}),
7476
+ // Parent execution authority is inherited as a tighten-only bundle and is
7477
+ // re-enforced by the child's executeTool path on every provider tool call.
7478
+ ...(ctx.permissionRules ? { permissionRules: ctx.permissionRules } : {}),
7479
+ ...(childHostManagedToolPolicy
7480
+ ? { hostManagedToolPolicy: childHostManagedToolPolicy }
7481
+ : {}),
7482
+ ...(ctx.planManager ? { planManager: ctx.planManager } : {}),
7483
+ ...(ctx.sandbox ? { sandbox: ctx.sandbox } : {}),
7484
+ ...(Array.isArray(ctx.additionalDirectories)
7485
+ ? { additionalDirectories: [...ctx.additionalDirectories] }
7486
+ : {}),
7487
+ ...(ctx.shellPolicyOverrides
7488
+ ? { shellPolicyOverrides: ctx.shellPolicyOverrides }
7489
+ : {}),
7490
+ ...(ctx.classifyAllShell ? { classifyAllShell: true } : {}),
7491
+ ...(ctx.unattendedActionPolicy
7492
+ ? { unattendedActionPolicy: ctx.unattendedActionPolicy }
7493
+ : {}),
6700
7494
  ...(ctx.toolAdmission ? { toolAdmission: ctx.toolAdmission } : {}),
6701
7495
  });
6702
7496
  subCtxRef = subCtx;
@@ -6777,7 +7571,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6777
7571
  emit("sub-agent.started", {
6778
7572
  task: subCtx.task,
6779
7573
  background: true,
6780
- allowedTools: allowedTools || null,
7574
+ allowedTools: allowedTools ?? null,
6781
7575
  maxIterations: subCtx.maxIterations,
6782
7576
  createdAt: subCtx.createdAt,
6783
7577
  });
@@ -6854,7 +7648,7 @@ async function _executeSpawnSubAgent(args, ctx) {
6854
7648
 
6855
7649
  emit("sub-agent.started", {
6856
7650
  task: subCtx.task,
6857
- allowedTools: allowedTools || null,
7651
+ allowedTools: allowedTools ?? null,
6858
7652
  maxIterations: subCtx.maxIterations,
6859
7653
  createdAt: subCtx.createdAt,
6860
7654
  });
@@ -6870,6 +7664,24 @@ async function _executeSpawnSubAgent(args, ctx) {
6870
7664
  }
6871
7665
  }
6872
7666
 
7667
+ if (result?.budgetReason) {
7668
+ emit("sub-agent.failed", {
7669
+ status: subCtx.status,
7670
+ error: result.summary,
7671
+ budgetReason: result.budgetReason,
7672
+ completedAt: subCtx.completedAt,
7673
+ });
7674
+ return {
7675
+ error: result.summary,
7676
+ code: "ERR_SESSION_RESOURCE_BUDGET",
7677
+ budgetReason: result.budgetReason,
7678
+ subAgentId: subCtx.id,
7679
+ role: subCtx.role,
7680
+ parentSessionId,
7681
+ childBinding: subCtx.recoveryBinding(result),
7682
+ };
7683
+ }
7684
+
6873
7685
  emit("sub-agent.completed", {
6874
7686
  status: subCtx.status,
6875
7687
  summary: result.summary,
@@ -6924,6 +7736,26 @@ async function _executeSpawnSubAgent(args, ctx) {
6924
7736
 
6925
7737
  // ─── LLM chat with tools ─────────────────────────────────────────────────
6926
7738
 
7739
+ function getEffectiveToolDefinitions(options = {}) {
7740
+ const persona = _loadProjectPersona(options.cwd);
7741
+ // Merge every deny source before both schema projection and execution-time
7742
+ // enforcement. Keeping this in one helper prevents those two fences from
7743
+ // drifting apart.
7744
+ const mergedDisabledTools = [
7745
+ ...(Array.isArray(persona?.toolsDisabled) ? persona.toolsDisabled : []),
7746
+ ...(Array.isArray(options.disabledTools) ? options.disabledTools : []),
7747
+ ];
7748
+ return getAgentToolDefinitions({
7749
+ names: options.enabledToolNames,
7750
+ disabledTools: mergedDisabledTools,
7751
+ exactToolNames: options.exactToolNames === true,
7752
+ extraTools: [
7753
+ ...(options.hostManagedToolPolicy?.toolDefinitions || []),
7754
+ ...(options.extraToolDefinitions || []),
7755
+ ],
7756
+ });
7757
+ }
7758
+
6927
7759
  /**
6928
7760
  * Send a chat completion request with tool definitions.
6929
7761
  * Supports 8 providers: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral, volcengine
@@ -6942,22 +7774,7 @@ export async function chatWithTools(rawMessages, options) {
6942
7774
  signal,
6943
7775
  } = options;
6944
7776
 
6945
- const persona = _loadProjectPersona(options.cwd);
6946
- // Merge the project-persona deny-list with any caller-supplied deny-list
6947
- // (e.g. headless `--disallowed-tools`). Without this merge the caller's
6948
- // deny-list is silently dropped and the tool stays callable.
6949
- const mergedDisabledTools = [
6950
- ...(Array.isArray(persona?.toolsDisabled) ? persona.toolsDisabled : []),
6951
- ...(Array.isArray(options.disabledTools) ? options.disabledTools : []),
6952
- ];
6953
- const tools = getAgentToolDefinitions({
6954
- names: options.enabledToolNames,
6955
- disabledTools: mergedDisabledTools,
6956
- extraTools: [
6957
- ...(options.hostManagedToolPolicy?.toolDefinitions || []),
6958
- ...(options.extraToolDefinitions || []),
6959
- ],
6960
- });
7777
+ const tools = getEffectiveToolDefinitions(options);
6961
7778
 
6962
7779
  const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
6963
7780
  const messages = ce
@@ -6986,7 +7803,14 @@ export async function chatWithTools(rawMessages, options) {
6986
7803
  () =>
6987
7804
  _chatOllamaStreaming(
6988
7805
  apiUrl,
6989
- { model, messages: ollamaMessages, tools },
7806
+ {
7807
+ model,
7808
+ messages: ollamaMessages,
7809
+ tools,
7810
+ ...(options.maxOutputTokens
7811
+ ? { options: { num_predict: options.maxOutputTokens } }
7812
+ : {}),
7813
+ },
6990
7814
  options.onToken,
6991
7815
  signal,
6992
7816
  options.onStall,
@@ -7013,6 +7837,9 @@ export async function chatWithTools(rawMessages, options) {
7013
7837
  messages: ollamaMessages,
7014
7838
  tools,
7015
7839
  stream: false,
7840
+ ...(options.maxOutputTokens
7841
+ ? { options: { num_predict: options.maxOutputTokens } }
7842
+ : {}),
7016
7843
  }),
7017
7844
  });
7018
7845
  if (!response.ok) {
@@ -7071,7 +7898,9 @@ export async function chatWithTools(rawMessages, options) {
7071
7898
  );
7072
7899
  const body = {
7073
7900
  model: effModel,
7074
- max_tokens: anthropicMaxTokens || 8192,
7901
+ max_tokens: options.maxOutputTokens
7902
+ ? Math.min(anthropicMaxTokens || 8192, options.maxOutputTokens)
7903
+ : anthropicMaxTokens || 8192,
7075
7904
  // Convert cc's internal OpenAI-shaped history (role:"tool" results,
7076
7905
  // assistant tool_calls[]) into Anthropic content blocks. Without this,
7077
7906
  // multi-turn tool use 400s on turn 2 (Anthropic rejects role:"tool" and
@@ -7230,6 +8059,9 @@ export async function chatWithTools(rawMessages, options) {
7230
8059
  tools,
7231
8060
  stream: true,
7232
8061
  stream_options: { include_usage: true },
8062
+ ...(options.maxOutputTokens
8063
+ ? { max_tokens: options.maxOutputTokens }
8064
+ : {}),
7233
8065
  },
7234
8066
  key,
7235
8067
  options.onToken,
@@ -7262,6 +8094,9 @@ export async function chatWithTools(rawMessages, options) {
7262
8094
  model: model || defaultModels[provider] || "gpt-4o-mini",
7263
8095
  messages,
7264
8096
  tools,
8097
+ ...(options.maxOutputTokens
8098
+ ? { max_tokens: options.maxOutputTokens }
8099
+ : {}),
7265
8100
  }),
7266
8101
  });
7267
8102
 
@@ -8267,9 +9102,45 @@ async function _getAutoCompactor(options) {
8267
9102
  if (feature("PROMPT_COMPRESSOR")) {
8268
9103
  const { PromptCompressor } =
8269
9104
  await import("../harness/prompt-compressor.js");
9105
+ const llmQuery =
9106
+ typeof options.compactionLlmQuery === "function"
9107
+ ? options.compactionLlmQuery
9108
+ : options.chatFn
9109
+ ? null
9110
+ : async (prompt) => {
9111
+ const maxOutputTokens = Math.min(
9112
+ 4096,
9113
+ Math.max(
9114
+ 256,
9115
+ Number(options.compactionMaxOutputTokens) || 2048,
9116
+ ),
9117
+ );
9118
+ const response = await chatWithTools(
9119
+ [{ role: "user", content: prompt }],
9120
+ {
9121
+ ...options,
9122
+ contextEngine: null,
9123
+ enabledToolNames: [],
9124
+ extraToolDefinitions: [],
9125
+ hostManagedToolPolicy: null,
9126
+ onToken: undefined,
9127
+ onStall: undefined,
9128
+ onStreamRetry: undefined,
9129
+ maxOutputTokens,
9130
+ },
9131
+ );
9132
+ return {
9133
+ summary: response?.message?.content || "",
9134
+ usage: response?.usage || null,
9135
+ provider: options.provider || null,
9136
+ model: options.model || null,
9137
+ };
9138
+ };
8270
9139
  compressor = new PromptCompressor({
8271
9140
  model: options.model,
8272
9141
  provider: options.provider,
9142
+ llmQuery,
9143
+ summaryInputMaxChars: options.compactionInputMaxChars,
8273
9144
  });
8274
9145
  }
8275
9146
  } catch {
@@ -8283,6 +9154,21 @@ async function _getAutoCompactor(options) {
8283
9154
  return compressor;
8284
9155
  }
8285
9156
 
9157
+ function _compactionTokenUsage(stats) {
9158
+ const summaryUsage = stats?.summaryUsage;
9159
+ if (!summaryUsage || typeof summaryUsage !== "object") return null;
9160
+ const tokenCount = (value) => {
9161
+ const number = Number(value);
9162
+ return Number.isFinite(number) && number > 0 ? Math.trunc(number) : 0;
9163
+ };
9164
+ return {
9165
+ input_tokens: tokenCount(summaryUsage.inputTokens),
9166
+ output_tokens: tokenCount(summaryUsage.outputTokens),
9167
+ cache_read_input_tokens: tokenCount(summaryUsage.cacheReadTokens),
9168
+ cache_creation_input_tokens: tokenCount(summaryUsage.cacheCreationTokens),
9169
+ };
9170
+ }
9171
+
8286
9172
  /**
8287
9173
  * Run `fn` inside an OpenTelemetry span when `options.recorder` is attached,
8288
9174
  * else run it bare (zero overhead on the un-instrumented path). `onResult`
@@ -8390,6 +9276,15 @@ export async function* agentLoop(messages, options) {
8390
9276
  options.runId ||
8391
9277
  `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8392
9278
 
9279
+ const effectiveAllowedToolNames = Object.freeze(
9280
+ getEffectiveToolDefinitions(options).map((tool) => tool.function.name),
9281
+ );
9282
+ const mcpCallLedger =
9283
+ options.mcpCallLedger ||
9284
+ createMcpCallLedger({ sink: options.mcpLedgerSink || null });
9285
+ const mcpConflictScheduler =
9286
+ options.mcpConflictScheduler || createMcpConflictScheduler();
9287
+
8393
9288
  const toolContext = {
8394
9289
  hookDb: options.hookDb || null,
8395
9290
  skillLoader: options.skillLoader || _defaultSkillLoader,
@@ -8404,6 +9299,7 @@ export async function* agentLoop(messages, options) {
8404
9299
  // unrestricted; [] = none; a list restricts run_skill/list_skills to those
8405
9300
  // ids/dirNames. Set by the spawn path from the resolved subagent contract.
8406
9301
  skillAllowlist: options.skillAllowlist ?? null,
9302
+ effectiveAllowedToolNames,
8407
9303
  cwd: options.cwd || process.cwd(),
8408
9304
  planManager: options.planManager || null,
8409
9305
  sessionId: options.sessionId || null,
@@ -8425,6 +9321,14 @@ export async function* agentLoop(messages, options) {
8425
9321
  // contract's mcpServers allow-list). Otherwise consumed only at agentLoop.
8426
9322
  extraToolDefinitions: options.extraToolDefinitions || null,
8427
9323
  mcpClient: options.mcpClient || null,
9324
+ mcpHostClient: options.mcpHostClient || options.mcpClient || null,
9325
+ mcpCallLedger,
9326
+ mcpConflictScheduler,
9327
+ // A loop-local identity prevents one session's lazy instruction commits
9328
+ // from suppressing first-access delivery in another session at the same
9329
+ // cwd. Hosts may inject a stable identity across resumed turns.
9330
+ subtreeInstructionScope:
9331
+ options.subtreeInstructionScope || options.sessionId || Symbol(runId),
8428
9332
  // Parent memory source — a spawn can inherit the parent's hierarchical
8429
9333
  // memory DB into the child ONLY when the resolved contract grants memory
8430
9334
  // (context:fork from a memory-bearing parent, or explicit memory:true).
@@ -8482,6 +9386,10 @@ export async function* agentLoop(messages, options) {
8482
9386
  spawned: 0,
8483
9387
  max: MAX_SUB_AGENTS_PER_RUN,
8484
9388
  },
9389
+ // Optional local adapter for a host-created SessionResourceBudget. The
9390
+ // CLI root does not create one yet; when supplied, every nested child sees
9391
+ // the same object and cannot reset concurrency/spawn/depth totals.
9392
+ sessionBudget: options.sessionBudget || null,
8485
9393
  // This loop's EFFECTIVE subagent contract (set when this loop IS a spawned
8486
9394
  // sub-agent). Threaded so a nested spawn_sub_agent sees it as the parent
8487
9395
  // ceiling (tighten-only). null at the top level (no ceiling).
@@ -8584,7 +9492,7 @@ export async function* agentLoop(messages, options) {
8584
9492
  // line) when provided; otherwise fall back to a clear stderr notice.
8585
9493
  const message =
8586
9494
  reason === "env-key"
8587
- ? `"${from}" 鉴权失败,已临时切换到不同厂商 "${to}"(请检查 ${from} 的 API key:cc config set llm.apiKey …)。`
9495
+ ? `"${from}" 鉴权失败,已临时切换到不同厂商 "${to}"(请检查 ${from} 的 API key:cc config set-secret llm.apiKey)。`
8588
9496
  : reason === "model-mismatch"
8589
9497
  ? `模型 "${fromModel}" 不属于 ${from},已改用其默认模型 "${toModel}"(用 cc config set llm.model 设置正确的 ${from} 模型)。`
8590
9498
  : `provider 配置与 baseUrl 不一致,已按 baseUrl 切换到 "${to}"。`;
@@ -8765,6 +9673,16 @@ export async function* agentLoop(messages, options) {
8765
9673
  preserveToolPairs: true,
8766
9674
  ...pinOpts,
8767
9675
  });
9676
+ const compactionUsage = _compactionTokenUsage(stats);
9677
+ if (stats.degraded === true) {
9678
+ yield {
9679
+ type: "compaction-degraded",
9680
+ runId,
9681
+ reason: stats.degradedReason || "semantic-summary-degraded",
9682
+ summaryMode: stats.summaryMode || "extractive-fallback",
9683
+ stats,
9684
+ };
9685
+ }
8768
9686
  if (stats.saved > 0 && compacted.length < messages.length) {
8769
9687
  messages.splice(0, messages.length, ...compacted);
8770
9688
  // Persist the compaction so a later --resume rebuilds from the
@@ -8806,10 +9724,31 @@ export async function* agentLoop(messages, options) {
8806
9724
  });
8807
9725
  yield { type: "compaction", stats, runId };
8808
9726
  }
9727
+ if (compactionUsage) {
9728
+ // PromptCompressor calls the provider directly, outside the main
9729
+ // chat request. Apply/persist the resulting compaction and surface
9730
+ // any degraded fallback before yielding billable usage: a
9731
+ // headless consumer may stop the generator as soon as its cost cap
9732
+ // is exceeded. Usage still arrives exactly once and before the
9733
+ // next model call, without entering the normal response path.
9734
+ yield {
9735
+ type: "token-usage",
9736
+ provider: stats.summaryProvider || options.provider || null,
9737
+ model: stats.summaryModel || options.model || null,
9738
+ usage: compactionUsage,
9739
+ source: "semantic-compaction",
9740
+ runId,
9741
+ };
9742
+ }
8809
9743
  }
8810
9744
  } catch (_e) {
8811
9745
  if (isAbortError(_e) || signal?.aborted) throw _e;
8812
- // Compaction is best-effort — proceed with the uncompacted messages.
9746
+ yield {
9747
+ type: "compaction-degraded",
9748
+ runId,
9749
+ reason: `compaction_failed:${_e?.message || String(_e)}`,
9750
+ summaryMode: "none",
9751
+ };
8813
9752
  }
8814
9753
  }
8815
9754