chainlesschain 0.163.2 → 0.163.4

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 (222) hide show
  1. package/package.json +3 -2
  2. package/scripts/command-lifecycle-report.mjs +330 -38
  3. package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
  44. package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
  50. package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
  60. package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
  61. package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
  108. package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
  112. package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
  125. package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +2 -2
  152. package/src/commands/agenda.js +6 -5
  153. package/src/commands/agent.js +237 -75
  154. package/src/commands/agents.js +6 -0
  155. package/src/commands/artifacts.js +98 -0
  156. package/src/commands/background-session.js +11 -1
  157. package/src/commands/mcp.js +218 -31
  158. package/src/commands/review.js +221 -22
  159. package/src/commands/session-mcp-recovery.js +5 -5
  160. package/src/data/changelog.json +27 -1
  161. package/src/gateways/ws/ws-agent-handler.js +492 -14
  162. package/src/harness/jsonl-session-store.js +39 -1
  163. package/src/harness/mcp-client.js +414 -71
  164. package/src/harness/prompt-compressor.js +24 -0
  165. package/src/harness/provider-backed-compaction.js +205 -0
  166. package/src/harness/worktree-isolator.js +26 -3
  167. package/src/lib/agent-worktree.js +41 -2
  168. package/src/lib/app-preview.js +3 -0
  169. package/src/lib/background-agent-supervisor.js +790 -186
  170. package/src/lib/background-command-argv.js +318 -0
  171. package/src/lib/background-launch-profile.js +1 -0
  172. package/src/lib/background-phase-reporter.js +22 -2
  173. package/src/lib/background-worktree-policy.js +109 -0
  174. package/src/lib/command-lifecycle-report.js +1340 -145
  175. package/src/lib/cowork-mcp-tools.js +51 -6
  176. package/src/lib/cowork-task-runner.js +1 -0
  177. package/src/lib/delivery-coordinator.js +30 -5
  178. package/src/lib/delivery-production-adapter.js +1777 -0
  179. package/src/lib/delivery-production-runner.js +240 -0
  180. package/src/lib/durable-security-store.js +29 -17
  181. package/src/lib/execution-trace/trace-context.cjs +179 -0
  182. package/src/lib/execution-trace/trace-context.js +8 -175
  183. package/src/lib/mcp-call-ledger-store.js +58 -6
  184. package/src/lib/mcp-headers-helper.js +76 -8
  185. package/src/lib/mcp-host-recovery-runtime.js +2 -0
  186. package/src/lib/mcp-ledger-recovery-admission.js +39 -3
  187. package/src/lib/mcp-recovery-adjudication.js +256 -19
  188. package/src/lib/mcp-sandbox-policy.js +184 -0
  189. package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
  190. package/src/lib/mcp-stdio-executable-identity.js +33 -2
  191. package/src/lib/mcp-stdio-execution-authority.js +163 -0
  192. package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
  193. package/src/lib/mcp-stdio-package-materialization.js +721 -333
  194. package/src/lib/mcp-stdio-workspace-authority.js +252 -0
  195. package/src/lib/observability/index.js +2 -0
  196. package/src/lib/observability/otlp-exporter.js +2 -2
  197. package/src/lib/packer/pack-update-applier.js +8 -1
  198. package/src/lib/packer/pkg-config-generator.js +34 -0
  199. package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
  200. package/src/lib/process-execution-broker/index.js +213 -109
  201. package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
  202. package/src/lib/process-tree-termination.js +17 -1
  203. package/src/lib/scheduler-kernel/contract.js +315 -0
  204. package/src/lib/scheduler-kernel/store.js +1345 -0
  205. package/src/lib/session-host-lease.js +403 -30
  206. package/src/lib/session-persistence-failure.js +4 -1
  207. package/src/lib/skill-mcp.js +65 -4
  208. package/src/lib/sub-agent-context.js +8 -0
  209. package/src/lib/web-fetch.js +4 -4
  210. package/src/repl/agent-repl.js +285 -38
  211. package/src/runtime/agent-core.js +1044 -90
  212. package/src/runtime/agent-runtime.js +61 -8
  213. package/src/runtime/headless-runner.js +260 -166
  214. package/src/runtime/headless-stream.js +254 -25
  215. package/src/runtime/mcp-config.js +235 -28
  216. package/src/workers/background-agent-worker.js +492 -71
  217. package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
  218. package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
  219. package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
  220. package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
  221. package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
  222. package/src/assets/web-panel/assets/index-hUpvk6Jl.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 { randomUUID } from "node:crypto";
21
22
  import { isProxy } from "node:util/types";
22
23
  import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
23
24
  import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
@@ -127,6 +128,10 @@ import {
127
128
  } from "../harness/structured-handoff.js";
128
129
  import { isMcpRpcError } from "../harness/mcp-client.js";
129
130
  import { projectCanonicalResumeMessages } from "../lib/session-message-provenance.js";
131
+ import {
132
+ pathMatchesOpenedFileIdentitySync,
133
+ sameOpenedFileIdentity,
134
+ } from "../lib/packer/file-identity.js";
130
135
 
131
136
  export { formatProviderHttpError };
132
137
 
