chainlesschain 0.164.0 → 0.165.2

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 (228) hide show
  1. package/README.md +248 -2
  2. package/package.json +7 -4
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
  40. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
  75. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  76. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
  104. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  109. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/cowork.js +559 -62
  154. package/src/commands/plugin.js +819 -64
  155. package/src/commands/session-location.js +318 -7
  156. package/src/constants.js +2 -2
  157. package/src/data/changelog.json +28 -2
  158. package/src/gateways/ws/action-protocol.js +293 -19
  159. package/src/gateways/ws/message-dispatcher.js +30 -0
  160. package/src/gateways/ws/remote-session-protocol.js +542 -35
  161. package/src/gateways/ws/ws-approval-gate.js +5 -0
  162. package/src/gateways/ws/ws-server.js +462 -38
  163. package/src/harness/jsonl-session-store.js +638 -2
  164. package/src/harness/prompt-compressor.js +4 -0
  165. package/src/harness/remote-session-audit.js +1 -0
  166. package/src/harness/remote-session-crypto.js +750 -50
  167. package/src/harness/remote-session-registry.js +795 -27
  168. package/src/lib/auto-mode-config.js +98 -3
  169. package/src/lib/background-agent-keeper-client.js +29 -0
  170. package/src/lib/background-agent-keeper-protocol.js +158 -10
  171. package/src/lib/background-agent-supervisor.js +870 -48
  172. package/src/lib/background-session-transport.js +23 -6
  173. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  174. package/src/lib/cowork-task-runner.js +70 -0
  175. package/src/lib/cowork-workflow.js +887 -62
  176. package/src/lib/delivery-production-adapter.js +38 -2
  177. package/src/lib/doctor-checkup.js +34 -10
  178. package/src/lib/dynamic-workflow-draft.js +535 -0
  179. package/src/lib/dynamic-workflow-facade.js +1105 -17
  180. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  181. package/src/lib/execution-location-contract.js +111 -7
  182. package/src/lib/execution-location-runtime.js +3 -1
  183. package/src/lib/execution-location-target.js +910 -0
  184. package/src/lib/llm-providers.js +1 -0
  185. package/src/lib/mcp-call-ledger-store.js +35 -0
  186. package/src/lib/mcp-call-ledger.js +30 -0
  187. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  188. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  189. package/src/lib/plugin-runtime/install.js +4956 -657
  190. package/src/lib/plugin-runtime/manifest.js +3 -1
  191. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  192. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  193. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  194. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  195. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  196. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  197. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  198. package/src/lib/plugin-runtime/policy.js +21 -6
  199. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  200. package/src/lib/plugin-runtime/remote-source.js +323 -27
  201. package/src/lib/plugin-runtime/scopes.js +317 -41
  202. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  203. package/src/lib/public-ide-capability-manifest.js +1 -1
  204. package/src/lib/remote-approval-bridge.js +745 -108
  205. package/src/lib/remote-control.js +2 -0
  206. package/src/lib/remote-membership-coordinator.js +612 -46
  207. package/src/lib/remote-membership-host-store.js +59 -10
  208. package/src/lib/runtime-provenance-ledger.js +171 -46
  209. package/src/lib/shell-approval.js +169 -6
  210. package/src/lib/sub-agent-context.js +268 -2
  211. package/src/lib/task-model-selector.js +1 -1
  212. package/src/lib/with-file-lock.js +167 -42
  213. package/src/repl/agent-repl.js +70 -29
  214. package/src/repl/remote-approval.js +29 -7
  215. package/src/runtime/agent-core.js +1695 -194
  216. package/src/runtime/headless-runner.js +22 -5
  217. package/src/runtime/headless-stream.js +19 -3
  218. package/src/workers/background-agent-keeper.js +279 -58
  219. package/src/workers/background-agent-worker.js +7 -0
  220. package/src/workers/background-turn-bootstrap.js +29 -2
  221. package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
  222. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  223. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
  224. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  225. package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
  226. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  227. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  228. package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
@@ -18,7 +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
+ import { createHash, randomUUID } from "node:crypto";
22
22
  import { isProxy } from "node:util/types";
23
23
  import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
24
24
  import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
@@ -28,6 +28,12 @@ import sharedHookRunner from "../lib/hook-runner.js";
28
28
  import sharedHookEvents from "../lib/settings-hook-events.js";
29
29
  import { mergeProviderOptions } from "../lib/provider-options.js";
30
30
  import { applyCredentialProxy } from "../lib/credential-proxy.js";
31
+ import {
32
+ commitShellApprovalSideEffects,
33
+ createShellExecutionDescriptor,
34
+ evaluateShellCommandWithApproval,
35
+ snapshotShellExecutionArgs,
36
+ } from "../lib/shell-approval.js";
31
37
  import { describeBackgroundCommand } from "../lib/terminal-context.js";
32
38
  import { buildTelemetryAttributes } from "../lib/telemetry-ids.js";
33
39
  import {
@@ -230,6 +236,36 @@ function collectWorkspacePluginBinSandboxPolicy(
230
236
  );
231
237
  }
232
238
 
239
+ function projectPluginBinInvocationAuthority(invocation) {
240
+ if (!invocation) return null;
241
+ const identity = invocation.executableIdentity || {};
242
+ return {
243
+ command: invocation.command || null,
244
+ runtime: invocation.runtime || null,
245
+ shell: invocation.shell === true,
246
+ pluginId: invocation.pluginId || null,
247
+ pluginVersion: invocation.pluginVersion || null,
248
+ pluginSource: invocation.pluginSource || null,
249
+ scope: invocation.scope || null,
250
+ binName: invocation.binName || null,
251
+ binPath: invocation.binPath || null,
252
+ pluginRoot: invocation.pluginRoot || null,
253
+ args: Array.isArray(invocation.args) ? [...invocation.args] : [],
254
+ requiredBoundaries: [
255
+ ...(invocation.sandboxPolicy?.requiredBoundaries || []),
256
+ ].sort(),
257
+ executableIdentity: {
258
+ realPath: identity.realPath || null,
259
+ sha256: identity.sha256 || null,
260
+ bytes: identity.bytes ?? null,
261
+ dev: identity.dev ?? null,
262
+ ino: identity.ino ?? null,
263
+ mtimeMs: identity.mtimeMs ?? null,
264
+ mode: identity.mode ?? null,
265
+ },
266
+ };
267
+ }
268
+
233
269
  function rethrowRunCodeSandboxFailure(error) {
234
270
  if (
235
271
  error?.sandboxFailClosed === true ||
@@ -2223,6 +2259,308 @@ function attachDiffReviewAudit(result, audit) {
2223
2259
  return result;
2224
2260
  }
2225
2261
 
2262
+ const RUN_CODE_ARGUMENT_KEYS = new Set([
2263
+ "language",
2264
+ "code",
2265
+ "timeout",
2266
+ "persist",
2267
+ ]);
2268
+
2269
+ function snapshotRunCodeExecutionArgs(args) {
2270
+ if (
2271
+ !args ||
2272
+ typeof args !== "object" ||
2273
+ Array.isArray(args) ||
2274
+ isProxy(args)
2275
+ ) {
2276
+ throw new TypeError("run_code arguments must be a plain data object");
2277
+ }
2278
+ const descriptors = Object.getOwnPropertyDescriptors(args);
2279
+ for (const key of Reflect.ownKeys(descriptors)) {
2280
+ if (typeof key !== "string" || !RUN_CODE_ARGUMENT_KEYS.has(key)) {
2281
+ throw new TypeError(`run_code contains unsupported field ${String(key)}`);
2282
+ }
2283
+ if (!Object.hasOwn(descriptors[key], "value")) {
2284
+ throw new TypeError(`run_code field ${key} must be a data property`);
2285
+ }
2286
+ }
2287
+ const language = descriptors.language?.value;
2288
+ const code = descriptors.code?.value;
2289
+ if (typeof language !== "string" || !language) {
2290
+ throw new TypeError("run_code language must be a non-empty string");
2291
+ }
2292
+ if (typeof code !== "string") {
2293
+ throw new TypeError("run_code code must be a string");
2294
+ }
2295
+ const snapshot = { language, code };
2296
+ if (descriptors.timeout) {
2297
+ const timeout = descriptors.timeout.value;
2298
+ if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
2299
+ throw new TypeError("run_code timeout must be a finite number");
2300
+ }
2301
+ snapshot.timeout = timeout;
2302
+ }
2303
+ if (descriptors.persist) {
2304
+ const persist = descriptors.persist.value;
2305
+ if (typeof persist !== "boolean") {
2306
+ throw new TypeError("run_code persist must be a boolean");
2307
+ }
2308
+ snapshot.persist = persist;
2309
+ }
2310
+ return Object.freeze(snapshot);
2311
+ }
2312
+
2313
+ function runCodeApprovalDescriptor(args, cwd) {
2314
+ return Object.freeze({
2315
+ language: args.language,
2316
+ codeSha256: createHash("sha256").update(args.code, "utf8").digest("hex"),
2317
+ codeBytes: Buffer.byteLength(args.code, "utf8"),
2318
+ timeout: Math.min(Math.max(args.timeout || 60, 1), 300),
2319
+ persist: args.persist === true,
2320
+ cwd,
2321
+ });
2322
+ }
2323
+
2324
+ function canonicalPolicyAuthorityData(value, state = null, depth = 0) {
2325
+ const budget = state || { nodes: 0, seen: new WeakSet() };
2326
+ budget.nodes += 1;
2327
+ if (budget.nodes > 4096 || depth > 12) {
2328
+ throw new TypeError("policy authority exceeds canonical bounds");
2329
+ }
2330
+ if (value === null) return "null";
2331
+ if (value === undefined) return '"$undefined"';
2332
+ if (typeof value === "string" || typeof value === "boolean") {
2333
+ return JSON.stringify(value);
2334
+ }
2335
+ if (typeof value === "number") {
2336
+ if (!Number.isFinite(value)) {
2337
+ throw new TypeError("policy authority contains a non-finite number");
2338
+ }
2339
+ return JSON.stringify(value);
2340
+ }
2341
+ if (typeof value !== "object" || isProxy(value)) {
2342
+ throw new TypeError("policy authority must contain data objects only");
2343
+ }
2344
+ if (budget.seen.has(value)) {
2345
+ throw new TypeError("policy authority must not contain cycles");
2346
+ }
2347
+ budget.seen.add(value);
2348
+ try {
2349
+ const descriptors = Object.getOwnPropertyDescriptors(value);
2350
+ const keys = Reflect.ownKeys(descriptors);
2351
+ if (keys.some((key) => typeof key !== "string") || keys.length > 512) {
2352
+ throw new TypeError("policy authority contains invalid keys");
2353
+ }
2354
+ if (Array.isArray(value)) {
2355
+ const length = descriptors.length?.value;
2356
+ if (!Number.isSafeInteger(length) || length < 0 || length > 512) {
2357
+ throw new TypeError("policy authority array exceeds bounds");
2358
+ }
2359
+ const entries = [];
2360
+ for (let index = 0; index < length; index += 1) {
2361
+ const descriptor = descriptors[String(index)];
2362
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) {
2363
+ throw new TypeError("policy authority arrays must be dense data");
2364
+ }
2365
+ entries.push(
2366
+ canonicalPolicyAuthorityData(descriptor.value, budget, depth + 1),
2367
+ );
2368
+ }
2369
+ const allowedKeys = new Set([
2370
+ "length",
2371
+ ...Array.from({ length }, (_, index) => String(index)),
2372
+ ]);
2373
+ if (keys.some((key) => !allowedKeys.has(key))) {
2374
+ throw new TypeError("policy authority arrays contain extra fields");
2375
+ }
2376
+ return `[${entries.join(",")}]`;
2377
+ }
2378
+ const prototype = Object.getPrototypeOf(value);
2379
+ if (prototype !== Object.prototype && prototype !== null) {
2380
+ throw new TypeError("policy authority must contain plain objects");
2381
+ }
2382
+ return `{${keys
2383
+ .sort()
2384
+ .map((key) => {
2385
+ const descriptor = descriptors[key];
2386
+ if (!Object.hasOwn(descriptor, "value")) {
2387
+ throw new TypeError("policy authority accessors are not allowed");
2388
+ }
2389
+ return `${JSON.stringify(key)}:${canonicalPolicyAuthorityData(
2390
+ descriptor.value,
2391
+ budget,
2392
+ depth + 1,
2393
+ )}`;
2394
+ })
2395
+ .join(",")}}`;
2396
+ } finally {
2397
+ budget.seen.delete(value);
2398
+ }
2399
+ }
2400
+
2401
+ function digestPolicyAuthority(projection) {
2402
+ return createHash("sha256")
2403
+ .update("chainlesschain.shell-policy-authority.v1\0", "utf8")
2404
+ .update(canonicalPolicyAuthorityData(projection), "utf8")
2405
+ .digest("hex");
2406
+ }
2407
+
2408
+ function validatePermissionRuleset(rules) {
2409
+ if (!rules) return null;
2410
+ if (typeof rules !== "object" || Array.isArray(rules)) {
2411
+ throw new TypeError(
2412
+ "permission authority provider returned an invalid ruleset",
2413
+ );
2414
+ }
2415
+ const normalized = {};
2416
+ for (const key of ["allow", "ask", "deny"]) {
2417
+ const value = rules[key];
2418
+ if (value !== undefined && !Array.isArray(value)) {
2419
+ throw new TypeError(
2420
+ "permission authority provider returned an invalid ruleset",
2421
+ );
2422
+ }
2423
+ normalized[key] = value ? [...value] : [];
2424
+ }
2425
+ return normalized;
2426
+ }
2427
+
2428
+ function projectPermissionAuthority({ authority, rules, tool, args, cwd }) {
2429
+ const verdict = rules
2430
+ ? evaluatePermissionRules({ tool, args, cwd, rules })
2431
+ : { decision: null, rule: null };
2432
+ const source = verdict.rule
2433
+ ? authority?.sources?.[`${verdict.decision}:${verdict.rule}`] || null
2434
+ : null;
2435
+ const scopedRule = source?.startsWith("scoped:")
2436
+ ? authority?.scoped?.rules?.find(
2437
+ (record) =>
2438
+ record.status === "active" && `scoped:${record.id}` === source,
2439
+ ) || null
2440
+ : null;
2441
+ return {
2442
+ rules: rules || null,
2443
+ sources: authority?.sources || null,
2444
+ scoped: authority?.scoped || null,
2445
+ version: authority?.version || authority?.policyVersion || null,
2446
+ revision: authority?.revision ?? null,
2447
+ headHash: authority?.headHash || null,
2448
+ verdict: {
2449
+ decision: verdict.decision || null,
2450
+ rule: verdict.rule || null,
2451
+ source,
2452
+ scopedRule: scopedRule
2453
+ ? {
2454
+ id: scopedRule.id || null,
2455
+ revision: scopedRule.revision ?? null,
2456
+ expiresAt: scopedRule.expiresAt ?? null,
2457
+ status: scopedRule.status || null,
2458
+ }
2459
+ : null,
2460
+ },
2461
+ };
2462
+ }
2463
+
2464
+ function selectedHostToolPolicy(hostAuthority, tool) {
2465
+ const policies = hostAuthority?.tools || hostAuthority?.toolPolicies || null;
2466
+ return policies && typeof policies === "object"
2467
+ ? policies[tool] || null
2468
+ : null;
2469
+ }
2470
+
2471
+ function snapshotPlanToolAuthority(planManager, tool, args) {
2472
+ const active = planManager?.isActive?.() === true;
2473
+ return {
2474
+ active,
2475
+ executionLockActive: planManager?.executionLock != null,
2476
+ toolAllowed:
2477
+ !active || (tool === "git" && isReadOnlyGitCommand(args?.command))
2478
+ ? true
2479
+ : planManager?.isToolAllowed?.(tool) === true,
2480
+ };
2481
+ }
2482
+
2483
+ function snapshotEffectiveToolSetAuthority(effectiveAllowedToolNames, tool) {
2484
+ const enforced = Array.isArray(effectiveAllowedToolNames);
2485
+ return {
2486
+ enforced,
2487
+ allowed: !enforced || effectiveAllowedToolNames.includes(tool),
2488
+ };
2489
+ }
2490
+
2491
+ function evaluateSelectedToolAdmissionAuthority({
2492
+ admission,
2493
+ tool,
2494
+ runtimeDescriptor,
2495
+ localToolDescriptor,
2496
+ hostManagedToolPolicy,
2497
+ }) {
2498
+ if (admission?.enforce !== true) {
2499
+ return {
2500
+ override: {},
2501
+ decision: null,
2502
+ projection: { enforce: false },
2503
+ };
2504
+ }
2505
+ const override =
2506
+ admission.tools && typeof admission.tools === "object"
2507
+ ? admission.tools[tool] || {}
2508
+ : {};
2509
+ const tier =
2510
+ override.tier ||
2511
+ runtimeDescriptor?.tier ||
2512
+ getCodingAgentToolPolicy(tool)?.tier ||
2513
+ (localToolDescriptor ? "extension" : "mvp");
2514
+ const inputs = {
2515
+ capabilityGranted:
2516
+ override.capabilityGranted ?? admission.capabilityGranted,
2517
+ policyAllowed:
2518
+ override.policyAllowed ??
2519
+ (hostManagedToolPolicy
2520
+ ? (hostManagedToolPolicy?.tools ||
2521
+ hostManagedToolPolicy?.toolPolicies ||
2522
+ {})[tool]?.allowed !== false
2523
+ : admission.policyAllowed),
2524
+ permissionGranted:
2525
+ override.permissionGranted ?? admission.permissionGranted,
2526
+ budgetOk: override.budgetOk ?? admission.budgetOk,
2527
+ uiSupported: override.uiSupported ?? admission.uiSupported,
2528
+ };
2529
+ const decision = admitTool({ tool, tier, ...inputs });
2530
+ return {
2531
+ override,
2532
+ decision,
2533
+ projection: {
2534
+ enforce: true,
2535
+ tier: decision.tier,
2536
+ capabilityGranted: inputs.capabilityGranted === true,
2537
+ policyAllowed: inputs.policyAllowed === true,
2538
+ permissionGranted: inputs.permissionGranted === true,
2539
+ budgetOk: inputs.budgetOk === true,
2540
+ uiSupported: inputs.uiSupported === true,
2541
+ admitted: decision.admitted === true,
2542
+ unmet: [...decision.unmet],
2543
+ },
2544
+ };
2545
+ }
2546
+
2547
+ function snapshotApprovalPolicyAuthority(approvalGate, sessionId) {
2548
+ if (!approvalGate) return null;
2549
+ const snapshot = approvalGate.getAuthorizationPolicySnapshot?.(sessionId);
2550
+ if (snapshot && typeof snapshot.then === "function") {
2551
+ throw new TypeError("approval policy snapshot must be synchronous");
2552
+ }
2553
+ if (snapshot) return snapshot;
2554
+ return {
2555
+ schema: "chainlesschain.approval-policy-authority/v1",
2556
+ kind: "legacy-gate",
2557
+ sessionId: sessionId ? String(sessionId) : null,
2558
+ policy: approvalGate.getSessionPolicy?.(sessionId) || null,
2559
+ hasAuthorizationConsumer:
2560
+ approvalGate.hasAuthorizationConsumer?.() === true,
2561
+ };
2562
+ }
2563
+
2226
2564
  /**
2227
2565
  * Execute a single tool call with plan-mode filtering and hook pipeline.
2228
2566
  *
@@ -2235,9 +2573,99 @@ function attachDiffReviewAudit(result, audit) {
2235
2573
  * @returns {Promise<object>} tool result
2236
2574
  */
