chainlesschain 0.164.0 → 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 (189) hide show
  1. package/package.json +4 -4
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-CYxFDrcm.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-CPBtdiU2.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-Dayq7Dp7.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-6U7bPb2s.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-D2Iqg_3D.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-CdoOB7WV.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-DrnAVB8w.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-CesMi5cy.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-DaX9ptax.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-OrwOPxDS.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-DiHbcysC.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-CbgCkaBJ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-Du5hgrh4.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-DsPOBW-p.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-BwHIFkNC.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BvrDQSgd.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-gwAE6arx.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DW-0l4uF.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-j-NsLesB.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-CibVEBOI.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-W4z9vSfe.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-DAcrkKxG.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-6GXah-89.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-BXBLlcNm.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-BoJJBkG3.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-okjsl46v.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Rvs7i6nn.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-CHSUxXXS.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-V3BlfxsB.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-Nn4LNxhA.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-GmB6DOr6.js → MarkdownRenderer-SVAgNnkt.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-DEmjFcg-.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-DlopXipI.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-BG3ynX60.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-DeRzWOfT.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-Tyd3o2A0.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-BoPbOv1C.js} +2 -2
  43. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-un-rTHp7.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-Ds0oveEB.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-D09JI-yY.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-DTRhiKJn.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BAnnNCim.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-Yr-1fSYi.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-S9EAolpj.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-CqcLKqJ8.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-Dw0F9EcU.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BDRwUevN.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-D3_c3nKB.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-CxdMV98x.js} +4 -4
  55. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-CgjNQFLI.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-BHghIIht.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-De6pqaKM.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-DDAjkUkT.js → Projects-CP8vPvzs.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CUnHbNf-.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-DA2f3Wts.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C1FeHDqp.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-B5guaUMN.js} +1 -1
  64. package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
  65. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-eTlRhgvK.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-D7AqSGY0.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-JiuLm4Ja.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-CwruahW3.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-CdhqaR0b.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CU0ZIlcT.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-CtduPfAb.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-CROXm6AY.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-DiZiABFD.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-D6UL3dA5.js → SpeechSettings-B5pLcXUw.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-DBX6J1Oe.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-BLyRJd3-.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-BKa5qzmk.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-DnD3a4M6.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-DEitQeTb.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-CjHggTDq.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-CsGT7sfl.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-yr4rFDrW.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-CLvu1Sjp.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-B0m7yUP8.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-C0VvP4lc.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-kllBAG1M.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-C54UQNLx.js} +4 -4
  88. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-B7y8qSvU.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DRzTh8CJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-BSMwSnCx.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTg602H5.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-B6EGsdvp.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-DDKqJ8RD.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-323Hhxxt.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-BICn3Qh1.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BMuGd2--.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-BRqt4fO-.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BVTAl5N2.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-BsR7Y2NG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-Bx044qDj.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-C1GFfuqN.js} +1 -1
  103. package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
  104. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-C66NpWy5.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-CDTvsX8t.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-CGNklbP6.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-CHfLYwcW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-CNxg61YP.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CQlHFN_g.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-CUEU48o1.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-CWGax2QW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-C_JzIXUq.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-Cf23wVMs.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-CmAInUkX.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-CmAubjDi.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-Co7oM1qr.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CwPUZbMI.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-D1naNj4U.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-D5CsBShd.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DBT3YeQ9.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-DLdrHhML.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-Dc77VR3H.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-Dfv2lKu3.js} +1 -1
  124. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-DiW0sDkz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-DpTHIRfZ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DqFXGzzC.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-XFe-1twF.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-p2HYn89m.js} +3 -3
  129. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-pqfux8vZ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-rjzk9z1Q.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-th2ihDc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BtobShro.js → index-u2YVld9W.js} +1 -1
  133. package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
  134. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-C3Djh9fa.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-BzqFmbSe.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-DJo_FFaW.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-_tlTiBvk.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-CB2ZyxUg.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DZwIFD4u.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-DxZY0dgk.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-CzzGt7GL.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-BaIK52yz.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-3-FHu9_M.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-BkRT68fK.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-DVVUhV8p.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-BuBLFz0c.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-kpm6LtXU.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/cowork.js +283 -61
  152. package/src/data/changelog.json +15 -2
  153. package/src/gateways/ws/action-protocol.js +293 -19
  154. package/src/gateways/ws/message-dispatcher.js +30 -0
  155. package/src/gateways/ws/remote-session-protocol.js +542 -35
  156. package/src/gateways/ws/ws-approval-gate.js +5 -0
  157. package/src/gateways/ws/ws-server.js +462 -38
  158. package/src/harness/remote-session-audit.js +1 -0
  159. package/src/harness/remote-session-crypto.js +750 -50
  160. package/src/harness/remote-session-registry.js +795 -27
  161. package/src/lib/auto-mode-config.js +98 -3
  162. package/src/lib/background-agent-keeper-protocol.js +154 -10
  163. package/src/lib/background-agent-supervisor.js +840 -48
  164. package/src/lib/background-session-transport.js +23 -6
  165. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  166. package/src/lib/cowork-workflow.js +853 -61
  167. package/src/lib/delivery-production-adapter.js +38 -2
  168. package/src/lib/dynamic-workflow-facade.js +1105 -17
  169. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  170. package/src/lib/remote-approval-bridge.js +745 -108
  171. package/src/lib/remote-control.js +2 -0
  172. package/src/lib/remote-membership-coordinator.js +604 -40
  173. package/src/lib/remote-membership-host-store.js +59 -10
  174. package/src/lib/runtime-provenance-ledger.js +171 -46
  175. package/src/lib/shell-approval.js +169 -6
  176. package/src/lib/with-file-lock.js +73 -38
  177. package/src/repl/agent-repl.js +70 -29
  178. package/src/repl/remote-approval.js +29 -7
  179. package/src/runtime/agent-core.js +1099 -152
  180. package/src/runtime/headless-runner.js +22 -5
  181. package/src/runtime/headless-stream.js +19 -3
  182. package/src/workers/background-agent-keeper.js +249 -58
  183. package/src/workers/background-agent-worker.js +7 -0
  184. package/src/workers/background-turn-bootstrap.js +29 -2
  185. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  186. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  187. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  188. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  189. 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