@@ -847,6 +852,22 @@ async function requestInteractivePermission(
847
852
  cwd,
848
853
  confirmArgs,
849
854
  ) {
855
+ if (context.hermeticExecution === true) {
856
+ // The coordinator-owned exact scope is itself the complete mutation
857
+ // authority for this hermetic run. It may authorize a sensitive *allowed*
858
+ // file, but it never authorizes credential reads or any tool outside the
859
+ // exact file-tool ceiling (those were rejected by the pure preflight).
860
+ if (
861
+ context.fileMutationScope != null &&
862
+ GUARDED_FILE_MUTATION_TOOLS.has(name)
863
+ ) {
864
+ return true;
865
+ }
866
+ const confirm = context.permissionConfirm || context.shellConfirm || null;
867
+ return typeof confirm === "function"
868
+ ? Boolean(await confirm(confirmArgs))
869
+ : false;
870
+ }
850
871
  const hooksV2 = await executeHooksV2Event(
851
872
  "PermissionRequest",
852
873
  {
@@ -1386,6 +1407,306 @@ const GUARDED_FILE_MUTATION_TOOLS = new Set([
1386
1407
  ...IDE_DIFF_EDIT_TOOLS,
1387
1408
  "notebook_edit",
1388
1409
  ]);
1410
+ const EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES = new Set([
1411
+ "read_file",
1412
+ "list_dir",
1413
+ "write_file",
1414
+ "edit_file",
1415
+ "edit_file_hashed",
1416
+ ]);
1417
+ const EXACT_FILE_MUTATION_OPENED_STATE_FIELDS = Object.freeze([
1418
+ "mode",
1419
+ "nlink",
1420
+ "size",
1421
+ "mtimeNs",
1422
+ "ctimeNs",
1423
+ ]);
1424
+ const EXACT_FILE_MUTATION_RENAME_STABLE_STATE_FIELDS = Object.freeze([
1425
+ "mode",
1426
+ "nlink",
1427
+ "size",
1428
+ "mtimeNs",
1429
+ ]);
1430
+ const NORMALIZED_EXACT_FILE_MUTATION_SCOPES = new WeakSet();
1431
+ const EXACT_FILE_MUTATION_SCOPE_STATES = new WeakMap();
1432
+ const WINDOWS_RESERVED_PATH_SEGMENT =
1433
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
1434
+
1435
+ function sameCanonicalPath(left, right) {
1436
+ return (
1437
+ typeof left === "string" &&
1438
+ typeof right === "string" &&
1439
+ path.relative(left, right) === "" &&
1440
+ path.relative(right, left) === ""
1441
+ );
1442
+ }
1443
+
1444
+ function exactRepoRelativeFilePath(value) {
1445
+ const segments = typeof value === "string" ? value.split("/") : [];
1446
+ if (
1447
+ typeof value !== "string" ||
1448
+ value.length === 0 ||
1449
+ value.includes("\0") ||
1450
+ value.includes("\\") ||
1451
+ path.posix.isAbsolute(value) ||
1452
+ /^[A-Za-z]:/u.test(value) ||
1453
+ path.posix.normalize(value) !== value ||
1454
+ value === "." ||
1455
+ value === ".." ||
1456
+ value.startsWith("../") ||
1457
+ segments.some(
1458
+ (segment) =>
1459
+ segment === "" ||
1460
+ segment === "." ||
1461
+ segment === ".." ||
1462
+ /[<>:"|?*]/u.test(segment) ||
1463
+ [...segment].some((character) => character.charCodeAt(0) <= 0x1f) ||
1464
+ /[ .]$/u.test(segment) ||
1465
+ WINDOWS_RESERVED_PATH_SEGMENT.test(segment),
1466
+ )
1467
+ ) {
1468
+ throw new Error(
1469
+ "path must be one portable canonical repo-relative file path",
1470
+ );
1471
+ }
1472
+ return value;
1473
+ }
1474
+
1475
+ function canonicalRealpath(candidate) {
1476
+ const realpath = fs.realpathSync.native || fs.realpathSync;
1477
+ return path.resolve(realpath(candidate));
1478
+ }
1479
+
1480
+ function filesystemIdentity(stats) {
1481
+ const mtimeNs =
1482
+ stats.mtimeNs != null
1483
+ ? String(stats.mtimeNs)
1484
+ : String(Math.trunc(Number(stats.mtimeMs) * 1_000_000));
1485
+ return Object.freeze({
1486
+ dev: String(stats.dev),
1487
+ ino: String(stats.ino),
1488
+ type: stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other",
1489
+ nlink: String(stats.nlink),
1490
+ size: String(stats.size),
1491
+ mtimeNs,
1492
+ });
1493
+ }
1494
+
1495
+ function sameFilesystemObjectIdentity(left, right) {
1496
+ return Boolean(
1497
+ left &&
1498
+ right &&
1499
+ left.dev === right.dev &&
1500
+ left.ino === right.ino &&
1501
+ left.type === right.type,
1502
+ );
1503
+ }
1504
+
1505
+ function sameFilesystemIdentity(left, right, { content = false } = {}) {
1506
+ return Boolean(
1507
+ sameFilesystemObjectIdentity(left, right) &&
1508
+ left.nlink === right.nlink &&
1509
+ (!content || (left.size === right.size && left.mtimeNs === right.mtimeNs)),
1510
+ );
1511
+ }
1512
+
1513
+ function lstatIdentity(candidate) {
1514
+ const stats = fs.lstatSync(candidate, { bigint: true });
1515
+ if (stats.isSymbolicLink()) {
1516
+ throw new Error(`filesystem alias is not allowed: ${candidate}`);
1517
+ }
1518
+ return { stats, identity: filesystemIdentity(stats) };
1519
+ }
1520
+
1521
+ function exactBindingState(canonicalRoot, relativePath, absolutePath) {
1522
+ const ancestors = [];
1523
+ let current = canonicalRoot;
1524
+ const root = lstatIdentity(current);
1525
+ if (!root.stats.isDirectory()) {
1526
+ throw new Error("mutation scope worktree root must be a directory");
1527
+ }
1528
+ ancestors.push(Object.freeze({ path: current, identity: root.identity }));
1529
+ const segments = relativePath.split("/");
1530
+ for (const segment of segments.slice(0, -1)) {
1531
+ current = path.join(current, segment);
1532
+ const ancestor = lstatIdentity(current);
1533
+ if (!ancestor.stats.isDirectory()) {
1534
+ throw new Error(`mutation scope parent is not a directory: ${current}`);
1535
+ }
1536
+ ancestors.push(
1537
+ Object.freeze({ path: current, identity: ancestor.identity }),
1538
+ );
1539
+ }
1540
+ const target = lstatIdentity(absolutePath);
1541
+ if (!target.stats.isFile()) {
1542
+ throw new Error(
1543
+ `mutation scope target must be an existing regular file: ${relativePath}`,
1544
+ );
1545
+ }
1546
+ if (target.stats.nlink !== 1n) {
1547
+ throw new Error(
1548
+ `mutation scope target must not have hard links: ${relativePath}`,
1549
+ );
1550
+ }
1551
+ return {
1552
+ ancestors: Object.freeze(ancestors),
1553
+ fileIdentity: target.identity,
1554
+ };
1555
+ }
1556
+
1557
+ function verifyExactScopeAncestors(state) {
1558
+ for (const ancestor of state.ancestors) {
1559
+ const current = lstatIdentity(ancestor.path);
1560
+ // Directory link counts are not portable identity fields. APFS counts all
1561
+ // directory entries, so creating our own staging file legitimately changes
1562
+ // nlink. Device + inode + directory type still fail closed on replacement.
1563
+ if (
1564
+ !current.stats.isDirectory() ||
1565
+ !sameFilesystemObjectIdentity(current.identity, ancestor.identity)
1566
+ ) {
1567
+ throw new Error(
1568
+ `mutation scope ancestor changed identity: ${ancestor.path}`,
1569
+ );
1570
+ }
1571
+ }
1572
+ }
1573
+
1574
+ function verifyExactBindingState(scope, binding) {
1575
+ const state = EXACT_FILE_MUTATION_SCOPE_STATES.get(scope)?.get(
1576
+ binding.relativePath,
1577
+ );
1578
+ if (!state) {
1579
+ throw new Error("mutation scope identity state is unavailable");
1580
+ }
1581
+ verifyExactScopeAncestors(state);
1582
+ const current = lstatIdentity(binding.absolutePath);
1583
+ if (
1584
+ !current.stats.isFile() ||
1585
+ current.stats.nlink !== 1n ||
1586
+ !sameFilesystemIdentity(current.identity, state.fileIdentity, {
1587
+ content: true,
1588
+ })
1589
+ ) {
1590
+ throw new Error(
1591
+ `mutation scope file changed identity or content: ${binding.relativePath}`,
1592
+ );
1593
+ }
1594
+ return state;
1595
+ }
1596
+
1597
+ /**
1598
+ * Bind an exact mutation scope to one canonical worktree and a fixed set of
1599
+ * repo-relative file identities. This is intentionally stricter than the
1600
+ * general workspace guard: aliases through symlinks/junctions are rejected,
1601
+ * even when they resolve to another location inside the same worktree.
1602
+ */
1603
+ export function normalizeExactFileMutationScope(
1604
+ scope,
1605
+ { cwd = process.cwd() } = {},
1606
+ ) {
1607
+ if (scope == null) return null;
1608
+ if (NORMALIZED_EXACT_FILE_MUTATION_SCOPES.has(scope)) {
1609
+ const canonicalCwd = canonicalRealpath(cwd);
1610
+ if (!sameCanonicalPath(canonicalCwd, scope.worktreeRoot)) {
1611
+ throw new Error("mutation scope worktree does not match the run cwd");
1612
+ }
1613
+ return scope;
1614
+ }
1615
+ if (
1616
+ !scope ||
1617
+ typeof scope !== "object" ||
1618
+ Array.isArray(scope) ||
1619
+ scope.exact !== true
1620
+ ) {
1621
+ throw new Error("mutation scope must be an exact object");
1622
+ }
1623
+ if (
1624
+ typeof scope.worktreeRoot !== "string" ||
1625
+ scope.worktreeRoot.length === 0 ||
1626
+ scope.worktreeRoot.includes("\0") ||
1627
+ !path.isAbsolute(scope.worktreeRoot) ||
1628
+ path.normalize(scope.worktreeRoot) !== scope.worktreeRoot
1629
+ ) {
1630
+ throw new Error(
1631
+ "mutation scope worktree root must be canonical and absolute",
1632
+ );
1633
+ }
1634
+ const canonicalRoot = canonicalRealpath(scope.worktreeRoot);
1635
+ if (!sameCanonicalPath(canonicalRoot, scope.worktreeRoot)) {
1636
+ throw new Error("mutation scope worktree root must be its real path");
1637
+ }
1638
+ if (!fs.statSync(canonicalRoot).isDirectory()) {
1639
+ throw new Error("mutation scope worktree root must be a directory");
1640
+ }
1641
+ const canonicalCwd = canonicalRealpath(cwd);
1642
+ if (!sameCanonicalPath(canonicalCwd, canonicalRoot)) {
1643
+ throw new Error("mutation scope worktree does not match the run cwd");
1644
+ }
1645
+ if (!Array.isArray(scope.allowedPaths) || scope.allowedPaths.length === 0) {
1646
+ throw new Error("mutation scope requires at least one allowed path");
1647
+ }
1648
+
1649
+ const allowedPaths = scope.allowedPaths.map(exactRepoRelativeFilePath);
1650
+ if (new Set(allowedPaths).size !== allowedPaths.length) {
1651
+ throw new Error("mutation scope allowed paths must be unique");
1652
+ }
1653
+ const bindings = allowedPaths.map((relativePath) => {
1654
+ const absolutePath = path.resolve(
1655
+ canonicalRoot,
1656
+ ...relativePath.split("/"),
1657
+ );
1658
+ const verdict = resolveSandboxPolicyPath(relativePath, {
1659
+ access: "write",
1660
+ cwd: canonicalRoot,
1661
+ workspaceRoots: [canonicalRoot],
1662
+ sandbox: null,
1663
+ });
1664
+ if (!verdict.ok) {
1665
+ throw new Error(
1666
+ `mutation scope path "${relativePath}" is unsafe: ${verdict.error}`,
1667
+ );
1668
+ }
1669
+ if (
1670
+ !sameCanonicalPath(verdict.path, absolutePath) ||
1671
+ !sameCanonicalPath(verdict.canonicalPath, absolutePath)
1672
+ ) {
1673
+ throw new Error(
1674
+ `mutation scope path "${relativePath}" traverses a filesystem alias`,
1675
+ );
1676
+ }
1677
+ if (!fs.existsSync(absolutePath)) {
1678
+ throw new Error(
1679
+ `mutation scope target must already exist: ${relativePath}`,
1680
+ );
1681
+ }
1682
+ return Object.freeze({
1683
+ relativePath,
1684
+ absolutePath,
1685
+ canonicalPath: verdict.canonicalPath,
1686
+ });
1687
+ });
1688
+ const normalized = Object.freeze({
1689
+ exact: true,
1690
+ worktreeRoot: canonicalRoot,
1691
+ allowedPaths: Object.freeze([...allowedPaths]),
1692
+ bindings: Object.freeze(bindings),
1693
+ });
1694
+ NORMALIZED_EXACT_FILE_MUTATION_SCOPES.add(normalized);
1695
+ EXACT_FILE_MUTATION_SCOPE_STATES.set(
1696
+ normalized,
1697
+ new Map(
1698
+ bindings.map((binding) => [
1699
+ binding.relativePath,
1700
+ exactBindingState(
1701
+ canonicalRoot,
1702
+ binding.relativePath,
1703
+ binding.absolutePath,
1704
+ ),
1705
+ ]),
1706
+ ),
1707
+ );
1708
+ return normalized;
1709
+ }
1389
1710
 
1390
1711
  function agentFileToolPathRequests(name, args = {}, workspaceRoots = []) {
1391
1712
  const one = (argument, access) =>
@@ -1477,6 +1798,129 @@ function fileMutationPaths(name, args = {}) {
1477
1798
  ].filter(Boolean);
1478
1799
  }
1479
1800
 
1801
+ function guardExactFileMutationScope(name, args, context, cwd) {
1802
+ if (context.fileMutationScope == null) {
1803
+ return null;
1804
+ }
1805
+
1806
+ if (!EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES.has(name)) {
1807
+ return {
1808
+ error: `[File Mutation Scope] Tool "${name}" is outside the hermetic file-tool ceiling.`,
1809
+ policy: {
1810
+ decision: "deny",
1811
+ via: "exact-file-mutation-scope",
1812
+ reason: "tool-not-allowed",
1813
+ },
1814
+ };
1815
+ }
1816
+ if (!GUARDED_FILE_MUTATION_TOOLS.has(name)) return null;
1817
+
1818
+ let scope;
1819
+ try {
1820
+ scope = normalizeExactFileMutationScope(context.fileMutationScope, { cwd });
1821
+ } catch (error) {
1822
+ return {
1823
+ error: `[File Mutation Scope] Invalid exact scope: ${error.message}.`,
1824
+ policy: {
1825
+ decision: "deny",
1826
+ via: "exact-file-mutation-scope",
1827
+ reason: "invalid-scope",
1828
+ },
1829
+ };
1830
+ }
1831
+
1832
+ const requestedPaths = fileMutationPaths(name, args);
1833
+ if (requestedPaths.length === 0) {
1834
+ return {
1835
+ error: `[File Mutation Scope] ${name} did not provide a mutation path.`,
1836
+ policy: {
1837
+ decision: "deny",
1838
+ via: "exact-file-mutation-scope",
1839
+ reason: "invalid-path",
1840
+ },
1841
+ };
1842
+ }
1843
+
1844
+ for (const requestedPath of requestedPaths) {
1845
+ let relativePath;
1846
+ try {
1847
+ relativePath = exactRepoRelativeFilePath(requestedPath);
1848
+ } catch (error) {
1849
+ return {
1850
+ error:
1851
+ `[File Mutation Scope] ${name} path "${requestedPath}" was blocked: ` +
1852
+ `${error.message}.`,
1853
+ policy: {
1854
+ decision: "deny",
1855
+ via: "exact-file-mutation-scope",
1856
+ reason: "non-canonical-path",
1857
+ },
1858
+ };
1859
+ }
1860
+ const binding = scope.bindings.find(
1861
+ (candidate) => candidate.relativePath === relativePath,
1862
+ );
1863
+ if (!binding) {
1864
+ return {
1865
+ error: `[File Mutation Scope] ${name} path "${requestedPath}" is not in the exact allowed file set.`,
1866
+ policy: {
1867
+ decision: "deny",
1868
+ via: "exact-file-mutation-scope",
1869
+ reason: "path-not-allowed",
1870
+ },
1871
+ };
1872
+ }
1873
+ const verdict = resolveSandboxPolicyPath(relativePath, {
1874
+ access: "write",
1875
+ cwd: scope.worktreeRoot,
1876
+ workspaceRoots: [scope.worktreeRoot],
1877
+ sandbox: null,
1878
+ });
1879
+ if (
1880
+ !verdict.ok ||
1881
+ !sameCanonicalPath(verdict.path, binding.absolutePath) ||
1882
+ !sameCanonicalPath(verdict.canonicalPath, binding.canonicalPath) ||
1883
+ !sameCanonicalPath(verdict.canonicalPath, binding.absolutePath)
1884
+ ) {
1885
+ return {
1886
+ error: `[File Mutation Scope] ${name} path "${requestedPath}" changed physical identity or escaped its exact binding.`,
1887
+ policy: {
1888
+ decision: "deny",
1889
+ via: "exact-file-mutation-scope",
1890
+ reason: verdict.ok ? "path-identity-changed" : verdict.reason,
1891
+ },
1892
+ };
1893
+ }
1894
+ try {
1895
+ verifyExactBindingState(scope, binding);
1896
+ } catch (error) {
1897
+ return {
1898
+ error: `[File Mutation Scope] ${name} path "${requestedPath}" was blocked: ${error.message}.`,
1899
+ policy: {
1900
+ decision: "deny",
1901
+ via: "exact-file-mutation-scope",
1902
+ reason: "path-identity-changed",
1903
+ },
1904
+ };
1905
+ }
1906
+ }
1907
+ return null;
1908
+ }
1909
+
1910
+ export function preflightToolExecutionAuthority(name, args, context = {}) {
1911
+ const cwd = context.cwd || process.cwd();
1912
+ if (
1913
+ Array.isArray(context.effectiveAllowedToolNames) &&
1914
+ !context.effectiveAllowedToolNames.includes(name)
1915
+ ) {
1916
+ return {
1917
+ error: `[Tool Capability] Tool "${name}" is outside this run's effective tool set.`,
1918
+ policy: { decision: "blocked", via: "effective-tool-set" },
1919
+ };
1920
+ }
1921
+ return guardExactFileMutationScope(name, args, context, cwd);
1922
+ }
1923
+
1480
1924
  /**
1481
1925
  * Quote-aware shell tokenizer (mirrors gateways/ws `tokenizeCommand`). The `git`
1482
1926
  * tool runs the model-supplied command via argv (spawnSync, NO shell), so a
@@ -1795,19 +2239,14 @@ export async function executeTool(name, args, context = {}) {
1795
2239
  const skillLoader = context.skillLoader || _defaultSkillLoader;
1796
2240
  const cwd = context.cwd || process.cwd();
1797
2241
  const planManager = context.planManager || getPlanModeManager();
1798
- // The provider receives a filtered tool schema, but an untrusted/buggy model
1799
- // can still emit an arbitrary tool_call. Enforce the exact same immutable
1800
- // capability set again at execution time; null/absent preserves direct-call
1801
- // compatibility, while [] is an explicit deny-all ceiling.
1802
- if (
1803
- Array.isArray(context.effectiveAllowedToolNames) &&
1804
- !context.effectiveAllowedToolNames.includes(name)
1805
- ) {
1806
- return {
1807
- error: `[Tool Capability] Tool "${name}" is outside this run's effective tool set.`,
1808
- policy: { decision: "blocked", via: "effective-tool-set" },
1809
- };
1810
- }
2242
+ // The provider receives a filtered schema, but an untrusted/buggy model can
2243
+ // still emit an arbitrary tool_call. Reuse the same pure preflight that the
2244
+ // outer loop runs before checkpoints or execution events.
2245
+ const authorityDenial = preflightToolExecutionAuthority(name, args, {
2246
+ ...context,
2247
+ cwd,
2248
+ });
2249
+ if (authorityDenial) return authorityDenial;
1811
2250
  const localToolDescriptor =
1812
2251
  context.externalToolDescriptors &&
1813
2252
  typeof context.externalToolDescriptors === "object"
@@ -1936,7 +2375,8 @@ export async function executeTool(name, args, context = {}) {
1936
2375
  };
1937
2376
 
1938
2377
  // Persona toolsDisabled guard
1939
- const persona = _loadProjectPersona(cwd);
2378
+ const persona =
2379
+ context.hermeticExecution === true ? null : _loadProjectPersona(cwd);
1940
2380
  if (persona?.toolsDisabled?.includes(name)) {
1941
2381
  return {
1942
2382
  error: `Tool "${name}" is disabled by project persona configuration.`,
@@ -2320,7 +2760,7 @@ export async function executeTool(name, args, context = {}) {
2320
2760
  // capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
2321
2761
  // `ask` routes to the confirmer. Runs after permission resolution so a
2322
2762
  // settings deny / host deny short-circuits before any hook process spawns.
2323
- if (!planReadOnlyFenceActive) {
2763
+ if (!planReadOnlyFenceActive && context.hermeticExecution !== true) {
2324
2764
  const hooksV2Pre = await executeHooksV2Event(
2325
2765
  "PreToolUse",
2326
2766
  {
@@ -2427,6 +2867,7 @@ export async function executeTool(name, args, context = {}) {
2427
2867
  mcpHostClient: context.mcpHostClient || context.mcpClient || null,
2428
2868
  mcpCallLedger: context.mcpCallLedger || null,
2429
2869
  mcpConflictScheduler: context.mcpConflictScheduler || null,
2870
+ mcpDispatchAdmission: context.mcpDispatchAdmission || null,
2430
2871
  subtreeInstructionScope:
2431
2872
  context.subtreeInstructionScope || context.sessionId || "__legacy__",
2432
2873
  shellPolicyOverrides: context.shellPolicyOverrides || null,
@@ -2453,9 +2894,15 @@ export async function executeTool(name, args, context = {}) {
2453
2894
  toolAdmission: context.toolAdmission || null,
2454
2895
  unattendedActionPolicy: context.unattendedActionPolicy || null,
2455
2896
  managedCheckpoint: context.managedCheckpoint === true,
2897
+ fileMutationScope: context.fileMutationScope || null,
2898
+ hermeticExecution: context.hermeticExecution === true,
2456
2899
  });
2457
2900
  } catch (err) {
2458
- if (hookDb && !planReadOnlyFenceActive) {
2901
+ if (
2902
+ hookDb &&
2903
+ !planReadOnlyFenceActive &&
2904
+ context.hermeticExecution !== true
2905
+ ) {
2459
2906
  try {
2460
2907
  await executeHooks(hookDb, HookEvents.ToolError, {
2461
2908
  tool: name,
@@ -2486,7 +2933,11 @@ export async function executeTool(name, args, context = {}) {
2486
2933
  }
2487
2934
 
2488
2935
  // PostToolUse hook
2489
- if (hookDb && !planReadOnlyFenceActive) {
2936
+ if (
2937
+ hookDb &&
2938
+ !planReadOnlyFenceActive &&
2939
+ context.hermeticExecution !== true
2940
+ ) {
2490
2941
  try {
2491
2942
  await executeHooks(hookDb, HookEvents.PostToolUse, {
2492
2943
  tool: name,
@@ -2506,6 +2957,7 @@ export async function executeTool(name, args, context = {}) {
2506
2957
  // reason is attached as `hookFeedback` to be surfaced back to the model.
2507
2958
  if (
2508
2959
  !planReadOnlyFenceActive &&
2960
+ context.hermeticExecution !== true &&
2509
2961
  context.settingsHooks &&
2510
2962
  toolResult &&
2511
2963
  typeof toolResult === "object"
@@ -2691,6 +3143,196 @@ export function writeFileVerified(filePath, content, fsImpl = fs) {
2691
3143
  return { size: actual };
2692
3144
  }
2693
3145
 
3146
+ function writeFileVerifiedWithinExactScope(
3147
+ fileMutationScope,
3148
+ requestedPath,
3149
+ content,
3150
+ cwd,
3151
+ ) {
3152
+ let scope;
3153
+ let relativePath;
3154
+ let binding;
3155
+ let state;
3156
+ try {
3157
+ scope = normalizeExactFileMutationScope(fileMutationScope, { cwd });
3158
+ relativePath = exactRepoRelativeFilePath(requestedPath);
3159
+ binding = scope.bindings.find(
3160
+ (candidate) => candidate.relativePath === relativePath,
3161
+ );
3162
+ if (!binding) throw new Error("path is not in the exact allowed file set");
3163
+ state = verifyExactBindingState(scope, binding);
3164
+ } catch (error) {
3165
+ return {
3166
+ error: `[File Mutation Scope] Bound write refused: ${error.message}`,
3167
+ policy: {
3168
+ decision: "deny",
3169
+ via: "exact-file-mutation-scope",
3170
+ reason: "path-identity-changed",
3171
+ },
3172
+ };
3173
+ }
3174
+
3175
+ const expected = Buffer.byteLength(content, "utf8");
3176
+ let descriptor;
3177
+ let temporaryPath;
3178
+ let temporaryIdentity;
3179
+ let replacementCommitted = false;
3180
+ try {
3181
+ // Never truncate the bound inode: it may be renamed outside the authorized
3182
+ // worktree after the guard. Stage new bytes in the same verified directory
3183
+ // and replace only the authorized pathname atomically. A concurrent rename
3184
+ // can move the old inode, but cannot turn this write into a mutation of it.
3185
+ const source = lstatIdentity(binding.absolutePath);
3186
+ if (
3187
+ !source.stats.isFile() ||
3188
+ source.stats.nlink !== 1n ||
3189
+ !sameFilesystemIdentity(source.identity, state.fileIdentity, {
3190
+ content: true,
3191
+ })
3192
+ ) {
3193
+ throw new Error("source file no longer matches the bound identity");
3194
+ }
3195
+
3196
+ const directory = path.dirname(binding.absolutePath);
3197
+ temporaryPath = path.join(
3198
+ directory,
3199
+ `.chainlesschain-fix-${process.pid}-${randomUUID()}.tmp`,
3200
+ );
3201
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
3202
+ descriptor = fs.openSync(
3203
+ temporaryPath,
3204
+ fs.constants.O_WRONLY |
3205
+ fs.constants.O_CREAT |
3206
+ fs.constants.O_EXCL |
3207
+ noFollow,
3208
+ Number(source.stats.mode & 0o777n),
3209
+ );
3210
+ const openedTemporary = fs.fstatSync(descriptor, { bigint: true });
3211
+ temporaryIdentity = filesystemIdentity(openedTemporary);
3212
+ if (!openedTemporary.isFile() || openedTemporary.nlink !== 1n) {
3213
+ throw new Error("staged file did not open as one regular file");
3214
+ }
3215
+ if (process.platform !== "win32") {
3216
+ fs.fchmodSync(descriptor, Number(source.stats.mode & 0o777n));
3217
+ }
3218
+ fs.writeFileSync(descriptor, content, { encoding: "utf8" });
3219
+ fs.fsyncSync(descriptor);
3220
+ const staged = fs.fstatSync(descriptor, { bigint: true });
3221
+ const stagedIdentity = filesystemIdentity(staged);
3222
+ if (
3223
+ !staged.isFile() ||
3224
+ staged.nlink !== 1n ||
3225
+ !sameFilesystemIdentity(stagedIdentity, temporaryIdentity) ||
3226
+ staged.size !== BigInt(expected)
3227
+ ) {
3228
+ throw new Error("staged write verification failed");
3229
+ }
3230
+
3231
+ // Revalidate both the original binding and the staged pathname immediately
3232
+ // before the atomic replacement. O_EXCL plus the descriptor/path identity
3233
+ // comparison prevents a pre-created alias from being substituted here.
3234
+ verifyExactBindingState(scope, binding);
3235
+ const stagedPath = lstatIdentity(temporaryPath);
3236
+ const stagedPathMatches =
3237
+ process.platform === "win32"
3238
+ ? pathMatchesOpenedFileIdentitySync(temporaryPath, staged, {
3239
+ stateFields: EXACT_FILE_MUTATION_OPENED_STATE_FIELDS,
3240
+ })
3241
+ : sameFilesystemIdentity(stagedPath.identity, stagedIdentity, {
3242
+ content: true,
3243
+ });
3244
+ if (
3245
+ !stagedPath.stats.isFile() ||
3246
+ stagedPath.stats.nlink !== 1n ||
3247
+ !stagedPathMatches
3248
+ ) {
3249
+ throw new Error("staged file path changed identity or content");
3250
+ }
3251
+
3252
+ fs.renameSync(temporaryPath, binding.absolutePath);
3253
+ replacementCommitted = true;
3254
+ temporaryPath = undefined;
3255
+
3256
+ const installed = lstatIdentity(binding.absolutePath);
3257
+ verifyExactScopeAncestors(state);
3258
+ const installedWriter =
3259
+ process.platform === "win32"
3260
+ ? fs.fstatSync(descriptor, { bigint: true })
3261
+ : staged;
3262
+ const installedPathMatches =
3263
+ process.platform === "win32"
3264
+ ? installedWriter.isFile() &&
3265
+ installedWriter.nlink === 1n &&
3266
+ sameOpenedFileIdentity(
3267
+ staged,
3268
+ installedWriter,
3269
+ EXACT_FILE_MUTATION_RENAME_STABLE_STATE_FIELDS,
3270
+ ) &&
3271
+ pathMatchesOpenedFileIdentitySync(
3272
+ binding.absolutePath,
3273
+ installedWriter,
3274
+ {
3275
+ stateFields: EXACT_FILE_MUTATION_OPENED_STATE_FIELDS,
3276
+ },
3277
+ )
3278
+ : sameFilesystemIdentity(installed.identity, stagedIdentity, {
3279
+ content: true,
3280
+ });
3281
+ if (
3282
+ !installed.stats.isFile() ||
3283
+ installed.stats.nlink !== 1n ||
3284
+ !installedPathMatches ||
3285
+ installed.stats.size !== BigInt(expected)
3286
+ ) {
3287
+ throw new Error("installed replacement failed identity verification");
3288
+ }
3289
+ state.fileIdentity = installed.identity;
3290
+
3291
+ // POSIX needs a directory fsync to durably publish the rename. Windows
3292
+ // does not support fsync on directory handles through Node.
3293
+ if (process.platform !== "win32") {
3294
+ const directoryDescriptor = fs.openSync(directory, fs.constants.O_RDONLY);
3295
+ try {
3296
+ fs.fsyncSync(directoryDescriptor);
3297
+ } finally {
3298
+ fs.closeSync(directoryDescriptor);
3299
+ }
3300
+ }
3301
+ return { size: Number(installed.stats.size) };
3302
+ } catch (error) {
3303
+ return {
3304
+ error: `[File Mutation Scope] Bound write failed: ${error.message}`,
3305
+ ...(replacementCommitted ? { outcomeUnknown: true } : {}),
3306
+ ...(temporaryPath
3307
+ ? {
3308
+ cleanupRequired: true,
3309
+ unsettledStage: path.basename(temporaryPath),
3310
+ }
3311
+ : {}),
3312
+ policy: {
3313
+ decision: "deny",
3314
+ via: "exact-file-mutation-scope",
3315
+ reason: replacementCommitted
3316
+ ? "bound-write-outcome-unknown"
3317
+ : "bound-write-failed",
3318
+ },
3319
+ };
3320
+ } finally {
3321
+ if (descriptor !== undefined) {
3322
+ try {
3323
+ fs.closeSync(descriptor);
3324
+ } catch {
3325
+ /* the write result already records the authoritative failure */
3326
+ }
3327
+ }
3328
+ // Never unlink a failed staging pathname here. Another process can replace
3329
+ // either that entry or its parent between any identity check and unlink;
3330
+ // preserving an explicitly reported orphan is safer than deleting an
3331
+ // unknown file. A separately authorized identity-aware maintenance pass
3332
+ // may reconcile it later.
3333
+ }
3334
+ }
3335
+
2694
3336
  // ── Edit-concurrency (read-freshness) guard ──────────────────────────────