2237
2575
  export async function executeTool(name, args, context = {}) {
2576
+ const liveExecutionContext = context;
2577
+ // run_shell can cross several asynchronous permission/hook boundaries before
2578
+ // reaching executeToolInner. Capture its declared tuple and working directory
2579
+ // before any of them so an in-process caller cannot swap the approved command
2580
+ // or reinterpret a relative cwd after process.chdir().
2581
+ let shellAuthorityCwd = null;
2582
+ let shellAuthorityWorkspaceCwd = null;
2583
+ let runCodeAuthorityCwd = null;
2584
+ if (name === "run_shell") {
2585
+ try {
2586
+ const entryProcessCwd = process.cwd();
2587
+ const declared = snapshotShellExecutionArgs(args);
2588
+ const contextCwd =
2589
+ context.cwd === undefined || context.cwd === null
2590
+ ? entryProcessCwd
2591
+ : context.cwd;
2592
+ if (typeof contextCwd !== "string" || !contextCwd) {
2593
+ throw new TypeError("run_shell context cwd must be a non-empty string");
2594
+ }
2595
+ if (
2596
+ declared.cwd !== undefined &&
2597
+ (typeof declared.cwd !== "string" || !declared.cwd)
2598
+ ) {
2599
+ throw new TypeError("run_shell cwd must be a non-empty string");
2600
+ }
2601
+ shellAuthorityWorkspaceCwd = fs.realpathSync.native(
2602
+ path.resolve(entryProcessCwd, contextCwd),
2603
+ );
2604
+ if (!fs.statSync(shellAuthorityWorkspaceCwd).isDirectory()) {
2605
+ throw new Error("run_shell context cwd is not a directory");
2606
+ }
2607
+ const requestedCwd = path.resolve(
2608
+ shellAuthorityWorkspaceCwd,
2609
+ declared.cwd || ".",
2610
+ );
2611
+ shellAuthorityCwd = fs.realpathSync.native(requestedCwd);
2612
+ if (!fs.statSync(shellAuthorityCwd).isDirectory()) {
2613
+ throw new Error("run_shell cwd is not a directory");
2614
+ }
2615
+ args = snapshotShellExecutionArgs({
2616
+ ...declared,
2617
+ cwd: shellAuthorityCwd,
2618
+ });
2619
+ } catch (error) {
2620
+ return {
2621
+ error: `[Shell Dispatch] ${error?.message || "invalid execution arguments"}`,
2622
+ policy: {
2623
+ decision: "blocked",
2624
+ via: "shell-execution-snapshot",
2625
+ code: "CC_SHELL_EXECUTION_SNAPSHOT_INVALID",
2626
+ },
2627
+ };
2628
+ }
2629
+ }
2630
+ if (name === "run_code") {
2631
+ try {
2632
+ const entryProcessCwd = process.cwd();
2633
+ args = snapshotRunCodeExecutionArgs(args);
2634
+ const contextCwd =
2635
+ context.cwd === undefined || context.cwd === null
2636
+ ? entryProcessCwd
2637
+ : context.cwd;
2638
+ if (typeof contextCwd !== "string" || !contextCwd) {
2639
+ throw new TypeError("run_code context cwd must be a non-empty string");
2640
+ }
2641
+ runCodeAuthorityCwd = fs.realpathSync.native(
2642
+ path.resolve(entryProcessCwd, contextCwd),
2643
+ );
2644
+ if (!fs.statSync(runCodeAuthorityCwd).isDirectory()) {
2645
+ throw new Error("run_code cwd is not a directory");
2646
+ }
2647
+ } catch (error) {
2648
+ return {
2649
+ error: `[Run Code Dispatch] ${error?.message || "invalid execution arguments"}`,
2650
+ policy: {
2651
+ decision: "blocked",
2652
+ via: "run-code-execution-snapshot",
2653
+ code: "CC_RUN_CODE_EXECUTION_SNAPSHOT_INVALID",
2654
+ },
2655
+ };
2656
+ }
2657
+ }
2238
2658
  const hookDb = context.hookDb || null;
2239
2659
  const skillLoader = context.skillLoader || _defaultSkillLoader;
2240
- const cwd = context.cwd || process.cwd();
2660
+ // Keep the authority-bearing workspace root distinct from the command's
2661
+ // requested cwd. A command may run in a nested directory, but Plugin policy
2662
+ // discovery and strong-sandbox issuance must remain anchored to the host
2663
+ // workspace that admitted the tool call.
2664
+ const cwd =
2665
+ shellAuthorityWorkspaceCwd ||
2666
+ runCodeAuthorityCwd ||
2667
+ context.cwd ||
2668
+ process.cwd();
2241
2669
  const planManager = context.planManager || getPlanModeManager();
2242
2670
  // The provider receives a filtered schema, but an untrusted/buggy model can
2243
2671
  // still emit an arbitrary tool_call. Reuse the same pure preflight that the
@@ -2247,6 +2675,27 @@ export async function executeTool(name, args, context = {}) {
2247
2675
  cwd,
2248
2676
  });
2249
2677
  if (authorityDenial) return authorityDenial;
2678
+ let admittedEffectiveToolSetDigest = null;
2679
+ if (name === "run_shell") {
2680
+ try {
2681
+ admittedEffectiveToolSetDigest = digestPolicyAuthority(
2682
+ snapshotEffectiveToolSetAuthority(
2683
+ context.effectiveAllowedToolNames,
2684
+ name,
2685
+ ),
2686
+ );
2687
+ } catch (error) {
2688
+ return {
2689
+ error:
2690
+ "[Shell Dispatch] The effective tool capability ceiling could not be bound. No command side effect was started.",
2691
+ policy: {
2692
+ decision: "blocked",
2693
+ via: "shell-policy-authority",
2694
+ code: error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE",
2695
+ },
2696
+ };
2697
+ }
2698
+ }
2250
2699
  const localToolDescriptor =
2251
2700
  context.externalToolDescriptors &&
2252
2701
  typeof context.externalToolDescriptors === "object"
