chainlesschain 0.163.8 → 0.165.1

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 (240) hide show
  1. package/package.json +7 -4
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
  10. package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
  51. package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
  52. package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
  67. package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
  68. package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
  91. package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
  107. package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
  136. package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
  137. package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +3 -3
  155. package/src/commands/cowork.js +418 -41
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/data/changelog.json +28 -2
  161. package/src/gateways/ws/action-protocol.js +316 -17
  162. package/src/gateways/ws/message-dispatcher.js +30 -0
  163. package/src/gateways/ws/remote-session-protocol.js +565 -35
  164. package/src/gateways/ws/ws-agent-handler.js +12 -0
  165. package/src/gateways/ws/ws-approval-gate.js +5 -0
  166. package/src/gateways/ws/ws-server.js +510 -50
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +2 -0
  170. package/src/harness/remote-session-crypto.js +929 -21
  171. package/src/harness/remote-session-registry.js +945 -24
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/auto-mode-config.js +98 -3
  175. package/src/lib/background-agent-keeper-client.js +248 -0
  176. package/src/lib/background-agent-keeper-protocol.js +291 -0
  177. package/src/lib/background-agent-supervisor.js +1357 -85
  178. package/src/lib/background-interaction-resolver.js +12 -0
  179. package/src/lib/background-session-transport.js +23 -6
  180. package/src/lib/background-turn-bootstrap-protocol.js +190 -0
  181. package/src/lib/cowork-workflow.js +1188 -76
  182. package/src/lib/delivery-production-adapter.js +38 -2
  183. package/src/lib/dynamic-workflow-facade.js +1736 -0
  184. package/src/lib/execution-location-contract.js +551 -0
  185. package/src/lib/execution-location-runtime.js +130 -0
  186. package/src/lib/ide-context.js +94 -6
  187. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  188. package/src/lib/packer/pkg-config-generator.js +2 -0
  189. package/src/lib/permission-authority.js +77 -0
  190. package/src/lib/permission-side-effect-center.js +495 -0
  191. package/src/lib/plugin-runtime/install.js +514 -13
  192. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  193. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  194. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  195. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  196. package/src/lib/plugin-security.js +60 -3
  197. package/src/lib/process-execution-broker/index.js +1095 -57
  198. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  199. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  200. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  201. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  202. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  203. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  204. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  205. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  206. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  207. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  208. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  209. package/src/lib/public-ide-capability-manifest.js +3 -0
  210. package/src/lib/remote-approval-bridge.js +777 -34
  211. package/src/lib/remote-control.js +2 -0
  212. package/src/lib/remote-membership-authority.js +632 -0
  213. package/src/lib/remote-membership-coordinator.js +2973 -0
  214. package/src/lib/remote-membership-host-store.js +1799 -0
  215. package/src/lib/runtime-provenance-ledger.js +171 -46
  216. package/src/lib/scoped-permission-store.js +369 -0
  217. package/src/lib/session-anti-rollback-anchor.js +262 -5
  218. package/src/lib/shell-approval.js +169 -6
  219. package/src/lib/sub-agent-context.js +4 -0
  220. package/src/lib/with-file-lock.js +73 -38
  221. package/src/lib/workflow-definition-contract.js +149 -0
  222. package/src/repl/agent-repl.js +111 -36
  223. package/src/repl/chat-repl.js +2 -0
  224. package/src/repl/clipboard-image.js +735 -16
  225. package/src/repl/ide-status.js +1 -1
  226. package/src/repl/prompt-interactions.js +59 -6
  227. package/src/repl/remote-approval.js +29 -7
  228. package/src/runtime/agent-core.js +1182 -155
  229. package/src/runtime/headless-runner.js +52 -30
  230. package/src/runtime/headless-stream.js +51 -28
  231. package/src/workers/background-agent-keeper.js +689 -0
  232. package/src/workers/background-agent-worker.js +365 -13
  233. package/src/workers/background-turn-bootstrap.js +170 -0
  234. package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
  235. package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
  236. package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
  237. package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
  238. package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
  239. package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
  240. package/src/assets/web-panel/assets/index-uWiUs7S_.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,