2695
3337
  //
2696
3338
  // Per-session map: resolved file path → the mtimeMs the agent last OBSERVED for
@@ -3592,8 +4234,10 @@ async function executeToolInner(
3592
4234
  externalToolExecutors,
3593
4235
  extraToolDefinitions = null,
3594
4236
  mcpClient,
4237
+ mcpHostClient = mcpClient,
3595
4238
  mcpCallLedger = null,
3596
4239
  mcpConflictScheduler = null,
4240
+ mcpDispatchAdmission = null,
3597
4241
  subtreeInstructionScope = "__legacy__",
3598
4242
  memoryDb = null,
3599
4243
  permanentMemory = null,
@@ -3618,6 +4262,8 @@ async function executeToolInner(
3618
4262
  toolAdmission = null,
3619
4263
  unattendedActionPolicy = null,
3620
4264
  managedCheckpoint = false,
4265
+ fileMutationScope = null,
4266
+ hermeticExecution = false,
3621
4267
  // Hook-envelope tracing: this run's trace id, threaded into child loops
3622
4268
  // (spawn_sub_agent / isolated run_skill) as their parent_id.
3623
4269
  hookTraceId = null,
@@ -3758,6 +4404,13 @@ async function executeToolInner(
3758
4404
  }
3759
4405
 
3760
4406
  case "write_file": {
4407
+ const sinkDenial = guardExactFileMutationScope(
4408
+ name,
4409
+ args,
4410
+ { fileMutationScope },
4411
+ cwd,
4412
+ );
4413
+ if (sinkDenial) return attachDescriptor(sinkDenial);
3761
4414
  const filePath = path.resolve(cwd, args.path);
3762
4415
  const dir = path.dirname(filePath);
3763
4416
  // Overwriting an existing file: refuse if it changed on disk since the
@@ -3766,11 +4419,18 @@ async function executeToolInner(
3766
4419
  const stale = _checkFileFreshness(filePath);
3767
4420
  if (stale) return attachDescriptor({ error: stale });
3768
4421
  }
3769
- if (!fs.existsSync(dir)) {
4422
+ if (!fileMutationScope && !fs.existsSync(dir)) {
3770
4423
  fs.mkdirSync(dir, { recursive: true });
3771
4424
  }
3772
- const wrote = writeFileVerified(filePath, args.content);
3773
- if (wrote.error) return attachDescriptor({ error: wrote.error });
4425
+ const wrote = fileMutationScope
4426
+ ? writeFileVerifiedWithinExactScope(
4427
+ fileMutationScope,
4428
+ args.path,
4429
+ args.content,
4430
+ cwd,
4431
+ )
4432
+ : writeFileVerified(filePath, args.content);
4433
+ if (wrote.error) return attachDescriptor(wrote);
3774
4434
  _recordFileObservation(filePath);
3775
4435
  return attachDescriptor(
3776
4436
  await _withSubtreeInstructions(
@@ -3779,7 +4439,7 @@ async function executeToolInner(
3779
4439
  filePath,
3780
4440
  cwd,
3781
4441
  additionalDirectories,
3782
- managedCheckpoint,
4442
+ managedCheckpoint || hermeticExecution,
3783
4443
  ),
3784
4444
  filePath,
3785
4445
  cwd,
@@ -3890,6 +4550,13 @@ async function executeToolInner(
3890
4550
  }
3891
4551
 
3892
4552
  case "edit_file": {
4553
+ const sinkDenial = guardExactFileMutationScope(
4554
+ name,
4555
+ args,
4556
+ { fileMutationScope },
4557
+ cwd,
4558
+ );
4559
+ if (sinkDenial) return attachDescriptor(sinkDenial);
3893
4560
  const filePath = path.resolve(cwd, args.path);
3894
4561
  if (!fs.existsSync(filePath)) {
3895
4562
  return attachDescriptor({ error: `File not found: ${filePath}` });
@@ -3947,8 +4614,15 @@ async function executeToolInner(
3947
4614
  occurrences: count,
3948
4615
  });
3949
4616
  }
3950
- const wrote = writeFileVerified(filePath, newContent);
3951
- if (wrote.error) return attachDescriptor({ error: wrote.error });
4617
+ const wrote = fileMutationScope
4618
+ ? writeFileVerifiedWithinExactScope(
4619
+ fileMutationScope,
4620
+ args.path,
4621
+ newContent,
4622
+ cwd,
4623
+ )
4624
+ : writeFileVerified(filePath, newContent);
4625
+ if (wrote.error) return attachDescriptor(wrote);
3952
4626
  _recordFileObservation(filePath);
3953
4627
  return attachDescriptor(
3954
4628
  await _withSubtreeInstructions(
@@ -3962,7 +4636,7 @@ async function executeToolInner(
3962
4636
  filePath,
3963
4637
  cwd,
3964
4638
  additionalDirectories,
3965
- managedCheckpoint,
4639
+ managedCheckpoint || hermeticExecution,
3966
4640
  ),
3967
4641
  filePath,
3968
4642
  cwd,
@@ -3972,6 +4646,13 @@ async function executeToolInner(
3972
4646
  }
3973
4647
 
3974
4648
  case "edit_file_hashed": {
4649
+ const sinkDenial = guardExactFileMutationScope(
4650
+ name,
4651
+ args,
4652
+ { fileMutationScope },
4653
+ cwd,
4654
+ );
4655
+ if (sinkDenial) return attachDescriptor(sinkDenial);
3975
4656
  // Hash-anchored edit (v5.0.2.9, inspired by oh-my-openagent).
3976
4657
  // Reference a line by its content hash rather than line number or
3977
4658
  // exact string — robust against whitespace drift and concurrent edits.
@@ -4012,8 +4693,15 @@ async function executeToolInner(
4012
4693
  ...(snippet && { current_snippet: snippet }),
4013
4694
  });
4014
4695
  }
4015
- const wrote = writeFileVerified(filePath, result.content);
4016
- if (wrote.error) return attachDescriptor({ error: wrote.error });
4696
+ const wrote = fileMutationScope
4697
+ ? writeFileVerifiedWithinExactScope(
4698
+ fileMutationScope,
4699
+ args.path,
4700
+ result.content,
4701
+ cwd,
4702
+ )
4703
+ : writeFileVerified(filePath, result.content);
4704
+ if (wrote.error) return attachDescriptor(wrote);
4017
4705
  _recordFileObservation(filePath);
4018
4706
  return attachDescriptor(
4019
4707
  await _withSubtreeInstructions(
@@ -4028,7 +4716,7 @@ async function executeToolInner(
4028
4716
  filePath,
4029
4717
  cwd,
4030
4718
  additionalDirectories,
4031
- managedCheckpoint,
4719
+ managedCheckpoint || hermeticExecution,
4032
4720
  ),
4033
4721
  filePath,
4034
4722
  cwd,
@@ -5014,8 +5702,10 @@ async function executeToolInner(
5014
5702
  // Parent MCP plumbing — a spawn can inherit these into the child,
5015
5703
  // filtered by the resolved contract's mcpServers allow-list.
5016
5704
  mcpClient,
5705
+ mcpHostClient,
5017
5706
  mcpCallLedger,
5018
5707
  mcpConflictScheduler,
5708
+ mcpDispatchAdmission,
5019
5709
  externalToolDescriptors,
5020
5710
  externalToolExecutors,
5021
5711
  extraToolDefinitions,
@@ -6174,6 +6864,21 @@ async function executeToolInner(
6174
6864
  });
6175
6865
  }
6176
6866
 
6867
+ if (
6868
+ mcpDispatchAdmission !== null &&
6869
+ typeof mcpDispatchAdmission !== "function"
6870
+ ) {
6871
+ return attachDescriptor({
6872
+ error:
6873
+ "MCP tool blocked because the host dispatch authority is invalid",
6874
+ policy: {
6875
+ decision: "blocked",
6876
+ via: "mcp-dispatch-admission",
6877
+ code: "CC_MCP_DISPATCH_ADMISSION_INVALID",
6878
+ },
6879
+ });
6880
+ }
6881
+
6177
6882
  const schedulerScopes = mcpLedgerScopes(mcpWireInput);
6178
6883
  const ledgerEffectContract = mcpLedgerEffectContract(
6179
6884
  localToolDescriptor,
@@ -6233,9 +6938,12 @@ async function executeToolInner(
6233
6938
 
6234
6939
  const transportFailure = async (callError, phase = "call") => {
6235
6940
  const definitiveRpcFailure = isMcpRpcError(callError);
6941
+ const definitelyNotDispatched =
6942
+ safeMcpProperty(callError, "dispatched") === false;
6236
6943
  if (
6237
6944
  mcpTransportOutcomeIsUnsafe(ledgerEffectContract) &&
6238
- !definitiveRpcFailure
6945
+ !definitiveRpcFailure &&
6946
+ !definitelyNotDispatched
6239
6947
  ) {
6240
6948
  const resultAdmissionFailure =
6241
6949
  isMcpToolResultAdmissionError(callError);
@@ -6281,7 +6989,14 @@ async function executeToolInner(
6281
6989
  localToolExecutor.toolName,
6282
6990
  mcpWireInput,
6283
6991
  ];
6284
- if (signal) callArguments.push({ signal });
6992
+ if (signal || mcpDispatchAdmission) {
6993
+ callArguments.push({
6994
+ ...(signal ? { signal } : {}),
6995
+ ...(mcpDispatchAdmission
6996
+ ? { dispatchAdmission: mcpDispatchAdmission }
6997
+ : {}),
6998
+ });
6999
+ }
6285
7000
  pendingResult = Reflect.apply(
6286
7001
  mcpClient.callTool,
6287
7002
  mcpClient,
@@ -7704,10 +8419,14 @@ async function _executeSpawnSubAgent(args, ctx) {
7704
8419
  externalToolDescriptors: inheritedMcp.externalToolDescriptors,
7705
8420
  externalToolExecutors: inheritedMcp.externalToolExecutors,
7706
8421
  mcpClient: inheritedMcp.mcpClient,
8422
+ mcpHostClient: ctx.mcpHostClient || inheritedMcp.mcpClient,
7707
8423
  ...(ctx.mcpCallLedger ? { mcpCallLedger: ctx.mcpCallLedger } : {}),
7708
8424
  ...(ctx.mcpConflictScheduler
7709
8425
  ? { mcpConflictScheduler: ctx.mcpConflictScheduler }
7710
8426
  : {}),
8427
+ ...(ctx.mcpDispatchAdmission
8428
+ ? { mcpDispatchAdmission: ctx.mcpDispatchAdmission }
8429
+ : {}),
7711
8430
  }
7712
8431
  : {}),
7713
8432
  ...(inheritedHooks ? { settingsHooks: inheritedHooks } : {}),
@@ -7997,7 +8716,10 @@ async function _executeSpawnSubAgent(args, ctx) {
7997
8716
  // ─── LLM chat with tools ─────────────────────────────────────────────────
7998
8717
 
7999
8718
  function getEffectiveToolDefinitions(options = {}) {
8000
- const persona = _loadProjectPersona(options.cwd);
8719
+ const persona =
8720
+ options.hermeticExecution === true
8721
+ ? null
8722
+ : _loadProjectPersona(options.cwd);
8001
8723
  // Merge every deny source before both schema projection and execution-time
8002
8724
  // enforcement. Keeping this in one helper prevents those two fences from
8003
8725
  // drifting apart.
@@ -9484,6 +10206,93 @@ function _compactionTokenUsage(stats) {
9484
10206
  };
9485
10207
  }
9486
10208
 
10209
+ function _sameMessageSnapshot(messages, expectedMessages) {
10210
+ return (
10211
+ messages.length === expectedMessages.length &&
10212
+ messages.every((message, index) => message === expectedMessages[index])
10213
+ );
10214
+ }
10215
+
10216
+ function _compactionSettlementError(code, message, cause = null) {
10217
+ const error = new Error(message, cause ? { cause } : undefined);
10218
+ error.code = code;
10219
+ return error;
10220
+ }
10221
+
10222
+ /**
10223
+ * Settle one automatic compaction candidate before changing the live array.
10224
+ * Provider work has already completed when this runs. The actual canonical
10225
+ * compare-and-append remains synchronous, so no JavaScript turn can interleave
10226
+ * between the durable CAS and the in-memory replacement.
10227
+ */
10228
+ async function _settleAutomaticCompaction({
10229
+ messages,
10230
+ liveExpectedMessages,
10231
+ authorityExpectedMessages,
10232
+ compacted,
10233
+ stats,
10234
+ options,
10235
+ }) {
10236
+ if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
10237
+ throw _compactionSettlementError(
10238
+ "SESSION_REVISION_STALE",
10239
+ "Session messages changed while automatic compaction was running",
10240
+ );
10241
+ }
10242
+
10243
+ let settlement = null;
10244
+ if (typeof options.onCompaction === "function") {
10245
+ if (options.onCompaction.constructor?.name === "AsyncFunction") {
10246
+ throw _compactionSettlementError(
10247
+ "CC_COMPACTION_SETTLEMENT_ASYNC",
10248
+ "Automatic compaction settlement must be synchronous",
10249
+ );
10250
+ }
10251
+ settlement = options.onCompaction(stats, compacted, {
10252
+ expectedMessages: authorityExpectedMessages,
10253
+ liveExpectedMessages,
10254
+ trigger: "auto",
10255
+ });
10256
+ } else if (options.sessionId && options.persistCompaction !== false) {
10257
+ const store = await import("../harness/jsonl-session-store.js");
10258
+ if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
10259
+ throw _compactionSettlementError(
10260
+ "SESSION_REVISION_STALE",
10261
+ "Session messages changed before automatic compaction settlement",
10262
+ );
10263
+ }
10264
+ if (store.sessionExists(options.sessionId)) {
10265
+ settlement = store.appendCompactEventIfMessagesMatch(
10266
+ options.sessionId,
10267
+ {
10268
+ ...stats,
10269
+ trigger: "auto",
10270
+ messages: projectCanonicalResumeMessages(compacted, { strict: true }),
10271
+ },
10272
+ authorityExpectedMessages,
10273
+ );
10274
+ }
10275
+ }
10276
+
10277
+ if (settlement && typeof settlement.then === "function") {
10278
+ // Observe a late rejection but never await/retry an authority settlement
10279
+ // whose commit state may already be unknown.
10280
+ void Promise.resolve(settlement).catch(() => {});
10281
+ throw _compactionSettlementError(
10282
+ "CC_COMPACTION_SETTLEMENT_ASYNC",
10283
+ "Automatic compaction settlement must be synchronous",
10284
+ );
10285
+ }
10286
+ if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
10287
+ throw _compactionSettlementError(
10288
+ "CC_COMPACTION_LOCAL_STATE_CHANGED",
10289
+ "Live messages changed after automatic compaction settlement",
10290
+ );
10291
+ }
10292
+ messages.splice(0, messages.length, ...compacted);
10293
+ return settlement;
10294
+ }
10295
+
9487
10296
  /**
9488
10297
  * Run `fn` inside an OpenTelemetry span when `options.recorder` is attached,
9489
10298
  * else run it bare (zero overhead on the un-instrumented path). `onResult`
@@ -9590,10 +10399,44 @@ export async function* agentLoop(messages, options) {
9590
10399
  const runId =
9591
10400
  options.runId ||
9592
10401
  `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
10402
+ let automaticCompactionSettlementBlocked = false;
10403
+
10404
+ const fileMutationScope = normalizeExactFileMutationScope(
10405
+ options.fileMutationScope,
10406
+ { cwd: options.cwd || process.cwd() },
10407
+ );
10408
+ const hermeticExecution = options.hermeticExecution === true;
10409
+ if (
10410
+ fileMutationScope &&
10411
+ (options.exactToolNames !== true || !hermeticExecution)
10412
+ ) {
10413
+ throw new Error(
10414
+ "exact file mutation scope requires exactToolNames=true and hermeticExecution=true",
10415
+ );
10416
+ }
9593
10417
 
10418
+ const effectiveToolOptions = hermeticExecution
10419
+ ? {
10420
+ ...options,
10421
+ hostManagedToolPolicy: null,
10422
+ extraToolDefinitions: [],
10423
+ }
10424
+ : options;
9594
10425
  const effectiveAllowedToolNames = Object.freeze(
9595
- getEffectiveToolDefinitions(options).map((tool) => tool.function.name),
10426
+ getEffectiveToolDefinitions(effectiveToolOptions).map(
10427
+ (tool) => tool.function.name,
10428
+ ),
9596
10429
  );
10430
+ if (
10431
+ fileMutationScope &&
10432
+ effectiveAllowedToolNames.some(
10433
+ (name) => !EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES.has(name),
10434
+ )
10435
+ ) {
10436
+ throw new Error(
10437
+ "exact file mutation scope contains a tool outside the hermetic file-tool ceiling",
10438
+ );
10439
+ }
9597
10440
  const mcpCallLedger =
9598
10441
  options.mcpCallLedger ||
9599
10442
  createMcpCallLedger({ sink: options.mcpLedgerSink || null });
@@ -9601,7 +10444,7 @@ export async function* agentLoop(messages, options) {
9601
10444
  options.mcpConflictScheduler || createMcpConflictScheduler();
9602
10445
 
9603
10446
  const toolContext = {
9604
- hookDb: options.hookDb || null,
10447
+ hookDb: hermeticExecution ? null : options.hookDb || null,
9605
10448
  skillLoader: options.skillLoader || _defaultSkillLoader,
9606
10449
  // Hook-envelope tracing (P2 unified event bus): every settings-hook payload
9607
10450
  // fired during this run carries trace_id = this run's id; a spawned child
@@ -9615,6 +10458,8 @@ export async function* agentLoop(messages, options) {
9615
10458
  // ids/dirNames. Set by the spawn path from the resolved subagent contract.
9616
10459
  skillAllowlist: options.skillAllowlist ?? null,
9617
10460
  effectiveAllowedToolNames,
10461
+ fileMutationScope,
10462
+ hermeticExecution,
9618
10463
  cwd: options.cwd || process.cwd(),
9619
10464
  planManager: options.planManager || null,
9620
10465
  sessionId: options.sessionId || null,
@@ -9622,23 +10467,38 @@ export async function* agentLoop(messages, options) {
9622
10467
  // several model/tool iterations. Hosts may supply their persisted turn id;
9623
10468
  // otherwise the run id is the stable in-process binding.
9624
10469
  turnId: options.turnId || runId,
9625
- hostManagedToolPolicy: options.hostManagedToolPolicy || null,
9626
- externalToolDescriptors: options.externalToolDescriptors || null,
9627
- externalToolExecutors: options.externalToolExecutors || null,
10470
+ hostManagedToolPolicy: hermeticExecution
10471
+ ? null
10472
+ : options.hostManagedToolPolicy || null,
10473
+ externalToolDescriptors: hermeticExecution
10474
+ ? null
10475
+ : options.externalToolDescriptors || null,
10476
+ externalToolExecutors: hermeticExecution
10477
+ ? null
10478
+ : options.externalToolExecutors || null,
9628
10479
  // Optional session-level Extension Tier gate. Hosts that can provide
9629
10480
  // capability/policy/permission/budget/UI signals opt in with
9630
10481
  // { enforce: true }; omitting it preserves the CLI's existing per-call
9631
10482
  // permission pipeline for backwards compatibility.
9632
- toolAdmission: options.toolAdmission || null,
9633
- unattendedActionPolicy: options.unattendedActionPolicy || null,
10483
+ toolAdmission: hermeticExecution ? null : options.toolAdmission || null,
10484
+ unattendedActionPolicy: hermeticExecution
10485
+ ? null
10486
+ : options.unattendedActionPolicy || null,
9634
10487
  // MCP tool DEFINITIONS the LLM sees (mcp__server__tool). Threaded here so a
9635
10488
  // spawn can inherit the parent's MCP tools into the child (filtered by the
9636
10489
  // contract's mcpServers allow-list). Otherwise consumed only at agentLoop.
9637
- extraToolDefinitions: options.extraToolDefinitions || null,
9638
- mcpClient: options.mcpClient || null,
9639
- mcpHostClient: options.mcpHostClient || options.mcpClient || null,
10490
+ extraToolDefinitions: hermeticExecution
10491
+ ? null
10492
+ : options.extraToolDefinitions || null,
10493
+ mcpClient: hermeticExecution ? null : options.mcpClient || null,
10494
+ mcpHostClient: hermeticExecution
10495
+ ? null
10496
+ : options.mcpHostClient || options.mcpClient || null,
9640
10497
  mcpCallLedger,
9641
10498
  mcpConflictScheduler,
10499
+ mcpDispatchAdmission: hermeticExecution
10500
+ ? null
10501
+ : options.mcpDispatchAdmission || null,
9642
10502
  // A loop-local identity prevents one session's lazy instruction commits
9643
10503
  // from suppressing first-access delivery in another session at the same
9644
10504
  // cwd. Hosts may inject a stable identity across resumed turns.
@@ -9666,21 +10526,25 @@ export async function* agentLoop(messages, options) {
9666
10526
  // verification allowlist (npm test / rg / …) is classified through the
9667
10527
  // ApprovalGate instead of fast-pathed, so no shell command auto-runs.
9668
10528
  classifyAllShell: options.classifyAllShell || false,
9669
- approvalGate: options.approvalGate || null,
9670
- shellConfirm: options.shellConfirm || null,
10529
+ approvalGate: hermeticExecution ? null : options.approvalGate || null,
10530
+ shellConfirm: hermeticExecution ? null : options.shellConfirm || null,
9671
10531
  // Interactive sessions (the REPL) set this so run_code is gated through the
9672
10532
  // ApprovalGate like run_shell — a human can approve. Headless leaves it
9673
10533
  // false so run_code keeps its existing per-permission-mode behavior.
9674
10534
  interactiveApproval: options.interactiveApproval || false,
9675
- additionalDirectories: options.additionalDirectories || null,
10535
+ additionalDirectories: hermeticExecution
10536
+ ? null
10537
+ : options.additionalDirectories || null,
9676
10538
  sandbox: options.sandbox || null,
9677
- permissionRules: options.permissionRules || null,
9678
- permissionConfirm: options.permissionConfirm || null,
9679
- settingsHooks: options.settingsHooks || null,
10539
+ permissionRules: hermeticExecution ? null : options.permissionRules || null,
10540
+ permissionConfirm: hermeticExecution
10541
+ ? null
10542
+ : options.permissionConfirm || null,
10543
+ settingsHooks: hermeticExecution ? null : options.settingsHooks || null,
9680
10544
  // Async-hook supervisor (REPL-owned): lets PostToolUse `async:true` hooks
9681
10545
  // run fire-and-forget instead of blocking the tool loop. Optional — when
9682
10546
  // absent, async PostToolUse hooks are simply skipped (never run sync).
9683
- hookSupervisor: options.hookSupervisor || null,
10547
+ hookSupervisor: hermeticExecution ? null : options.hookSupervisor || null,
9684
10548
  autoCheckpoint: options.autoCheckpoint || false,
9685
10549
  checkpointSession:
9686
10550
  options.checkpointSession || options.sessionId || "agent",
@@ -9730,6 +10594,7 @@ export async function* agentLoop(messages, options) {
9730
10594
  };
9731
10595
  const backgroundSubAgents = toolContext.backgroundSubAgents;
9732
10596
  const subAgentUsageSink = toolContext.subAgentUsageSink;
10597
+ const lifecycleHookEmitter = hermeticExecution ? () => {} : emitHooksV2Event;
9733
10598
 
9734
10599
  throwIfAborted(signal);
9735
10600
 
@@ -9737,7 +10602,7 @@ export async function* agentLoop(messages, options) {
9737
10602
  // Before calling the LLM, check if the user's message matches a known
9738
10603
  // intent with missing required parameters. If so, interactively fill them
9739
10604
  // and append the gathered context to the user message.
9740
- if (options.slotFiller && options.interaction) {
10605
+ if (!hermeticExecution && options.slotFiller && options.interaction) {
9741
10606
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
9742
10607
  if (lastUserMsg) {
9743
10608
  try {
@@ -9916,10 +10781,19 @@ export async function* agentLoop(messages, options) {
9916
10781
  // its full tool_call→tool_result cycle before we loop, so `messages` has no
9917
10782
  // dangling call; `preserveToolPairs` then guarantees compaction never
9918
10783
  // orphans a tool result. Best-effort — a failure never aborts the run.
9919
- if (options.autoCompact !== false && messages.length > 4) {
10784
+ if (
10785
+ options.autoCompact !== false &&
10786
+ !automaticCompactionSettlementBlocked &&
10787
+ messages.length > 4
10788
+ ) {
9920
10789
  try {
9921
10790
  const compactor = await _getAutoCompactor(options);
9922
10791
  if (compactor && compactor.shouldAutoCompact(messages)) {
10792
+ // This is the canonical pre-provider authority snapshot. A local
10793
+ // micro-compact may change the working input, but the durable CAS must
10794
+ // still compare against the transcript state that existed before any
10795
+ // in-memory compaction happened.
10796
+ const authorityExpectedMessages = [...messages];
9923
10797
  // Cheap surgical pre-pass (Claude-Code microcompact parity): trim old
9924
10798
  // large tool results IN PLACE before the disruptive full
9925
10799
  // summarization. If the trim brings the context back under threshold,
@@ -9945,7 +10819,7 @@ export async function* agentLoop(messages, options) {
9945
10819
  // right before the history would be compacted.
9946
10820
  let preCompactBlocked = false;
9947
10821
  let preCompactReason = null;
9948
- if (needFull && options.settingsHooks) {
10822
+ if (!hermeticExecution && needFull && options.settingsHooks) {
9949
10823
  try {
9950
10824
  const pc = runObserveHooks(
9951
10825
  options.settingsHooks,
@@ -9999,44 +10873,68 @@ export async function* agentLoop(messages, options) {
9999
10873
  };
10000
10874
  }
10001
10875
  if (stats.saved > 0 && compacted.length < messages.length) {
10002
- messages.splice(0, messages.length, ...compacted);
10003
- // Persist the compaction so a later --resume rebuilds from the
10004
- // shortened history. An explicit `onCompaction` hook (if a caller
10005
- // provides one) takes precedence; otherwise self-persist a `compact`
10006
- // event — but only when the session is already being persisted to
10007
- // disk (the JSONL file exists), so a one-shot `cc agent -p` (which
10008
- // never creates a session file) writes nothing. Opt out with
10009
- // `persistCompaction: false`. Best-effort throughout.
10010
- if (typeof options.onCompaction === "function") {
10011
- try {
10012
- options.onCompaction(stats, compacted);
10013
- } catch {
10014
- // persistence is best-effort
10015
- }
10016
- } else if (
10017
- options.sessionId &&
10018
- options.persistCompaction !== false
10019
- ) {
10020
- try {
10021
- const store = await import("../harness/jsonl-session-store.js");
10022
- if (store.sessionExists(options.sessionId)) {
10023
- store.appendCompactEvent(options.sessionId, {
10024
- ...stats,
10025
- messages: compacted,
10026
- });
10027
- }
10028
- } catch {
10029
- // self-persist is best-effort
10876
+ if (stats.summaryUsageUnknown === true) {
10877
+ automaticCompactionSettlementBlocked = true;
10878
+ yield {
10879
+ type: "compaction-usage-unknown",
10880
+ runId,
10881
+ provider: stats.summaryProvider || options.provider || null,
10882
+ model: stats.summaryModel || options.model || null,
10883
+ reason:
10884
+ stats.summaryUsageUnknownReason ||
10885
+ "provider_transport_outcome_unknown",
10886
+ source: "semantic-compaction",
10887
+ };
10888
+ // Do not apply the fallback or start another paid model request.
10889
+ // A transport failure may already have incurred unknown usage.
10890
+ return;
10891
+ }
10892
+ const liveExpectedMessages = [...messages];
10893
+ try {
10894
+ await _settleAutomaticCompaction({
10895
+ messages,
10896
+ liveExpectedMessages,
10897
+ authorityExpectedMessages,
10898
+ compacted,
10899
+ stats,
10900
+ options,
10901
+ });
10902
+ } catch (error) {
10903
+ automaticCompactionSettlementBlocked = true;
10904
+ yield {
10905
+ type: "compaction-degraded",
10906
+ runId,
10907
+ reason:
10908
+ error?.code === "SESSION_REVISION_STALE"
10909
+ ? "session_messages_changed_during_compaction"
10910
+ : "canonical_compaction_settlement_failed",
10911
+ summaryMode: "none",
10912
+ code: error?.code || "CC_COMPACTION_SETTLEMENT_FAILED",
10913
+ };
10914
+ if (compactionUsage) {
10915
+ yield {
10916
+ type: "token-usage",
10917
+ provider: stats.summaryProvider || options.provider || null,
10918
+ model: stats.summaryModel || options.model || null,
10919
+ usage: compactionUsage,
10920
+ source: "semantic-compaction",
10921
+ runId,
10922
+ };
10030
10923
  }
10924
+ // The paid summary call is never retried, and an unknown/stale
10925
+ // canonical settlement cannot be followed by another model call.
10926
+ return;
10927
+ }
10928
+ if (!hermeticExecution) {
10929
+ emitHooksV2Event("PostCompact", {
10930
+ schema_version: 1,
10931
+ trigger: "auto",
10932
+ session_id: options.sessionId || null,
10933
+ messages_after: messages.length,
10934
+ stats,
10935
+ cwd: options.cwd || process.cwd(),
10936
+ });
10031
10937
  }
10032
- emitHooksV2Event("PostCompact", {
10033
- schema_version: 1,
10034
- trigger: "auto",
10035
- session_id: options.sessionId || null,
10036
- messages_after: messages.length,
10037
- stats,
10038
- cwd: options.cwd || process.cwd(),
10039
- });
10040
10938
  yield { type: "compaction", stats, runId };
10041
10939
  }
10042
10940
  if (compactionUsage) {
@@ -10077,7 +10975,7 @@ export async function* agentLoop(messages, options) {
10077
10975
  )) {
10078
10976
  // SubagentStop fires at RESULT time for background spawns (the
10079
10977
  // spawn-time handle skips it); a block reason rides along as feedback.
10080
- if (options.settingsHooks) {
10978
+ if (!hermeticExecution && options.settingsHooks) {
10081
10979
  try {
10082
10980
  const outcome = runObserveHooks(
10083
10981
  options.settingsHooks,
@@ -10118,7 +11016,7 @@ export async function* agentLoop(messages, options) {
10118
11016
  // prepareCall runs fresh each iteration and returns an ephemeral
10119
11017
  // system-message supplement that is NOT persisted to messages history.
10120
11018
  let callMessages = messages;
10121
- if (typeof options.prepareCall === "function") {
11019
+ if (!hermeticExecution && typeof options.prepareCall === "function") {
10122
11020
  try {
10123
11021
  const hook = await options.prepareCall({
10124
11022
  iteration: budget.consumed,
@@ -10161,7 +11059,7 @@ export async function* agentLoop(messages, options) {
10161
11059
  "agent.iteration": budget.consumed,
10162
11060
  ...(modelIdAttrs || {}),
10163
11061
  },
10164
- () => llmCall(callMessages, options),
11062
+ () => llmCall(callMessages, effectiveToolOptions),
10165
11063
  (span, r) => {
10166
11064
  const t = _usageTokens(r?.usage);
10167
11065
  if (t) {
@@ -10296,7 +11194,7 @@ export async function* agentLoop(messages, options) {
10296
11194
  // going instead of stopping — the reason is injected as a new instruction.
10297
11195
  // `stop_hook_active` lets the hook avoid an infinite loop; the iteration
10298
11196
  // budget is the hard backstop.
10299
- if (options.settingsHooks) {
11197
+ if (!hermeticExecution && options.settingsHooks) {
10300
11198
  let stopOutcome = null;
10301
11199
  try {
10302
11200
  stopOutcome = runObserveHooks(
@@ -10441,6 +11339,7 @@ export async function* agentLoop(messages, options) {
10441
11339
  turnId,
10442
11340
  toolUseId: call.id,
10443
11341
  cwd: options.cwd || process.cwd(),
11342
+ emit: lifecycleHookEmitter,
10444
11343
  });
10445
11344
  toolBatchRecords.push({
10446
11345
  tool: call.function.name,
@@ -10481,6 +11380,7 @@ export async function* agentLoop(messages, options) {
10481
11380
  turnId,
10482
11381
  cwd: options.cwd || process.cwd(),
10483
11382
  parallel: true,
11383
+ emit: lifecycleHookEmitter,
10484
11384
  });
10485
11385
  continue; // all read results in place — back to the LLM call
10486
11386
  }
@@ -10509,6 +11409,7 @@ export async function* agentLoop(messages, options) {
10509
11409
  turnId,
10510
11410
  toolUseId: call?.id || null,
10511
11411
  cwd: options.cwd || process.cwd(),
11412
+ emit: lifecycleHookEmitter,
10512
11413
  });
10513
11414
  toolBatchRecords.push({
10514
11415
  tool: "(unknown)",
@@ -10541,6 +11442,57 @@ export async function* agentLoop(messages, options) {
10541
11442
  toolArgs = {};
10542
11443
  }
10543
11444
 
11445
+ // Capability and exact-path authority must settle before any checkpoint,
11446
+ // hook, or observationally stronger `tool-executing` event. The same
11447
+ // preflight runs again inside executeTool to prevent call-site drift.
11448
+ const earlyAuthorityDenial = preflightToolExecutionAuthority(
11449
+ toolName,
11450
+ toolArgs,
11451
+ toolContext,
11452
+ );
11453
+ if (earlyAuthorityDenial) {
11454
+ const resultStr = capToolResultString(
11455
+ safeStringifyToolResult(earlyAuthorityDenial),
11456
+ );
11457
+ const decision = permissionDecision(
11458
+ call.id,
11459
+ toolName,
11460
+ earlyAuthorityDenial,
11461
+ );
11462
+ yield {
11463
+ type: "tool-result",
11464
+ tool: toolName,
11465
+ result: earlyAuthorityDenial,
11466
+ error: earlyAuthorityDenial.error,
11467
+ tool_use_id: call.id,
11468
+ turn_id: `${runId}:t${budget.consumed}`,
11469
+ permission_decision_id: decision?.id || null,
11470
+ permission_decision: decision,
11471
+ };
11472
+ messages.push({
11473
+ role: "tool",
11474
+ content: resultStr,
11475
+ tool_call_id: call.id,
11476
+ });
11477
+ const failed = emitToolHookLifecycle({
11478
+ tool: toolName,
11479
+ args: toolArgs,
11480
+ result: earlyAuthorityDenial,
11481
+ error: earlyAuthorityDenial.error,
11482
+ sessionId: options.sessionId || null,
11483
+ turnId,
11484
+ toolUseId: call.id,
11485
+ cwd: options.cwd || process.cwd(),
11486
+ emit: lifecycleHookEmitter,
11487
+ });
11488
+ toolBatchRecords.push({
11489
+ tool: toolName,
11490
+ toolUseId: call.id,
11491
+ failed,
11492
+ });
11493
+ continue;
11494
+ }
11495
+
10544
11496
  // Auto-checkpoint the work tree before a mutating tool (opt-in), so the
10545
11497
  // user can `cc checkpoint restore` back to just before this call.
10546
11498
  const cpId = await _autoCheckpointBeforeTool(
@@ -10825,6 +11777,7 @@ export async function* agentLoop(messages, options) {
10825
11777
  turnId,
10826
11778
  toolUseId: call.id,
10827
11779
  cwd: options.cwd || process.cwd(),
11780
+ emit: lifecycleHookEmitter,
10828
11781
  });
10829
11782
  toolBatchRecords.push({
10830
11783
  tool: toolName,
@@ -10865,6 +11818,7 @@ export async function* agentLoop(messages, options) {
10865
11818
  turnId,
10866
11819
  cwd: options.cwd || process.cwd(),
10867
11820
  parallel: false,
11821
+ emit: lifecycleHookEmitter,
10868
11822
  });
10869
11823
  }
10870
11824