@@ -2951,12 +3612,14 @@ export async function executeTool(name, args, context = {}) {
2951
3612
  subtreeInstructionScope:
2952
3613
  context.subtreeInstructionScope || context.sessionId || "__legacy__",
2953
3614
  shellPolicyOverrides: context.shellPolicyOverrides || null,
3615
+ classifyAllShell: context.classifyAllShell === true,
2954
3616
  approvalGate: context.approvalGate || null,
2955
3617
  shellConfirm: context.shellConfirm || null,
2956
3618
  additionalDirectories: context.additionalDirectories || null,
2957
3619
  sandbox: context.sandbox || null,
2958
3620
  ruleAllowed,
2959
3621
  settingsVerdict,
3622
+ shellDispatchPolicyAuthority,
2960
3623
  subAgentDepth: context.subAgentDepth || 0,
2961
3624
  subAgentBudget: context.subAgentBudget || null,
2962
3625
  sessionBudget: context.sessionBudget || null,
@@ -4331,6 +4994,7 @@ async function executeToolInner(
4331
4994
  sandbox,
4332
4995
  ruleAllowed = false,
4333
4996
  settingsVerdict = null,
4997
+ shellDispatchPolicyAuthority = null,
4334
4998
  subAgentDepth = 0,
4335
4999
  subAgentBudget = null,
4336
5000
  sessionBudget = null,
@@ -4812,6 +5476,9 @@ async function executeToolInner(
4812
5476
  }
4813
5477
 
4814
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);
4815
5482
  if (unattendedActionPolicy?.unattended === true) {
4816
5483
  const unattendedVerdict = evaluateUnattendedShellAction(args.command, {
4817
5484
  ...unattendedActionPolicy,
@@ -4829,12 +5496,12 @@ async function executeToolInner(
4829
5496
  }
4830
5497
  }
4831
5498
 
4832
- const shellPolicyOpts = {
5499
+ const shellPolicyOpts = Object.freeze({
4833
5500
  ...(shellPolicyOverrides
4834
- ? { overrideRuleIds: shellPolicyOverrides }
5501
+ ? { overrideRuleIds: Object.freeze([...shellPolicyOverrides]) }
4835
5502
  : {}),
4836
5503
  ...(classifyAllShell ? { classifyAllShell: true } : {}),
4837
- };
5504
+ });
4838
5505
  // Layer-by-layer explanation chain for a blocked command: which layers
4839
5506
  // were consulted and what each said (settings rules → shell policy →
4840
5507
  // approval gate). Attached to denial results so `/permissions denials`
@@ -4870,60 +5537,80 @@ async function executeToolInner(
4870
5537
  return chain;
4871
5538
  };
4872
5539
  const override = getRuntimeToolDescriptorByCommand(args.command);
4873
- let shellPolicy;
5540
+ let shellPolicy = evaluateShellCommandPolicy(
5541
+ args.command,
5542
+ shellPolicyOpts,
5543
+ );
4874
5544
  let approvalOutcome = null;
5545
+ let approvalAuthorization = null;
5546
+ let approvalAuthorizationContext = null;
5547
+ let approvalGateResult = null;
4875
5548
  // A settings `allow` rule (ruleAllowed) pre-authorizes: skip the
4876
5549
  // ApprovalGate tier confirm, but still run the hard shell-policy denylist
4877
5550
  // 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,
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,
4924
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
+ }
4925
5611
  }
4926
- }
5612
+ return null;
5613
+ };
4927
5614
 