@@ -2401,20 +2850,112 @@ export async function executeTool(name, args, context = {}) {
2401
2850
  // shell-policy denylist also still applies.
2402
2851
  // No matching rule + no host policy → every existing layer runs unchanged
2403
2852
  // (default behaviour is byte-for-byte).
2404
- const settingsVerdict = context.permissionRules
2853
+ let permissionAuthority = null;
2854
+ let effectivePermissionRules = context.permissionRules || null;
2855
+ if (typeof context.permissionRulesProvider === "function") {
2856
+ try {
2857
+ permissionAuthority = await context.permissionRulesProvider({
2858
+ cwd,
2859
+ tool: name,
2860
+ args,
2861
+ });
2862
+ effectivePermissionRules =
2863
+ permissionAuthority?.rules || permissionAuthority || null;
2864
+ if (
2865
+ effectivePermissionRules &&
2866
+ (!Array.isArray(effectivePermissionRules.allow) ||
2867
+ !Array.isArray(effectivePermissionRules.ask) ||
2868
+ !Array.isArray(effectivePermissionRules.deny))
2869
+ ) {
2870
+ throw new TypeError(
2871
+ "permission authority provider returned an invalid ruleset",
2872
+ );
2873
+ }
2874
+ } catch (error) {
2875
+ return {
2876
+ error:
2877
+ `[Permission Authority] Tool "${name}" was blocked because the current permission authority could not be resolved. ` +
2878
+ "No tool side effect was started.",
2879
+ policy: {
2880
+ decision: "blocked",
2881
+ via: "permission-authority-load",
2882
+ code: error?.code || "CC_PERMISSION_AUTHORITY_UNAVAILABLE",
2883
+ },
2884
+ };
2885
+ }
2886
+ }
2887
+ const evaluatedSettingsVerdict = effectivePermissionRules
2405
2888
  ? evaluatePermissionRules({
2406
2889
  tool: name,
2407
2890
  args,
2408
2891
  cwd,
2409
- rules: context.permissionRules,
2892
+ rules: effectivePermissionRules,
2410
2893
  })
2411
2894
  : { decision: null, rule: null };
2895
+ const permissionRuleSource = evaluatedSettingsVerdict.rule
2896
+ ? permissionAuthority?.sources?.[
2897
+ `${evaluatedSettingsVerdict.decision}:${evaluatedSettingsVerdict.rule}`
2898
+ ] || null
2899
+ : null;
2900
+ const matchedScopedRule = permissionRuleSource?.startsWith("scoped:")
2901
+ ? permissionAuthority?.scoped?.rules?.find(
2902
+ (record) =>
2903
+ record.status === "active" &&
2904
+ `scoped:${record.id}` === permissionRuleSource,
2905
+ )
2906
+ : null;
2907
+ const settingsVerdict = {
2908
+ ...evaluatedSettingsVerdict,
2909
+ source: permissionRuleSource,
2910
+ priority:
2911
+ evaluatedSettingsVerdict.decision === "deny"
2912
+ ? 1
2913
+ : evaluatedSettingsVerdict.decision === "ask"
2914
+ ? 3
2915
+ : evaluatedSettingsVerdict.decision === "allow"
2916
+ ? 4
2917
+ : null,
2918
+ scopedRule: matchedScopedRule || null,
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
+ }
2412
2944
 
2413
2945
  // 1. settings deny
2414
2946
  if (settingsVerdict.decision === "deny") {
2415
2947
  return {
2416
2948
  error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}. This is a configured policy — retrying won't help; tell the user if the task genuinely needs it.`,
2417
- policy: { decision: "deny", rule: settingsVerdict.rule, via: "settings" },
2949
+ policy: {
2950
+ decision: "deny",
2951
+ rule: settingsVerdict.rule,
2952
+ via: "settings",
2953
+ source: settingsVerdict.source,
2954
+ priority: settingsVerdict.priority,
2955
+ scopedRuleId: settingsVerdict.scopedRule?.id || null,
2956
+ revision: settingsVerdict.scopedRule?.revision || null,
2957
+ expiresAt: settingsVerdict.scopedRule?.expiresAt || null,
2958
+ },
2418
2959
  };
2419
2960
  }
2420
2961
 
@@ -2429,6 +2970,22 @@ export async function executeTool(name, args, context = {}) {
2429
2970
  toolPolicies && typeof toolPolicies === "object"
2430
2971
  ? toolPolicies[name]
2431
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
+ }
2432
2989
  const hostPolicyAllowsReadOnlyGit =
2433
2990
  name === "git" &&
2434
2991
  hostToolPolicy?.planModeBehavior === "readonly-conditional" &&
@@ -2462,6 +3019,24 @@ export async function executeTool(name, args, context = {}) {
2462
3019
  planManager.isActive() && !executionLockActive;
2463
3020
  const externalToolBlockedDuringPlanning =
2464
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
+ }
2465
3040
  context = { ...context, planReadOnlyFenceActive };
2466
3041
  if (
2467
3042
  planManager.isActive() &&
@@ -2523,6 +3098,11 @@ export async function executeTool(name, args, context = {}) {
2523
3098
  decision: "ask",
2524
3099
  rule: settingsVerdict.rule,
2525
3100
  via: "settings",
3101
+ source: settingsVerdict.source,
3102
+ priority: settingsVerdict.priority,
3103
+ scopedRuleId: settingsVerdict.scopedRule?.id || null,
3104
+ revision: settingsVerdict.scopedRule?.revision || null,
3105
+ expiresAt: settingsVerdict.scopedRule?.expiresAt || null,
2526
3106
  },
2527
3107
  };
2528
3108
  }
@@ -2755,6 +3335,160 @@ export async function executeTool(name, args, context = {}) {
2755
3335
  if (instructionPreflight) return instructionPreflight;
2756
3336
  }
2757
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
+
2758
3492
  // PreToolUse hooks. DB hooks (cc hook add) stay observe-only — a failure
2759
3493
  // never blocks. settings.json hooks (context.settingsHooks) are decision-
2760
3494
  // capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
