chainlesschain 0.166.7 → 0.166.9

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 (187) hide show
  1. package/README.md +21 -0
  2. package/package.json +3 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-BlrmQxa2.js → AIOps-DJvpifGx.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-zyRyLITg.js → ActionButton-D2UhuF_A.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DwGz7OGN.js → Analytics-C9ho9obu.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-Dc-nj7Zd.js → AppLayout-BvRCpFK3.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-7Hu3O73p.js → Artifacts-jxrySvpk.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bnoe3-fR.js → Audit-ovEw0ZYl.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-BqYFrDKZ.js → BackgroundAgents-D7H5z6zS.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-B3hwW1nz.js → Backup-BuXob8za.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-XFVxLA4w.js → BaseInput-CtnszuQ2.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CWRDNw7e.js → Chat-B_mYE8fJ.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-Bf30VM8G.js → ChatBubbleRenderer-DaiA6CsX.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-kFeeBgac.js → Checkbox-ierdtcJD.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-CCxp8xoI.js → Codegen-3VKPJD_B.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-BLKZalle.js → Col-1EdMyf5P.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-DD0eWCez.js → Community-D6H7UA8K.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-CHqHWdy0.js → Compact-BBAZvYIm.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-Bw54uPiK.js → Compliance-CgsTrAoj.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DZ13zKNJ.js → Cowork-tnZgLoTE.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-Dlj5Jevg.js → Cron-CPpZw2Nh.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-Nvz0aR0j.js → Crosschain-BymBcp5z.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-Ct89eUyY.js → DID-DKPxS_K1.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-BHUH5IAe.js → Dashboard-DvJhEmH9.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-BhZG-PO7.js → Dropdown-b8iQyQIZ.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-ae4RVwwJ.js → EmailListRenderer--0-A-zKb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-AOcxLfLr.js → FamilyGuardDashboard-C12iDN06.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BK2PJQOF.js → Federation-BRhaE2Z8.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-C2n3xO2_.js → FormItemContext-C4dnq7yf.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-DcyUVFZq.js → GenericCardRenderer-CGAdbR5n.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-tBae9Lt8.js → Git-4eA2fYzL.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-DmNR3AP4.js → Governance-BJuY9Y-9.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-CoseEigi.js → Inference-DZ5Dy4up.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-Bdjd1sxr.js → KnowledgeGraph-BMTjRs4A.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-CtlkKIcn.js → Logs-BKEeQEfJ.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-B05DgASC.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-BTYb7nEx.js → Marketplace-Dhho4fEg.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D0iUdW3P.js → McpTools-BW6xxSlU.js} +2 -2
  40. package/src/assets/web-panel/assets/{Memory--gwI7w4Y.js → Memory-mPqlpSkq.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-JmFBi24T.js → MobileBridge-Cb9N_DQI.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-BK01cQRx.js → MobileProjects-BNs8tah_.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-u7avzyoO.js → Mtc-CaxBt8tn.js} +5 -5
  44. package/src/assets/web-panel/assets/{MtcAudit-gT3z5fo_.js → MtcAudit-DRPQbp3t.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-BIqZTz5n.js → Multisig-CRm-d8bK.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdhVfXYq.js → NLProgramming-BCCk3LPs.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-CBlyUmY3.js → Notes-Cv8SN8gN.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-DSyI26N3.js → NotificationSettings-CggIvTh5.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-B7IEuNz4.js → OrderTableRenderer-BW3_bV12.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-ozhi4kGS.js → Organization-BuRE-VCO.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-CBy57RVM.js → Overflow-DBrm1gju.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-CA-ZRv77.js → P2P-CWVnDYgS.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-DOHc9zm8.js → PdhVaultBrowser-8RKUIpte.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-DiP632GP.js → Permissions-tDUAxogW.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-AnU8O-SS.js → PersonalDataHub-Dv2-CFis.js} +5 -5
  56. package/src/assets/web-panel/assets/{Pipeline-DDibOUaP.js → Pipeline-DM6kvFni.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-D421t36T.js → Privacy-DzK-yL0d.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-DR879ZPJ.js → ProjectInit-B5UrNpkj.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-Ded8o6aY.js → ProjectSettings-CxJD43oW.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-Z2Orm8Ma.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-Boivuu-E.js → Providers-CYl4jRYK.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-xKD_ntKp.js → QrScannerModal-DeH2T0fH.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-D3y_A6FV.js → QuickAsk-Bw9k_kTk.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-BjKM-YC2.js → Recommend-Bl_jGwoi.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-d6u01Kq0.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-Ci2GjH5p.js → Reputation-Ds_Dfr-k.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-B0-T2BSh.js → Row-BpQUQTNj.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-Bet-ur-r.js → RssFeed-BatpZBGt.js} +2 -2
  69. package/src/assets/web-panel/assets/{Search-DWtRAS2R.js → Search-D7hOObzi.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-BDfu2Qn6.js → Security-dXQ4FKNt.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-CvJmvSMM.js → Services-D8wInGPa.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-CEbrKpdC.js → Skeleton-DXQCoYpM.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CjVeiETf.js → Skills-D3oLRIVn.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-Dibypxum.js → Sla-BmP4-LnJ.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-XflYXp7a.js → SpeechSettings-DLj-xzro.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-ro1Ivx1E.js → SyncSettings-8KKaaRMr.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-CHq1qFbo.js → Tasks-GyK42I7C.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-CRnUZxtZ.js → Templates-n1y_8UBG.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-jUX72JCA.js → Tenant-B39nBph8.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-BBhAs7pt.js → Terminal-DCQoOKXe.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-CD5M3RGp.js → TimelineRenderer-BkhbDfXk.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-sah3v0gu.js → Tokens-BeHIIU8D.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BNT3Uz3s.js → Trigger-BHvWXVOb.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-BytMGPMo.js → Trust-y2WaOSto.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-BkYVMRQs.js → UkeySign-DBFeJBzt.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-psvb5Q7Y.js → VideoEditing-BEOWitC-.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-PFQTW0MH.js → Wallet-BITedTe6.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-DgO-OwRT.js → WebAuthn-DMwlJVin.js} +5 -5
  89. package/src/assets/web-panel/assets/{WorkflowEditor-DL2l9VNl.js → WorkflowEditor-CKfGFEu1.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-C84akEzN.js → chat-DR6gnBGF.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-DzTWuQKP.js → colors-C_Hw5NCn.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-CR9LJQFv.js → compact-item-B8IPwsEu.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-CTTov5Kn.js → createContext-D-u4JVnJ.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Cq6lSd_q.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DALj523o.js → hasIn-CZubdS4D.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-Be6Os5S8.js → index-11ASqx-A.js} +1 -1
  97. package/src/assets/web-panel/assets/index-BQ3213fz.js +1 -0
  98. package/src/assets/web-panel/assets/{index-CXaCRXN4.js → index-BXS6wYJZ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-7ux_XohH.js → index-Bi21Etg4.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CXTEpfGe.js → index-Bizm87cX.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DH6OG-ci.js → index-Bv2132PO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CZhhFQi9.js → index-BzoFanEQ.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-B6QOJbSn.js → index-C25rOz7O.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CunGDV0g.js → index-C2VmtNaQ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CmELknPe.js → index-Cbm8F0M2.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DZER6Lj3.js → index-CfniPP5p.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B2qpjU-D.js → index-Cg2bCeol.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-QW0EXx14.js → index-CiXkhOkY.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C5eeMl_2.js → index-Cl5RpreD.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-s4CFyn-n.js → index-CqmLw8cs.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-eJcFleIV.js → index-Ctje8Vl3.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-3-XJoYgX.js → index-D1w96gg9.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-_8vetPbU.js → index-D47jt8ET.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cef0n4k_.js → index-DJ6pG79m.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-AXnERsyV.js → index-DTosx2Wu.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DtW6Zr_i.js → index-DY6MHo9D.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CNmTDXmf.js → index-DcFjRT-K.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Dah8M4l2.js → index-DfnzrUxU.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D0MqUnKO.js → index-DjRrqooR.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CqUgXYIQ.js → index-DmRcgiYT.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D9ScRFxz.js → index-DpjHpRoq.js} +3 -3
  122. package/src/assets/web-panel/assets/{index-DjE_ew7U.js → index-Dt-w3xQ7.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-W9axN_Ce.js → index-DuOU5apg.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BDyt57zL.js → index-LvjAKDSl.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-hzzTITR9.js → index-_nL79NT8.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BNaQt49r.js → index-auyNYjLo.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DBIPnoTj.js → index-cVqbas5C.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CnxjXIsK.js → index-ffe7bvqP.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BHRXKiU1.js → index-idZKyADQ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-D_Bg0LdK.js → index-k1Xa5iw1.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BKNIzw0U.js → index-mYZJxGH6.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DxdzSljS.js → index-rYBHSDcJ.js} +1 -1
  133. package/src/assets/web-panel/assets/index-vJagfET2.js +1 -0
  134. package/src/assets/web-panel/assets/{index-CjkPDSX4.js → index-xoNssgz_.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-JuW8mFj7.js → initDefaultProps-lcU_dT2r.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-Cwb0HBve.js → motion-Dc7UNXZ1.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-DHokzMgu.js → move-Cll8ZXaL.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-DX31y6QS.js → mtc-parser-CAb_yLIM.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-BCIUYDA6.js → omit-DurYMyTe.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DMCj4EoZ.js → pickAttrs-BITQyOlw.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-Dy5K-u1s.js → placementArrow-Dg1rH91i.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-DkGeZu54.js → responsiveObserve-D_EWIeac.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-Dc5dyJdg.js → slide-lK4d3zsh.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-DIUBjOaC.js → statusUtils-Bflq-Git.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-DmyEOxm5.js → styleChecker-Ccp0I-wt.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-FRhakk1e.js → useFlexGapSupport-CYZbByYA.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-clXyV1WQ.js → useFs-B_x0nxFD.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-UF6DG41z.js → usePersonalDataHub-DW55rL85.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-CoCt95G7.js → vnode-Ca6MH3W8.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-yVWt-ENm.js → zoom-NdHelOog.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/orchestrate.js +61 -9
  154. package/src/data/changelog.json +34 -1
  155. package/src/gateways/ws/message-dispatcher.js +4 -0
  156. package/src/gateways/ws/session-protocol.js +89 -11
  157. package/src/gateways/ws/ws-agent-handler.js +65 -9
  158. package/src/gateways/ws/ws-approval-gate.js +181 -16
  159. package/src/gateways/ws/ws-server.js +16 -6
  160. package/src/generated/cc-agent-protocol.schema.json +148 -2
  161. package/src/lib/agent-team/task-lease.js +172 -3
  162. package/src/lib/agent-team/team-runner.js +60 -1
  163. package/src/lib/app-server/graph-runtime.js +326 -2
  164. package/src/lib/app-server/server.js +60 -0
  165. package/src/lib/approval-grant-ledger.js +69 -0
  166. package/src/lib/codex-app-server-adapter.js +11 -20
  167. package/src/lib/graph-kernel/compiler.js +66 -13
  168. package/src/lib/graph-kernel/cutover-authority-resolver.js +26 -0
  169. package/src/lib/graph-kernel/cutover-ledger.js +53 -11
  170. package/src/lib/graph-kernel/retirement-evidence.js +567 -0
  171. package/src/lib/graph-kernel/runtime.js +76 -14
  172. package/src/lib/interaction-adapter.js +61 -9
  173. package/src/lib/policy-decision-event.js +149 -0
  174. package/src/lib/process-execution-broker/platform-sandbox.js +15 -1
  175. package/src/lib/record-replay/index.js +1 -0
  176. package/src/lib/record-replay/playwright-ui-driver.js +327 -0
  177. package/src/lib/webhook-security.js +484 -36
  178. package/src/runtime/coding-agent-events.cjs +13 -11
  179. package/src/runtime/headless-runner.js +17 -1
  180. package/src/runtime/headless-stream.js +66 -3
  181. package/src/runtime/session-slash-commands.js +57 -6
  182. package/src/assets/web-panel/assets/MarkdownRenderer-CZsTBEQE.js +0 -1
  183. package/src/assets/web-panel/assets/Projects-BHPxiucB.js +0 -1
  184. package/src/assets/web-panel/assets/RemoteSession-CgqKMXG4.js +0 -5
  185. package/src/assets/web-panel/assets/devWarning-BOhvmlpc.js +0 -1
  186. package/src/assets/web-panel/assets/index-DDyMJexx.js +0 -1
  187. package/src/assets/web-panel/assets/index-btiojFYn.js +0 -1
@@ -1,4 +1,11 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
1
+ import {
2
+ createDecipheriv,
3
+ createHash,
4
+ createHmac,
5
+ timingSafeEqual,
6
+ } from "node:crypto";
7
+
8
+ const VENDOR_CHANNELS = new Set(["dingtalk", "feishu", "wecom"]);
2
9
 
3
10
  function webhookError(code, message, statusCode, details = {}) {
4
11
  const error = new Error(message);
@@ -14,6 +21,87 @@ function header(headers, name) {
14
21
  return Array.isArray(value) ? value[0] : value == null ? "" : String(value);
15
22
  }
16
23
 
24
+ function queryValue(query, name) {
25
+ const value = query?.[name];
26
+ return Array.isArray(value) ? value[0] : value == null ? "" : String(value);
27
+ }
28
+
29
+ function safeTextEqual(expected, supplied) {
30
+ const expectedBytes = Buffer.from(String(expected), "utf8");
31
+ const suppliedBytes = Buffer.from(String(supplied), "utf8");
32
+ return (
33
+ expectedBytes.length === suppliedBytes.length &&
34
+ timingSafeEqual(expectedBytes, suppliedBytes)
35
+ );
36
+ }
37
+
38
+ function requireCredential(value, channel, name) {
39
+ if (typeof value !== "string" || value.length === 0) {
40
+ throw webhookError(
41
+ "CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
42
+ `${channel} vendor authentication requires ${name}`,
43
+ 503,
44
+ { channel, credential: name },
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function parseTimestamp(timestampText) {
51
+ const value = Number(timestampText);
52
+ if (!Number.isFinite(value) || value <= 0) return Number.NaN;
53
+ return value < 100_000_000_000 ? value * 1000 : value;
54
+ }
55
+
56
+ function assertFreshTimestamp(timestampText, now, maxClockSkewMs) {
57
+ const timestamp = parseTimestamp(timestampText);
58
+ if (
59
+ !Number.isFinite(timestamp) ||
60
+ Math.abs(now - timestamp) > maxClockSkewMs
61
+ ) {
62
+ throw webhookError(
63
+ "CC_WEBHOOK_AUTH_INVALID",
64
+ "webhook timestamp is invalid or stale",
65
+ 401,
66
+ );
67
+ }
68
+ }
69
+
70
+ function fallbackDeliveryId(channel, signature, body) {
71
+ return `${channel}-${createHash("sha256")
72
+ .update(signature)
73
+ .update("\0")
74
+ .update(body)
75
+ .digest("hex")
76
+ .slice(0, 40)}`;
77
+ }
78
+
79
+ function normalizeDeliveryId(value, channel, signature, body) {
80
+ const deliveryId = value == null ? "" : String(value);
81
+ if (deliveryId && deliveryId.length <= 256) return deliveryId;
82
+ return fallbackDeliveryId(channel, signature, body);
83
+ }
84
+
85
+ function jsonObject(body) {
86
+ try {
87
+ const parsed = JSON.parse(body);
88
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
89
+ ? parsed
90
+ : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+
96
+ function xmlTag(xml, tag) {
97
+ const pattern = new RegExp(
98
+ `<${tag}>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))<\\/${tag}>`,
99
+ "i",
100
+ );
101
+ const match = String(xml).match(pattern);
102
+ return match ? (match[1] ?? match[2] ?? "").trim() : "";
103
+ }
104
+
17
105
  export function signWebhookBody(secret, timestamp, body) {
18
106
  return `sha256=${createHmac("sha256", secret)
19
107
  .update(String(timestamp))
@@ -22,22 +110,179 @@ export function signWebhookBody(secret, timestamp, body) {
22
110
  .digest("hex")}`;
23
111
  }
24
112
 
113
+ export function signDingTalkWebhook(secret, timestamp) {
114
+ return createHmac("sha256", secret)
115
+ .update(`${timestamp}\n${secret}`)
116
+ .digest("base64");
117
+ }
118
+
119
+ export function signFeishuWebhook(encryptKey, timestamp, nonce, body) {
120
+ return createHash("sha256")
121
+ .update(`${timestamp}${nonce}${encryptKey}${body}`)
122
+ .digest("hex");
123
+ }
124
+
125
+ export function signWeComWebhook(token, timestamp, nonce, encrypted) {
126
+ return createHash("sha1")
127
+ .update([token, timestamp, nonce, encrypted].map(String).sort().join(""))
128
+ .digest("hex");
129
+ }
130
+
131
+ function decryptFeishuWebhook(encryptKey, encrypted) {
132
+ let encryptedBytes;
133
+ try {
134
+ encryptedBytes = Buffer.from(encrypted, "base64");
135
+ } catch {
136
+ encryptedBytes = Buffer.alloc(0);
137
+ }
138
+ if (encryptedBytes.length < 32 || encryptedBytes.length % 16 !== 0) {
139
+ throw webhookError(
140
+ "CC_WEBHOOK_AUTH_INVALID",
141
+ "Feishu encrypted webhook body is invalid",
142
+ 401,
143
+ );
144
+ }
145
+ try {
146
+ const key = createHash("sha256").update(encryptKey).digest();
147
+ const decipher = createDecipheriv(
148
+ "aes-256-cbc",
149
+ key,
150
+ encryptedBytes.subarray(0, 16),
151
+ );
152
+ return Buffer.concat([
153
+ decipher.update(encryptedBytes.subarray(16)),
154
+ decipher.final(),
155
+ ]).toString("utf8");
156
+ } catch {
157
+ throw webhookError(
158
+ "CC_WEBHOOK_AUTH_INVALID",
159
+ "Feishu encrypted webhook body could not be decrypted",
160
+ 401,
161
+ );
162
+ }
163
+ }
164
+
165
+ function decodeWeComAesKey(encodingAesKey) {
166
+ if (!/^[A-Za-z0-9+/]{43}={0,1}$/u.test(encodingAesKey)) {
167
+ throw webhookError(
168
+ "CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
169
+ "WeCom EncodingAESKey must contain 43 base64 characters",
170
+ 503,
171
+ { channel: "wecom", credential: "encodingAesKey" },
172
+ );
173
+ }
174
+ const key = Buffer.from(`${encodingAesKey.replace(/=+$/u, "")}=`, "base64");
175
+ if (key.length !== 32) {
176
+ throw webhookError(
177
+ "CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
178
+ "WeCom EncodingAESKey must decode to 32 bytes",
179
+ 503,
180
+ { channel: "wecom", credential: "encodingAesKey" },
181
+ );
182
+ }
183
+ return key;
184
+ }
185
+
186
+ function decryptWeComWebhook(encodingAesKey, encrypted, receiveId) {
187
+ const key = decodeWeComAesKey(encodingAesKey);
188
+ let ciphertext;
189
+ try {
190
+ ciphertext = Buffer.from(encrypted, "base64");
191
+ } catch {
192
+ ciphertext = Buffer.alloc(0);
193
+ }
194
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
195
+ throw webhookError(
196
+ "CC_WEBHOOK_AUTH_INVALID",
197
+ "WeCom encrypted webhook body is invalid",
198
+ 401,
199
+ );
200
+ }
201
+
202
+ let padded;
203
+ try {
204
+ const decipher = createDecipheriv("aes-256-cbc", key, key.subarray(0, 16));
205
+ decipher.setAutoPadding(false);
206
+ padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
207
+ } catch {
208
+ throw webhookError(
209
+ "CC_WEBHOOK_AUTH_INVALID",
210
+ "WeCom encrypted webhook body could not be decrypted",
211
+ 401,
212
+ );
213
+ }
214
+
215
+ const padding = padded.at(-1) || 0;
216
+ if (
217
+ padding < 1 ||
218
+ padding > 32 ||
219
+ padded.length <= padding ||
220
+ !padded.subarray(padded.length - padding).every((byte) => byte === padding)
221
+ ) {
222
+ throw webhookError(
223
+ "CC_WEBHOOK_AUTH_INVALID",
224
+ "WeCom encrypted webhook padding is invalid",
225
+ 401,
226
+ );
227
+ }
228
+ const plaintext = padded.subarray(0, padded.length - padding);
229
+ if (plaintext.length < 20) {
230
+ throw webhookError(
231
+ "CC_WEBHOOK_AUTH_INVALID",
232
+ "WeCom encrypted webhook envelope is truncated",
233
+ 401,
234
+ );
235
+ }
236
+ const messageLength = plaintext.readUInt32BE(16);
237
+ const messageEnd = 20 + messageLength;
238
+ if (messageLength <= 0 || messageEnd > plaintext.length) {
239
+ throw webhookError(
240
+ "CC_WEBHOOK_AUTH_INVALID",
241
+ "WeCom encrypted webhook message length is invalid",
242
+ 401,
243
+ );
244
+ }
245
+ const actualReceiveId = plaintext.subarray(messageEnd).toString("utf8");
246
+ if (receiveId && !safeTextEqual(receiveId, actualReceiveId)) {
247
+ throw webhookError(
248
+ "CC_WEBHOOK_AUTH_INVALID",
249
+ "WeCom webhook receiver identity does not match",
250
+ 401,
251
+ );
252
+ }
253
+ return plaintext.subarray(20, messageEnd).toString("utf8");
254
+ }
255
+
25
256
  export class WebhookSecurityGate {
26
257
  constructor({
27
258
  secret,
259
+ authMode = "cc",
260
+ vendorCredentials = {},
28
261
  now = Date.now,
29
262
  maxClockSkewMs = 5 * 60_000,
30
263
  maxRequestsPerMinute = 30,
31
264
  maxReplayEntries = 10_000,
32
265
  } = {}) {
33
- if (typeof secret !== "string" || secret.length < 16) {
266
+ if (!new Set(["cc", "vendor"]).has(authMode)) {
267
+ throw webhookError(
268
+ "CC_WEBHOOK_AUTH_MODE_INVALID",
269
+ "webhook authentication mode must be cc or vendor",
270
+ 503,
271
+ );
272
+ }
273
+ if (
274
+ authMode === "cc" &&
275
+ (typeof secret !== "string" || secret.length < 16)
276
+ ) {
34
277
  throw webhookError(
35
278
  "CC_WEBHOOK_SECRET_REQUIRED",
36
279
  "webhook mode requires an authentication secret of at least 16 characters",
37
280
  503,
38
281
  );
39
282
  }
40
- this.secret = secret;
283
+ this.secret = secret || "";
284
+ this.authMode = authMode;
285
+ this.vendorCredentials = Object.freeze({ ...vendorCredentials });
41
286
  this.now = now;
42
287
  this.maxClockSkewMs = Math.max(1_000, Number(maxClockSkewMs) || 300_000);
43
288
  this.maxRequestsPerMinute = Math.max(1, Number(maxRequestsPerMinute) || 30);
@@ -46,52 +291,255 @@ export class WebhookSecurityGate {
46
291
  this.rates = new Map();
47
292
  }
48
293
 
49
- verify({ channel, headers, body, remoteAddress = "unknown" }) {
294
+ verify({ channel, headers, body, query = {}, remoteAddress = "unknown" }) {
50
295
  const now = this.now();
51
- const window = Math.floor(now / 60_000);
52
- const rateKey = `${channel}\0${remoteAddress}\0${window}`;
53
- const requests = (this.rates.get(rateKey) || 0) + 1;
54
- this.rates.set(rateKey, requests);
55
- for (const key of this.rates.keys()) {
56
- if (!key.endsWith(`\0${window}`)) this.rates.delete(key);
296
+ this._checkRate(channel, remoteAddress, now);
297
+ const authenticated =
298
+ this.authMode === "vendor"
299
+ ? this._verifyVendor({ channel, headers, body, query, now })
300
+ : this._verifyCc({ channel, headers, body, now });
301
+ this._claimDelivery(channel, authenticated.deliveryId, now);
302
+ return Object.freeze({
303
+ deliveryId: authenticated.deliveryId,
304
+ body: authenticated.body,
305
+ authMode: this.authMode,
306
+ dataPolicy: this._dataPolicy(channel, authenticated.deliveryId),
307
+ });
308
+ }
309
+
310
+ verifyWeComUrl({ query = {}, remoteAddress = "unknown" }) {
311
+ if (this.authMode !== "vendor") {
312
+ throw webhookError(
313
+ "CC_WEBHOOK_AUTH_INVALID",
314
+ "WeCom URL verification requires vendor authentication mode",
315
+ 401,
316
+ );
57
317
  }
58
- if (requests > this.maxRequestsPerMinute) {
318
+ const now = this.now();
319
+ this._checkRate("wecom", remoteAddress, now);
320
+ const token = requireCredential(
321
+ this.vendorCredentials.wecomToken,
322
+ "wecom",
323
+ "token",
324
+ );
325
+ const encodingAesKey = requireCredential(
326
+ this.vendorCredentials.wecomEncodingAesKey,
327
+ "wecom",
328
+ "encodingAesKey",
329
+ );
330
+ const receiveId = requireCredential(
331
+ this.vendorCredentials.wecomReceiveId,
332
+ "wecom",
333
+ "receiveId",
334
+ );
335
+ const timestamp = queryValue(query, "timestamp");
336
+ const nonce = queryValue(query, "nonce");
337
+ const encryptedEcho = queryValue(query, "echostr");
338
+ const supplied = queryValue(query, "msg_signature");
339
+ assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
340
+ if (!nonce || nonce.length > 256 || !encryptedEcho || !supplied) {
59
341
  throw webhookError(
60
- "CC_WEBHOOK_RATE_LIMITED",
61
- "webhook rate limit exceeded",
62
- 429,
63
- { retryAfterSeconds: 60 - Math.floor((now % 60_000) / 1000) },
342
+ "CC_WEBHOOK_AUTH_INVALID",
343
+ "WeCom URL verification parameters are invalid",
344
+ 401,
345
+ );
346
+ }
347
+ const expected = signWeComWebhook(token, timestamp, nonce, encryptedEcho);
348
+ if (!safeTextEqual(expected, supplied)) {
349
+ throw webhookError(
350
+ "CC_WEBHOOK_AUTH_INVALID",
351
+ "WeCom URL verification signature is invalid",
352
+ 401,
64
353
  );
65
354
  }
355
+ return Object.freeze({
356
+ echo: decryptWeComWebhook(encodingAesKey, encryptedEcho, receiveId),
357
+ });
358
+ }
359
+
360
+ _verifyCc({ headers, body, now }) {
66
361
  const timestampText = header(headers, "x-cc-webhook-timestamp");
67
362
  const deliveryId = header(headers, "x-cc-webhook-delivery");
68
363
  const supplied = header(headers, "x-cc-webhook-signature");
69
- const timestamp = Number(timestampText);
70
- if (
71
- !Number.isFinite(timestamp) ||
72
- Math.abs(now - timestamp) > this.maxClockSkewMs ||
73
- !deliveryId ||
74
- deliveryId.length > 256
75
- ) {
364
+ assertFreshTimestamp(timestampText, now, this.maxClockSkewMs);
365
+ if (!deliveryId || deliveryId.length > 256) {
76
366
  throw webhookError(
77
367
  "CC_WEBHOOK_AUTH_INVALID",
78
- "webhook timestamp or delivery identity is invalid",
368
+ "webhook delivery identity is invalid",
79
369
  401,
80
370
  );
81
371
  }
82
372
  const expected = signWebhookBody(this.secret, timestampText, body);
83
- const expectedBytes = Buffer.from(expected, "utf8");
84
- const suppliedBytes = Buffer.from(supplied, "utf8");
85
- if (
86
- expectedBytes.length !== suppliedBytes.length ||
87
- !timingSafeEqual(expectedBytes, suppliedBytes)
88
- ) {
373
+ if (!safeTextEqual(expected, supplied)) {
89
374
  throw webhookError(
90
375
  "CC_WEBHOOK_AUTH_INVALID",
91
376
  "webhook signature is invalid",
92
377
  401,
93
378
  );
94
379
  }
380
+ return { deliveryId, body };
381
+ }
382
+
383
+ _verifyVendor({ channel, headers, body, query, now }) {
384
+ if (!VENDOR_CHANNELS.has(channel)) {
385
+ throw webhookError(
386
+ "CC_WEBHOOK_AUTH_INVALID",
387
+ "unsupported vendor webhook channel",
388
+ 401,
389
+ );
390
+ }
391
+ if (channel === "dingtalk") {
392
+ return this._verifyDingTalk({ headers, body, now });
393
+ }
394
+ if (channel === "feishu") {
395
+ return this._verifyFeishu({ headers, body, now });
396
+ }
397
+ return this._verifyWeCom({ body, query, now });
398
+ }
399
+
400
+ _verifyDingTalk({ headers, body, now }) {
401
+ const secret = requireCredential(
402
+ this.vendorCredentials.dingtalkSecret,
403
+ "dingtalk",
404
+ "secret",
405
+ );
406
+ const timestamp = header(headers, "timestamp");
407
+ const supplied = header(headers, "sign");
408
+ assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
409
+ const expected = signDingTalkWebhook(secret, timestamp);
410
+ let decodedSupplied = supplied;
411
+ try {
412
+ decodedSupplied = decodeURIComponent(supplied);
413
+ } catch {
414
+ // A raw Base64 header is the normal form.
415
+ }
416
+ if (!safeTextEqual(expected, decodedSupplied)) {
417
+ throw webhookError(
418
+ "CC_WEBHOOK_AUTH_INVALID",
419
+ "DingTalk webhook signature is invalid",
420
+ 401,
421
+ );
422
+ }
423
+ const parsed = jsonObject(body);
424
+ return {
425
+ deliveryId: normalizeDeliveryId(parsed.msgId, "dingtalk", supplied, body),
426
+ body,
427
+ };
428
+ }
429
+
430
+ _verifyFeishu({ headers, body, now }) {
431
+ const encryptKey = requireCredential(
432
+ this.vendorCredentials.feishuEncryptKey,
433
+ "feishu",
434
+ "encryptKey",
435
+ );
436
+ const timestamp = header(headers, "x-lark-request-timestamp");
437
+ const nonce = header(headers, "x-lark-request-nonce");
438
+ const supplied = header(headers, "x-lark-signature");
439
+ assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
440
+ if (!nonce || nonce.length > 256) {
441
+ throw webhookError(
442
+ "CC_WEBHOOK_AUTH_INVALID",
443
+ "Feishu webhook nonce is invalid",
444
+ 401,
445
+ );
446
+ }
447
+ const expected = signFeishuWebhook(encryptKey, timestamp, nonce, body);
448
+ if (!safeTextEqual(expected, supplied)) {
449
+ throw webhookError(
450
+ "CC_WEBHOOK_AUTH_INVALID",
451
+ "Feishu webhook signature is invalid",
452
+ 401,
453
+ );
454
+ }
455
+
456
+ const envelope = jsonObject(body);
457
+ const authenticatedBody = envelope.encrypt
458
+ ? decryptFeishuWebhook(encryptKey, envelope.encrypt)
459
+ : body;
460
+ const parsed = jsonObject(authenticatedBody);
461
+ return {
462
+ deliveryId: normalizeDeliveryId(
463
+ parsed.header?.event_id ?? parsed.uuid,
464
+ "feishu",
465
+ supplied,
466
+ body,
467
+ ),
468
+ body: authenticatedBody,
469
+ };
470
+ }
471
+
472
+ _verifyWeCom({ body, query, now }) {
473
+ const token = requireCredential(
474
+ this.vendorCredentials.wecomToken,
475
+ "wecom",
476
+ "token",
477
+ );
478
+ const encodingAesKey = requireCredential(
479
+ this.vendorCredentials.wecomEncodingAesKey,
480
+ "wecom",
481
+ "encodingAesKey",
482
+ );
483
+ const receiveId = requireCredential(
484
+ this.vendorCredentials.wecomReceiveId,
485
+ "wecom",
486
+ "receiveId",
487
+ );
488
+ const timestamp = queryValue(query, "timestamp");
489
+ const nonce = queryValue(query, "nonce");
490
+ const supplied = queryValue(query, "msg_signature");
491
+ const encrypted = xmlTag(body, "Encrypt");
492
+ assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
493
+ if (!nonce || nonce.length > 256 || !encrypted || !supplied) {
494
+ throw webhookError(
495
+ "CC_WEBHOOK_AUTH_INVALID",
496
+ "WeCom webhook authentication parameters are invalid",
497
+ 401,
498
+ );
499
+ }
500
+ const expected = signWeComWebhook(token, timestamp, nonce, encrypted);
501
+ if (!safeTextEqual(expected, supplied)) {
502
+ throw webhookError(
503
+ "CC_WEBHOOK_AUTH_INVALID",
504
+ "WeCom webhook signature is invalid",
505
+ 401,
506
+ );
507
+ }
508
+ const authenticatedBody = decryptWeComWebhook(
509
+ encodingAesKey,
510
+ encrypted,
511
+ receiveId,
512
+ );
513
+ return {
514
+ deliveryId: normalizeDeliveryId(
515
+ xmlTag(authenticatedBody, "MsgId"),
516
+ "wecom",
517
+ supplied,
518
+ body,
519
+ ),
520
+ body: authenticatedBody,
521
+ };
522
+ }
523
+
524
+ _checkRate(channel, remoteAddress, now) {
525
+ const window = Math.floor(now / 60_000);
526
+ const rateKey = `${channel}\0${remoteAddress}\0${window}`;
527
+ const requests = (this.rates.get(rateKey) || 0) + 1;
528
+ this.rates.set(rateKey, requests);
529
+ for (const key of this.rates.keys()) {
530
+ if (!key.endsWith(`\0${window}`)) this.rates.delete(key);
531
+ }
532
+ if (requests > this.maxRequestsPerMinute) {
533
+ throw webhookError(
534
+ "CC_WEBHOOK_RATE_LIMITED",
535
+ "webhook rate limit exceeded",
536
+ 429,
537
+ { retryAfterSeconds: 60 - Math.floor((now % 60_000) / 1000) },
538
+ );
539
+ }
540
+ }
541
+
542
+ _claimDelivery(channel, deliveryId, now) {
95
543
  for (const [key, expiresAt] of this.replays) {
96
544
  if (expiresAt <= now) this.replays.delete(key);
97
545
  }
@@ -111,14 +559,14 @@ export class WebhookSecurityGate {
111
559
  );
112
560
  }
113
561
  this.replays.set(replayKey, now + this.maxClockSkewMs);
562
+ }
563
+
564
+ _dataPolicy(channel, deliveryId) {
114
565
  return Object.freeze({
115
- deliveryId,
116
- dataPolicy: Object.freeze({
117
- origin: `webhook:${channel}:${deliveryId}`,
118
- trust: "authenticated_user",
119
- sensitivity: "internal",
120
- allowedSinks: Object.freeze(["agent:*", "artifact:local"]),
121
- }),
566
+ origin: `webhook:${channel}:${deliveryId}`,
567
+ trust: "authenticated_user",
568
+ sensitivity: "internal",
569
+ allowedSinks: Object.freeze(["agent:*", "artifact:local"]),
122
570
  });
123
571
  }
124
572
  }
@@ -55,6 +55,10 @@ const CODING_AGENT_EVENT_TYPES = Object.freeze({
55
55
  APPROVAL_DENIED: "approval.denied",
56
56
  APPROVAL_EXPIRED: "approval.expired",
57
57
 
58
+ // Policy decisions from both Hook and tool admission surfaces use one
59
+ // bounded, digest-bearing payload so hosts never need to parse tool errors.
60
+ POLICY_DECISION: "policy.decision",
61
+
58
62
  // Context compaction
59
63
  CONTEXT_COMPACTION_STARTED: "context.compaction.started",
60
64
  CONTEXT_COMPACTION_COMPLETED: "context.compaction.completed",
@@ -173,6 +177,7 @@ const LEGACY_TO_UNIFIED_TYPE = Object.freeze({
173
177
  "approval-granted": CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
174
178
  "approval-denied": CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
175
179
  "approval-expired": CODING_AGENT_EVENT_TYPES.APPROVAL_EXPIRED,
180
+ "policy-decision": CODING_AGENT_EVENT_TYPES.POLICY_DECISION,
176
181
  "high-risk-confirmation-required":
177
182
  CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
178
183
  "high-risk-confirmed": CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
@@ -261,6 +266,9 @@ function projectAgentStreamMessage(message) {
261
266
  ? CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED
262
267
  : CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED;
263
268
  break;
269
+ case "policy_decision":
270
+ type = CODING_AGENT_EVENT_TYPES.POLICY_DECISION;
271
+ break;
264
272
  case "plan_update":
265
273
  type = CODING_AGENT_EVENT_TYPES.PLAN_UPDATED;
266
274
  break;
@@ -344,19 +352,14 @@ const defaultSequenceTracker = new CodingAgentSequenceTracker();
344
352
  */
345
353
  function createCodingAgentEvent(type, payload = {}, context = {}) {
346
354
  if (payload && typeof payload !== "object") {
347
- throw new TypeError(
348
- "createCodingAgentEvent: payload must be an object",
349
- );
355
+ throw new TypeError("createCodingAgentEvent: payload must be an object");
350
356
  }
351
357
 
352
358
  const normalizedType = mapLegacyType(type);
353
359
  const tracker = context.tracker || defaultSequenceTracker;
354
- const sessionId =
355
- context.sessionId || (payload && payload.sessionId) || null;
360
+ const sessionId = context.sessionId || (payload && payload.sessionId) || null;
356
361
  const requestId =
357
- context.requestId ||
358
- (payload && (payload.requestId || payload.id)) ||
359
- null;
362
+ context.requestId || (payload && (payload.requestId || payload.id)) || null;
360
363
 
361
364
  let sequence;
362
365
  if (Number.isInteger(context.sequence)) {
@@ -404,9 +407,7 @@ function createCodingAgentEvent(type, payload = {}, context = {}) {
404
407
  */
405
408
  function wrapLegacyMessage(message, context = {}) {
406
409
  if (!message || typeof message !== "object") {
407
- throw new TypeError(
408
- "wrapLegacyMessage: message must be a non-null object",
409
- );
410
+ throw new TypeError("wrapLegacyMessage: message must be a non-null object");
410
411
  }
411
412
 
412
413
  const { type, ...payload } = message;
@@ -475,6 +476,7 @@ const CodingAgentEventType = Object.freeze({
475
476
  APPROVAL_REQUESTED: CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED,
476
477
  APPROVAL_GRANTED: CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
477
478
  APPROVAL_DENIED: CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
479
+ POLICY_DECISION: CODING_AGENT_EVENT_TYPES.POLICY_DECISION,
478
480
  HIGH_RISK_CONFIRMATION_REQUIRED:
479
481
  CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
480
482
  HIGH_RISK_CONFIRMED: CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
@@ -89,6 +89,10 @@ import {
89
89
  executeHooksV2Event,
90
90
  resolvePromptExpansion,
91
91
  } from "../lib/hooks-v2-producers.js";
92
+ import {
93
+ projectHookPolicyDecision,
94
+ projectToolPolicyDecision,
95
+ } from "../lib/policy-decision-event.js";
92
96
  import { runWithHostHooksV2Workspace } from "../lib/hooks-v2-workspace-context.js";
93
97
  import { CLISkillLoader } from "../lib/skill-loader.js";
94
98
  import { expandFileRefsAsync } from "./file-ref-expander.js";
@@ -731,7 +735,11 @@ async function runAgentHeadlessInWorkspace(
731
735
  if (includeHookEvents) {
732
736
  const subscribeHookEvents =
733
737
  deps.addHooksV2EventObserver || addHooksV2EventObserver;
734
- removeHookObserver = subscribeHookEvents(sessionId, queuePendingHookEvent);
738
+ removeHookObserver = subscribeHookEvents(sessionId, (event) => {
739
+ queuePendingHookEvent(event);
740
+ const policyEvent = projectHookPolicyDecision(event);
741
+ if (policyEvent) queuePendingHookEvent(policyEvent);
742
+ });
735
743
  }
736
744
 
737
745
  const emitHeadlessError = (resultMsg) => {
@@ -3098,6 +3106,14 @@ async function runAgentHeadlessInWorkspace(
3098
3106
  ? { permission_decision: event.permission_decision }
3099
3107
  : {}),
3100
3108
  });
3109
+ if (includeHookEvents) {
3110
+ const policyEvent = projectToolPolicyDecision(event, {
3111
+ sessionId,
3112
+ turnId: event.turn_id,
3113
+ toolUseId: event.tool_use_id,
3114
+ });
3115
+ if (policyEvent) emitStream(policyEvent);
3116
+ }
3101
3117
  const settledCall =
3102
3118
  (event.tool_use_id
3103
3119
  ? activeToolCalls.get(event.tool_use_id)