4928
5615
  // P1 #8 Windows/PowerShell first-class: per-call args.shell or the
4929
5616
  // configured settings `shell.windowsDefault` may route this command
@@ -4932,7 +5619,7 @@ async function executeToolInner(
4932
5619
  // the historical default shell byte-identical (useDefaultShell=true).
4933
5620
  const { resolveShellInvocation } =
4934
5621
  await import("../lib/shell-selector.cjs");
4935
- const shellInv = resolveShellInvocation({
5622
+ let shellInv = resolveShellInvocation({
4936
5623
  command: args.command,
4937
5624
  requested: args.shell,
4938
5625
  cwd: args.cwd || cwd,
@@ -4949,12 +5636,16 @@ async function executeToolInner(
4949
5636
  // Resolver failures for a matching strict alias are terminal — falling
4950
5637
  // back to a shell would bypass target identity and manifest requirements.
4951
5638
  let pluginBinInvocation = null;
4952
- let reattestPluginBinInvocation = null;
5639
+ let pluginBinRuntime = null;
4953
5640
  let pluginBinSandboxPolicy = null;
4954
5641
  let pluginBinSandboxExecutionContract = null;
5642
+ const backgroundPlatform =
5643
+ typeof _backgroundProcessDeps.platform === "function"
5644
+ ? _backgroundProcessDeps.platform()
5645
+ : process.platform;
4955
5646
  try {
4956
5647
  const pluginBin = await import("../lib/plugin-runtime/bin.js");
4957
- reattestPluginBinInvocation = pluginBin.reattestPluginBinInvocation;
5648
+ pluginBinRuntime = pluginBin;
4958
5649
  pluginBinSandboxPolicy = collectWorkspacePluginBinSandboxPolicy(
4959
5650
  pluginBin,
4960
5651
  cwd,
@@ -4978,8 +5669,8 @@ async function executeToolInner(
4978
5669
  { cwd },
4979
5670
  );
4980
5671
  if (
4981
- process.platform === "linux" ||
4982
- (process.platform === "win32" &&
5672
+ backgroundPlatform === "linux" ||
5673
+ (backgroundPlatform === "win32" &&
4983
5674
  pluginBinInvocation.runtime === "node")
4984
5675
  ) {
4985
5676
  try {
@@ -5024,7 +5715,7 @@ async function executeToolInner(
5024
5715
  const processOrigin = pluginBinInvocation
5025
5716
  ? "plugin:bin"
5026
5717
  : "tool:run_shell";
5027
- const processProvenance = {
5718
+ let processProvenance = {
5028
5719
  ...(pluginBinInvocation
5029
5720
  ? {
5030
5721
  pluginId: pluginBinInvocation.pluginId,
@@ -5058,21 +5749,262 @@ async function executeToolInner(
5058
5749
  }
5059
5750
  : {};
5060
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
+
5061
5997
  // Background: spawn, register, return a task_id immediately. The agent
5062
5998
  // polls output + completion via check_shell. No timeout — that's the whole
5063
5999
  // point of backgrounding (builds, test suites, dev servers).
5064
6000
  if (args.run_in_background === true) {
5065
- if (sandbox) {
6001
+ if (shellSandbox) {
5066
6002
  return attachDescriptor({
5067
6003
  error:
5068
6004
  "[Sandbox] Background shell tasks are not supported in the ephemeral sandbox. Run in the foreground or explicitly disable --sandbox.",
5069
6005
  policy: { decision: "deny", via: "sandbox" },
5070
6006
  });
5071
6007
  }
5072
- const backgroundPlatform =
5073
- typeof _backgroundProcessDeps.platform === "function"
5074
- ? _backgroundProcessDeps.platform()
5075
- : process.platform;
5076
6008
  const pinnedBackgroundBoundaries = [
5077
6009
  ...(pluginBinSandboxPolicy?.requiredBoundaries || []),
5078
6010
  ];
@@ -5124,16 +6056,7 @@ async function executeToolInner(
5124
6056
  cwd: workingDirectory,
5125
6057
  shell: false,
5126
6058
  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,
6059
+ env: shellChildEnvironment,
5137
6060
  detached: false,
5138
6061
  origin: "tool:run_shell",
5139
6062
  policy: "allow",
@@ -5188,6 +6111,9 @@ async function executeToolInner(
5188
6111
  },
5189
6112
  };
5190
6113
  }
6114
+ // Admission precedes task cleanup, sequence allocation, listener
6115
+ // registration, proxy startup and process creation.
6116
+ await admitShellDispatch();
5191
6117
  // Free memory from idle background tasks before adding another, so a
5192
6118
  // long agent run can't accumulate forgotten dev servers (no-op unless
5193
6119
  // the machine is actually under memory pressure).
@@ -5199,7 +6125,7 @@ async function executeToolInner(
5199
6125
  cwd:
5200
6126
  linuxGenericBackgroundLaunch?.options.cwd ||
5201
6127
  (linuxDirectPluginStrongBackground
5202
- ? pluginBinSandboxExecutionContract.workingDirectory
6128
+ ? executionPlugin.sandboxExecution.workingDirectory
5203
6129
  : args.cwd || cwd),
5204
6130
  status: "running",
5205
6131
  exitCode: null,
@@ -5230,16 +6156,7 @@ async function executeToolInner(
5230
6156
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5231
6157
  // real long-lived secrets out of the spawned command's env — a
5232
6158
  // 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,
6159
+ env: shellChildEnvironment,
5243
6160
  // POSIX: own process group so check_shell{kill}/teardown can signal
5244
6161
  // the whole tree (shell + its grandchild command). No-op on Windows
5245
6162
  // where the tree is killed via taskkill /T instead.
@@ -5266,18 +6183,16 @@ async function executeToolInner(
5266
6183
  linuxGenericBackgroundLaunch.options,
5267
6184
  );
5268
6185
  } else if (pluginBinInvocation) {
5269
- reattestPluginBinInvocation(pluginBinInvocation);
5270
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5271
6186
  child = broker.spawn(
5272
- pluginBinSandboxExecutionContract?.kind ===
6187
+ executionPlugin.sandboxExecution?.kind ===
5273
6188
  "strict-plugin-node-bin"
5274
- ? pluginBinSandboxExecutionContract.runtimePath
5275
- : pluginBinInvocation.command,
5276
- pluginBinInvocation.args,
6189
+ ? executionPlugin.sandboxExecution.runtimePath
6190
+ : executionPlugin.command,
6191
+ executionPlugin.argv,
5277
6192
  {
5278
6193
  ...brokerOpts,
5279
6194
  cwd:
5280
- pluginBinSandboxExecutionContract?.workingDirectory ||
6195
+ executionPlugin.sandboxExecution?.workingDirectory ||
5281
6196
  brokerOpts.cwd,
5282
6197
  shell: false,
5283
6198
  },
@@ -5373,7 +6288,7 @@ async function executeToolInner(
5373
6288
  );
5374
6289
  }
5375
6290
 
5376
- if (sandbox) {
6291
+ if (shellSandbox) {
5377
6292
  if (pluginBinSandboxPolicy) {
5378
6293
  return attachDescriptor(
5379
6294
  {
@@ -5391,15 +6306,16 @@ async function executeToolInner(
5391
6306
  override || runtimeDescriptor,
5392
6307
  );
5393
6308
  }
6309
+ await admitShellDispatch();
5394
6310
  const { executeSandboxedShell, sandboxSummary } =
5395
6311
  await import("../lib/agent-sandbox.js");
5396
6312
  // Domain-restricted networking is ENFORCED by routing the sandboxed
5397
6313
  // process's egress through a local filtering proxy (see
5398
6314
  // sandbox-egress-proxy.js). Start it only when the policy actually
5399
6315
  // restricts domains and network is on; tear it down after the command.
5400
- const sboxPolicy = sandbox.policy || {};
6316
+ const sboxPolicy = shellSandbox.policy || {};
5401
6317
  const needsEgress =
5402
- sandbox.network === true &&
6318
+ shellSandbox.network === true &&
5403
6319
  (sboxPolicy.allowedDomains?.length || 0) +
5404
6320
  (sboxPolicy.deniedDomains?.length || 0) >
5405
6321
  0;
@@ -5428,20 +6344,25 @@ async function executeToolInner(
5428
6344
  }
5429
6345
  let result;
5430
6346
  try {
5431
- 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, {
5432
6361
  cwd: args.cwd || cwd,
5433
6362
  timeout: _resolveShellTimeout(args.timeout),
5434
6363
  maxBuffer: 1024 * 1024,
5435
6364
  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
- },
6365
+ env: sandboxChildEnvironment,
5445
6366
  });
5446
6367
  } finally {
5447
6368
  if (proxyHandle) {
@@ -5453,7 +6374,7 @@ async function executeToolInner(
5453
6374
  }
5454
6375
  }
5455
6376
  const common = {
5456
- sandbox: sandboxSummary(sandbox),
6377
+ sandbox: sandboxSummary(shellSandbox),
5457
6378
  shellCommandPolicy: shellPolicy,
5458
6379
  approval: approvalOutcome,
5459
6380
  policyTrace: ["shell-policy", "approval", "sandbox"],
@@ -5492,18 +6413,10 @@ async function executeToolInner(
5492
6413
  // Credential proxy (opt-in, CC_CREDENTIAL_PROXY): keeps the agent's
5493
6414
  // real long-lived secrets out of the shell's env — a no-op (same
5494
6415
  // 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,
6416
+ env: shellChildEnvironment,
5505
6417
  };
5506
6418
  let output;
6419
+ await admitShellDispatch();
5507
6420
  const brokerExecOpts = {
5508
6421
  ...fgExecOpts,
5509
6422
  origin: processOrigin,
@@ -5512,17 +6425,15 @@ async function executeToolInner(
5512
6425
  ...processProvenance,
5513
6426
  };
5514
6427
  if (pluginBinInvocation) {
5515
- reattestPluginBinInvocation(pluginBinInvocation);
5516
- pluginBinResult.plugin_bin.launch_identity_reattested = true;
5517
6428
  const res = broker.spawnSync(
5518
- pluginBinSandboxExecutionContract?.kind === "strict-plugin-node-bin"
5519
- ? pluginBinSandboxExecutionContract.runtimePath
5520
- : pluginBinInvocation.command,
5521
- pluginBinInvocation.args,
6429
+ executionPlugin.sandboxExecution?.kind === "strict-plugin-node-bin"
6430
+ ? executionPlugin.sandboxExecution.runtimePath
6431
+ : executionPlugin.command,
6432
+ executionPlugin.argv,
5522
6433
  {
5523
6434
  ...brokerExecOpts,
5524
6435
  cwd:
5525
- pluginBinSandboxExecutionContract?.workingDirectory ||
6436
+ executionPlugin.sandboxExecution?.workingDirectory ||
5526
6437
  brokerExecOpts.cwd,
5527
6438
  shell: false,
5528
6439
  windowsHide: true,
@@ -5601,6 +6512,15 @@ async function executeToolInner(
5601
6512
  : {}),
5602
6513
  stderr: (err.stderr || "").substring(0, 2000),
5603
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
+ : {}),
5604
6524
  ...shellMeta,
5605
6525
  ...pluginBinResult,
5606
6526
  shellCommandPolicy: shellPolicy,
@@ -5724,8 +6644,18 @@ async function executeToolInner(
5724
6644
  approvalGate &&
5725
6645
  typeof approvalGate.hasConfirmer === "function" &&
5726
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;
5727
6655
  if (
5728
- (interactiveApproval || policyGateNoConfirmer) &&
6656
+ (interactiveApproval ||
6657
+ policyGateNoConfirmer ||
6658
+ durableAuthorizationGate) &&
5729
6659
  approvalGate &&
5730
6660
  !ruleAllowed &&
5731
6661
  typeof approvalGate.decide === "function"
@@ -5736,7 +6666,7 @@ async function executeToolInner(
5736
6666
  sessionId,
5737
6667
  riskLevel: APPROVAL_RISK.HIGH,
5738
6668
  tool: "run_code",
5739
- args: { language: args.language },
6669
+ args: runCodeApprovalDescriptor(args, cwd),
5740
6670
  });
5741
6671
  if (gate.decision !== APPROVAL_DECISION.ALLOW) {
5742
6672
  // Actionable denial (matches run_shell, Claude-Code 2.1.193): tell the
@@ -5753,6 +6683,23 @@ async function executeToolInner(
5753
6683
  },
5754
6684
  });
5755
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
+ }
5756
6703
  }
5757
6704
  return attachDescriptor(await _executeRunCode(args, cwd));
5758
6705
  }