@@ -2865,6 +3599,7 @@ export async function executeTool(name, args, context = {}) {
2865
3599
  backgroundUsageFailureState: context.backgroundUsageFailureState || null,
2866
3600
  planManager,
2867
3601
  permissionRules: context.permissionRules || null,
3602
+ permissionRulesProvider: context.permissionRulesProvider || null,
2868
3603
  effectiveAllowedToolNames: context.effectiveAllowedToolNames ?? null,
2869
3604
  hostManagedToolPolicy: context.hostManagedToolPolicy || null,
2870
3605
  externalToolDescriptors: context.externalToolDescriptors || null,
@@ -2877,12 +3612,14 @@ export async function executeTool(name, args, context = {}) {
2877
3612
  subtreeInstructionScope:
2878
3613
  context.subtreeInstructionScope || context.sessionId || "__legacy__",
2879
3614
  shellPolicyOverrides: context.shellPolicyOverrides || null,
3615
+ classifyAllShell: context.classifyAllShell === true,
2880
3616
  approvalGate: context.approvalGate || null,
2881
3617
  shellConfirm: context.shellConfirm || null,
2882
3618
  additionalDirectories: context.additionalDirectories || null,
2883
3619
  sandbox: context.sandbox || null,
2884
3620
  ruleAllowed,
2885
3621
  settingsVerdict,
3622
+ shellDispatchPolicyAuthority,
2886
3623
  subAgentDepth: context.subAgentDepth || 0,
2887
3624
  subAgentBudget: context.subAgentBudget || null,
2888
3625
  sessionBudget: context.sessionBudget || null,
@@ -4257,6 +4994,7 @@ async function executeToolInner(
4257
4994
  sandbox,
4258
4995
  ruleAllowed = false,
4259
4996
  settingsVerdict = null,
4997
+ shellDispatchPolicyAuthority = null,
4260
4998
  subAgentDepth = 0,
4261
4999
  subAgentBudget = null,
4262
5000
  sessionBudget = null,
@@ -4738,6 +5476,9 @@ async function executeToolInner(
4738
5476
  }
4739
5477
 
4740
5478
  case "run_shell": {
5479
+ // Bound the declared run_shell data once. Approval and dispatch never
5480
+ // observe later mutation of the caller-owned tool arguments.
5481
+ args = Object.isFrozen(args) ? args : snapshotShellExecutionArgs(args);
4741
5482
  if (unattendedActionPolicy?.unattended === true) {
4742
5483
  const unattendedVerdict = evaluateUnattendedShellAction(args.command, {
4743
5484
  ...unattendedActionPolicy,
@@ -4755,12 +5496,12 @@ async function executeToolInner(
4755
5496
  }
4756
5497
  }
4757
5498
 
4758
- const shellPolicyOpts = {
5499
+ const shellPolicyOpts = Object.freeze({
4759
5500
  ...(shellPolicyOverrides
4760
- ? { overrideRuleIds: shellPolicyOverrides }
5501
+ ? { overrideRuleIds: Object.freeze([...shellPolicyOverrides]) }
4761
5502
  : {}),
4762
5503
  ...(classifyAllShell ? { classifyAllShell: true } : {}),
4763
- };
5504
+ });
4764
5505
  // Layer-by-layer explanation chain for a blocked command: which layers
4765
5506
  // were consulted and what each said (settings rules → shell policy →
4766
5507
  // approval gate). Attached to denial results so `/permissions denials`
@@ -4796,60 +5537,80 @@ async function executeToolInner(
4796
5537
  return chain;
4797
5538
  };
4798
5539
  const override = getRuntimeToolDescriptorByCommand(args.command);
4799
- let shellPolicy;
5540
+ let shellPolicy = evaluateShellCommandPolicy(
5541
+ args.command,
5542
+ shellPolicyOpts,
5543
+ );
4800
5544
  let approvalOutcome = null;
5545
+ let approvalAuthorization = null;
5546
+ let approvalAuthorizationContext = null;
5547
+ let approvalGateResult = null;
4801
5548
  // A settings `allow` rule (ruleAllowed) pre-authorizes: skip the
4802
5549
  // ApprovalGate tier confirm, but still run the hard shell-policy denylist
4803
5550
  // below so an allow rule can never re-enable a blocked unsafe command.
4804
- if (approvalGate && !ruleAllowed) {
4805
- const { evaluateShellCommandWithApproval } =
4806
- await import("../lib/shell-approval.js");
4807
- const gated = await evaluateShellCommandWithApproval({
4808
- command: args.command,
4809
- sessionId,
4810
- approvalGate,
4811
- shellPolicyOptions: shellPolicyOpts,
4812
- });
4813
- shellPolicy = gated.shellPolicy;
4814
- approvalOutcome = {
4815
- decision: gated.decision,
4816
- via: gated.via,
4817
- riskLevel: gated.riskLevel,
4818
- policy: gated.policy,
4819
- };
4820
- if (!gated.allowed) {
4821
- // Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
4822
- // "denial reasons to transcripts"): tell it this won't change on
4823
- // retry and to involve the user, so it stops re-issuing the same
4824
- // blocked command (which otherwise just burns turns + tokens).
4825
- const tierLabel =
4826
- typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
4827
- return attachDescriptor(
4828
- {
4829
- error:
4830
- gated.via === "shell-policy"
4831
- ? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
4832
- : `[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.`,
4833
- shellCommandPolicy: shellPolicy,
4834
- approval: approvalOutcome,
4835
- permissionChain: buildPermissionChain(gated),
4836
- },
4837
- override || runtimeDescriptor,
4838
- );
4839
- }
4840
- } else {
4841
- shellPolicy = evaluateShellCommandPolicy(args.command, shellPolicyOpts);
4842
- if (!shellPolicy.allowed) {
4843
- return attachDescriptor(
4844
- {
4845
- error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
4846
- shellCommandPolicy: shellPolicy,
4847
- permissionChain: buildPermissionChain(null),
4848
- },
4849
- override || runtimeDescriptor,
5551
+ const requestShellApproval = async (operationArgs) => {
5552
+ await shellDispatchPolicyAuthority?.revalidate?.();
5553
+ if (approvalGate && !ruleAllowed) {
5554
+ const approvalPolicyVersion = `${shellDispatchPolicyAuthority?.policyVersion || "cc-shell-policy-authority/v1:unobserved"}:shell:${shellPolicy.decision}:${shellPolicy.ruleId || "none"}`;
5555
+ const gated = await evaluateShellCommandWithApproval({
5556
+ command: args.command,
5557
+ args: operationArgs,
5558
+ sessionId,
5559
+ approvalGate,
5560
+ shellPolicyOptions: shellPolicyOpts,
5561
+ workspace: args.cwd || cwd,
5562
+ policyEnv: shellParentEnvironment,
5563
+ policyVersion: approvalPolicyVersion,
5564
+ deferSideEffects: true,
5565
+ });
5566
+ approvalGateResult = gated;
5567
+ shellPolicy = gated.shellPolicy;
5568
+ approvalOutcome = {
5569
+ decision: gated.decision,
5570
+ via: gated.via,
5571
+ riskLevel: gated.riskLevel,
5572
+ policy: gated.policy,
5573
+ };
5574
+ approvalAuthorization = gated.authorization;
5575
+ approvalAuthorizationContext = gated.authorizationContext;
5576
+ if (!gated.allowed) {
5577
+ // Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
5578
+ // "denial reasons to transcripts"): tell it this won't change on
5579
+ // retry and to involve the user, so it stops re-issuing the same
5580
+ // blocked command (which otherwise just burns turns + tokens).
5581
+ const tierLabel =
5582
+ typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
5583
+ return attachDescriptor(
5584
+ {
5585
+ error:
5586
+ gated.via === "shell-policy"
5587
+ ? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
5588
+ : `[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.`,
5589
+ shellCommandPolicy: shellPolicy,
5590
+ approval: approvalOutcome,
5591
+ permissionChain: buildPermissionChain(gated),
5592
+ },
5593
+ override || runtimeDescriptor,
5594
+ );
5595
+ }
5596
+ } else {
5597
+ shellPolicy = evaluateShellCommandPolicy(
5598
+ args.command,
5599
+ shellPolicyOpts,
4850
5600
  );
5601
+ if (!shellPolicy.allowed) {
5602
+ return attachDescriptor(
5603
+ {
5604
+ error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
5605
+ shellCommandPolicy: shellPolicy,
5606
+ permissionChain: buildPermissionChain(null),
5607
+ },
5608
+ override || runtimeDescriptor,
5609
+ );
5610
+ }
4851
5611
  }
4852
- }
5612
+ return null;
5613
+ };
4853
5614
 
4854
5615
  // P1 #8 Windows/PowerShell first-class: per-call args.shell or the
4855
5616
  // configured settings `shell.windowsDefault` may route this command
@@ -4858,7 +5619,7 @@ async function executeToolInner(
4858
5619
  // the historical default shell byte-identical (useDefaultShell=true).
4859
5620
  const { resolveShellInvocation } =
4860
5621
  await import("../lib/shell-selector.cjs");
4861
- const shellInv = resolveShellInvocation({
5622
+ let shellInv = resolveShellInvocation({
4862
5623
  command: args.command,
4863
5624
  requested: args.shell,
4864
5625
  cwd: args.cwd || cwd,
@@ -4875,12 +5636,16 @@ async function executeToolInner(
4875
5636
  // Resolver failures for a matching strict alias are terminal — falling
4876
5637
  // back to a shell would bypass target identity and manifest requirements.
4877
5638
  let pluginBinInvocation = null;
4878
- let reattestPluginBinInvocation = null;
5639
+ let pluginBinRuntime = null;
4879
5640
  let pluginBinSandboxPolicy = null;
4880
5641
  let pluginBinSandboxExecutionContract = null;
5642
+ const backgroundPlatform =
5643
+ typeof _backgroundProcessDeps.platform === "function"
5644
+ ? _backgroundProcessDeps.platform()
5645
+ : process.platform;
4881
5646
  try {
4882
5647
  const pluginBin = await import("../lib/plugin-runtime/bin.js");
4883
- reattestPluginBinInvocation = pluginBin.reattestPluginBinInvocation;
5648
+ pluginBinRuntime = pluginBin;
4884
5649
  pluginBinSandboxPolicy = collectWorkspacePluginBinSandboxPolicy(
4885
5650
  pluginBin,
4886
5651
  cwd,
@@ -4904,8 +5669,8 @@ async function executeToolInner(
4904
5669
  { cwd },
4905
5670
  );
4906
5671
  if (
4907
- process.platform === "linux" ||
4908
- (process.platform === "win32" &&
5672
+ backgroundPlatform === "linux" ||
5673
+ (backgroundPlatform === "win32" &&
4909
5674
  pluginBinInvocation.runtime === "node")
4910
5675
  ) {
4911
5676
  try {
@@ -4950,7 +5715,7 @@ async function executeToolInner(
4950
5715
  const processOrigin = pluginBinInvocation
4951
5716
  ? "plugin:bin"
4952
5717
  : "tool:run_shell";
4953
- const processProvenance = {
5718
+ let processProvenance = {
4954
5719
  ...(pluginBinInvocation
4955
5720
  ? {
4956
5721
  pluginId: pluginBinInvocation.pluginId,
@@ -4984,21 +5749,262 @@ async function executeToolInner(
4984
5749
  }
4985
5750
  : {};
4986
5751
 
5752
+ // Snapshot process-derived execution inputs before confirmation. The
5753
+ // descriptor carries only an opaque, secret-free environment reference;
5754
+ // the actual child environment remains in this invocation-local closure.
5755
+ const shellParentEnvironment = Object.freeze({ ...process.env });
5756
+ const agentIdentityEnvironment = Object.freeze({
5757
+ CLAUDECODE: "1",
5758
+ ...(sessionId
5759
+ ? {
5760
+ CC_SESSION_ID: String(sessionId),
5761
+ CLAUDE_CODE_SESSION_ID: String(sessionId),
5762
+ }
5763
+ : {}),
5764
+ });
5765
+ const credentialProxyResult = applyCredentialProxy(
5766
+ {
5767
+ ...shellParentEnvironment,
5768
+ ...agentIdentityEnvironment,
5769
+ },
5770
+ { env: shellParentEnvironment },
5771
+ );
5772
+ const shellChildEnvironment = Object.freeze({
5773
+ ...credentialProxyResult.env,
5774
+ });
5775
+ const sandboxChildEnvironment = agentIdentityEnvironment;
5776
+ const shellExecutionDescriptor = createShellExecutionDescriptor({
5777
+ args,
5778
+ workspace: args.cwd || cwd,
5779
+ shellInvocation: shellInv,
5780
+ pluginInvocation: pluginBinInvocation,
5781
+ pluginSandboxPolicy: pluginBinSandboxPolicy,
5782
+ pluginSandboxExecutionContract: pluginBinSandboxExecutionContract,
5783
+ sandbox,
5784
+ environmentRef: {
5785
+ id: `run-shell-env:${randomUUID()}`,
5786
+ inheritance: sandbox ? "sandbox-minimal" : "host-snapshot",
5787
+ credentialProxy: credentialProxyResult.enabled === true,
5788
+ maskedNames: Object.freeze([...credentialProxyResult.masked]),
5789
+ },
5790
+ });
5791
+ // From here onward the descriptor itself is the tool-argument snapshot.
5792
+ args = shellExecutionDescriptor;
5793
+ shellInv = shellExecutionDescriptor.execution.shell;
5794
+ const shellSandbox = shellExecutionDescriptor.execution.sandbox;
5795
+ const executionPlugin = shellExecutionDescriptor.execution.plugin;
5796
+ const approvedPluginProjection = executionPlugin
5797
+ ? canonicalPolicyAuthorityData(executionPlugin)
5798
+ : null;
5799
+ const approvedWorkspacePluginPolicy = canonicalPolicyAuthorityData(
5800
+ pluginBinSandboxPolicy,
5801
+ );
5802
+ const approvedPluginInvocationAuthority = pluginBinInvocation
5803
+ ? canonicalPolicyAuthorityData(
5804
+ projectPluginBinInvocationAuthority(pluginBinInvocation),
5805
+ )
5806
+ : null;
5807
+ const refreshPluginBinExecution = () => {
5808
+ if (!executionPlugin) return;
5809
+ if (!pluginBinRuntime) {
5810
+ const error = new Error(
5811
+ "Plugin bin runtime is unavailable during launch revalidation",
5812
+ );
5813
+ error.code = "ERR_PLUGIN_BIN_DISCOVERY_FAILED";
5814
+ error.pluginBinFailClosed = true;
5815
+ throw error;
5816
+ }
5817
+ const freshInvocation = pluginBinRuntime.resolvePluginBinInvocation(
5818
+ shellExecutionDescriptor.command,
5819
+ {
5820
+ cwd,
5821
+ commandCwd: shellExecutionDescriptor.cwd || cwd,
5822
+ failClosed: true,
5823
+ },
5824
+ );
5825
+ if (!freshInvocation) {
5826
+ const error = new Error(
5827
+ "Approved plugin bin alias is no longer available",
5828
+ );
5829
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5830
+ error.pluginBinFailClosed = true;
5831
+ throw error;
5832
+ }
5833
+ if (
5834
+ canonicalPolicyAuthorityData(
5835
+ projectPluginBinInvocationAuthority(freshInvocation),
5836
+ ) !== approvedPluginInvocationAuthority
5837
+ ) {
5838
+ const error = new Error(
5839
+ "Plugin bin manifest, trust, alias, or sandbox authority changed after approval",
5840
+ );
5841
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5842
+ error.pluginBinFailClosed = true;
5843
+ throw error;
5844
+ }
5845
+ let freshPolicy = collectWorkspacePluginBinSandboxPolicy(
5846
+ pluginBinRuntime,
5847
+ cwd,
5848
+ shellExecutionDescriptor.cwd || cwd,
5849
+ );
5850
+ if (freshInvocation.sandboxPolicy) {
5851
+ freshPolicy = pluginBinRuntime.pinPluginBinSandboxPolicy(
5852
+ {
5853
+ requiredBoundaries: [
5854
+ ...(freshPolicy?.requiredBoundaries || []),
5855
+ ...(freshInvocation.sandboxPolicy.requiredBoundaries || []),
5856
+ ],
5857
+ },
5858
+ { cwd },
5859
+ );
5860
+ }
5861
+ let freshContract = null;
5862
+ if (
5863
+ freshInvocation.sandboxPolicy &&
5864
+ (backgroundPlatform === "linux" ||
5865
+ (backgroundPlatform === "win32" &&
5866
+ freshInvocation.runtime === "node"))
5867
+ ) {
5868
+ freshContract = pluginBinRuntime.createPluginSandboxExecutionContract(
5869
+ freshInvocation,
5870
+ { sync: shellExecutionDescriptor.run_in_background !== true },
5871
+ );
5872
+ }
5873
+ const freshProjection = createShellExecutionDescriptor({
5874
+ args: shellExecutionDescriptor,
5875
+ workspace: shellExecutionDescriptor.execution.workspace,
5876
+ shellInvocation: shellExecutionDescriptor.execution.shell,
5877
+ pluginInvocation: freshInvocation,
5878
+ pluginSandboxPolicy: freshPolicy,
5879
+ pluginSandboxExecutionContract: freshContract,
5880
+ sandbox: shellExecutionDescriptor.execution.sandbox,
5881
+ environmentRef: shellExecutionDescriptor.execution.environment,
5882
+ }).execution.plugin;
5883
+ if (
5884
+ canonicalPolicyAuthorityData(freshProjection) !==
5885
+ approvedPluginProjection
5886
+ ) {
5887
+ const error = new Error(
5888
+ "Plugin bin manifest, trust, alias, or sandbox authority changed after approval",
5889
+ );
5890
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5891
+ error.pluginBinFailClosed = true;
5892
+ throw error;
5893
+ }
5894
+ pluginBinInvocation = freshInvocation;
5895
+ pluginBinSandboxPolicy = freshPolicy;
5896
+ pluginBinSandboxExecutionContract = freshContract;
5897
+ processProvenance = {
5898
+ pluginId: freshInvocation.pluginId,
5899
+ pluginVersion: freshInvocation.pluginVersion,
5900
+ pluginSource: freshInvocation.pluginSource,
5901
+ pluginExecutableIdentity: freshInvocation.executableIdentity,
5902
+ ...(freshPolicy ? { sandboxPolicy: freshPolicy } : {}),
5903
+ ...(freshContract ? { sandboxExecutionContract: freshContract } : {}),
5904
+ };
5905
+ };
5906
+ const refreshPluginExecutionAuthority = () => {
5907
+ if (executionPlugin) {
5908
+ refreshPluginBinExecution();
5909
+ return;
5910
+ }
5911
+ if (!pluginBinRuntime) return;
5912
+ const freshPolicy = collectWorkspacePluginBinSandboxPolicy(
5913
+ pluginBinRuntime,
5914
+ cwd,
5915
+ shellExecutionDescriptor.cwd || cwd,
5916
+ );
5917
+ if (
5918
+ canonicalPolicyAuthorityData(freshPolicy) !==
5919
+ approvedWorkspacePluginPolicy
5920
+ ) {
5921
+ const error = new Error(
5922
+ "Workspace plugin sandbox authority changed after approval",
5923
+ );
5924
+ error.code = "ERR_PLUGIN_BIN_LAUNCH_AUTHORITY_CHANGED";
5925
+ error.pluginBinFailClosed = true;
5926
+ throw error;
5927
+ }
5928
+ pluginBinSandboxPolicy = freshPolicy;
5929
+ processProvenance = freshPolicy ? { sandboxPolicy: freshPolicy } : {};
5930
+ };
5931
+
5932
+ const approvalDenial = await requestShellApproval(
5933
+ shellExecutionDescriptor,
5934
+ );
5935
+ if (approvalDenial) return approvalDenial;
5936
+
5937
+ let shellDispatchAdmitted = false;
5938
+ const admitShellDispatch = async () => {
5939
+ if (shellDispatchAdmitted) {
5940
+ throw new Error("Shell dispatch authorization was already consumed");
5941
+ }
5942
+ await shellDispatchPolicyAuthority?.revalidate?.();
5943
+ refreshPluginExecutionAuthority();
5944
+ const revalidated = evaluateShellCommandPolicy(
5945
+ shellExecutionDescriptor.command,
5946
+ shellPolicyOpts,
5947
+ );
5948
+ if (
5949
+ !revalidated.allowed ||
5950
+ revalidated.decision !== shellPolicy.decision ||
5951
+ (revalidated.ruleId || null) !== (shellPolicy.ruleId || null)
5952
+ ) {
5953
+ const error = new Error(
5954
+ "Shell policy changed after approval; command dispatch was denied",
5955
+ );
5956
+ error.code = "ERR_SHELL_POLICY_REVALIDATION_FAILED";
5957
+ throw error;
5958
+ }
5959
+ if (approvalAuthorization) {
5960
+ if (typeof approvalGate?.consumeAuthorization !== "function") {
5961
+ throw new Error(
5962
+ "Remote shell approval cannot be consumed by this ApprovalGate",
5963
+ );
5964
+ }
5965
+ await approvalGate.consumeAuthorization(
5966
+ approvalAuthorization,
5967
+ approvalAuthorizationContext,
5968
+ );
5969
+ approvalAuthorization = null;
5970
+ }
5971
+ await shellDispatchPolicyAuthority?.revalidate?.();
5972
+ refreshPluginExecutionAuthority();
5973
+ const finalPolicy = evaluateShellCommandPolicy(
5974
+ shellExecutionDescriptor.command,
5975
+ shellPolicyOpts,
5976
+ );
5977
+ if (
5978
+ !finalPolicy.allowed ||
5979
+ finalPolicy.decision !== shellPolicy.decision ||
5980
+ (finalPolicy.ruleId || null) !== (shellPolicy.ruleId || null)
5981
+ ) {
5982
+ const error = new Error(
5983
+ "Shell policy changed while authorization was consumed; command dispatch was denied",
5984
+ );
5985
+ error.code = "ERR_SHELL_POLICY_REVALIDATION_FAILED";
5986
+ throw error;
5987
+ }
5988
+ // Auditing is intentionally committed only after durable authorization
5989
+ // consumption and immediately before the first external side effect.
5990
+ commitShellApprovalSideEffects(approvalGateResult);
5991
+ if (pluginBinInvocation) {
5992
+ pluginBinResult.plugin_bin.launch_identity_reattested = true;
5993
+ }
5994
+ shellDispatchAdmitted = true;
5995
+ };
5996
+
4987
5997
  // Background: spawn, register, return a task_id immediately. The agent
4988
5998
  // polls output + completion via check_shell. No timeout — that's the whole
4989
5999
  // point of backgrounding (builds, test suites, dev servers).
4990
6000
  if (args.run_in_background === true) {
4991
- if (sandbox) {
6001
+ if (shellSandbox) {
4992
6002
  return attachDescriptor({
4993
6003
  error:
4994
6004
  "[Sandbox] Background shell tasks are not supported in the ephemeral sandbox. Run in the foreground or explicitly disable --sandbox.",
4995
6005
  policy: { decision: "deny", via: "sandbox" },
4996
6006
  });
4997
6007
  }
4998
- const backgroundPlatform =
4999
- typeof _backgroundProcessDeps.platform === "function"
5000
- ? _backgroundProcessDeps.platform()
5001
- : process.platform;
5002
6008
  const pinnedBackgroundBoundaries = [
5003
6009
  ...(pluginBinSandboxPolicy?.requiredBoundaries || []),
5004
6010
  ];
@@ -5050,16 +6056,7 @@ async function executeToolInner(
5050
6056
  cwd: workingDirectory,
5051
6057
  shell: false,
5052
6058
  windowsHide: true,
5053
- env: applyCredentialProxy({
5054
- ...process.env,
5055
- CLAUDECODE: "1",
5056
- ...(sessionId
5057
- ? {
5058
- CC_SESSION_ID: String(sessionId),
5059
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5060
- }
5061
- : {}),
5062
- }).env,
6059
+ env: shellChildEnvironment,
5063
6060
  detached: false,
5064
6061
  origin: "tool:run_shell",
5065
6062
  policy: "allow",
@@ -5114,6 +6111,9 @@ async function executeToolInner(
5114
6111
  },
5115
6112
  };
5116
6113
  }
6114
+ // Admission precedes task cleanup, sequence allocation, listener
6115
+ // registration, proxy startup and process creation.
6116
+ await admitShellDispatch();
5117
6117
  // Free memory from idle background tasks before adding another, so a
5118
6118
  // long agent run can't accumulate forgotten dev servers (no-op unless
5119
6119
  // the machine is actually under memory pressure).
@@ -5125,7 +6125,7 @@ async function executeToolInner(
5125
6125
  cwd:
5126
6126
  linuxGenericBackgroundLaunch?.options.cwd ||
5127
6127
  (linuxDirectPluginStrongBackground
5128
- ? pluginBinSandboxExecutionContract.workingDirectory
6128
+ ? executionPlugin.sandboxExecution.workingDirectory
5129
6129
  : args.cwd || cwd),
5130
6130
  status: "running",
5131
6131
  exitCode: null,
@@ -5156,16 +6156,7 @@ async function executeToolInner(
5156
6156
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5157
6157
  // real long-lived secrets out of the spawned command's env — a
5158
6158
  // no-op (same object) when disabled. See credential-proxy.js.
5159
- env: applyCredentialProxy({
5160
- ...process.env,
5161
- CLAUDECODE: "1",
5162
- ...(sessionId
5163
- ? {
5164
- CC_SESSION_ID: String(sessionId),
5165
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5166
- }
5167
- : {}),
5168
- }).env,
6159
+ env: shellChildEnvironment,
5169
6160
  // POSIX: own process group so check_shell{kill}/teardown can signal
5170
6161
  // the whole tree (shell + its grandchild command). No-op on Windows
5171
6162
  // where the tree is killed via taskkill /T instead.
@@ -5192,18 +6183,16 @@ async function executeToolInner(
5192
6183
  linuxGenericBackgroundLaunch.options,
5193
6184
  );
5194
6185
  } else if (pluginBinInvocation) {
5195
- reattestPluginBinInvocation(pluginBinInvocation);
5196
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5197
6186
  child = broker.spawn(
5198
- pluginBinSandboxExecutionContract?.kind ===
6187
+ executionPlugin.sandboxExecution?.kind ===
5199
6188
  "strict-plugin-node-bin"
5200
- ? pluginBinSandboxExecutionContract.runtimePath
5201
- : pluginBinInvocation.command,
5202
- pluginBinInvocation.args,
6189
+ ? executionPlugin.sandboxExecution.runtimePath
6190
+ : executionPlugin.command,
6191
+ executionPlugin.argv,
5203
6192
  {
5204
6193
  ...brokerOpts,
5205
6194
  cwd:
5206
- pluginBinSandboxExecutionContract?.workingDirectory ||
6195
+ executionPlugin.sandboxExecution?.workingDirectory ||
5207
6196
  brokerOpts.cwd,
5208
6197
  shell: false,
5209
6198
  },
@@ -5299,7 +6288,7 @@ async function executeToolInner(
5299
6288
  );
5300
6289
  }
5301
6290
 
5302
- if (sandbox) {
6291
+ if (shellSandbox) {
5303
6292
  if (pluginBinSandboxPolicy) {
5304
6293
  return attachDescriptor(
5305
6294
  {
@@ -5317,15 +6306,16 @@ async function executeToolInner(
5317
6306
  override || runtimeDescriptor,
5318
6307
  );
5319
6308
  }
6309
+ await admitShellDispatch();
5320
6310
  const { executeSandboxedShell, sandboxSummary } =
5321
6311
  await import("../lib/agent-sandbox.js");
5322
6312
  // Domain-restricted networking is ENFORCED by routing the sandboxed
5323
6313
  // process's egress through a local filtering proxy (see
5324
6314
  // sandbox-egress-proxy.js). Start it only when the policy actually
5325
6315
  // restricts domains and network is on; tear it down after the command.
5326
- const sboxPolicy = sandbox.policy || {};
6316
+ const sboxPolicy = shellSandbox.policy || {};
5327
6317
  const needsEgress =
5328
- sandbox.network === true &&
6318
+ shellSandbox.network === true &&
5329
6319
  (sboxPolicy.allowedDomains?.length || 0) +
5330
6320
  (sboxPolicy.deniedDomains?.length || 0) >
5331
6321
  0;
@@ -5354,20 +6344,25 @@ async function executeToolInner(
5354
6344
  }
5355
6345
  let result;
5356
6346
  try {
5357
- result = executeSandboxedShell(args.command, sandbox, {
6347
+ // Proxy startup and dynamic imports are asynchronous. Re-read the
6348
+ // live authority once more after they settle and immediately before
6349
+ // the synchronous sandbox dispatch.
6350
+ await shellDispatchPolicyAuthority?.revalidate?.();
6351
+ refreshPluginExecutionAuthority();
6352
+ if (pluginBinSandboxPolicy) {
6353
+ const error = new Error(
6354
+ "A plugin sandbox authority appeared while the legacy shell sandbox was starting",
6355
+ );
6356
+ error.code = "ERR_PLUGIN_BIN_SANDBOX_ROUTE_CONFLICT";
6357
+ error.pluginBinFailClosed = true;
6358
+ throw error;
6359
+ }
6360
+ result = executeSandboxedShell(args.command, shellSandbox, {
5358
6361
  cwd: args.cwd || cwd,
5359
6362
  timeout: _resolveShellTimeout(args.timeout),
5360
6363
  maxBuffer: 1024 * 1024,
5361
6364
  egressProxy,
5362
- env: {
5363
- CLAUDECODE: "1",
5364
- ...(sessionId
5365
- ? {
5366
- CC_SESSION_ID: String(sessionId),
5367
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5368
- }
5369
- : {}),
5370
- },
6365
+ env: sandboxChildEnvironment,
5371
6366
  });
5372
6367
  } finally {
5373
6368
  if (proxyHandle) {
@@ -5379,7 +6374,7 @@ async function executeToolInner(
5379
6374
  }
5380
6375
  }
5381
6376
  const common = {
5382
- sandbox: sandboxSummary(sandbox),
6377
+ sandbox: sandboxSummary(shellSandbox),
5383
6378
  shellCommandPolicy: shellPolicy,
5384
6379
  approval: approvalOutcome,
5385
6380
  policyTrace: ["shell-policy", "approval", "sandbox"],
@@ -5418,18 +6413,10 @@ async function executeToolInner(
5418
6413
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5419
6414
  // real long-lived secrets out of the shell's env — a no-op (same
5420
6415
  // object) when disabled. See credential-proxy.js.
5421
- env: applyCredentialProxy({
5422
- ...process.env,
5423
- CLAUDECODE: "1",
5424
- ...(sessionId
5425
- ? {
5426
- CC_SESSION_ID: String(sessionId),
5427
- CLAUDE_CODE_SESSION_ID: String(sessionId),
5428
- }
5429
- : {}),
5430
- }).env,
6416
+ env: shellChildEnvironment,
5431
6417
  };
5432
6418
  let output;
6419
+ await admitShellDispatch();
5433
6420
  const brokerExecOpts = {
5434
6421
  ...fgExecOpts,
5435
6422
  origin: processOrigin,
@@ -5438,17 +6425,15 @@ async function executeToolInner(
5438
6425
  ...processProvenance,
5439
6426
  };
5440
6427
  if (pluginBinInvocation) {
5441
- reattestPluginBinInvocation(pluginBinInvocation);
5442
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5443
6428
  const res = broker.spawnSync(
5444
- pluginBinSandboxExecutionContract?.kind === "strict-plugin-node-bin"
5445
- ? pluginBinSandboxExecutionContract.runtimePath
5446
- : pluginBinInvocation.command,
5447
- pluginBinInvocation.args,
6429
+ executionPlugin.sandboxExecution?.kind === "strict-plugin-node-bin"
6430
+ ? executionPlugin.sandboxExecution.runtimePath
6431
+ : executionPlugin.command,
6432
+ executionPlugin.argv,
5448
6433
  {
5449
6434
  ...brokerExecOpts,
5450
6435
  cwd:
5451
- pluginBinSandboxExecutionContract?.workingDirectory ||
6436
+ executionPlugin.sandboxExecution?.workingDirectory ||
5452
6437
  brokerExecOpts.cwd,
5453
6438
  shell: false,
5454
6439
  windowsHide: true,
@@ -5527,6 +6512,15 @@ async function executeToolInner(
5527
6512
  : {}),
5528
6513
  stderr: (err.stderr || "").substring(0, 2000),
5529
6514
  exitCode: err.status,
6515
+ ...(typeof err.code === "string" && err.code
6516
+ ? {
6517
+ policy: {
6518
+ decision: "deny",
6519
+ via: "dispatch-revalidation",
6520
+ code: err.code,
6521
+ },
6522
+ }
6523
+ : {}),
5530
6524
  ...shellMeta,
5531
6525
  ...pluginBinResult,
5532
6526
  shellCommandPolicy: shellPolicy,
@@ -5650,8 +6644,18 @@ async function executeToolInner(
5650
6644
  approvalGate &&
5651
6645
  typeof approvalGate.hasConfirmer === "function" &&
5652
6646
  !approvalGate.hasConfirmer();
6647
+ // A durable remote-approval consumer is an explicit fail-closed mode.
6648
+ // run_code does not yet carry an exact lease tuple, so it must still
6649
+ // consult the remote confirmer in headless sessions; that confirmer
6650
+ // returns lease-unavailable and no code is persisted or launched.
6651
+ const durableAuthorizationGate =
6652
+ approvalGate &&
6653
+ typeof approvalGate.hasAuthorizationConsumer === "function" &&
6654
+ approvalGate.hasAuthorizationConsumer() === true;
5653
6655
  if (
5654
- (interactiveApproval || policyGateNoConfirmer) &&
6656
+ (interactiveApproval ||
6657
+ policyGateNoConfirmer ||
6658
+ durableAuthorizationGate) &&
5655
6659
  approvalGate &&
5656
6660
  !ruleAllowed &&
5657
6661
  typeof approvalGate.decide === "function"
@@ -5662,7 +6666,7 @@ async function executeToolInner(
5662
6666
  sessionId,
5663
6667
  riskLevel: APPROVAL_RISK.HIGH,
5664
6668
  tool: "run_code",
5665
- args: { language: args.language },
6669
+ args: runCodeApprovalDescriptor(args, cwd),
5666
6670
  });
5667
6671
  if (gate.decision !== APPROVAL_DECISION.ALLOW) {
5668
6672
  // Actionable denial (matches run_shell, Claude-Code 2.1.193): tell the
@@ -5679,6 +6683,23 @@ async function executeToolInner(
5679
6683
  },
5680
6684
  });
5681
6685
  }
6686
+ if (gate.authorization) {
6687
+ return attachDescriptor({
6688
+ error:
6689
+ "[ApprovalGate] run_code received a durable authorization, but this tool does not yet implement an exact one-shot consume tuple. No code was launched.",
6690
+ approval: {
6691
+ decision: APPROVAL_DECISION.DENY,
6692
+ via: "authorization-consume-unsupported",
6693
+ riskLevel: "high",
6694
+ policy: gate.policy,
6695
+ },
6696
+ policy: {
6697
+ decision: "deny",
6698
+ via: "authorization-consume-unsupported",
6699
+ code: "CC_RUN_CODE_AUTHORIZATION_CONSUME_UNSUPPORTED",
6700
+ },
6701
+ });
6702
+ }
5682
6703
  }
5683
6704
  return attachDescriptor(await _executeRunCode(args, cwd));
5684
6705
  }
@@ -8719,6 +9740,9 @@ async function _executeSpawnSubAgent(args, ctx) {
8719
9740
  // Parent execution authority is inherited as a tighten-only bundle and is
8720
9741
  // re-enforced by the child's executeTool path on every provider tool call.
8721
9742
  ...(ctx.permissionRules ? { permissionRules: ctx.permissionRules } : {}),
9743
+ ...(ctx.permissionRulesProvider
9744
+ ? { permissionRulesProvider: ctx.permissionRulesProvider }
9745
+ : {}),
8722
9746
  ...(childHostManagedToolPolicy
8723
9747
  ? { hostManagedToolPolicy: childHostManagedToolPolicy }
8724
9748
  : {}),
@@ -11351,6 +12375,9 @@ export async function* agentLoop(messages, options) {
11351
12375
  : options.additionalDirectories || null,
11352
12376
  sandbox: options.sandbox || null,
11353
12377
  permissionRules: hermeticExecution ? null : options.permissionRules || null,
12378
+ permissionRulesProvider: hermeticExecution
12379
+ ? null
12380
+ : options.permissionRulesProvider || null,
11354
12381
  permissionConfirm: hermeticExecution
11355
12382
  ? null
11356
12383
  : options.permissionConfirm || null,