@@ -2259,41 +2708,41 @@ export async function executeTool(name, args, context = {}) {
2259
2708
  : null;
2260
2709
  const runtimeDescriptor =
2261
2710
  getRuntimeToolDescriptor(name) || localToolDescriptor;
2262
- const admission = context.toolAdmission;
2263
- if (admission?.enforce === true) {
2264
- const override =
2265
- admission.tools && typeof admission.tools === "object"
2266
- ? admission.tools[name] || {}
2267
- : {};
2268
- const tier =
2269
- override.tier ||
2270
- runtimeDescriptor?.tier ||
2271
- getCodingAgentToolPolicy(name)?.tier ||
2272
- (localToolDescriptor ? "extension" : "mvp");
2273
- const decision = admitTool({
2711
+ let admittedToolAdmissionDigest = null;
2712
+ let toolAdmissionEvaluation;
2713
+ try {
2714
+ toolAdmissionEvaluation = evaluateSelectedToolAdmissionAuthority({
2715
+ admission: context.toolAdmission,
2274
2716
  tool: name,
2275
- tier,
2276
- capabilityGranted:
2277
- override.capabilityGranted ?? admission.capabilityGranted,
2278
- policyAllowed:
2279
- override.policyAllowed ??
2280
- (context.hostManagedToolPolicy
2281
- ? (context.hostManagedToolPolicy?.tools ||
2282
- context.hostManagedToolPolicy?.toolPolicies ||
2283
- {})[name]?.allowed !== false
2284
- : admission.policyAllowed),
2285
- permissionGranted:
2286
- override.permissionGranted ?? admission.permissionGranted,
2287
- budgetOk: override.budgetOk ?? admission.budgetOk,
2288
- uiSupported: override.uiSupported ?? admission.uiSupported,
2717
+ runtimeDescriptor,
2718
+ localToolDescriptor,
2719
+ hostManagedToolPolicy: context.hostManagedToolPolicy,
2289
2720
  });
2721
+ if (name === "run_shell") {
2722
+ admittedToolAdmissionDigest = digestPolicyAuthority(
2723
+ toolAdmissionEvaluation.projection,
2724
+ );
2725
+ }
2726
+ } catch (error) {
2727
+ return {
2728
+ error:
2729
+ "[Tool Admission] The selected tool authority could not be bound. No tool side effect was started.",
2730
+ policy: {
2731
+ decision: "blocked",
2732
+ via: "tool-admission",
2733
+ code: error?.code || "CC_TOOL_ADMISSION_AUTHORITY_UNAVAILABLE",
2734
+ },
2735
+ };
2736
+ }
2737
+ if (toolAdmissionEvaluation.decision) {
2738
+ const { override, decision } = toolAdmissionEvaluation;
2290
2739
  const attribution = buildToolAttribution({
2291
2740
  tool: name,
2292
2741
  source:
2293
2742
  override.source ||
2294
2743
  runtimeDescriptor?.source ||
2295
2744
  localToolDescriptor?.source ||
2296
- admission.source ||
2745
+ context.toolAdmission?.source ||
2297
2746
  null,
2298
2747
  version: override.version || runtimeDescriptor?.version || null,
2299
2748
  scope: override.scope || localToolDescriptor?.scope || null,
@@ -2468,6 +2917,30 @@ export async function executeTool(name, args, context = {}) {
2468
2917
  : null,
2469
2918
  scopedRule: matchedScopedRule || null,
2470
2919
  };
2920
+ let admittedPermissionPolicyDigest = null;
2921
+ if (name === "run_shell") {
2922
+ try {
2923
+ admittedPermissionPolicyDigest = digestPolicyAuthority(
2924
+ projectPermissionAuthority({
2925
+ authority: permissionAuthority,
2926
+ rules: validatePermissionRuleset(effectivePermissionRules),
2927
+ tool: name,
2928
+ args,
2929
+ cwd,
2930
+ }),
2931
+ );
2932
+ } catch (error) {
2933
+ return {
2934
+ error:
2935
+ "[Shell Dispatch] The evaluated permission authority could not be bound. No command side effect was started.",
2936
+ policy: {
2937
+ decision: "blocked",
2938
+ via: "shell-policy-authority",
2939
+ code: error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE",
2940
+ },
2941
+ };
2942
+ }
2943
+ }
2471
2944
 
2472
2945
  // 1. settings deny
2473
2946
  if (settingsVerdict.decision === "deny") {
@@ -2497,6 +2970,22 @@ export async function executeTool(name, args, context = {}) {
2497
2970
  toolPolicies && typeof toolPolicies === "object"
2498
2971
  ? toolPolicies[name]
2499
2972
  : null;
2973
+ let admittedHostPolicyDigest = null;
2974
+ if (name === "run_shell") {
2975
+ try {
2976
+ admittedHostPolicyDigest = digestPolicyAuthority(hostToolPolicy);
2977
+ } catch (error) {
2978
+ return {
2979
+ error:
2980
+ "[Shell Dispatch] The evaluated host policy could not be bound. No command side effect was started.",
2981
+ policy: {
2982
+ decision: "blocked",
2983
+ via: "shell-policy-authority",
2984
+ code: error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE",
2985
+ },
2986
+ };
2987
+ }
2988
+ }
2500
2989
  const hostPolicyAllowsReadOnlyGit =
2501
2990
  name === "git" &&
2502
2991
  hostToolPolicy?.planModeBehavior === "readonly-conditional" &&
@@ -2530,6 +3019,24 @@ export async function executeTool(name, args, context = {}) {
2530
3019
  planManager.isActive() && !executionLockActive;
2531
3020
  const externalToolBlockedDuringPlanning =
2532
3021
  planReadOnlyFenceActive && !STATIC_AGENT_TOOL_NAMES.has(name);
3022
+ let admittedPlanPolicyDigest = null;
3023
+ if (name === "run_shell") {
3024
+ try {
3025
+ admittedPlanPolicyDigest = digestPolicyAuthority(
3026
+ snapshotPlanToolAuthority(planManager, name, args),
3027
+ );
3028
+ } catch (error) {
3029
+ return {
3030
+ error:
3031
+ "[Shell Dispatch] The evaluated plan authority could not be bound. No command side effect was started.",
3032
+ policy: {
3033
+ decision: "blocked",
3034
+ via: "shell-policy-authority",
3035
+ code: error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE",
3036
+ },
3037
+ };
3038
+ }
3039
+ }
2533
3040
  context = { ...context, planReadOnlyFenceActive };
2534
3041
  if (
2535
3042
  planManager.isActive() &&
@@ -2828,6 +3335,160 @@ export async function executeTool(name, args, context = {}) {
2828
3335
  if (instructionPreflight) return instructionPreflight;
2829
3336
  }
2830
3337
 
3338
+ let shellDispatchPolicyAuthority = null;
3339
+ if (name === "run_shell") {
3340
+ try {
3341
+ const permissionRulesProvider = context.permissionRulesProvider || null;
3342
+ const approvalGate = context.approvalGate || null;
3343
+ const initialProjection = {
3344
+ permission: projectPermissionAuthority({
3345
+ authority: permissionAuthority,
3346
+ rules: validatePermissionRuleset(effectivePermissionRules),
3347
+ tool: name,
3348
+ args,
3349
+ cwd,
3350
+ }),
3351
+ host: selectedHostToolPolicy(
3352
+ context.hostManagedToolPolicy || null,
3353
+ name,
3354
+ ),
3355
+ plan: snapshotPlanToolAuthority(planManager, name, args),
3356
+ effectiveToolSet: snapshotEffectiveToolSetAuthority(
3357
+ context.effectiveAllowedToolNames,
3358
+ name,
3359
+ ),
3360
+ toolAdmission: evaluateSelectedToolAdmissionAuthority({
3361
+ admission: context.toolAdmission,
3362
+ tool: name,
3363
+ runtimeDescriptor,
3364
+ localToolDescriptor,
3365
+ hostManagedToolPolicy: context.hostManagedToolPolicy,
3366
+ }).projection,
3367
+ approval: snapshotApprovalPolicyAuthority(
3368
+ approvalGate,
3369
+ context.sessionId || null,
3370
+ ),
3371
+ unattended: context.unattendedActionPolicy || null,
3372
+ shellPolicyOverrides: context.shellPolicyOverrides || null,
3373
+ classifyAllShell: context.classifyAllShell === true,
3374
+ };
3375
+ if (
3376
+ digestPolicyAuthority(initialProjection.permission) !==
3377
+ admittedPermissionPolicyDigest ||
3378
+ digestPolicyAuthority(initialProjection.host) !==
3379
+ admittedHostPolicyDigest ||
3380
+ digestPolicyAuthority(initialProjection.plan) !==
3381
+ admittedPlanPolicyDigest ||
3382
+ digestPolicyAuthority(initialProjection.effectiveToolSet) !==
3383
+ admittedEffectiveToolSetDigest ||
3384
+ digestPolicyAuthority(initialProjection.toolAdmission) !==
3385
+ admittedToolAdmissionDigest
3386
+ ) {
3387
+ const error = new Error(
3388
+ "Shell policy authority changed while an earlier confirmation was pending",
3389
+ );
3390
+ error.code = "CC_SHELL_POLICY_AUTHORITY_CHANGED";
3391
+ throw error;
3392
+ }
3393
+ const initialDigest = digestPolicyAuthority(initialProjection);
3394
+ shellDispatchPolicyAuthority = Object.freeze({
3395
+ policyVersion: `cc-shell-policy-authority/v1:${initialDigest}`,
3396
+ async revalidate() {
3397
+ try {
3398
+ if (
3399
+ (liveExecutionContext.permissionRulesProvider || null) !==
3400
+ permissionRulesProvider ||
3401
+ (liveExecutionContext.approvalGate || null) !== approvalGate ||
3402
+ (liveExecutionContext.planManager || getPlanModeManager()) !==
3403
+ planManager
3404
+ ) {
3405
+ const error = new Error("policy authority provider changed");
3406
+ error.code = "CC_SHELL_POLICY_AUTHORITY_CHANGED";
3407
+ throw error;
3408
+ }
3409
+ let currentPermissionAuthority = null;
3410
+ let currentPermissionRules =
3411
+ liveExecutionContext.permissionRules || null;
3412
+ if (permissionRulesProvider) {
3413
+ currentPermissionAuthority = await permissionRulesProvider({
3414
+ cwd,
3415
+ tool: name,
3416
+ args,
3417
+ });
3418
+ currentPermissionRules =
3419
+ currentPermissionAuthority?.rules ||
3420
+ currentPermissionAuthority ||
3421
+ null;
3422
+ }
3423
+ const currentProjection = {
3424
+ permission: projectPermissionAuthority({
3425
+ authority: currentPermissionAuthority,
3426
+ rules: validatePermissionRuleset(currentPermissionRules),
3427
+ tool: name,
3428
+ args,
3429
+ cwd,
3430
+ }),
3431
+ host: selectedHostToolPolicy(
3432
+ liveExecutionContext.hostManagedToolPolicy || null,
3433
+ name,
3434
+ ),
3435
+ plan: snapshotPlanToolAuthority(planManager, name, args),
3436
+ effectiveToolSet: snapshotEffectiveToolSetAuthority(
3437
+ liveExecutionContext.effectiveAllowedToolNames,
3438
+ name,
3439
+ ),
3440
+ toolAdmission: evaluateSelectedToolAdmissionAuthority({
3441
+ admission: liveExecutionContext.toolAdmission,
3442
+ tool: name,
3443
+ runtimeDescriptor,
3444
+ localToolDescriptor,
3445
+ hostManagedToolPolicy:
3446
+ liveExecutionContext.hostManagedToolPolicy,
3447
+ }).projection,
3448
+ approval: snapshotApprovalPolicyAuthority(
3449
+ approvalGate,
3450
+ context.sessionId || null,
3451
+ ),
3452
+ unattended: liveExecutionContext.unattendedActionPolicy || null,
3453
+ shellPolicyOverrides:
3454
+ liveExecutionContext.shellPolicyOverrides || null,
3455
+ classifyAllShell: liveExecutionContext.classifyAllShell === true,
3456
+ };
3457
+ if (digestPolicyAuthority(currentProjection) !== initialDigest) {
3458
+ const error = new Error(
3459
+ "Shell permission authority changed after initial admission",
3460
+ );
3461
+ error.code = "CC_SHELL_POLICY_AUTHORITY_CHANGED";
3462
+ throw error;
3463
+ }
3464
+ return true;
3465
+ } catch (error) {
3466
+ if (error?.code === "CC_SHELL_POLICY_AUTHORITY_CHANGED") {
3467
+ throw error;
3468
+ }
3469
+ const wrapped = new Error(
3470
+ "Current shell permission authority could not be revalidated",
3471
+ { cause: error },
3472
+ );
3473
+ wrapped.code =
3474
+ error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE";
3475
+ throw wrapped;
3476
+ }
3477
+ },
3478
+ });
3479
+ } catch (error) {
3480
+ return {
3481
+ error:
3482
+ "[Shell Dispatch] The current permission authority could not be bound. No command side effect was started.",
3483
+ policy: {
3484
+ decision: "blocked",
3485
+ via: "shell-policy-authority",
3486
+ code: error?.code || "CC_SHELL_POLICY_AUTHORITY_UNAVAILABLE",
3487
+ },
3488
+ };
3489
+ }
3490
+ }
3491
+
2831
3492
  // PreToolUse hooks. DB hooks (cc hook add) stay observe-only — a failure
2832
3493
  // never blocks. settings.json hooks (context.settingsHooks) are decision-
2833
3494
  // capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
@@ -2922,6 +3583,10 @@ export async function executeTool(name, args, context = {}) {
2922
3583
  sessionId: context.sessionId || null,
2923
3584
  turnId: context.turnId || null,
2924
3585
  toolCallId: context.toolCallId || null,
3586
+ workflowEffectId: context.workflowEffectId || null,
3587
+ workflowChildEffectId: context.workflowChildEffectId || null,
3588
+ workflowChildSequence: context.workflowChildSequence || null,
3589
+ workflowEffectProtocol: context.workflowEffectProtocol || null,
2925
3590
  // Parent LLM config — documented at toolContext as forwarded to
2926
3591
  // spawn_sub_agent for provider/key inheritance; without this line it
2927
3592
  // never actually reached executeToolInner (sub-agents silently fell
@@ -2951,12 +3616,14 @@ export async function executeTool(name, args, context = {}) {
2951
3616
  subtreeInstructionScope:
2952
3617
  context.subtreeInstructionScope || context.sessionId || "__legacy__",
2953
3618
  shellPolicyOverrides: context.shellPolicyOverrides || null,
3619
+ classifyAllShell: context.classifyAllShell === true,
2954
3620
  approvalGate: context.approvalGate || null,
2955
3621
  shellConfirm: context.shellConfirm || null,
2956
3622
  additionalDirectories: context.additionalDirectories || null,
2957
3623
  sandbox: context.sandbox || null,
2958
3624
  ruleAllowed,
2959
3625
  settingsVerdict,
3626
+ shellDispatchPolicyAuthority,
2960
3627
  subAgentDepth: context.subAgentDepth || 0,
2961
3628
  subAgentBudget: context.subAgentBudget || null,
2962
3629
  sessionBudget: context.sessionBudget || null,
@@ -4306,6 +4973,10 @@ async function executeToolInner(
4306
4973
  sessionId,
4307
4974
  turnId,
4308
4975
  toolCallId,
4976
+ workflowEffectId = null,
4977
+ workflowChildEffectId = null,
4978
+ workflowChildSequence = null,
4979
+ workflowEffectProtocol = null,
4309
4980
  planManager = null,
4310
4981
  permissionRules = null,
4311
4982
  effectiveAllowedToolNames = null,
@@ -4331,6 +5002,7 @@ async function executeToolInner(
4331
5002
  sandbox,
4332
5003
  ruleAllowed = false,
4333
5004
  settingsVerdict = null,
5005
+ shellDispatchPolicyAuthority = null,
4334
5006
  subAgentDepth = 0,
4335
5007
  subAgentBudget = null,
4336
5008
  sessionBudget = null,
@@ -4812,6 +5484,9 @@ async function executeToolInner(
4812
5484
  }
4813
5485
 
4814
5486
  case "run_shell": {
5487
+ // Bound the declared run_shell data once. Approval and dispatch never
5488
+ // observe later mutation of the caller-owned tool arguments.
5489
+ args = Object.isFrozen(args) ? args : snapshotShellExecutionArgs(args);
4815
5490
  if (unattendedActionPolicy?.unattended === true) {
4816
5491
  const unattendedVerdict = evaluateUnattendedShellAction(args.command, {
4817
5492
  ...unattendedActionPolicy,
@@ -4829,12 +5504,12 @@ async function executeToolInner(
4829
5504
  }
4830
5505
  }
4831
5506
 
4832
- const shellPolicyOpts = {
5507
+ const shellPolicyOpts = Object.freeze({
4833
5508
  ...(shellPolicyOverrides
4834
- ? { overrideRuleIds: shellPolicyOverrides }
5509
+ ? { overrideRuleIds: Object.freeze([...shellPolicyOverrides]) }
4835
5510
  : {}),
4836
5511
  ...(classifyAllShell ? { classifyAllShell: true } : {}),
4837
- };
5512
+ });
4838
5513
  // Layer-by-layer explanation chain for a blocked command: which layers
4839
5514
  // were consulted and what each said (settings rules → shell policy →
4840
5515
  // approval gate). Attached to denial results so `/permissions denials`
@@ -4870,60 +5545,80 @@ async function executeToolInner(
4870
5545
  return chain;
4871
5546
  };
4872
5547
  const override = getRuntimeToolDescriptorByCommand(args.command);
4873
- let shellPolicy;
5548
+ let shellPolicy = evaluateShellCommandPolicy(
5549
+ args.command,
5550
+ shellPolicyOpts,
5551
+ );
4874
5552
  let approvalOutcome = null;
5553
+ let approvalAuthorization = null;
5554
+ let approvalAuthorizationContext = null;
5555
+ let approvalGateResult = null;
4875
5556
  // A settings `allow` rule (ruleAllowed) pre-authorizes: skip the
4876
5557
  // ApprovalGate tier confirm, but still run the hard shell-policy denylist
4877
5558
  // below so an allow rule can never re-enable a blocked unsafe command.
4878
- if (approvalGate && !ruleAllowed) {
4879
- const { evaluateShellCommandWithApproval } =
4880
- await import("../lib/shell-approval.js");
4881
- const gated = await evaluateShellCommandWithApproval({
4882
- command: args.command,
4883
- sessionId,
4884
- approvalGate,
4885
- shellPolicyOptions: shellPolicyOpts,
4886
- });
4887
- shellPolicy = gated.shellPolicy;
4888
- approvalOutcome = {
4889
- decision: gated.decision,
4890
- via: gated.via,
4891
- riskLevel: gated.riskLevel,
4892
- policy: gated.policy,
4893
- };
4894
- if (!gated.allowed) {
4895
- // Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
4896
- // "denial reasons to transcripts"): tell it this won't change on
4897
- // retry and to involve the user, so it stops re-issuing the same
4898
- // blocked command (which otherwise just burns turns + tokens).
4899
- const tierLabel =
4900
- typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
4901
- return attachDescriptor(
4902
- {
4903
- error:
4904
- gated.via === "shell-policy"
4905
- ? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
4906
- : `[ApprovalGate] command denied by the ${tierLabel}approval policy (via ${gated.via}). Retrying the same command will not help — it needs user approval. Tell the user (they can run it themselves, approve it, or relax the policy) and continue with other work.`,
4907
- shellCommandPolicy: shellPolicy,
4908
- approval: approvalOutcome,
4909
- permissionChain: buildPermissionChain(gated),
4910
- },
4911
- override || runtimeDescriptor,
4912
- );
4913
- }
4914
- } else {
4915
- shellPolicy = evaluateShellCommandPolicy(args.command, shellPolicyOpts);
4916
- if (!shellPolicy.allowed) {
4917
- return attachDescriptor(
4918
- {
4919
- error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
4920
- shellCommandPolicy: shellPolicy,
4921
- permissionChain: buildPermissionChain(null),
4922
- },
4923
- override || runtimeDescriptor,
5559
+ const requestShellApproval = async (operationArgs) => {
5560
+ await shellDispatchPolicyAuthority?.revalidate?.();
5561
+ if (approvalGate && !ruleAllowed) {
5562
+ const approvalPolicyVersion = `${shellDispatchPolicyAuthority?.policyVersion || "cc-shell-policy-authority/v1:unobserved"}:shell:${shellPolicy.decision}:${shellPolicy.ruleId || "none"}`;
5563
+ const gated = await evaluateShellCommandWithApproval({
5564
+ command: args.command,
5565
+ args: operationArgs,
5566
+ sessionId,
5567
+ approvalGate,
5568
+ shellPolicyOptions: shellPolicyOpts,
5569
+ workspace: args.cwd || cwd,
5570
+ policyEnv: shellParentEnvironment,
5571
+ policyVersion: approvalPolicyVersion,
5572
+ deferSideEffects: true,
5573
+ });
5574
+ approvalGateResult = gated;
5575
+ shellPolicy = gated.shellPolicy;
5576
+ approvalOutcome = {
5577
+ decision: gated.decision,
5578
+ via: gated.via,
5579
+ riskLevel: gated.riskLevel,
5580
+ policy: gated.policy,
5581
+ };
5582
+ approvalAuthorization = gated.authorization;
5583
+ approvalAuthorizationContext = gated.authorizationContext;
5584
+ if (!gated.allowed) {
5585
+ // Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
5586
+ // "denial reasons to transcripts"): tell it this won't change on
5587
+ // retry and to involve the user, so it stops re-issuing the same
5588
+ // blocked command (which otherwise just burns turns + tokens).
5589
+ const tierLabel =
5590
+ typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
5591
+ return attachDescriptor(
5592
+ {
5593
+ error:
5594
+ gated.via === "shell-policy"
5595
+ ? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
5596
+ : `[ApprovalGate] command denied by the ${tierLabel}approval policy (via ${gated.via}). Retrying the same command will not help — it needs user approval. Tell the user (they can run it themselves, approve it, or relax the policy) and continue with other work.`,
5597
+ shellCommandPolicy: shellPolicy,
5598
+ approval: approvalOutcome,
5599
+ permissionChain: buildPermissionChain(gated),
5600
+ },
5601
+ override || runtimeDescriptor,
5602
+ );
5603
+ }
5604
+ } else {
5605
+ shellPolicy = evaluateShellCommandPolicy(
5606
+ args.command,
5607
+ shellPolicyOpts,
4924
5608
  );
5609
+ if (!shellPolicy.allowed) {
5610
+ return attachDescriptor(
5611
+ {
5612
+ error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
5613
+ shellCommandPolicy: shellPolicy,
5614
+ permissionChain: buildPermissionChain(null),
5615
+ },
5616
+ override || runtimeDescriptor,
5617
+ );
5618
+ }
4925
5619
  }
4926
- }
5620
+ return null;
5621
+ };
4927
5622
 
4928
5623
  // P1 #8 Windows/PowerShell first-class: per-call args.shell or the
4929
5624
  // configured settings `shell.windowsDefault` may route this command
@@ -4932,7 +5627,7 @@ async function executeToolInner(
4932
5627
  // the historical default shell byte-identical (useDefaultShell=true).
4933
5628
  const { resolveShellInvocation } =
4934
5629
  await import("../lib/shell-selector.cjs");
4935
- const shellInv = resolveShellInvocation({
5630
+ let shellInv = resolveShellInvocation({
4936
5631
  command: args.command,
4937
5632
  requested: args.shell,
4938
5633
  cwd: args.cwd || cwd,
@@ -4949,12 +5644,16 @@ async function executeToolInner(
4949
5644
  // Resolver failures for a matching strict alias are terminal — falling
4950
5645
  // back to a shell would bypass target identity and manifest requirements.
4951
5646
  let pluginBinInvocation = null;
4952
- let reattestPluginBinInvocation = null;
5647
+ let pluginBinRuntime = null;
4953
5648
  let pluginBinSandboxPolicy = null;
4954
5649
  let pluginBinSandboxExecutionContract = null;
5650
+ const backgroundPlatform =
5651
+ typeof _backgroundProcessDeps.platform === "function"
5652
+ ? _backgroundProcessDeps.platform()
5653
+ : process.platform;
4955
5654
  try {
4956
5655
  const pluginBin = await import("../lib/plugin-runtime/bin.js");
4957
- reattestPluginBinInvocation = pluginBin.reattestPluginBinInvocation;
5656
+ pluginBinRuntime = pluginBin;
4958
5657
  pluginBinSandboxPolicy = collectWorkspacePluginBinSandboxPolicy(
4959
5658
  pluginBin,
4960
5659
  cwd,
@@ -4978,8 +5677,8 @@ async function executeToolInner(
4978
5677
  { cwd },
4979
5678
  );
4980
5679
  if (
4981
- process.platform === "linux" ||
4982
- (process.platform === "win32" &&
5680
+ backgroundPlatform === "linux" ||
5681
+ (backgroundPlatform === "win32" &&
4983
5682
  pluginBinInvocation.runtime === "node")
4984
5683
  ) {
4985
5684
  try {
@@ -5024,7 +5723,7 @@ async function executeToolInner(
5024
5723
  const processOrigin = pluginBinInvocation
5025
5724
  ? "plugin:bin"
5026
5725
  : "tool:run_shell";
5027
- const processProvenance = {
5726
+ let processProvenance = {
5028
5727
  ...(pluginBinInvocation
5029
5728
  ? {
5030
5729
  pluginId: pluginBinInvocation.pluginId,
@@ -5056,23 +5755,264 @@ async function executeToolInner(
5056
5755
  direct_argv: true,
5057
5756
  },
5058
5757
  }
5059
- : {};
5758
+ : {};
5759
+
5760
+ // Snapshot process-derived execution inputs before confirmation. The
5761
+ // descriptor carries only an opaque, secret-free environment reference;
5762
+ // the actual child environment remains in this invocation-local closure.
5763
+ const shellParentEnvironment = Object.freeze({ ...process.env });
5764
+ const agentIdentityEnvironment = Object.freeze({
5765
+ CLAUDECODE: "1",
5766
+ ...(sessionId
5767
+ ? {
5768
+ CC_SESSION_ID: String(sessionId),
5769
+ CLAUDE_CODE_SESSION_ID: String(sessionId),
5770
+ }
5771
+ : {}),
5772
+ });
5773
+ const credentialProxyResult = applyCredentialProxy(
5774
+ {
5775
+ ...shellParentEnvironment,
5776
+ ...agentIdentityEnvironment,
5777
+ },
5778
+ { env: shellParentEnvironment },
5779
+ );
5780
+ const shellChildEnvironment = Object.freeze({
5781
+ ...credentialProxyResult.env,
5782
+ });
5783
+ const sandboxChildEnvironment = agentIdentityEnvironment;
5784
+ const shellExecutionDescriptor = createShellExecutionDescriptor({
5785
+ args,
5786
+ workspace: args.cwd || cwd,
5787
+ shellInvocation: shellInv,
5788
+ pluginInvocation: pluginBinInvocation,
5789
+ pluginSandboxPolicy: pluginBinSandboxPolicy,
5790
+ pluginSandboxExecutionContract: pluginBinSandboxExecutionContract,
5791
+ sandbox,
5792
+ environmentRef: {
5793
+ id: `run-shell-env:${randomUUID()}`,
5794
+ inheritance: sandbox ? "sandbox-minimal" : "host-snapshot",
5795
+ credentialProxy: credentialProxyResult.enabled === true,
5796
+ maskedNames: Object.freeze([...credentialProxyResult.masked]),
5797
+ },
5798
+ });
5799
+ // From here onward the descriptor itself is the tool-argument snapshot.
5800
+ args = shellExecutionDescriptor;
5801
+ shellInv = shellExecutionDescriptor.execution.shell;
5802
+ const shellSandbox = shellExecutionDescriptor.execution.sandbox;
5803
+ const executionPlugin = shellExecutionDescriptor.execution.plugin;
5804
+ const approvedPluginProjection = executionPlugin
5805
+ ? canonicalPolicyAuthorityData(executionPlugin)
5806
+ : null;
5807
+ const approvedWorkspacePluginPolicy = canonicalPolicyAuthorityData(
5808
+ pluginBinSandboxPolicy,
5809
+ );
5810
+ const approvedPluginInvocationAuthority = pluginBinInvocation
5811
+ ? canonicalPolicyAuthorityData(
5812
+ projectPluginBinInvocationAuthority(pluginBinInvocation),
5813
+ )
5814
+ : null;
5815
+ const refreshPluginBinExecution = () => {
5816
+ if (!executionPlugin) return;
5817
+ if (!pluginBinRuntime) {
5818
+ const error = new Error(
5819
+ "Plugin bin runtime is unavailable during launch revalidation",
5820
+ );
5821
+ error.code = "ERR_PLUGIN_BIN_DISCOVERY_FAILED";
5822
+ error.pluginBinFailClosed = true;
5823
+ throw error;
5824
+ }
5825
+ const freshInvocation = pluginBinRuntime.resolvePluginBinInvocation(
5826
+ shellExecutionDescriptor.command,
5827
+ {
5828
+ cwd,
5829
+ commandCwd: shellExecutionDescriptor.cwd || cwd,
5830
+ failClosed: true,
5831
+ },
5832
+ );
5833
+ if (!freshInvocation) {
5834
+ const error = new Error(
5835
+ "Approved plugin bin alias is no longer available",
5836
+ );
5837
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5838
+ error.pluginBinFailClosed = true;
5839
+ throw error;
5840
+ }
5841
+ if (
5842
+ canonicalPolicyAuthorityData(
5843
+ projectPluginBinInvocationAuthority(freshInvocation),
5844
+ ) !== approvedPluginInvocationAuthority
5845
+ ) {
5846
+ const error = new Error(
5847
+ "Plugin bin manifest, trust, alias, or sandbox authority changed after approval",
5848
+ );
5849
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5850
+ error.pluginBinFailClosed = true;
5851
+ throw error;
5852
+ }
5853
+ let freshPolicy = collectWorkspacePluginBinSandboxPolicy(
5854
+ pluginBinRuntime,
5855
+ cwd,
5856
+ shellExecutionDescriptor.cwd || cwd,
5857
+ );
5858
+ if (freshInvocation.sandboxPolicy) {
5859
+ freshPolicy = pluginBinRuntime.pinPluginBinSandboxPolicy(
5860
+ {
5861
+ requiredBoundaries: [
5862
+ ...(freshPolicy?.requiredBoundaries || []),
5863
+ ...(freshInvocation.sandboxPolicy.requiredBoundaries || []),
5864
+ ],
5865
+ },
5866
+ { cwd },
5867
+ );
5868
+ }
5869
+ let freshContract = null;
5870
+ if (
5871
+ freshInvocation.sandboxPolicy &&
5872
+ (backgroundPlatform === "linux" ||
5873
+ (backgroundPlatform === "win32" &&
5874
+ freshInvocation.runtime === "node"))
5875
+ ) {
5876
+ freshContract = pluginBinRuntime.createPluginSandboxExecutionContract(
5877
+ freshInvocation,
5878
+ { sync: shellExecutionDescriptor.run_in_background !== true },
5879
+ );
5880
+ }
5881
+ const freshProjection = createShellExecutionDescriptor({
5882
+ args: shellExecutionDescriptor,
5883
+ workspace: shellExecutionDescriptor.execution.workspace,
5884
+ shellInvocation: shellExecutionDescriptor.execution.shell,
5885
+ pluginInvocation: freshInvocation,
5886
+ pluginSandboxPolicy: freshPolicy,
5887
+ pluginSandboxExecutionContract: freshContract,
5888
+ sandbox: shellExecutionDescriptor.execution.sandbox,
5889
+ environmentRef: shellExecutionDescriptor.execution.environment,
5890
+ }).execution.plugin;
5891
+ if (
5892
+ canonicalPolicyAuthorityData(freshProjection) !==
5893
+ approvedPluginProjection
5894
+ ) {
5895
+ const error = new Error(
5896
+ "Plugin bin manifest, trust, alias, or sandbox authority changed after approval",
5897
+ );
5898
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5899
+ error.pluginBinFailClosed = true;
5900
+ throw error;
5901
+ }
5902
+ pluginBinInvocation = freshInvocation;
5903
+ pluginBinSandboxPolicy = freshPolicy;
5904
+ pluginBinSandboxExecutionContract = freshContract;
5905
+ processProvenance = {
5906
+ pluginId: freshInvocation.pluginId,
5907
+ pluginVersion: freshInvocation.pluginVersion,
5908
+ pluginSource: freshInvocation.pluginSource,
5909
+ pluginExecutableIdentity: freshInvocation.executableIdentity,
5910
+ ...(freshPolicy ? { sandboxPolicy: freshPolicy } : {}),
5911
+ ...(freshContract ? { sandboxExecutionContract: freshContract } : {}),
5912
+ };
5913
+ };
5914
+ const refreshPluginExecutionAuthority = () => {
5915
+ if (executionPlugin) {
5916
+ refreshPluginBinExecution();
5917
+ return;
5918
+ }
5919
+ if (!pluginBinRuntime) return;
5920
+ const freshPolicy = collectWorkspacePluginBinSandboxPolicy(
5921
+ pluginBinRuntime,
5922
+ cwd,
5923
+ shellExecutionDescriptor.cwd || cwd,
5924
+ );
5925
+ if (
5926
+ canonicalPolicyAuthorityData(freshPolicy) !==
5927
+ approvedWorkspacePluginPolicy
5928
+ ) {
5929
+ const error = new Error(
5930
+ "Workspace plugin sandbox authority changed after approval",
5931
+ );
5932
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5933
+ error.pluginBinFailClosed = true;
5934
+ throw error;
5935
+ }
5936
+ pluginBinSandboxPolicy = freshPolicy;
5937
+ processProvenance = freshPolicy ? { sandboxPolicy: freshPolicy } : {};
5938
+ };
5939
+
5940
+ const approvalDenial = await requestShellApproval(
5941
+ shellExecutionDescriptor,
5942
+ );
5943
+ if (approvalDenial) return approvalDenial;
5944
+
5945
+ let shellDispatchAdmitted = false;
5946
+ const admitShellDispatch = async () => {
5947
+ if (shellDispatchAdmitted) {
5948
+ throw new Error("Shell dispatch authorization was already consumed");
5949
+ }
5950
+ await shellDispatchPolicyAuthority?.revalidate?.();
5951
+ refreshPluginExecutionAuthority();
5952
+ const revalidated = evaluateShellCommandPolicy(
5953
+ shellExecutionDescriptor.command,
5954
+ shellPolicyOpts,
5955
+ );
5956
+ if (
5957
+ !revalidated.allowed ||
5958
+ revalidated.decision !== shellPolicy.decision ||
5959
+ (revalidated.ruleId || null) !== (shellPolicy.ruleId || null)
5960
+ ) {
5961
+ const error = new Error(
5962
+ "Shell policy changed after approval; command dispatch was denied",
5963
+ );
5964
+ error.code = "ERR_SHELL_POLICY_REVALIDATION_FAILED";
5965
+ throw error;
5966
+ }
5967
+ if (approvalAuthorization) {
5968
+ if (typeof approvalGate?.consumeAuthorization !== "function") {
5969
+ throw new Error(
5970
+ "Remote shell approval cannot be consumed by this ApprovalGate",
5971
+ );
5972
+ }
5973
+ await approvalGate.consumeAuthorization(
5974
+ approvalAuthorization,
5975
+ approvalAuthorizationContext,
5976
+ );
5977
+ approvalAuthorization = null;
5978
+ }
5979
+ await shellDispatchPolicyAuthority?.revalidate?.();
5980
+ refreshPluginExecutionAuthority();
5981
+ const finalPolicy = evaluateShellCommandPolicy(
5982
+ shellExecutionDescriptor.command,
5983
+ shellPolicyOpts,
5984
+ );
5985
+ if (
5986
+ !finalPolicy.allowed ||
5987
+ finalPolicy.decision !== shellPolicy.decision ||
5988
+ (finalPolicy.ruleId || null) !== (shellPolicy.ruleId || null)
5989
+ ) {
5990
+ const error = new Error(
5991
+ "Shell policy changed while authorization was consumed; command dispatch was denied",
5992
+ );
5993
+ error.code = "ERR_SHELL_POLICY_REVALIDATION_FAILED";
5994
+ throw error;
5995
+ }
5996
+ // Auditing is intentionally committed only after durable authorization
5997
+ // consumption and immediately before the first external side effect.
5998
+ commitShellApprovalSideEffects(approvalGateResult);
5999
+ if (pluginBinInvocation) {
6000
+ pluginBinResult.plugin_bin.launch_identity_reattested = true;
6001
+ }
6002
+ shellDispatchAdmitted = true;
6003
+ };
5060
6004
 
5061
6005
  // Background: spawn, register, return a task_id immediately. The agent
5062
6006
  // polls output + completion via check_shell. No timeout — that's the whole
5063
6007
  // point of backgrounding (builds, test suites, dev servers).
5064
6008
  if (args.run_in_background === true) {
5065
- if (sandbox) {
6009
+ if (shellSandbox) {
5066
6010
  return attachDescriptor({
5067
6011
  error:
5068
6012
  "[Sandbox] Background shell tasks are not supported in the ephemeral sandbox. Run in the foreground or explicitly disable --sandbox.",
5069
6013
  policy: { decision: "deny", via: "sandbox" },
5070
6014
  });
5071
6015
  }
5072
- const backgroundPlatform =
5073
- typeof _backgroundProcessDeps.platform === "function"
5074
- ? _backgroundProcessDeps.platform()
5075
- : process.platform;
5076
6016
  const pinnedBackgroundBoundaries = [
5077
6017
  ...(pluginBinSandboxPolicy?.requiredBoundaries || []),
5078
6018
  ];
@@ -5124,16 +6064,7 @@ async function executeToolInner(
5124
6064
  cwd: workingDirectory,
5125
6065
  shell: false,
5126
6066
  windowsHide: true,
5127
- env: applyCredentialProxy({
5128
- ...process.env,
5129
- CLAUDECODE: "1",
5130
- ...(sessionId
5131
- ? {
5132
- CC_SESSION_ID: String(sessionId),
5133
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5134
- }
5135
- : {}),
5136
- }).env,
6067
+ env: shellChildEnvironment,
5137
6068
  detached: false,
5138
6069
  origin: "tool:run_shell",
5139
6070
  policy: "allow",
@@ -5188,6 +6119,9 @@ async function executeToolInner(
5188
6119
  },
5189
6120
  };
5190
6121
  }
6122
+ // Admission precedes task cleanup, sequence allocation, listener
6123
+ // registration, proxy startup and process creation.
6124
+ await admitShellDispatch();
5191
6125
  // Free memory from idle background tasks before adding another, so a
5192
6126
  // long agent run can't accumulate forgotten dev servers (no-op unless
5193
6127
  // the machine is actually under memory pressure).
@@ -5199,7 +6133,7 @@ async function executeToolInner(
5199
6133
  cwd:
5200
6134
  linuxGenericBackgroundLaunch?.options.cwd ||
5201
6135
  (linuxDirectPluginStrongBackground
5202
- ? pluginBinSandboxExecutionContract.workingDirectory
6136
+ ? executionPlugin.sandboxExecution.workingDirectory
5203
6137
  : args.cwd || cwd),
5204
6138
  status: "running",
5205
6139
  exitCode: null,
@@ -5230,16 +6164,7 @@ async function executeToolInner(
5230
6164
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5231
6165
  // real long-lived secrets out of the spawned command's env — a
5232
6166
  // no-op (same object) when disabled. See credential-proxy.js.
5233
- env: applyCredentialProxy({
5234
- ...process.env,
5235
- CLAUDECODE: "1",
5236
- ...(sessionId
5237
- ? {
5238
- CC_SESSION_ID: String(sessionId),
5239
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5240
- }
5241
- : {}),
5242
- }).env,
6167
+ env: shellChildEnvironment,
5243
6168
  // POSIX: own process group so check_shell{kill}/teardown can signal
5244
6169
  // the whole tree (shell + its grandchild command). No-op on Windows
5245
6170
  // where the tree is killed via taskkill /T instead.
@@ -5266,18 +6191,16 @@ async function executeToolInner(
5266
6191
  linuxGenericBackgroundLaunch.options,
5267
6192
  );
5268
6193
  } else if (pluginBinInvocation) {
5269
- reattestPluginBinInvocation(pluginBinInvocation);
5270
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5271
6194
  child = broker.spawn(
5272
- pluginBinSandboxExecutionContract?.kind ===
6195
+ executionPlugin.sandboxExecution?.kind ===
5273
6196
  "strict-plugin-node-bin"
5274
- ? pluginBinSandboxExecutionContract.runtimePath
5275
- : pluginBinInvocation.command,
5276
- pluginBinInvocation.args,
6197
+ ? executionPlugin.sandboxExecution.runtimePath
6198
+ : executionPlugin.command,
6199
+ executionPlugin.argv,
5277
6200
  {
5278
6201
  ...brokerOpts,
5279
6202
  cwd:
5280
- pluginBinSandboxExecutionContract?.workingDirectory ||
6203
+ executionPlugin.sandboxExecution?.workingDirectory ||
5281
6204
  brokerOpts.cwd,
5282
6205
  shell: false,
5283
6206
  },
@@ -5373,7 +6296,7 @@ async function executeToolInner(
5373
6296
  );
5374
6297
  }
5375
6298
 
5376
- if (sandbox) {
6299
+ if (shellSandbox) {
5377
6300
  if (pluginBinSandboxPolicy) {
5378
6301
  return attachDescriptor(
5379
6302
  {
@@ -5391,15 +6314,16 @@ async function executeToolInner(
5391
6314
  override || runtimeDescriptor,
5392
6315
  );
5393
6316
  }
6317
+ await admitShellDispatch();
5394
6318
  const { executeSandboxedShell, sandboxSummary } =
5395
6319
  await import("../lib/agent-sandbox.js");
5396
6320
  // Domain-restricted networking is ENFORCED by routing the sandboxed
5397
6321
  // process's egress through a local filtering proxy (see
5398
6322
  // sandbox-egress-proxy.js). Start it only when the policy actually
5399
6323
  // restricts domains and network is on; tear it down after the command.
5400
- const sboxPolicy = sandbox.policy || {};
6324
+ const sboxPolicy = shellSandbox.policy || {};
5401
6325
  const needsEgress =
5402
- sandbox.network === true &&
6326
+ shellSandbox.network === true &&
5403
6327
  (sboxPolicy.allowedDomains?.length || 0) +
5404
6328
  (sboxPolicy.deniedDomains?.length || 0) >
5405
6329
  0;
@@ -5428,20 +6352,25 @@ async function executeToolInner(
5428
6352
  }
5429
6353
  let result;
5430
6354
  try {
5431
- result = executeSandboxedShell(args.command, sandbox, {
6355
+ // Proxy startup and dynamic imports are asynchronous. Re-read the
6356
+ // live authority once more after they settle and immediately before
6357
+ // the synchronous sandbox dispatch.
6358
+ await shellDispatchPolicyAuthority?.revalidate?.();
6359
+ refreshPluginExecutionAuthority();
6360
+ if (pluginBinSandboxPolicy) {
6361
+ const error = new Error(
6362
+ "A plugin sandbox authority appeared while the legacy shell sandbox was starting",
6363
+ );
6364
+ error.code = "ERR_PLUGIN_BIN_SANDBOX_ROUTE_CONFLICT";
6365
+ error.pluginBinFailClosed = true;
6366
+ throw error;
6367
+ }
6368
+ result = executeSandboxedShell(args.command, shellSandbox, {
5432
6369
  cwd: args.cwd || cwd,
5433
6370
  timeout: _resolveShellTimeout(args.timeout),
5434
6371
  maxBuffer: 1024 * 1024,
5435
6372
  egressProxy,
5436
- env: {
5437
- CLAUDECODE: "1",
5438
- ...(sessionId
5439
- ? {
5440
- CC_SESSION_ID: String(sessionId),
5441
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5442
- }
5443
- : {}),
5444
- },
6373
+ env: sandboxChildEnvironment,
5445
6374
  });
5446
6375
  } finally {
5447
6376
  if (proxyHandle) {
@@ -5453,7 +6382,7 @@ async function executeToolInner(
5453
6382
  }
5454
6383
  }
5455
6384
  const common = {
5456
- sandbox: sandboxSummary(sandbox),
6385
+ sandbox: sandboxSummary(shellSandbox),
5457
6386
  shellCommandPolicy: shellPolicy,
5458
6387
  approval: approvalOutcome,
5459
6388
  policyTrace: ["shell-policy", "approval", "sandbox"],
@@ -5492,18 +6421,10 @@ async function executeToolInner(
5492
6421
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5493
6422
  // real long-lived secrets out of the shell's env — a no-op (same
5494
6423
  // object) when disabled. See credential-proxy.js.
5495
- env: applyCredentialProxy({
5496
- ...process.env,
5497
- CLAUDECODE: "1",
5498
- ...(sessionId
5499
- ? {
5500
- CC_SESSION_ID: String(sessionId),
5501
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5502
- }
5503
- : {}),
5504
- }).env,
6424
+ env: shellChildEnvironment,
5505
6425
  };
5506
6426
  let output;
6427
+ await admitShellDispatch();
5507
6428
  const brokerExecOpts = {
5508
6429
  ...fgExecOpts,
5509
6430
  origin: processOrigin,
@@ -5512,17 +6433,15 @@ async function executeToolInner(
5512
6433
  ...processProvenance,
5513
6434
  };
5514
6435
  if (pluginBinInvocation) {
5515
- reattestPluginBinInvocation(pluginBinInvocation);
5516
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5517
6436
  const res = broker.spawnSync(
5518
- pluginBinSandboxExecutionContract?.kind === "strict-plugin-node-bin"
5519
- ? pluginBinSandboxExecutionContract.runtimePath
5520
- : pluginBinInvocation.command,
5521
- pluginBinInvocation.args,
6437
+ executionPlugin.sandboxExecution?.kind === "strict-plugin-node-bin"
6438
+ ? executionPlugin.sandboxExecution.runtimePath
6439
+ : executionPlugin.command,
6440
+ executionPlugin.argv,
5522
6441
  {
5523
6442
  ...brokerExecOpts,
5524
6443
  cwd:
5525
- pluginBinSandboxExecutionContract?.workingDirectory ||
6444
+ executionPlugin.sandboxExecution?.workingDirectory ||
5526
6445
  brokerExecOpts.cwd,
5527
6446
  shell: false,
5528
6447
  windowsHide: true,
@@ -5601,6 +6520,15 @@ async function executeToolInner(
5601
6520
  : {}),
5602
6521
  stderr: (err.stderr || "").substring(0, 2000),
5603
6522
  exitCode: err.status,
6523
+ ...(typeof err.code === "string" && err.code
6524
+ ? {
6525
+ policy: {
6526
+ decision: "deny",
6527
+ via: "dispatch-revalidation",
6528
+ code: err.code,
6529
+ },
6530
+ }
6531
+ : {}),
5604
6532
  ...shellMeta,
5605
6533
  ...pluginBinResult,
5606
6534
  shellCommandPolicy: shellPolicy,
@@ -5724,8 +6652,18 @@ async function executeToolInner(
5724
6652
  approvalGate &&
5725
6653
  typeof approvalGate.hasConfirmer === "function" &&
5726
6654
  !approvalGate.hasConfirmer();
6655
+ // A durable remote-approval consumer is an explicit fail-closed mode.
6656
+ // run_code does not yet carry an exact lease tuple, so it must still
6657
+ // consult the remote confirmer in headless sessions; that confirmer
6658
+ // returns lease-unavailable and no code is persisted or launched.
6659
+ const durableAuthorizationGate =
6660
+ approvalGate &&
6661
+ typeof approvalGate.hasAuthorizationConsumer === "function" &&
6662
+ approvalGate.hasAuthorizationConsumer() === true;
5727
6663
  if (
5728
- (interactiveApproval || policyGateNoConfirmer) &&
6664
+ (interactiveApproval ||
6665
+ policyGateNoConfirmer ||
6666
+ durableAuthorizationGate) &&
5729
6667
  approvalGate &&
5730
6668
  !ruleAllowed &&
5731
6669
  typeof approvalGate.decide === "function"
@@ -5736,7 +6674,7 @@ async function executeToolInner(
5736
6674
  sessionId,
5737
6675
  riskLevel: APPROVAL_RISK.HIGH,
5738
6676
  tool: "run_code",
5739
- args: { language: args.language },
6677
+ args: runCodeApprovalDescriptor(args, cwd),
5740
6678
  });
5741
6679
  if (gate.decision !== APPROVAL_DECISION.ALLOW) {
5742
6680
  // Actionable denial (matches run_shell, Claude-Code 2.1.193): tell the
@@ -5753,6 +6691,23 @@ async function executeToolInner(
5753
6691
  },
5754
6692
  });
5755
6693
  }
6694
+ if (gate.authorization) {
6695
+ return attachDescriptor({
6696
+ error:
6697
+ "[ApprovalGate] run_code received a durable authorization, but this tool does not yet implement an exact one-shot consume tuple. No code was launched.",
6698
+ approval: {
6699
+ decision: APPROVAL_DECISION.DENY,
6700
+ via: "authorization-consume-unsupported",
6701
+ riskLevel: "high",
6702
+ policy: gate.policy,
6703
+ },
6704
+ policy: {
6705
+ decision: "deny",
6706
+ via: "authorization-consume-unsupported",
6707
+ code: "CC_RUN_CODE_AUTHORIZATION_CONSUME_UNSUPPORTED",
6708
+ },
6709
+ });
6710
+ }
5756
6711
  }
5757
6712
  return attachDescriptor(await _executeRunCode(args, cwd));
5758
6713
  }
@@ -5766,6 +6721,10 @@ async function executeToolInner(
5766
6721
  interaction,
5767
6722
  sessionId,
5768
6723
  llmOptions,
6724
+ workflowEffectId,
6725
+ workflowChildEffectId,
6726
+ workflowChildSequence,
6727
+ workflowEffectProtocol,
5769
6728
  subAgentDepth,
5770
6729
  subAgentBudget,
5771
6730
  sessionBudget,
@@ -6699,6 +7658,9 @@ async function executeToolInner(
6699
7658
  ? event
6700
7659
  : {
6701
7660
  ...event,
7661
+ ...(["model", "semantic-compaction"].includes(event?.source)
7662
+ ? { workflowRequestSource: event.source }
7663
+ : {}),
6702
7664
  source:
6703
7665
  event?.source === "semantic-compaction"
6704
7666
  ? "semantic-compaction"
@@ -6774,6 +7736,9 @@ async function executeToolInner(
6774
7736
  toolAdmission,
6775
7737
  cwd,
6776
7738
  llmOptions: skillLlmOptions,
7739
+ ...(strictUsageTelemetry === true && workflowChildEffectId
7740
+ ? { workflowEffectId: workflowChildEffectId }
7741
+ : {}),
6777
7742
  ...(sessionBudget ? { sessionBudget } : {}),
6778
7743
  ...(permissionRules ? { permissionRules } : {}),
6779
7744
  ...(hostManagedToolPolicy
@@ -6864,7 +7829,12 @@ async function executeToolInner(
6864
7829
  toolsUsed: result.toolsUsed,
6865
7830
  });
6866
7831
  } catch (err) {
6867
- if (err?.runtimeLedgerPersistence === true) throw err;
7832
+ if (
7833
+ err?.runtimeLedgerPersistence === true ||
7834
+ err?.workflowEffectOutcomeUnknown === true
7835
+ ) {
7836
+ throw err;
7837
+ }
6868
7838
  if (
6869
7839
  signal?.aborted ||
6870
7840
  skillExecutionSignal?.aborted ||
@@ -7088,6 +8058,14 @@ async function executeToolInner(
7088
8058
  ledgerTicket = await ledger.begin({
7089
8059
  sessionId,
7090
8060
  turnId,
8061
+ ...(workflowEffectId
8062
+ ? {
8063
+ workflowEffectId,
8064
+ workflowChildEffectId,
8065
+ workflowChildSequence,
8066
+ workflowEffectProtocol,
8067
+ }
8068
+ : {}),
7091
8069
  toolName: localToolExecutor.toolName,
7092
8070
  serverName: localToolExecutor.serverName,
7093
8071
  input: mcpWireInput,
@@ -7161,12 +8139,20 @@ async function executeToolInner(
7161
8139
  localToolExecutor.toolName,
7162
8140
  mcpWireInput,
7163
8141
  ];
7164
- if (signal || mcpDispatchAdmission) {
8142
+ if (signal || mcpDispatchAdmission || workflowEffectId) {
7165
8143
  callArguments.push({
7166
8144
  ...(signal ? { signal } : {}),
7167
8145
  ...(mcpDispatchAdmission
7168
8146
  ? { dispatchAdmission: mcpDispatchAdmission }
7169
8147
  : {}),
8148
+ ...(workflowEffectId
8149
+ ? {
8150
+ workflowEffectId,
8151
+ workflowChildEffectId,
8152
+ workflowChildSequence,
8153
+ workflowEffectProtocol,
8154
+ }
8155
+ : {}),
7170
8156
  });
7171
8157
  }
7172
8158
  pendingResult = Reflect.apply(
@@ -7295,8 +8281,9 @@ async function executeToolInner(
7295
8281
  );
7296
8282
  }
7297
8283
 
8284
+ let settledLedgerRecord;
7298
8285
  try {
7299
- await ledgerTicket.settle(
8286
+ settledLedgerRecord = await ledgerTicket.settle(
7300
8287
  protocolError
7301
8288
  ? {
7302
8289
  status: "failed",
@@ -7318,11 +8305,26 @@ async function executeToolInner(
7318
8305
  }
7319
8306
 
7320
8307
  const mcpLedgerId = safeMcpProperty(ledgerTicket, "ledgerId") || null;
8308
+ const mcpLedgerPrewritePersisted =
8309
+ safeMcpProperty(ledgerTicket, "prewritePersisted") === true;
8310
+ const mcpLedgerSettlementPersisted =
8311
+ safeMcpProperty(settledLedgerRecord, "settlementPersistence") ===
8312
+ "persisted";
7321
8313
  try {
7322
8314
  if (result && typeof result === "object") {
7323
- return attachDescriptor({ ...result, mcpLedgerId });
8315
+ return attachDescriptor({
8316
+ ...result,
8317
+ mcpLedgerId,
8318
+ mcpLedgerPrewritePersisted,
8319
+ mcpLedgerSettlementPersisted,
8320
+ });
7324
8321
  }
7325
- return attachDescriptor({ result, mcpLedgerId });
8322
+ return attachDescriptor({
8323
+ result,
8324
+ mcpLedgerId,
8325
+ mcpLedgerPrewritePersisted,
8326
+ mcpLedgerSettlementPersisted,
8327
+ });
7326
8328
  } catch (projectionError) {
7327
8329
  return attachDescriptor({
7328
8330
  error:
@@ -7349,7 +8351,18 @@ async function executeToolInner(
7349
8351
  interaction &&
7350
8352
  typeof interaction.requestHostTool === "function"
7351
8353
  ) {
7352
- const hostedResult = await interaction.requestHostTool(name, args);
8354
+ const hostedResult = workflowEffectId
8355
+ ? await interaction.requestHostTool(
8356
+ name,
8357
+ args,
8358
+ Object.freeze({
8359
+ workflowEffectId,
8360
+ workflowChildEffectId,
8361
+ workflowChildSequence,
8362
+ workflowEffectProtocol,
8363
+ }),
8364
+ )
8365
+ : await interaction.requestHostTool(name, args);
7353
8366
  if (hostedResult?.success === false) {
7354
8367
  return attachDescriptor({
7355
8368
  error:
@@ -8012,7 +9025,12 @@ function _backgroundSubAgentResultText(entry) {
8012
9025
 
8013
9026
  function _throwBackgroundSubAgentUsageFailure(entry) {
8014
9027
  const error = entry?.outcome?.fatalError;
8015
- if (error?.runtimeLedgerPersistence === true) throw error;
9028
+ if (
9029
+ error?.runtimeLedgerPersistence === true ||
9030
+ error?.workflowEffectOutcomeUnknown === true
9031
+ ) {
9032
+ throw error;
9033
+ }
8016
9034
  }
8017
9035
 
8018
9036
  function _throwSettledBackgroundUsageFailure(map) {
@@ -8567,6 +9585,9 @@ async function _executeSpawnSubAgent(args, ctx) {
8567
9585
  ? u
8568
9586
  : {
8569
9587
  ...u,
9588
+ ...(["model", "semantic-compaction"].includes(u?.source)
9589
+ ? { workflowRequestSource: u.source }
9590
+ : {}),
8570
9591
  source:
8571
9592
  u?.source === "semantic-compaction"
8572
9593
  ? "semantic-compaction"
@@ -8716,6 +9737,9 @@ async function _executeSpawnSubAgent(args, ctx) {
8716
9737
  cwd: ctx.cwd,
8717
9738
  profile: profile || null,
8718
9739
  llmOptions: subLlmOptions,
9740
+ ...(ctx.strictUsageTelemetry === true && ctx.workflowChildEffectId
9741
+ ? { workflowEffectId: ctx.workflowChildEffectId }
9742
+ : {}),
8719
9743
  depth: currentDepth + 1, // nested spawns see their own level
8720
9744
  // Same shared counter object so the child's own spawns draw from the run's
8721
9745
  // single total-sub-agent pool (breadth cap spans the whole tree).
@@ -8917,7 +9941,8 @@ async function _executeSpawnSubAgent(args, ctx) {
8917
9941
  return {
8918
9942
  result: subCtx.result,
8919
9943
  error: err.message,
8920
- ...(err?.runtimeLedgerPersistence === true
9944
+ ...(err?.runtimeLedgerPersistence === true ||
9945
+ err?.workflowEffectOutcomeUnknown === true
8921
9946
  ? { fatalError: err }
8922
9947
  : {}),
8923
9948
  };
@@ -9050,7 +10075,12 @@ async function _executeSpawnSubAgent(args, ctx) {
9050
10075
  completedAt: subCtx.completedAt,
9051
10076
  });
9052
10077
 
9053
- if (err?.runtimeLedgerPersistence === true) throw err;
10078
+ if (
10079
+ err?.runtimeLedgerPersistence === true ||
10080
+ err?.workflowEffectOutcomeUnknown === true
10081
+ ) {
10082
+ throw err;
10083
+ }
9054
10084
 
9055
10085
  return {
9056
10086
  error: `Sub-agent failed: ${err.message}`,
@@ -9130,6 +10160,72 @@ function _hasCompleteProviderUsage(usage) {
9130
10160
  );
9131
10161
  }
9132
10162
 
10163
+ const PROVIDER_REQUEST_ID_RE = /^[\x21-\x7e]{1,512}$/;
10164
+ const PROVIDER_RECEIPT_ID_RE = /^[A-Za-z0-9._:-]{1,256}$/;
10165
+
10166
+ function _normalizeProviderRequestId(value) {
10167
+ if (value == null) return null;
10168
+ if (typeof value !== "string" || !PROVIDER_REQUEST_ID_RE.test(value)) {
10169
+ throw new TypeError(
10170
+ "providerRequestId must be 1-512 printable ASCII characters",
10171
+ );
10172
+ }
10173
+ return value;
10174
+ }
10175
+
10176
+ function _physicalProviderRequestId(logicalRequestId, attempt) {
10177
+ if (!logicalRequestId) return null;
10178
+ const retry = Number(attempt);
10179
+ if (!Number.isSafeInteger(retry) || retry <= 0) return logicalRequestId;
10180
+ const suffix = `-r${retry}`;
10181
+ return `${logicalRequestId.slice(0, 512 - suffix.length)}${suffix}`;
10182
+ }
10183
+
10184
+ function _providerReceiptId(value) {
10185
+ return typeof value === "string" && PROVIDER_RECEIPT_ID_RE.test(value)
10186
+ ? value
10187
+ : null;
10188
+ }
10189
+
10190
+ function _isOfficialOpenAIEndpoint(provider, baseUrl) {
10191
+ if (provider !== "openai") return false;
10192
+ try {
10193
+ const parsed = new URL(String(baseUrl));
10194
+ return (
10195
+ parsed.protocol === "https:" &&
10196
+ parsed.hostname.toLowerCase() === "api.openai.com" &&
10197
+ !parsed.port &&
10198
+ !parsed.username &&
10199
+ !parsed.password
10200
+ );
10201
+ } catch {
10202
+ return false;
10203
+ }
10204
+ }
10205
+
10206
+ /**
10207
+ * Capture only provider-returned identifiers. X-Client-Request-Id is a
10208
+ * trace/correlation header, not an idempotency guarantee, so a locally sent
10209
+ * client id alone is deliberately not called a receipt.
10210
+ */
10211
+ function _openAIProviderRequestReceipt(response, data, clientRequestId) {
10212
+ if (!clientRequestId) return null;
10213
+ const requestId = _providerReceiptId(
10214
+ response?.headers?.get?.("x-request-id"),
10215
+ );
10216
+ const responseId = _providerReceiptId(data?.id);
10217
+ if (!requestId && !responseId) return null;
10218
+ return Object.freeze({
10219
+ protocol: "cc-provider-request-receipt/v1",
10220
+ provider: "openai",
10221
+ clientRequestId,
10222
+ requestId,
10223
+ responseId,
10224
+ requestIdentitySemantics: "trace-only",
10225
+ independentlyReadable: false,
10226
+ });
10227
+ }
10228
+
9133
10229
  /**
9134
10230
  * Send a chat completion request with tool definitions.
9135
10231
  * Supports 8 providers: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral, volcengine
@@ -9148,6 +10244,10 @@ export async function chatWithTools(rawMessages, options) {
9148
10244
  signal,
9149
10245
  } = options;
9150
10246
 
10247
+ const providerRequestId = _normalizeProviderRequestId(
10248
+ options.providerRequestId,
10249
+ );
10250
+
9151
10251
  const tools = getEffectiveToolDefinitions(options);
9152
10252
 
9153
10253
  const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
@@ -9193,6 +10293,7 @@ export async function chatWithTools(rawMessages, options) {
9193
10293
  ),
9194
10294
  {
9195
10295
  signal,
10296
+ retries: options.workflowEffectId ? 0 : undefined,
9196
10297
  strictRetryObserver: options.strictUsageTelemetry === true,
9197
10298
  ...(typeof options.onStreamRetry === "function"
9198
10299
  ? {
@@ -9342,6 +10443,7 @@ export async function chatWithTools(rawMessages, options) {
9342
10443
  ),
9343
10444
  {
9344
10445
  signal,
10446
+ retries: options.workflowEffectId ? 0 : undefined,
9345
10447
  strictRetryObserver: options.strictUsageTelemetry === true,
9346
10448
  ...(typeof options.onStreamRetry === "function"
9347
10449
  ? {
@@ -9434,6 +10536,10 @@ export async function chatWithTools(rawMessages, options) {
9434
10536
  `Unsupported provider: ${provider}. Supported: ollama, anthropic, openai, deepseek, dashscope, mistral, gemini, volcengine`,
9435
10537
  );
9436
10538
  }
10539
+ const supportsOpenAIRequestIdentity = _isOfficialOpenAIEndpoint(
10540
+ provider,
10541
+ url,
10542
+ );
9437
10543
 
9438
10544
  const envKey = providerApiKeyEnvs[provider] || "OPENAI_API_KEY";
9439
10545
  const key = apiKey || process.env[envKey];
@@ -9445,7 +10551,7 @@ export async function chatWithTools(rawMessages, options) {
9445
10551
  dashscope: "qwen-turbo",
9446
10552
  mistral: "mistral-large-latest",
9447
10553
  gemini: "gemini-2.0-flash",
9448
- volcengine: "doubao-seed-2-1-pro-260628",
10554
+ volcengine: "deepseek-v4-flash-260425",
9449
10555
  };
9450
10556
 
9451
10557
  // Real token streaming (--include-partial-messages) for every OpenAI-compatible
@@ -9454,7 +10560,7 @@ export async function chatWithTools(rawMessages, options) {
9454
10560
  // delta-fragmented tool_calls into the standard {message, usage} shape.
9455
10561
  if (typeof options.onToken === "function") {
9456
10562
  return await _retryStreamingChat(
9457
- () =>
10563
+ (attempt = 0) =>
9458
10564
  _chatOpenAIStreaming(
9459
10565
  `${url}/chat/completions`,
9460
10566
  {
@@ -9474,9 +10580,13 @@ export async function chatWithTools(rawMessages, options) {
9474
10580
  options.onStall,
9475
10581
  options.streamStallMs,
9476
10582
  options.streamStallTimeoutMs,
10583
+ supportsOpenAIRequestIdentity
10584
+ ? _physicalProviderRequestId(providerRequestId, attempt)
10585
+ : null,
9477
10586
  ),
9478
10587
  {
9479
10588
  signal,
10589
+ retries: options.workflowEffectId ? 0 : undefined,
9480
10590
  strictRetryObserver: options.strictUsageTelemetry === true,
9481
10591
  ...(typeof options.onStreamRetry === "function"
9482
10592
  ? {
@@ -9497,6 +10607,9 @@ export async function chatWithTools(rawMessages, options) {
9497
10607
  headers: {
9498
10608
  "Content-Type": "application/json",
9499
10609
  Authorization: `Bearer ${key}`,
10610
+ ...(supportsOpenAIRequestIdentity && providerRequestId
10611
+ ? { "X-Client-Request-Id": providerRequestId }
10612
+ : {}),
9500
10613
  },
9501
10614
  signal,
9502
10615
  body: JSON.stringify({
@@ -9519,6 +10632,10 @@ export async function chatWithTools(rawMessages, options) {
9519
10632
  }
9520
10633
  const choice = data.choices[0];
9521
10634
  const out = { message: choice.message };
10635
+ const providerReceipt = supportsOpenAIRequestIdentity
10636
+ ? _openAIProviderRequestReceipt(response, data, providerRequestId)
10637
+ : null;
10638
+ if (providerReceipt) out.providerReceipt = providerReceipt;
9522
10639
  if (_hasCompleteProviderUsage(data.usage)) {
9523
10640
  // OpenAI/DeepSeek/volcengine report cached prompt tokens AS PART of
9524
10641
  // prompt_tokens — split them out so cost prices the cached prefix at the
@@ -9761,7 +10878,7 @@ export async function _retryStreamingChat(streamFn, opts = {}) {
9761
10878
  for (;;) {
9762
10879
  const attemptStartedAt = now();
9763
10880
  try {
9764
- return await streamFn();
10881
+ return await streamFn(attempt);
9765
10882
  } catch (err) {
9766
10883
  if (attempt >= retries || !_isRetryableStreamError(err, signal))
9767
10884
  throw err;
@@ -10241,6 +11358,7 @@ function _openaiCachedTokens(usage) {
10241
11358
 
10242
11359
  function _openaiInitState() {
10243
11360
  return {
11361
+ responseId: null,
10244
11362
  text: "",
10245
11363
  tools: [],
10246
11364
  inputTokens: null,
@@ -10261,6 +11379,7 @@ function _openaiReduceLine(state, raw, onToken) {
10261
11379
  } catch {
10262
11380
  return state;
10263
11381
  }
11382
+ if (!state.responseId) state.responseId = _providerReceiptId(obj.id);
10264
11383
  const delta = obj.choices?.[0]?.delta;
10265
11384
  if (delta?.content) {
10266
11385
  state.text += delta.content;
@@ -10347,12 +11466,16 @@ async function _chatOpenAIStreaming(
10347
11466
  onStall,
10348
11467
  stallMs,
10349
11468
  stallTimeoutMs,
11469
+ providerRequestId = null,
10350
11470
  ) {
10351
11471
  const response = await fetch(apiUrl, {
10352
11472
  method: "POST",
10353
11473
  headers: {
10354
11474
  "Content-Type": "application/json",
10355
11475
  Authorization: `Bearer ${apiKey}`,
11476
+ ...(providerRequestId
11477
+ ? { "X-Client-Request-Id": providerRequestId }
11478
+ : {}),
10356
11479
  },
10357
11480
  signal,
10358
11481
  body: JSON.stringify(body),
@@ -10386,9 +11509,23 @@ async function _chatOpenAIStreaming(
10386
11509
  } catch (err) {
10387
11510
  if (_streamErrorDisposition(err, signal, state.text) === "rethrow")
10388
11511
  throw err;
10389
- return _finalizeTruncatedStream(_openaiFinalize, state);
10390
- }
10391
- return _openaiFinalize(state);
11512
+ const out = _finalizeTruncatedStream(_openaiFinalize, state);
11513
+ const providerReceipt = _openAIProviderRequestReceipt(
11514
+ response,
11515
+ { id: state.responseId },
11516
+ providerRequestId,
11517
+ );
11518
+ if (providerReceipt) out.providerReceipt = providerReceipt;
11519
+ return out;
11520
+ }
11521
+ const out = _openaiFinalize(state);
11522
+ const providerReceipt = _openAIProviderRequestReceipt(
11523
+ response,
11524
+ { id: state.responseId },
11525
+ providerRequestId,
11526
+ );
11527
+ if (providerReceipt) out.providerReceipt = providerReceipt;
11528
+ return out;
10392
11529
  }
10393
11530
 
10394
11531
  /**
@@ -10685,6 +11822,7 @@ function _managedCheckpointUncoveredWriterReason(toolContext) {
10685
11822
  * { type: "tool-executing", tool, args }
10686
11823
  * { type: "tool-result", tool, result, error }
10687
11824
  * { type: "model-usage-started", callId, provider, model, source }
11825
+ * { type: "provider-request-receipt", source, workflowEffectId, clientRequestId, requestId, responseId }
10688
11826
  * { type: "token-usage", callId, provider, model, usage, source? }
10689
11827
  * { type: "model-usage-unknown", callId, provider, model, source, code }
10690
11828
  * { type: "response-complete", content }
@@ -10708,6 +11846,154 @@ function _newModelUsageCallId(source = "model") {
10708
11846
  return `${prefix}-${randomUUID()}`;
10709
11847
  }
10710
11848
 
11849
+ const WORKFLOW_EFFECT_ID_RE = /^sha256:[a-f0-9]{64}$/;
11850
+ const WORKFLOW_CHILD_EFFECT_PROTOCOL = "cc-workflow-child-effect/v1";
11851
+ const WORKFLOW_TOOL_CALL_ID_RE = /^[\x21-\x7e]{1,512}$/;
11852
+
11853
+ function _normalizeWorkflowEffectId(value) {
11854
+ if (value == null) return null;
11855
+ if (typeof value !== "string" || !WORKFLOW_EFFECT_ID_RE.test(value)) {
11856
+ throw new TypeError("workflowEffectId must be a canonical sha256 identity");
11857
+ }
11858
+ return value;
11859
+ }
11860
+
11861
+ function _workflowProviderRequestId(workflowEffectId, source, sequence) {
11862
+ if (!workflowEffectId) return null;
11863
+ return `ccwf_${createHash("sha256")
11864
+ .update(
11865
+ `${workflowEffectId}\0${String(source || "model")}\0${String(sequence)}`,
11866
+ "utf8",
11867
+ )
11868
+ .digest("hex")}`;
11869
+ }
11870
+
11871
+ function _workflowToolEffectBinding(
11872
+ workflowEffectId,
11873
+ sequence,
11874
+ toolCallId,
11875
+ toolName,
11876
+ ) {
11877
+ if (!workflowEffectId) return null;
11878
+ if (
11879
+ !Number.isSafeInteger(sequence) ||
11880
+ sequence < 1 ||
11881
+ typeof toolCallId !== "string" ||
11882
+ !WORKFLOW_TOOL_CALL_ID_RE.test(toolCallId) ||
11883
+ typeof toolName !== "string" ||
11884
+ !toolName
11885
+ ) {
11886
+ const error = new TypeError(
11887
+ "workflow-bound tool call identity is malformed",
11888
+ );
11889
+ error.code = "CC_WORKFLOW_CHILD_EFFECT_IDENTITY_INVALID";
11890
+ throw error;
11891
+ }
11892
+ const childEffectId = `sha256:${createHash("sha256")
11893
+ .update(
11894
+ `${workflowEffectId}\0tool\0${String(sequence)}\0${toolCallId}\0${toolName}`,
11895
+ "utf8",
11896
+ )
11897
+ .digest("hex")}`;
11898
+ return Object.freeze({
11899
+ workflowEffectProtocol: WORKFLOW_CHILD_EFFECT_PROTOCOL,
11900
+ workflowEffectId,
11901
+ workflowChildEffectId: childEffectId,
11902
+ workflowChildSequence: sequence,
11903
+ });
11904
+ }
11905
+
11906
+ function _workflowNestedToolOutcomeUnknown(binding, toolName, result) {
11907
+ if (!binding || safeMcpProperty(result, "outcomeUnknown") !== true) {
11908
+ return null;
11909
+ }
11910
+ const error = new Error(
11911
+ `Workflow-bound nested tool ${toolName} has an unknown outcome and requires reconciliation`,
11912
+ );
11913
+ error.code = "CC_WORKFLOW_NESTED_TOOL_OUTCOME_UNKNOWN";
11914
+ error.workflowEffectOutcomeUnknown = true;
11915
+ error.workflowEffectId = binding.workflowEffectId;
11916
+ error.workflowChildEffectId = binding.workflowChildEffectId;
11917
+ return error;
11918
+ }
11919
+
11920
+ function _workflowCompactionOutcomeUnknown(
11921
+ cause,
11922
+ code = "CC_WORKFLOW_COMPACTION_PROVIDER_OUTCOME_UNKNOWN",
11923
+ message = "Workflow-bound semantic compaction provider outcome is unknown",
11924
+ ) {
11925
+ const error = new Error(message, cause ? { cause } : undefined);
11926
+ error.code = code;
11927
+ error.workflowEffectOutcomeUnknown = true;
11928
+ error.compactionFailureReported = false;
11929
+ return error;
11930
+ }
11931
+
11932
+ function _markWorkflowCompactionFailureReported(error) {
11933
+ if (error?.workflowEffectOutcomeUnknown === true) {
11934
+ error.compactionFailureReported = true;
11935
+ }
11936
+ return error;
11937
+ }
11938
+
11939
+ function _snapshotProviderRequestReceipt(
11940
+ value,
11941
+ expectedClientRequestId,
11942
+ expectedProvider,
11943
+ ) {
11944
+ if (value == null) return null;
11945
+ if (isProxy(value)) {
11946
+ throw new TypeError("provider request receipt must not be a Proxy");
11947
+ }
11948
+ const descriptors = Object.getOwnPropertyDescriptors(value);
11949
+ const expectedFields = [
11950
+ "clientRequestId",
11951
+ "independentlyReadable",
11952
+ "protocol",
11953
+ "provider",
11954
+ "requestId",
11955
+ "requestIdentitySemantics",
11956
+ "responseId",
11957
+ ];
11958
+ const fields = Object.keys(descriptors).sort();
11959
+ const hasOnlyDataFields =
11960
+ fields.length === expectedFields.length &&
11961
+ fields.every(
11962
+ (field, index) =>
11963
+ field === expectedFields[index] &&
11964
+ Object.hasOwn(descriptors[field], "value") &&
11965
+ descriptors[field].enumerable === true,
11966
+ );
11967
+ if (
11968
+ !value ||
11969
+ typeof value !== "object" ||
11970
+ Array.isArray(value) ||
11971
+ !hasOnlyDataFields ||
11972
+ descriptors.protocol.value !== "cc-provider-request-receipt/v1" ||
11973
+ !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(descriptors.provider.value || "") ||
11974
+ descriptors.provider.value !== expectedProvider ||
11975
+ descriptors.clientRequestId.value !== expectedClientRequestId ||
11976
+ descriptors.requestIdentitySemantics.value !== "trace-only" ||
11977
+ descriptors.independentlyReadable.value !== false
11978
+ ) {
11979
+ throw new TypeError("provider request receipt is malformed");
11980
+ }
11981
+ const requestId = _providerReceiptId(descriptors.requestId.value);
11982
+ const responseId = _providerReceiptId(descriptors.responseId.value);
11983
+ if (!requestId && !responseId) {
11984
+ throw new TypeError("provider request receipt has no provider identifier");
11985
+ }
11986
+ return Object.freeze({
11987
+ protocol: "cc-provider-request-receipt/v1",
11988
+ provider: descriptors.provider.value,
11989
+ clientRequestId: expectedClientRequestId,
11990
+ requestId,
11991
+ responseId,
11992
+ requestIdentitySemantics: "trace-only",
11993
+ independentlyReadable: false,
11994
+ });
11995
+ }
11996
+
10711
11997
  function _runtimeUsageBoundaryFailure(error, code, fallbackMessage) {
10712
11998
  const failure =
10713
11999
  error && (typeof error === "object" || typeof error === "function")
@@ -10877,7 +12163,7 @@ function _assertStrictUsageObserver(observer, phase) {
10877
12163
  function _autoCompactionUsageState(options) {
10878
12164
  let state = _autoCompactionUsageStates.get(options);
10879
12165
  if (!state) {
10880
- state = { calls: [] };
12166
+ state = { calls: [], callSequence: null };
10881
12167
  _autoCompactionUsageStates.set(options, state);
10882
12168
  }
10883
12169
  return state;
@@ -10901,27 +12187,59 @@ function _instrumentAutoCompactorUsage(compactor, options) {
10901
12187
  const original = installed?.original || compactor.llmQuery;
10902
12188
  const state = _autoCompactionUsageState(options);
10903
12189
  const wrapper = async function instrumentedCompactionQuery(...args) {
12190
+ const workflowEffectId = _normalizeWorkflowEffectId(
12191
+ options.workflowEffectId,
12192
+ );
12193
+ const callSequence =
12194
+ Number.isSafeInteger(state.callSequence) && state.callSequence > 0
12195
+ ? state.callSequence
12196
+ : null;
12197
+ if (workflowEffectId && callSequence == null) {
12198
+ throw _runtimeUsageBoundaryFailure(
12199
+ null,
12200
+ "CC_COMPACTION_REQUEST_SEQUENCE_REQUIRED",
12201
+ "Workflow-bound semantic compaction requires a stable call sequence",
12202
+ );
12203
+ }
12204
+ const providerRequestId = _workflowProviderRequestId(
12205
+ workflowEffectId,
12206
+ "semantic-compaction",
12207
+ callSequence,
12208
+ );
10904
12209
  const call = {
10905
12210
  callId: _newModelUsageCallId("semantic-compaction"),
10906
- provider: options.provider || null,
10907
- model: options.model || null,
12211
+ provider: options.provider || "ollama",
12212
+ model: options.model || "unknown",
10908
12213
  source: "semantic-compaction",
12214
+ workflowEffectId,
12215
+ callSequence,
12216
+ providerRequestId,
12217
+ providerReceipt: null,
10909
12218
  observerError: null,
10910
12219
  observerFailed: false,
10911
12220
  boundaryNotified: false,
10912
12221
  };
10913
12222
  state.calls.push(call);
12223
+ const boundaryEvent = {
12224
+ type: "model-usage-started",
12225
+ callId: call.callId,
12226
+ provider: call.provider,
12227
+ model: call.model,
12228
+ source: call.source,
12229
+ ...(providerRequestId
12230
+ ? {
12231
+ workflowEffectId,
12232
+ callSequence,
12233
+ providerRequestId,
12234
+ requestIdentitySemantics: "trace-only",
12235
+ }
12236
+ : {}),
12237
+ };
10914
12238
  try {
10915
12239
  if (typeof options.onUsageBoundary === "function") {
10916
12240
  // Synchronous and before `original`: a host can durably append the
10917
12241
  // start row, and any append failure prevents provider spend.
10918
- const observation = options.onUsageBoundary({
10919
- type: "model-usage-started",
10920
- callId: call.callId,
10921
- provider: call.provider,
10922
- model: call.model,
10923
- source: call.source,
10924
- });
12242
+ const observation = options.onUsageBoundary(boundaryEvent);
10925
12243
  if (observation && typeof observation.then === "function") {
10926
12244
  void Promise.resolve(observation).catch(() => {});
10927
12245
  const error = new Error(
@@ -10932,13 +12250,54 @@ function _instrumentAutoCompactorUsage(compactor, options) {
10932
12250
  }
10933
12251
  call.boundaryNotified = true;
10934
12252
  }
12253
+ if (
12254
+ providerRequestId &&
12255
+ typeof options.onProviderRequestBoundary === "function"
12256
+ ) {
12257
+ const observation = options.onProviderRequestBoundary(boundaryEvent);
12258
+ if (observation && typeof observation.then === "function") {
12259
+ void Promise.resolve(observation).catch(() => {});
12260
+ throw _runtimeUsageBoundaryFailure(
12261
+ null,
12262
+ "CC_PROVIDER_REQUEST_BOUNDARY_ASYNC",
12263
+ "Provider request boundary observer must be synchronous",
12264
+ );
12265
+ }
12266
+ }
10935
12267
  } catch (error) {
10936
12268
  call.observerError = error;
10937
12269
  call.observerFailed = true;
10938
12270
  throw error;
10939
12271
  }
10940
12272
 
10941
- return await original.apply(this, args);
12273
+ const requestBinding = providerRequestId
12274
+ ? Object.freeze({
12275
+ workflowEffectId,
12276
+ callSequence,
12277
+ source: "semantic-compaction",
12278
+ providerRequestId,
12279
+ requestIdentitySemantics: "trace-only",
12280
+ })
12281
+ : null;
12282
+ try {
12283
+ const result = await original.apply(
12284
+ this,
12285
+ requestBinding ? [...args, requestBinding] : args,
12286
+ );
12287
+ call.providerReceipt = providerRequestId
12288
+ ? _snapshotProviderRequestReceipt(
12289
+ result?.providerReceipt,
12290
+ providerRequestId,
12291
+ call.provider,
12292
+ )
12293
+ : null;
12294
+ return result;
12295
+ } catch (error) {
12296
+ if (workflowEffectId) {
12297
+ throw _workflowCompactionOutcomeUnknown(error);
12298
+ }
12299
+ throw error;
12300
+ }
10942
12301
  };
10943
12302
 
10944
12303
  try {
@@ -10992,7 +12351,7 @@ async function _getAutoCompactor(options) {
10992
12351
  ? options.compactionLlmQuery
10993
12352
  : options.chatFn
10994
12353
  ? null
10995
- : async (prompt) => {
12354
+ : async (prompt, requestBinding = null) => {
10996
12355
  const maxOutputTokens = Math.min(
10997
12356
  4096,
10998
12357
  Math.max(
@@ -11011,14 +12370,17 @@ async function _getAutoCompactor(options) {
11011
12370
  onToken: undefined,
11012
12371
  onStall: undefined,
11013
12372
  onStreamRetry: undefined,
12373
+ providerRequestId:
12374
+ requestBinding?.providerRequestId || undefined,
11014
12375
  maxOutputTokens,
11015
12376
  },
11016
12377
  );
11017
12378
  return {
11018
12379
  summary: response?.message?.content || "",
11019
12380
  usage: response?.usage || null,
11020
- provider: options.provider || null,
11021
- model: options.model || null,
12381
+ provider: options.provider || "ollama",
12382
+ model: options.model || "unknown",
12383
+ providerReceipt: response?.providerReceipt || null,
11022
12384
  };
11023
12385
  };
11024
12386
  compressor = new PromptCompressor({
@@ -11258,6 +12620,7 @@ export async function* agentLoop(messages, options) {
11258
12620
  await import("../lib/iteration-budget.js");
11259
12621
  const budget = options.iterationBudget || new IterationBudget();
11260
12622
  const signal = options.signal || null;
12623
+ const workflowEffectId = _normalizeWorkflowEffectId(options.workflowEffectId);
11261
12624
  // Optional OpenTelemetry recorder (TelemetryRecorder). When present, the loop
11262
12625
  // emits model/tool/retry spans + a per-run counter; when absent, zero cost.
11263
12626
  const recorder = options.recorder || null;
@@ -11268,6 +12631,7 @@ export async function* agentLoop(messages, options) {
11268
12631
  options.runId ||
11269
12632
  `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11270
12633
  let automaticCompactionSettlementBlocked = false;
12634
+ let workflowChildEffectSequence = 0;
11271
12635
 
11272
12636
  const fileMutationScope = normalizeExactFileMutationScope(
11273
12637
  options.fileMutationScope,
@@ -11367,6 +12731,7 @@ export async function* agentLoop(messages, options) {
11367
12731
  mcpDispatchAdmission: hermeticExecution
11368
12732
  ? null
11369
12733
  : options.mcpDispatchAdmission || null,
12734
+ workflowEffectId,
11370
12735
  // A loop-local identity prevents one session's lazy instruction commits
11371
12736
  // from suppressing first-access delivery in another session at the same
11372
12737
  // cwd. Hosts may inject a stable identity across resumed turns.
@@ -11560,10 +12925,13 @@ export async function* agentLoop(messages, options) {
11560
12925
  // attempt inside one logical `llmCall`, where this generator cannot durably
11561
12926
  // bracket each transport attempt. Strict call-ledger sessions therefore
11562
12927
  // disable that recovery path and surface the configured provider's failure
11563
- // as unknown; legacy sessions keep the existing self-healing behavior.
12928
+ // as unknown. A workflow-bound request does the same: a hidden fallback
12929
+ // would be a second physical provider attempt under one durable effect.
12930
+ // Legacy sessions keep the existing self-healing behavior.
11564
12931
  if (
11565
12932
  options.runnableProviderFallback !== false &&
11566
- options.strictUsageTelemetry !== true
12933
+ options.strictUsageTelemetry !== true &&
12934
+ workflowEffectId == null
11567
12935
  ) {
11568
12936
  const { makeRunnableProviderFallback } =
11569
12937
  await import("../lib/runnable-provider.js");
@@ -11707,6 +13075,7 @@ export async function* agentLoop(messages, options) {
11707
13075
  if (compactor && compactor.shouldAutoCompact(messages)) {
11708
13076
  compactionUsageState = _autoCompactionUsageState(options);
11709
13077
  compactionUsageState.calls = [];
13078
+ compactionUsageState.callSequence = budget.consumed;
11710
13079
  // This is the canonical pre-provider authority snapshot. A local
11711
13080
  // micro-compact may change the working input, but the durable CAS must
11712
13081
  // still compare against the transcript state that existed before any
@@ -11803,6 +13172,16 @@ export async function* agentLoop(messages, options) {
11803
13172
  }
11804
13173
  compactionUsageCall = compactionUsageState.calls.at(-1) || null;
11805
13174
  const compactionUsage = _compactionTokenUsage(stats);
13175
+ if (compactionUsageCall?.providerReceipt) {
13176
+ yield {
13177
+ type: "provider-request-receipt",
13178
+ callId: compactionUsageCall.callId,
13179
+ workflowEffectId,
13180
+ callSequence: compactionUsageCall.callSequence,
13181
+ source: "semantic-compaction",
13182
+ ...compactionUsageCall.providerReceipt,
13183
+ };
13184
+ }
11806
13185
  if (stats.degraded === true) {
11807
13186
  const usageOutcomeUnknown = stats.summaryUsageUnknown === true;
11808
13187
  const projectedStats = usageOutcomeUnknown
@@ -11869,6 +13248,15 @@ export async function* agentLoop(messages, options) {
11869
13248
  backgroundSubAgents,
11870
13249
  backgroundUsageFailureState,
11871
13250
  );
13251
+ if (workflowEffectId) {
13252
+ throw _markWorkflowCompactionFailureReported(
13253
+ _workflowCompactionOutcomeUnknown(
13254
+ null,
13255
+ "CC_WORKFLOW_COMPACTION_USAGE_UNKNOWN",
13256
+ "Workflow-bound semantic compaction usage is unknown",
13257
+ ),
13258
+ );
13259
+ }
11872
13260
  return;
11873
13261
  }
11874
13262
  if (
@@ -11922,6 +13310,15 @@ export async function* agentLoop(messages, options) {
11922
13310
  backgroundSubAgents,
11923
13311
  backgroundUsageFailureState,
11924
13312
  );
13313
+ if (workflowEffectId) {
13314
+ throw _markWorkflowCompactionFailureReported(
13315
+ _workflowCompactionOutcomeUnknown(
13316
+ error,
13317
+ "CC_WORKFLOW_COMPACTION_SETTLEMENT_UNKNOWN",
13318
+ "Workflow-bound semantic compaction settlement is unknown",
13319
+ ),
13320
+ );
13321
+ }
11925
13322
  return;
11926
13323
  }
11927
13324
  if (!hermeticExecution) {
@@ -11969,6 +13366,12 @@ export async function* agentLoop(messages, options) {
11969
13366
  } catch (_e) {
11970
13367
  if (isAbortError(_e) || signal?.aborted) throw _e;
11971
13368
  if (_e?.runtimeLedgerPersistence === true) throw _e;
13369
+ if (
13370
+ _e?.workflowEffectOutcomeUnknown === true &&
13371
+ _e?.compactionFailureReported === true
13372
+ ) {
13373
+ throw _e;
13374
+ }
11972
13375
  if (compactionObserverFailure === _e) throw _e;
11973
13376
  const observerFailure = compactionUsageState?.calls?.find(
11974
13377
  (call) => call.observerFailed && call.observerError === _e,
@@ -11995,6 +13398,13 @@ export async function* agentLoop(messages, options) {
11995
13398
  backgroundSubAgents,
11996
13399
  backgroundUsageFailureState,
11997
13400
  );
13401
+ if (workflowEffectId) {
13402
+ throw _markWorkflowCompactionFailureReported(
13403
+ _e?.workflowEffectOutcomeUnknown === true
13404
+ ? _e
13405
+ : _workflowCompactionOutcomeUnknown(_e),
13406
+ );
13407
+ }
11998
13408
  return;
11999
13409
  }
12000
13410
  yield {
@@ -12101,12 +13511,28 @@ export async function* agentLoop(messages, options) {
12101
13511
  throwIfAborted(signal);
12102
13512
  _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12103
13513
  _throwSettledBackgroundUsageFailure(backgroundSubAgents);
13514
+ const providerRequestId = _workflowProviderRequestId(
13515
+ workflowEffectId,
13516
+ "model",
13517
+ budget.consumed,
13518
+ );
13519
+ const providerCallOptions = providerRequestId
13520
+ ? { ...effectiveToolOptions, providerRequestId }
13521
+ : effectiveToolOptions;
12104
13522
  const modelUsageCall = {
12105
13523
  type: "model-usage-started",
12106
13524
  callId: _newModelUsageCallId("model"),
12107
13525
  provider: options.provider || "ollama",
12108
13526
  model: options.model || "unknown",
12109
13527
  source: "model",
13528
+ ...(providerRequestId
13529
+ ? {
13530
+ workflowEffectId,
13531
+ callSequence: budget.consumed,
13532
+ providerRequestId,
13533
+ requestIdentitySemantics: "trace-only",
13534
+ }
13535
+ : {}),
12110
13536
  };
12111
13537
  // The consumer receives this boundary before `.next()` resumes into the
12112
13538
  // provider call, so a durable append failure can prevent spend.
@@ -12132,6 +13558,7 @@ export async function* agentLoop(messages, options) {
12132
13558
  }
12133
13559
 
12134
13560
  let result;
13561
+ let providerReceipt = null;
12135
13562
  try {
12136
13563
  result = await _withSpan(
12137
13564
  recorder,
@@ -12142,7 +13569,7 @@ export async function* agentLoop(messages, options) {
12142
13569
  "agent.iteration": budget.consumed,
12143
13570
  ...(modelIdAttrs || {}),
12144
13571
  },
12145
- () => llmCall(callMessages, effectiveToolOptions),
13572
+ () => llmCall(callMessages, providerCallOptions),
12146
13573
  (span, r) => {
12147
13574
  const t = _usageTokens(r?.usage);
12148
13575
  if (t) {
@@ -12183,6 +13610,13 @@ export async function* agentLoop(messages, options) {
12183
13610
  },
12184
13611
  "model_error",
12185
13612
  );
13613
+ providerReceipt = providerRequestId
13614
+ ? _snapshotProviderRequestReceipt(
13615
+ result?.providerReceipt,
13616
+ providerRequestId,
13617
+ modelUsageCall.provider,
13618
+ )
13619
+ : null;
12186
13620
  } catch (error) {
12187
13621
  // Never retain provider error text in the usage ledger. Pause on a fixed
12188
13622
  // unknown settlement, then rethrow only after the consumer resumes.
@@ -12203,6 +13637,20 @@ export async function* agentLoop(messages, options) {
12203
13637
  if (recorder) recorder.counter("agent.model.calls", 1);
12204
13638
  const msg = result?.message;
12205
13639
 
13640
+ if (providerReceipt) {
13641
+ // Yield the provider-returned identifiers before billable usage. A host
13642
+ // that stops after a cost boundary still retains the exact request/effect
13643
+ // binding. This is trace evidence only, never an exactly-once assertion.
13644
+ yield {
13645
+ type: "provider-request-receipt",
13646
+ callId: modelUsageCall.callId,
13647
+ workflowEffectId,
13648
+ callSequence: budget.consumed,
13649
+ source: modelUsageCall.source,
13650
+ ...providerReceipt,
13651
+ };
13652
+ }
13653
+
12206
13654
  // Close the already-persisted call boundary before honoring an abort that
12207
13655
  // raced with a successful provider response. Once the transport returned,
12208
13656
  // dropping this settlement would strand a durable `started` row forever.
@@ -12428,7 +13876,13 @@ export async function* agentLoop(messages, options) {
12428
13876
  } catch {
12429
13877
  toolArgs = {};
12430
13878
  }
12431
- return { call, toolArgs };
13879
+ const workflowBinding = _workflowToolEffectBinding(
13880
+ workflowEffectId,
13881
+ workflowEffectId ? (workflowChildEffectSequence += 1) : 0,
13882
+ call.id,
13883
+ call.function.name,
13884
+ );
13885
+ return { call, toolArgs, workflowBinding };
12432
13886
  });
12433
13887
  // Yield every start boundary before dispatch. Async-generator consumers
12434
13888
  // persist each boundary before asking for the next item, so by the time
@@ -12437,20 +13891,21 @@ export async function* agentLoop(messages, options) {
12437
13891
  try {
12438
13892
  throwIfAborted(signal);
12439
13893
  _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12440
- for (const { call, toolArgs } of prepared) {
13894
+ for (const { call, toolArgs, workflowBinding } of prepared) {
12441
13895
  yield {
12442
13896
  type: "tool-executing",
12443
13897
  tool: call.function.name,
12444
13898
  args: toolArgs,
12445
13899
  tool_use_id: call.id,
12446
13900
  turn_id: `${runId}:t${budget.consumed}`,
13901
+ ...(workflowBinding || {}),
12447
13902
  };
12448
- startedPrepared.push({ call, toolArgs });
13903
+ startedPrepared.push({ call, toolArgs, workflowBinding });
12449
13904
  throwIfAborted(signal);
12450
13905
  }
12451
13906
  _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12452
13907
  } catch (admissionFailure) {
12453
- for (const { call } of startedPrepared) {
13908
+ for (const { call, workflowBinding } of startedPrepared) {
12454
13909
  yield {
12455
13910
  type: "tool-result",
12456
13911
  tool: call.function.name,
@@ -12458,6 +13913,7 @@ export async function* agentLoop(messages, options) {
12458
13913
  error: "tool execution cancelled before dispatch",
12459
13914
  tool_use_id: call.id,
12460
13915
  turn_id: `${runId}:t${budget.consumed}`,
13916
+ ...(workflowBinding || {}),
12461
13917
  };
12462
13918
  }
12463
13919
  throw admissionFailure;
@@ -12465,18 +13921,30 @@ export async function* agentLoop(messages, options) {
12465
13921
  // Only after every start has been consumed do we kick every read off.
12466
13922
  // Settle each promise into {result,error} so an abort before its ordered
12467
13923
  // result is consumed can never surface an unhandled rejection.
12468
- const inflight = prepared.map(({ call, toolArgs }) => {
13924
+ const inflight = prepared.map(({ call, toolArgs, workflowBinding }) => {
12469
13925
  const promise = executeTool(call.function.name, toolArgs, {
12470
13926
  ...toolContext,
12471
13927
  toolCallId: call.id,
12472
13928
  turnId: `${runId}:t${budget.consumed}`,
13929
+ ...(workflowBinding || {}),
12473
13930
  }).then(
12474
13931
  (result) => ({ result, error: null }),
12475
- (err) => ({ result: { error: err.message }, error: err.message }),
13932
+ (err) => ({
13933
+ result: {
13934
+ error: err.message,
13935
+ ...(workflowBinding
13936
+ ? {
13937
+ code: "CC_WORKFLOW_NESTED_TOOL_EXECUTION_THROWN",
13938
+ outcomeUnknown: true,
13939
+ }
13940
+ : {}),
13941
+ },
13942
+ error: err.message,
13943
+ }),
12476
13944
  );
12477
- return { call, toolArgs, promise };
13945
+ return { call, toolArgs, workflowBinding, promise };
12478
13946
  });
12479
- for (const { call, toolArgs, promise } of inflight) {
13947
+ for (const { call, toolArgs, workflowBinding, promise } of inflight) {
12480
13948
  throwIfAborted(signal);
12481
13949
  _throwBackgroundUsageFailureState(backgroundUsageFailureState);
12482
13950
  const { result: toolResult, error: toolError } = await promise;
@@ -12516,9 +13984,16 @@ export async function* agentLoop(messages, options) {
12516
13984
  error: toolError,
12517
13985
  tool_use_id: call.id,
12518
13986
  turn_id: `${runId}:t${budget.consumed}`,
13987
+ ...(workflowBinding || {}),
12519
13988
  permission_decision_id: decision?.id || null,
12520
13989
  permission_decision: decision,
12521
13990
  };
13991
+ const nestedOutcomeUnknown = _workflowNestedToolOutcomeUnknown(
13992
+ workflowBinding,
13993
+ call.function.name,
13994
+ toolResult,
13995
+ );
13996
+ if (nestedOutcomeUnknown) throw nestedOutcomeUnknown;
12522
13997
  messages.push({
12523
13998
  role: "tool",
12524
13999
  content: toolContent,
@@ -12663,6 +14138,7 @@ export async function* agentLoop(messages, options) {
12663
14138
 
12664
14139
  let managedCheckpointHandle = null;
12665
14140
  let managedCheckpointPreparationError = null;
14141
+ let workflowBinding = null;
12666
14142
  try {
12667
14143
  managedCheckpointHandle = beginManagedToolCheckpoint({
12668
14144
  enabled: toolContext.managedCheckpoint === true,
@@ -12716,12 +14192,19 @@ export async function* agentLoop(messages, options) {
12716
14192
  // event. Consumers use that event to bind hooks, telemetry and turns to
12717
14193
  // an actual invocation.
12718
14194
  if (!managedCheckpointPreparationError) {
14195
+ workflowBinding = _workflowToolEffectBinding(
14196
+ workflowEffectId,
14197
+ workflowEffectId ? (workflowChildEffectSequence += 1) : 0,
14198
+ call.id,
14199
+ toolName,
14200
+ );
12719
14201
  yield {
12720
14202
  type: "tool-executing",
12721
14203
  tool: toolName,
12722
14204
  args: toolArgs,
12723
14205
  tool_use_id: call.id,
12724
14206
  turn_id: `${runId}:t${budget.consumed}`,
14207
+ ...(workflowBinding || {}),
12725
14208
  };
12726
14209
  // The event yield is a host persistence boundary. A detached child's
12727
14210
  // fatal usage write can settle while the consumer handles it; fence
@@ -12739,6 +14222,7 @@ export async function* agentLoop(messages, options) {
12739
14222
  error: "authoritative background usage persistence failure",
12740
14223
  tool_use_id: call.id,
12741
14224
  turn_id: `${runId}:t${budget.consumed}`,
14225
+ ...(workflowBinding || {}),
12742
14226
  };
12743
14227
  throw backgroundFailure;
12744
14228
  }
@@ -12788,6 +14272,7 @@ export async function* agentLoop(messages, options) {
12788
14272
  ...toolContext,
12789
14273
  toolCallId: call.id,
12790
14274
  turnId: `${runId}:t${budget.consumed}`,
14275
+ ...(workflowBinding || {}),
12791
14276
  }),
12792
14277
  (span, r) => {
12793
14278
  span.setAttribute(
@@ -12827,7 +14312,15 @@ export async function* agentLoop(messages, options) {
12827
14312
  );
12828
14313
  } catch (err) {
12829
14314
  if (err?.runtimeLedgerPersistence === true) throw err;
12830
- toolResult = { error: err.message };
14315
+ toolResult = {
14316
+ error: err.message,
14317
+ ...(workflowBinding
14318
+ ? {
14319
+ code: "CC_WORKFLOW_NESTED_TOOL_EXECUTION_THROWN",
14320
+ outcomeUnknown: true,
14321
+ }
14322
+ : {}),
14323
+ };
12831
14324
  toolError = err.message;
12832
14325
  }
12833
14326
  }
@@ -12931,10 +14424,18 @@ export async function* agentLoop(messages, options) {
12931
14424
  error: toolError,
12932
14425
  tool_use_id: call.id,
12933
14426
  turn_id: `${runId}:t${budget.consumed}`,
14427
+ ...(workflowBinding || {}),
12934
14428
  permission_decision_id: decision?.id || null,
12935
14429
  permission_decision: decision,
12936
14430
  };
12937
14431
 
14432
+ const nestedOutcomeUnknown = _workflowNestedToolOutcomeUnknown(
14433
+ workflowBinding,
14434
+ toolName,
14435
+ toolResult,
14436
+ );
14437
+ if (nestedOutcomeUnknown) throw nestedOutcomeUnknown;
14438
+
12938
14439
  messages.push({
12939
14440
  role: "tool",
12940
14441
  content: toolContent,