chainlesschain 0.163.8 → 0.165.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/package.json +7 -4
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
  10. package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
  51. package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
  52. package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
  67. package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
  68. package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
  91. package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
  107. package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
  136. package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
  137. package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +3 -3
  155. package/src/commands/cowork.js +418 -41
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/data/changelog.json +28 -2
  161. package/src/gateways/ws/action-protocol.js +316 -17
  162. package/src/gateways/ws/message-dispatcher.js +30 -0
  163. package/src/gateways/ws/remote-session-protocol.js +565 -35
  164. package/src/gateways/ws/ws-agent-handler.js +12 -0
  165. package/src/gateways/ws/ws-approval-gate.js +5 -0
  166. package/src/gateways/ws/ws-server.js +510 -50
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +2 -0
  170. package/src/harness/remote-session-crypto.js +929 -21
  171. package/src/harness/remote-session-registry.js +945 -24
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/auto-mode-config.js +98 -3
  175. package/src/lib/background-agent-keeper-client.js +248 -0
  176. package/src/lib/background-agent-keeper-protocol.js +291 -0
  177. package/src/lib/background-agent-supervisor.js +1357 -85
  178. package/src/lib/background-interaction-resolver.js +12 -0
  179. package/src/lib/background-session-transport.js +23 -6
  180. package/src/lib/background-turn-bootstrap-protocol.js +190 -0
  181. package/src/lib/cowork-workflow.js +1188 -76
  182. package/src/lib/delivery-production-adapter.js +38 -2
  183. package/src/lib/dynamic-workflow-facade.js +1736 -0
  184. package/src/lib/execution-location-contract.js +551 -0
  185. package/src/lib/execution-location-runtime.js +130 -0
  186. package/src/lib/ide-context.js +94 -6
  187. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  188. package/src/lib/packer/pkg-config-generator.js +2 -0
  189. package/src/lib/permission-authority.js +77 -0
  190. package/src/lib/permission-side-effect-center.js +495 -0
  191. package/src/lib/plugin-runtime/install.js +514 -13
  192. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  193. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  194. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  195. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  196. package/src/lib/plugin-security.js +60 -3
  197. package/src/lib/process-execution-broker/index.js +1095 -57
  198. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  199. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  200. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  201. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  202. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  203. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  204. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  205. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  206. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  207. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  208. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  209. package/src/lib/public-ide-capability-manifest.js +3 -0
  210. package/src/lib/remote-approval-bridge.js +777 -34
  211. package/src/lib/remote-control.js +2 -0
  212. package/src/lib/remote-membership-authority.js +632 -0
  213. package/src/lib/remote-membership-coordinator.js +2973 -0
  214. package/src/lib/remote-membership-host-store.js +1799 -0
  215. package/src/lib/runtime-provenance-ledger.js +171 -46
  216. package/src/lib/scoped-permission-store.js +369 -0
  217. package/src/lib/session-anti-rollback-anchor.js +262 -5
  218. package/src/lib/shell-approval.js +169 -6
  219. package/src/lib/sub-agent-context.js +4 -0
  220. package/src/lib/with-file-lock.js +73 -38
  221. package/src/lib/workflow-definition-contract.js +149 -0
  222. package/src/repl/agent-repl.js +111 -36
  223. package/src/repl/chat-repl.js +2 -0
  224. package/src/repl/clipboard-image.js +735 -16
  225. package/src/repl/ide-status.js +1 -1
  226. package/src/repl/prompt-interactions.js +59 -6
  227. package/src/repl/remote-approval.js +29 -7
  228. package/src/runtime/agent-core.js +1182 -155
  229. package/src/runtime/headless-runner.js +52 -30
  230. package/src/runtime/headless-stream.js +51 -28
  231. package/src/workers/background-agent-keeper.js +689 -0
  232. package/src/workers/background-agent-worker.js +365 -13
  233. package/src/workers/background-turn-bootstrap.js +170 -0
  234. package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
  235. package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
  236. package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
  237. package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
  238. package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
  239. package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
  240. package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
@@ -11,9 +11,10 @@
11
11
  * 3. substitutes placeholders in `message` from completed step outputs
12
12
  * 4. halts on first failure unless `continueOnError` is set
13
13
  *
14
- * Persistence mirrors the cron scheduler: one JSON file per workflow under
15
- * `.chainlesschain/cowork/workflows/<id>.json`, plus a `run-history.jsonl`
16
- * capturing each execution.
14
+ * Persistence keeps a versioned current envelope under
15
+ * `.chainlesschain/cowork/workflows/<id>.json`, immutable content-addressed
16
+ * definitions under `workflow-versions/<id>/<digest>.json`, plus a
17
+ * `run-history.jsonl` capturing each execution.
17
18
  *
18
19
  * The runner itself is injected via `_deps.runTask` to avoid a circular import
19
20
  * with `cowork-task-runner.js`.
@@ -21,7 +22,9 @@
21
22
  * @module cowork-workflow
22
23
  */
23
24
 
24
- import {
25
+ import { createHash } from "node:crypto";
26
+ import { types as utilTypes } from "node:util";
27
+ import fs, {
25
28
  existsSync,
26
29
  mkdirSync,
27
30
  readFileSync,
@@ -30,15 +33,537 @@ import {
30
33
  unlinkSync,
31
34
  appendFileSync,
32
35
  renameSync,
36
+ lstatSync,
37
+ openSync,
38
+ fstatSync,
39
+ closeSync,
40
+ realpathSync,
41
+ constants as fsConstants,
33
42
  } from "node:fs";
34
- import { join } from "node:path";
43
+ import { isAbsolute, join, relative } from "node:path";
44
+ import {
45
+ sameFileStatIdentity,
46
+ samePathHandleFileIdentity,
47
+ withTrustedFileParentSync,
48
+ } from "./secure-file-identity.js";
35
49
  import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
50
+ import {
51
+ MAX_WORKFLOW_DEFINITION_BYTES,
52
+ createCoworkWorkflowRecord,
53
+ createWorkflowDefinitionAuthority,
54
+ normalizeWorkflowDefinitionDigest,
55
+ verifyCoworkWorkflowRecord,
56
+ } from "./workflow-definition-contract.js";
57
+ import { canonicalJson } from "./scheduler-kernel/contract.js";
36
58
 
37
59
  /** Maximum number of items a single forEach step can expand into. */
38
60
  export const MAX_FAN_OUT = 500;
39
61
 
40
62
  /** Absolute ceiling on a single loop step's iterations (infinite-loop guard). */
41
63
  export const MAX_LOOP_ITERATIONS = 100;
64
+ export const COWORK_WORKFLOW_RUN_RECORD_SCHEMA = "cc-cowork-workflow-run/v1";
65
+ export const COWORK_WORKFLOW_RUN_ADMISSION_INVALID_CODE =
66
+ "COWORK_WORKFLOW_RUN_ADMISSION_INVALID";
67
+ export const COWORK_WORKFLOW_RUN_RESULT_INVALID_CODE =
68
+ "COWORK_WORKFLOW_RUN_RESULT_INVALID";
69
+
70
+ const DYNAMIC_WORKFLOW_RUN_ADMISSION_SCHEMA =
71
+ "cc-dynamic-workflow-run-admission/v1";
72
+ const EXECUTION_LOCATION_AUTHORITY_SCHEMA =
73
+ "cc-session-execution-location-authority/v1";
74
+ const EXECUTION_LOCATION_BINDING_SCHEMA = "cc-execution-location-binding/v1";
75
+ const COWORK_WORKFLOW_RECORD_SCHEMA = "cc-cowork-workflow-record/v1";
76
+ const RUN_ADMISSION_FIELDS = new Set([
77
+ "schema",
78
+ "definition",
79
+ "executionLocation",
80
+ "executionPolicy",
81
+ "definitionDigest",
82
+ "executionLocationDigest",
83
+ "preflightDigest",
84
+ "maxParallel",
85
+ "credentialValuesTransferred",
86
+ "admissionDigest",
87
+ ]);
88
+ const RUN_ADMISSION_DEFINITION_FIELDS = new Set([
89
+ "schema",
90
+ "definitionDigest",
91
+ "authority",
92
+ ]);
93
+ const RUN_ADMISSION_DEFINITION_AUTHORITY_FIELDS = new Set([
94
+ "status",
95
+ "recordSchema",
96
+ "definitionSchema",
97
+ "definitionDigest",
98
+ ]);
99
+ const RUN_ADMISSION_EXECUTION_LOCATION_FIELDS = new Set([
100
+ "authoritySchema",
101
+ "authority",
102
+ "session",
103
+ "bindingSchema",
104
+ "location",
105
+ ]);
106
+ const RUN_ADMISSION_SESSION_FIELDS = new Set([
107
+ "sessionId",
108
+ "headHash",
109
+ "eventCount",
110
+ ]);
111
+ const RUN_ADMISSION_EXECUTION_POLICY_FIELDS = new Set([
112
+ "cwd",
113
+ "continueOnError",
114
+ "pipeline",
115
+ "provider",
116
+ "model",
117
+ ]);
118
+ const RUN_ADMISSION_LLM_OPTION_FIELDS = new Set(["provider", "model"]);
119
+ const ADMITTED_TASK_ENTRY_REQUIRED_FIELDS = new Set([
120
+ "taskId",
121
+ "status",
122
+ "result",
123
+ ]);
124
+ const ADMITTED_STEP_STATUSES = new Set([
125
+ "completed",
126
+ "failed",
127
+ "partial",
128
+ "skipped",
129
+ ]);
130
+ const MAX_ADMITTED_RUN_STEPS = 64;
131
+ const MAX_ADMITTED_SNAPSHOT_NODES = 25_000;
132
+ const MAX_ADMITTED_SNAPSHOT_BYTES = 1024 * 1024;
133
+ const STEP_TIMEOUT_CODE = "COWORK_WORKFLOW_STEP_TIMEOUT";
134
+
135
+ function runAdmissionError(message) {
136
+ const error = new Error(message);
137
+ error.code = COWORK_WORKFLOW_RUN_ADMISSION_INVALID_CODE;
138
+ return error;
139
+ }
140
+
141
+ function runResultError(message) {
142
+ const error = new Error(message);
143
+ error.code = COWORK_WORKFLOW_RUN_RESULT_INVALID_CODE;
144
+ return error;
145
+ }
146
+
147
+ /**
148
+ * Snapshot an admitted runner value without invoking accessors or accepting
149
+ * proxies/non-JSON values. The returned graph is detached and deeply frozen,
150
+ * so callbacks and a hostile runner cannot mutate executor control state.
151
+ */
152
+ function snapshotAdmittedCanonicalValue(
153
+ value,
154
+ name,
155
+ {
156
+ maxNodes = MAX_ADMITTED_SNAPSHOT_NODES,
157
+ maxBytes = MAX_ADMITTED_SNAPSHOT_BYTES,
158
+ } = {},
159
+ ) {
160
+ const seen = new WeakSet();
161
+ let nodes = 0;
162
+ let roughCharacters = 0;
163
+ const invalid = (reason) => {
164
+ throw runResultError(`${name} ${reason}`);
165
+ };
166
+ const account = (characters = 1) => {
167
+ nodes += 1;
168
+ roughCharacters += characters;
169
+ if (nodes > maxNodes || roughCharacters > maxBytes) {
170
+ invalid("exceeds snapshot limits");
171
+ }
172
+ };
173
+ const visit = (current, depth) => {
174
+ if (depth > 64) invalid("exceeds snapshot depth");
175
+ if (current === null || typeof current === "boolean") {
176
+ account();
177
+ return current;
178
+ }
179
+ if (typeof current === "string") {
180
+ account(current.length);
181
+ return current;
182
+ }
183
+ if (typeof current === "number") {
184
+ if (!Number.isFinite(current)) invalid("contains a non-finite number");
185
+ account();
186
+ return current;
187
+ }
188
+ if (!current || typeof current !== "object" || utilTypes.isProxy(current)) {
189
+ invalid("must contain only non-proxy canonical JSON values");
190
+ }
191
+ if (seen.has(current)) invalid("contains a cycle or repeated reference");
192
+ seen.add(current);
193
+ account();
194
+
195
+ if (Array.isArray(current)) {
196
+ if (Object.getPrototypeOf(current) !== Array.prototype) {
197
+ invalid("contains a non-plain array");
198
+ }
199
+ if (current.length > maxNodes) invalid("contains an oversized array");
200
+ const keys = Reflect.ownKeys(current);
201
+ if (keys.length !== current.length + 1) {
202
+ invalid("contains a sparse or extended array");
203
+ }
204
+ const output = [];
205
+ for (let index = 0; index < current.length; index += 1) {
206
+ const descriptor = Object.getOwnPropertyDescriptor(
207
+ current,
208
+ String(index),
209
+ );
210
+ if (
211
+ !descriptor ||
212
+ descriptor.enumerable !== true ||
213
+ !Object.hasOwn(descriptor, "value")
214
+ ) {
215
+ invalid("contains an accessor or sparse array entry");
216
+ }
217
+ output.push(visit(descriptor.value, depth + 1));
218
+ }
219
+ return Object.freeze(output);
220
+ }
221
+
222
+ const prototype = Object.getPrototypeOf(current);
223
+ if (prototype !== Object.prototype && prototype !== null) {
224
+ invalid("contains a non-plain object");
225
+ }
226
+ const output = {};
227
+ for (const key of Reflect.ownKeys(current)) {
228
+ if (typeof key !== "string") invalid("contains a symbol key");
229
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
230
+ if (
231
+ !descriptor ||
232
+ descriptor.enumerable !== true ||
233
+ !Object.hasOwn(descriptor, "value")
234
+ ) {
235
+ invalid("contains an accessor or non-enumerable property");
236
+ }
237
+ roughCharacters += key.length;
238
+ if (roughCharacters > maxBytes) invalid("exceeds snapshot limits");
239
+ output[key] = visit(descriptor.value, depth + 1);
240
+ }
241
+ return Object.freeze(output);
242
+ };
243
+
244
+ const snapshot = visit(value, 0);
245
+ let serialized;
246
+ try {
247
+ serialized = JSON.stringify(snapshot);
248
+ } catch {
249
+ invalid("cannot be serialized");
250
+ }
251
+ if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
252
+ invalid("exceeds snapshot limits");
253
+ }
254
+ return snapshot;
255
+ }
256
+
257
+ function safeRunErrorMessage(error) {
258
+ if (!error || typeof error !== "object" || utilTypes.isProxy(error)) {
259
+ return "task failed";
260
+ }
261
+ const descriptor = Object.getOwnPropertyDescriptor(error, "message");
262
+ if (
263
+ descriptor &&
264
+ Object.hasOwn(descriptor, "value") &&
265
+ typeof descriptor.value === "string"
266
+ ) {
267
+ return descriptor.value.slice(0, 4096);
268
+ }
269
+ return "task failed";
270
+ }
271
+
272
+ function normalizeAdmittedTaskEntry(value) {
273
+ const entry = snapshotAdmittedCanonicalValue(value, "task result");
274
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
275
+ throw runResultError("task result must be a plain object");
276
+ }
277
+ if (
278
+ [...ADMITTED_TASK_ENTRY_REQUIRED_FIELDS].some(
279
+ (field) => !Object.hasOwn(entry, field),
280
+ ) ||
281
+ (entry.status !== "completed" && entry.status !== "failed") ||
282
+ typeof entry.taskId !== "string" ||
283
+ entry.taskId.length === 0 ||
284
+ entry.taskId.length > 512 ||
285
+ !entry.result ||
286
+ typeof entry.result !== "object" ||
287
+ Array.isArray(entry.result)
288
+ ) {
289
+ throw runResultError("task result has an invalid schema");
290
+ }
291
+ return Object.freeze({
292
+ taskId: entry.taskId,
293
+ status: entry.status,
294
+ result: entry.result,
295
+ });
296
+ }
297
+
298
+ function normalizeAdmittedOutcome(value, name = "workflow step outcome") {
299
+ const outcome = snapshotAdmittedCanonicalValue(value, name);
300
+ if (
301
+ !outcome ||
302
+ typeof outcome !== "object" ||
303
+ Array.isArray(outcome) ||
304
+ Reflect.ownKeys(outcome).length !== 4 ||
305
+ !["id", "status", "taskId", "result"].every((field) =>
306
+ Object.hasOwn(outcome, field),
307
+ ) ||
308
+ typeof outcome.id !== "string" ||
309
+ outcome.id.length === 0 ||
310
+ outcome.id.length > 512 ||
311
+ !ADMITTED_STEP_STATUSES.has(outcome.status) ||
312
+ (outcome.taskId !== null &&
313
+ (typeof outcome.taskId !== "string" ||
314
+ outcome.taskId.length === 0 ||
315
+ outcome.taskId.length > 512)) ||
316
+ !outcome.result ||
317
+ typeof outcome.result !== "object" ||
318
+ Array.isArray(outcome.result)
319
+ ) {
320
+ throw runResultError(`${name} has an invalid schema`);
321
+ }
322
+ return outcome;
323
+ }
324
+
325
+ function notifyWorkflowCallback(callback, value, admitted, name) {
326
+ if (typeof callback !== "function") return;
327
+ const view = admitted
328
+ ? snapshotAdmittedCanonicalValue(value, `${name} callback view`)
329
+ : value;
330
+ try {
331
+ callback(view);
332
+ } catch {
333
+ // Observation callbacks must not mutate or steer workflow control flow.
334
+ }
335
+ }
336
+
337
+ function assertAdmissionObject(value, fields, name) {
338
+ if (
339
+ !value ||
340
+ typeof value !== "object" ||
341
+ Array.isArray(value) ||
342
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
343
+ Object.keys(value).length !== fields.size ||
344
+ Object.keys(value).some((key) => !fields.has(key))
345
+ ) {
346
+ throw runAdmissionError(`${name} is invalid`);
347
+ }
348
+ }
349
+
350
+ function normalizeAdmittedLlmOptions(value) {
351
+ if (
352
+ !value ||
353
+ typeof value !== "object" ||
354
+ Array.isArray(value) ||
355
+ utilTypes.isProxy(value) ||
356
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))
357
+ ) {
358
+ throw runAdmissionError("admitted workflow llmOptions is invalid");
359
+ }
360
+ const keys = Reflect.ownKeys(value);
361
+ if (
362
+ keys.length !== RUN_ADMISSION_LLM_OPTION_FIELDS.size ||
363
+ keys.some(
364
+ (key) =>
365
+ typeof key !== "string" || !RUN_ADMISSION_LLM_OPTION_FIELDS.has(key),
366
+ )
367
+ ) {
368
+ throw runAdmissionError("admitted workflow llmOptions is not exact");
369
+ }
370
+ const snapshot = {};
371
+ for (const key of RUN_ADMISSION_LLM_OPTION_FIELDS) {
372
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
373
+ if (
374
+ !descriptor ||
375
+ !Object.hasOwn(descriptor, "value") ||
376
+ descriptor.enumerable !== true
377
+ ) {
378
+ throw runAdmissionError(
379
+ "admitted workflow llmOptions must use enumerable data properties",
380
+ );
381
+ }
382
+ snapshot[key] = descriptor.value;
383
+ }
384
+ return Object.freeze(snapshot);
385
+ }
386
+
387
+ function normalizeAdmissionDigest(value, name) {
388
+ const digest = normalizeWorkflowDefinitionDigest(value);
389
+ if (!digest || digest !== value) {
390
+ throw runAdmissionError(`${name} is invalid`);
391
+ }
392
+ return digest;
393
+ }
394
+
395
+ function admissionDigest(material) {
396
+ return `sha256:${createHash("sha256")
397
+ .update("chainlesschain.dynamic-workflow.admission.v1\0", "utf8")
398
+ .update(canonicalJson(material, "dynamicWorkflowAdmission"), "utf8")
399
+ .digest("hex")}`;
400
+ }
401
+
402
+ function normalizeWorkflowRunAdmission(
403
+ value,
404
+ definitionAuthority,
405
+ maxParallel,
406
+ execution,
407
+ ) {
408
+ try {
409
+ value = structuredClone(value);
410
+ } catch {
411
+ throw runAdmissionError("run admission could not be safely snapshotted");
412
+ }
413
+ assertAdmissionObject(value, RUN_ADMISSION_FIELDS, "run admission");
414
+ assertAdmissionObject(
415
+ value.definition,
416
+ RUN_ADMISSION_DEFINITION_FIELDS,
417
+ "run admission definition",
418
+ );
419
+ assertAdmissionObject(
420
+ value.definition.authority,
421
+ RUN_ADMISSION_DEFINITION_AUTHORITY_FIELDS,
422
+ "run admission definition authority",
423
+ );
424
+ assertAdmissionObject(
425
+ value.executionLocation,
426
+ RUN_ADMISSION_EXECUTION_LOCATION_FIELDS,
427
+ "run admission execution location",
428
+ );
429
+ assertAdmissionObject(
430
+ value.executionLocation.session,
431
+ RUN_ADMISSION_SESSION_FIELDS,
432
+ "run admission session proof",
433
+ );
434
+ assertAdmissionObject(
435
+ value.executionPolicy,
436
+ RUN_ADMISSION_EXECUTION_POLICY_FIELDS,
437
+ "run admission execution policy",
438
+ );
439
+ assertAdmissionObject(
440
+ execution.llmOptions,
441
+ RUN_ADMISSION_LLM_OPTION_FIELDS,
442
+ "admitted workflow llmOptions",
443
+ );
444
+
445
+ const definitionDigest = normalizeAdmissionDigest(
446
+ value.definitionDigest,
447
+ "run admission definition digest",
448
+ );
449
+ const executionLocationDigest = normalizeAdmissionDigest(
450
+ value.executionLocationDigest,
451
+ "run admission execution-location digest",
452
+ );
453
+ const preflightDigest = normalizeAdmissionDigest(
454
+ value.preflightDigest,
455
+ "run admission preflight digest",
456
+ );
457
+ const declaredAdmissionDigest = normalizeAdmissionDigest(
458
+ value.admissionDigest,
459
+ "run admission digest",
460
+ );
461
+ const authority = value.definition.authority;
462
+ const session = value.executionLocation.session;
463
+ const executionPolicy = value.executionPolicy;
464
+ const hasControlCharacters = (input) =>
465
+ [...input].some((character) => {
466
+ const codePoint = character.codePointAt(0);
467
+ return codePoint <= 0x1f || codePoint === 0x7f;
468
+ });
469
+ const validOptionalString = (input) =>
470
+ input === null ||
471
+ (typeof input === "string" &&
472
+ input.length > 0 &&
473
+ input.trim() === input &&
474
+ !hasControlCharacters(input));
475
+
476
+ if (
477
+ value.schema !== DYNAMIC_WORKFLOW_RUN_ADMISSION_SCHEMA ||
478
+ value.definition.schema !== definitionAuthority.schema ||
479
+ value.definition.definitionDigest !== definitionDigest ||
480
+ definitionDigest !== definitionAuthority.definitionDigest ||
481
+ authority.status !== "versioned" ||
482
+ authority.recordSchema !== COWORK_WORKFLOW_RECORD_SCHEMA ||
483
+ authority.definitionSchema !== definitionAuthority.schema ||
484
+ authority.definitionDigest !== definitionDigest ||
485
+ value.executionLocation.authoritySchema !==
486
+ EXECUTION_LOCATION_AUTHORITY_SCHEMA ||
487
+ value.executionLocation.authority !== "verified-session-start" ||
488
+ value.executionLocation.bindingSchema !==
489
+ EXECUTION_LOCATION_BINDING_SCHEMA ||
490
+ typeof value.executionLocation.location !== "string" ||
491
+ value.executionLocation.location.trim() !==
492
+ value.executionLocation.location ||
493
+ value.executionLocation.location.length === 0 ||
494
+ typeof session.sessionId !== "string" ||
495
+ session.sessionId.trim() !== session.sessionId ||
496
+ session.sessionId.length === 0 ||
497
+ !/^[a-f0-9]{64}$/u.test(session.headHash) ||
498
+ !Number.isSafeInteger(session.eventCount) ||
499
+ session.eventCount < 1 ||
500
+ typeof executionPolicy.cwd !== "string" ||
501
+ executionPolicy.cwd.length === 0 ||
502
+ hasControlCharacters(executionPolicy.cwd) ||
503
+ typeof executionPolicy.continueOnError !== "boolean" ||
504
+ typeof executionPolicy.pipeline !== "boolean" ||
505
+ !validOptionalString(executionPolicy.provider) ||
506
+ !validOptionalString(executionPolicy.model) ||
507
+ executionPolicy.cwd !== execution.cwd ||
508
+ executionPolicy.continueOnError !== execution.continueOnError ||
509
+ executionPolicy.pipeline !== execution.pipeline ||
510
+ executionPolicy.provider !== execution.llmOptions.provider ||
511
+ executionPolicy.model !== execution.llmOptions.model ||
512
+ !Number.isSafeInteger(maxParallel) ||
513
+ maxParallel < 1 ||
514
+ maxParallel > 64 ||
515
+ value.maxParallel !== maxParallel ||
516
+ value.credentialValuesTransferred !== false
517
+ ) {
518
+ throw runAdmissionError(
519
+ "run admission does not match the workflow definition or execution options",
520
+ );
521
+ }
522
+
523
+ const normalized = Object.freeze({
524
+ schema: DYNAMIC_WORKFLOW_RUN_ADMISSION_SCHEMA,
525
+ definition: Object.freeze({
526
+ schema: definitionAuthority.schema,
527
+ definitionDigest,
528
+ authority: Object.freeze({
529
+ status: "versioned",
530
+ recordSchema: COWORK_WORKFLOW_RECORD_SCHEMA,
531
+ definitionSchema: definitionAuthority.schema,
532
+ definitionDigest,
533
+ }),
534
+ }),
535
+ executionLocation: Object.freeze({
536
+ authoritySchema: EXECUTION_LOCATION_AUTHORITY_SCHEMA,
537
+ authority: "verified-session-start",
538
+ session: Object.freeze({
539
+ sessionId: session.sessionId,
540
+ headHash: session.headHash,
541
+ eventCount: session.eventCount,
542
+ }),
543
+ bindingSchema: EXECUTION_LOCATION_BINDING_SCHEMA,
544
+ location: value.executionLocation.location,
545
+ }),
546
+ executionPolicy: Object.freeze({
547
+ cwd: executionPolicy.cwd,
548
+ continueOnError: executionPolicy.continueOnError,
549
+ pipeline: executionPolicy.pipeline,
550
+ provider: executionPolicy.provider,
551
+ model: executionPolicy.model,
552
+ }),
553
+ definitionDigest,
554
+ executionLocationDigest,
555
+ preflightDigest,
556
+ maxParallel,
557
+ credentialValuesTransferred: false,
558
+ });
559
+ if (admissionDigest(normalized) !== declaredAdmissionDigest) {
560
+ throw runAdmissionError("run admission digest does not match its material");
561
+ }
562
+ return Object.freeze({
563
+ ...normalized,
564
+ admissionDigest: declaredAdmissionDigest,
565
+ });
566
+ }
42
567
 
43
568
  export const _deps = {
44
569
  existsSync,
@@ -49,6 +574,12 @@ export const _deps = {
49
574
  unlinkSync,
50
575
  appendFileSync,
51
576
  renameSync,
577
+ lstatSync,
578
+ openSync,
579
+ fstatSync,
580
+ closeSync,
581
+ realpathSync,
582
+ fsConstants,
52
583
  now: () => Date.now(),
53
584
  runTask: null, // injected by CLI
54
585
  // Timer seams (injectable for deterministic retry/timeout tests).
@@ -83,14 +614,174 @@ function _atomicWriteJson(file, data) {
83
614
  }
84
615
  }
85
616
 
617
+ function _parseBoundedWorkflowJson(raw) {
618
+ const text =
619
+ typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
620
+ const bytes = Buffer.byteLength(text, "utf8");
621
+ if (bytes <= 0 || bytes > MAX_WORKFLOW_DEFINITION_BYTES) {
622
+ throw new Error(
623
+ `workflow record must be 1..${MAX_WORKFLOW_DEFINITION_BYTES} bytes`,
624
+ );
625
+ }
626
+ return JSON.parse(text);
627
+ }
628
+
629
+ function _pathIsInside(root, candidate) {
630
+ const offset = relative(root, candidate);
631
+ return offset === "" || (!offset.startsWith("..") && !isAbsolute(offset));
632
+ }
633
+
634
+ function _canonicalWorkflowStoragePath(candidate, runtimeRealpath) {
635
+ const nativeRealpath = runtimeRealpath?.native;
636
+ return typeof nativeRealpath === "function"
637
+ ? nativeRealpath(candidate)
638
+ : runtimeRealpath(candidate);
639
+ }
640
+
641
+ export function workflowStoragePathIsContained(
642
+ cwd,
643
+ candidate,
644
+ runtimeRealpath = realpathSync,
645
+ ) {
646
+ const realRoot = _canonicalWorkflowStoragePath(cwd, runtimeRealpath);
647
+ const realCandidate = _canonicalWorkflowStoragePath(
648
+ candidate,
649
+ runtimeRealpath,
650
+ );
651
+ return _pathIsInside(realRoot, realCandidate);
652
+ }
653
+
654
+ function _assertWorkflowStoragePath(cwd, candidate) {
655
+ const usingRuntimeFs =
656
+ _deps.readFileSync === readFileSync &&
657
+ _deps.realpathSync === realpathSync &&
658
+ _deps.lstatSync === lstatSync;
659
+ if (!usingRuntimeFs) return;
660
+ // Canonicalize both sides through the same native API. Windows hosted
661
+ // runners can expose cwd through an 8.3 alias while a trusted-parent read
662
+ // returns the expanded path; mixing legacy/native realpath projections can
663
+ // otherwise misclassify the same directory as an escape.
664
+ if (!workflowStoragePathIsContained(cwd, candidate, _deps.realpathSync)) {
665
+ throw new Error("workflow storage path escapes the working directory");
666
+ }
667
+ }
668
+
669
+ function _assertWorkflowStorageDirectory(cwd, directory) {
670
+ _assertWorkflowStoragePath(cwd, directory);
671
+ if (
672
+ _deps.readFileSync === readFileSync &&
673
+ _deps.lstatSync === lstatSync &&
674
+ _deps.realpathSync === realpathSync
675
+ ) {
676
+ const stat = _deps.lstatSync(directory);
677
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
678
+ throw new Error("workflow storage directory must not be a symlink");
679
+ }
680
+ }
681
+ }
682
+
683
+ function _readBoundedWorkflowJson(cwd, file) {
684
+ const usingRuntimeFs =
685
+ _deps.readFileSync === readFileSync &&
686
+ _deps.lstatSync === lstatSync &&
687
+ _deps.openSync === openSync &&
688
+ _deps.fstatSync === fstatSync &&
689
+ _deps.closeSync === closeSync &&
690
+ _deps.realpathSync === realpathSync;
691
+ if (!usingRuntimeFs) {
692
+ return _parseBoundedWorkflowJson(_deps.readFileSync(file, "utf8"));
693
+ }
694
+
695
+ _assertWorkflowStoragePath(cwd, file);
696
+ return withTrustedFileParentSync(
697
+ fs,
698
+ file,
699
+ ({ canonicalPath, parentDevice }) => {
700
+ _assertWorkflowStoragePath(cwd, canonicalPath);
701
+ const before = _deps.lstatSync(canonicalPath, { bigint: true });
702
+ if (
703
+ !before.isFile() ||
704
+ before.isSymbolicLink() ||
705
+ Number(before.nlink) !== 1
706
+ ) {
707
+ throw new Error("workflow record must be a regular, single-link file");
708
+ }
709
+ if (
710
+ Number(before.size) <= 0 ||
711
+ Number(before.size) > MAX_WORKFLOW_DEFINITION_BYTES
712
+ ) {
713
+ throw new Error(
714
+ `workflow record must be 1..${MAX_WORKFLOW_DEFINITION_BYTES} bytes`,
715
+ );
716
+ }
717
+
718
+ let descriptor;
719
+ try {
720
+ let flags = Number(_deps.fsConstants.O_RDONLY || 0);
721
+ if (typeof _deps.fsConstants.O_NOFOLLOW === "number") {
722
+ flags |= _deps.fsConstants.O_NOFOLLOW;
723
+ }
724
+ descriptor = _deps.openSync(canonicalPath, flags);
725
+ const opened = _deps.fstatSync(descriptor, { bigint: true });
726
+ if (
727
+ !opened.isFile() ||
728
+ Number(opened.nlink) !== 1 ||
729
+ !samePathHandleFileIdentity(before, opened, parentDevice)
730
+ ) {
731
+ throw new Error("workflow record identity changed while opening");
732
+ }
733
+ const value = _parseBoundedWorkflowJson(
734
+ _deps.readFileSync(descriptor, "utf8"),
735
+ );
736
+ const after = _deps.fstatSync(descriptor, { bigint: true });
737
+ if (!sameFileStatIdentity(opened, after)) {
738
+ throw new Error("workflow record changed while being read");
739
+ }
740
+ return value;
741
+ } finally {
742
+ if (descriptor !== undefined) _deps.closeSync(descriptor);
743
+ }
744
+ },
745
+ );
746
+ }
747
+
86
748
  // ─── Paths ───────────────────────────────────────────────────────────────────
87
749
 
88
750
  function workflowsDir(cwd) {
89
751
  return join(cwd, ".chainlesschain", "cowork", "workflows");
90
752
  }
91
753
 
754
+ function assertWorkflowId(id) {
755
+ if (
756
+ typeof id !== "string" ||
757
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(id) ||
758
+ id.includes("..")
759
+ ) {
760
+ throw new TypeError(
761
+ "workflow id must be 1..128 safe filename characters without '..'",
762
+ );
763
+ }
764
+ return id;
765
+ }
766
+
92
767
  function workflowFile(cwd, id) {
93
- return join(workflowsDir(cwd), `${id}.json`);
768
+ return join(workflowsDir(cwd), `${assertWorkflowId(id)}.json`);
769
+ }
770
+
771
+ function workflowVersionsDir(cwd, id) {
772
+ return join(
773
+ cwd,
774
+ ".chainlesschain",
775
+ "cowork",
776
+ "workflow-versions",
777
+ assertWorkflowId(id),
778
+ );
779
+ }
780
+
781
+ function workflowVersionFile(cwd, id, definitionDigest) {
782
+ const digest = normalizeWorkflowDefinitionDigest(definitionDigest);
783
+ if (!digest) throw new TypeError("workflow definition digest is invalid");
784
+ return join(workflowVersionsDir(cwd, id), `${digest.slice(7)}.json`);
94
785
  }
95
786
 
96
787
  function historyFile(cwd) {
@@ -107,7 +798,15 @@ export function validateWorkflow(wf) {
107
798
  if (!wf || typeof wf !== "object") {
108
799
  return { valid: false, errors: ["workflow must be an object"] };
109
800
  }
110
- if (!wf.id || typeof wf.id !== "string") errors.push("id required");
801
+ if (!wf.id || typeof wf.id !== "string") {
802
+ errors.push("id required");
803
+ } else {
804
+ try {
805
+ assertWorkflowId(wf.id);
806
+ } catch (error) {
807
+ errors.push(error.message);
808
+ }
809
+ }
111
810
  if (!wf.name || typeof wf.name !== "string") errors.push("name required");
112
811
  if (!Array.isArray(wf.steps) || wf.steps.length === 0) {
113
812
  errors.push("steps must be a non-empty array");
@@ -375,6 +1074,40 @@ export function substitutePlaceholders(template, resultsById) {
375
1074
 
376
1075
  // ─── Persistence ─────────────────────────────────────────────────────────────
377
1076
 
1077
+ function _storedWorkflowError(message) {
1078
+ const error = new Error(message);
1079
+ error.code = "WORKFLOW_DEFINITION_INTEGRITY";
1080
+ return error;
1081
+ }
1082
+
1083
+ function _readStoredWorkflowRecord(
1084
+ cwd,
1085
+ file,
1086
+ { allowLegacy = true, expectedId, expectedDigest } = {},
1087
+ ) {
1088
+ const record = verifyCoworkWorkflowRecord(
1089
+ _readBoundedWorkflowJson(cwd, file),
1090
+ {
1091
+ allowLegacy,
1092
+ },
1093
+ );
1094
+ const validation = validateWorkflow(record.definition);
1095
+ if (!validation.valid) {
1096
+ throw _storedWorkflowError(
1097
+ `stored workflow is invalid: ${validation.errors.join("; ")}`,
1098
+ );
1099
+ }
1100
+ if (expectedId && record.definition.id !== expectedId) {
1101
+ throw _storedWorkflowError("stored workflow id does not match its path");
1102
+ }
1103
+ if (expectedDigest && record.definitionDigest !== expectedDigest) {
1104
+ throw _storedWorkflowError(
1105
+ "stored workflow digest does not match its version path",
1106
+ );
1107
+ }
1108
+ return record;
1109
+ }
1110
+
378
1111
  export function listWorkflows(cwd) {
379
1112
  const dir = workflowsDir(cwd);
380
1113
  if (!_deps.existsSync(dir)) return [];
@@ -383,21 +1116,28 @@ export function listWorkflows(cwd) {
383
1116
  for (const name of entries) {
384
1117
  if (!name.endsWith(".json")) continue;
385
1118
  try {
386
- const body = _deps.readFileSync(join(dir, name), "utf-8");
387
- out.push(JSON.parse(body));
388
- } catch (_e) {
1119
+ const id = name.slice(0, -5);
1120
+ const record = _readStoredWorkflowRecord(cwd, join(dir, name), {
1121
+ expectedId: id,
1122
+ });
1123
+ out.push(record.definition);
1124
+ } catch {
389
1125
  // skip malformed files
390
1126
  }
391
1127
  }
392
1128
  return out;
393
1129
  }
394
1130
 
395
- export function getWorkflow(cwd, id) {
1131
+ export function getWorkflowRecord(cwd, id) {
396
1132
  const file = workflowFile(cwd, id);
397
1133
  if (!_deps.existsSync(file)) return null;
1134
+ return _readStoredWorkflowRecord(cwd, file, { expectedId: id });
1135
+ }
1136
+
1137
+ export function getWorkflow(cwd, id) {
398
1138
  try {
399
- return JSON.parse(_deps.readFileSync(file, "utf-8"));
400
- } catch (_e) {
1139
+ return getWorkflowRecord(cwd, id)?.definition || null;
1140
+ } catch {
401
1141
  return null;
402
1142
  }
403
1143
  }
@@ -405,14 +1145,82 @@ export function getWorkflow(cwd, id) {
405
1145
  export function saveWorkflow(cwd, wf) {
406
1146
  const { valid, errors } = validateWorkflow(wf);
407
1147
  if (!valid) throw new Error(`Invalid workflow: ${errors.join("; ")}`);
1148
+ const record = createCoworkWorkflowRecord(wf);
1149
+ const serialized = JSON.stringify(record, null, 2);
408
1150
  const dir = workflowsDir(cwd);
409
1151
  _deps.mkdirSync(dir, { recursive: true });
410
- _atomicWriteJson(workflowFile(cwd, wf.id), JSON.stringify(wf, null, 2));
1152
+ _assertWorkflowStorageDirectory(cwd, dir);
1153
+ const versionDir = workflowVersionsDir(cwd, wf.id);
1154
+ _deps.mkdirSync(versionDir, { recursive: true });
1155
+ _assertWorkflowStorageDirectory(cwd, versionDir);
1156
+ const versionFile = workflowVersionFile(cwd, wf.id, record.definitionDigest);
1157
+ if (_deps.existsSync(versionFile)) {
1158
+ _readStoredWorkflowRecord(cwd, versionFile, {
1159
+ allowLegacy: false,
1160
+ expectedId: wf.id,
1161
+ expectedDigest: record.definitionDigest,
1162
+ });
1163
+ } else {
1164
+ _atomicWriteJson(versionFile, serialized);
1165
+ }
1166
+ const currentFile = workflowFile(cwd, wf.id);
1167
+ if (_deps.existsSync(currentFile)) {
1168
+ _readStoredWorkflowRecord(cwd, currentFile, {
1169
+ expectedId: wf.id,
1170
+ });
1171
+ }
1172
+ _atomicWriteJson(currentFile, serialized);
411
1173
  return wf;
412
1174
  }
413
1175
 
1176
+ export function getWorkflowVersion(cwd, id, definitionDigest) {
1177
+ const digest = normalizeWorkflowDefinitionDigest(definitionDigest);
1178
+ if (!digest) throw new TypeError("workflow definition digest is invalid");
1179
+ const file = workflowVersionFile(cwd, id, digest);
1180
+ if (!_deps.existsSync(file)) return null;
1181
+ return _readStoredWorkflowRecord(cwd, file, {
1182
+ allowLegacy: false,
1183
+ expectedId: id,
1184
+ expectedDigest: digest,
1185
+ });
1186
+ }
1187
+
1188
+ export function listWorkflowVersions(cwd, id) {
1189
+ const dir = workflowVersionsDir(cwd, id);
1190
+ if (!_deps.existsSync(dir)) return [];
1191
+ const versions = [];
1192
+ for (const name of _deps.readdirSync(dir) || []) {
1193
+ const match = /^([a-f0-9]{64})\.json$/u.exec(name);
1194
+ if (!match) continue;
1195
+ const digest = `sha256:${match[1]}`;
1196
+ const record = _readStoredWorkflowRecord(cwd, join(dir, name), {
1197
+ allowLegacy: false,
1198
+ expectedId: id,
1199
+ expectedDigest: digest,
1200
+ });
1201
+ versions.push(
1202
+ Object.freeze({
1203
+ status: record.status,
1204
+ recordSchema: record.recordSchema,
1205
+ definitionSchema: record.definitionSchema,
1206
+ definitionDigest: record.definitionDigest,
1207
+ id: record.definition.id,
1208
+ name: record.definition.name,
1209
+ }),
1210
+ );
1211
+ }
1212
+ return versions.sort((left, right) =>
1213
+ left.definitionDigest.localeCompare(right.definitionDigest),
1214
+ );
1215
+ }
1216
+
414
1217
  export function removeWorkflow(cwd, id) {
415
- const file = workflowFile(cwd, id);
1218
+ let file;
1219
+ try {
1220
+ file = workflowFile(cwd, id);
1221
+ } catch {
1222
+ return false;
1223
+ }
416
1224
  if (!_deps.existsSync(file)) return false;
417
1225
  _deps.unlinkSync(file);
418
1226
  return true;
@@ -448,7 +1256,9 @@ export async function withTimeout(factory, timeoutMs) {
448
1256
  let timer = null;
449
1257
  const timeout = new Promise((_resolve, reject) => {
450
1258
  timer = _deps.setTimeout(() => {
451
- reject(new Error(`step timed out after ${timeoutMs}ms`));
1259
+ const error = new Error(`step timed out after ${timeoutMs}ms`);
1260
+ error.code = STEP_TIMEOUT_CODE;
1261
+ reject(error);
452
1262
  }, timeoutMs);
453
1263
  });
454
1264
  try {
@@ -459,36 +1269,102 @@ export async function withTimeout(factory, timeoutMs) {
459
1269
  }
460
1270
  }
461
1271
 
1272
+ function createWorkflowTaskSemaphore(limit) {
1273
+ let active = 0;
1274
+ const waiters = [];
1275
+ return Object.freeze({
1276
+ acquire() {
1277
+ return new Promise((resolve) => {
1278
+ const grant = () => {
1279
+ active += 1;
1280
+ let released = false;
1281
+ resolve(() => {
1282
+ if (released) return;
1283
+ released = true;
1284
+ active -= 1;
1285
+ const next = waiters.shift();
1286
+ if (next) next();
1287
+ });
1288
+ };
1289
+ if (active < limit) grant();
1290
+ else waiters.push(grant);
1291
+ });
1292
+ },
1293
+ });
1294
+ }
1295
+
462
1296
  /**
463
1297
  * Run one step's task with optional `timeoutMs` and `retries` (with `fixed` or
464
1298
  * `exponential` `retryDelayMs` backoff). An attempt counts as a failure if the
465
1299
  * task throws, times out, or returns a non-`completed` status. Returns
466
1300
  * `{ ok, entry?, error?, attempts }`. `attempts` is the total number of tries.
467
1301
  */
468
- export async function runStepWithRetry({ step, message, cwd, llmOptions }) {
1302
+ export async function runStepWithRetry({
1303
+ step,
1304
+ message,
1305
+ cwd,
1306
+ llmOptions,
1307
+ taskSemaphore,
1308
+ runTask = _deps.runTask,
1309
+ recordId = step.id,
1310
+ onTaskStart,
1311
+ admitted = false,
1312
+ }) {
469
1313
  const maxRetries = Math.max(0, Math.floor(Number(step.retries) || 0));
470
1314
  const timeoutMs = Number(step.timeoutMs) || 0;
471
1315
  let lastEntry = null;
472
1316
  let lastErr = null;
473
1317
  for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
1318
+ let taskPromise = null;
1319
+ let abortController = null;
1320
+ let release = null;
474
1321
  try {
475
- const entry = await withTimeout(
476
- () =>
477
- _deps.runTask({
478
- templateId: step.templateId || null,
479
- userMessage: message,
480
- files: step.files || [],
481
- cwd,
482
- llmOptions,
483
- }),
484
- timeoutMs,
1322
+ release = taskSemaphore ? await taskSemaphore.acquire() : () => {};
1323
+ if (admitted && timeoutMs > 0) abortController = new AbortController();
1324
+ if (attempt === 1) {
1325
+ notifyWorkflowCallback(
1326
+ onTaskStart,
1327
+ Object.freeze({ stepId: recordId, message }),
1328
+ admitted,
1329
+ "step start",
1330
+ );
1331
+ }
1332
+ taskPromise = Promise.resolve().then(() =>
1333
+ runTask({
1334
+ templateId: step.templateId || null,
1335
+ userMessage: message,
1336
+ files: step.files || [],
1337
+ cwd,
1338
+ llmOptions,
1339
+ ...(abortController ? { signal: abortController.signal } : {}),
1340
+ }),
485
1341
  );
1342
+ // A timeout abandons only the caller's wait. Keep the physical task's
1343
+ // permit until its promise really settles; retries and parallel children
1344
+ // must queue behind that late task instead of exceeding maxParallel.
1345
+ taskPromise.then(release, release);
1346
+ release = null; // the physical task now owns the permit
1347
+ const rawEntry = await withTimeout(() => taskPromise, timeoutMs);
1348
+ const entry = admitted ? normalizeAdmittedTaskEntry(rawEntry) : rawEntry;
486
1349
  if (entry && entry.status === "completed") {
487
1350
  return { ok: true, entry, attempts: attempt };
488
1351
  }
489
1352
  lastEntry = entry; // ran but not completed → retry-eligible
490
1353
  lastErr = null;
491
1354
  } catch (err) {
1355
+ if (release) release();
1356
+ if (err?.code === COWORK_WORKFLOW_RUN_RESULT_INVALID_CODE) throw err;
1357
+ if (admitted && err?.code === STEP_TIMEOUT_CODE && taskPromise != null) {
1358
+ abortController.abort(err);
1359
+ // A terminal admitted record must never be written while the physical
1360
+ // task can still produce side effects. Confirm settlement before a
1361
+ // retry or final failure; a runner that ignores abort safely blocks.
1362
+ try {
1363
+ await taskPromise;
1364
+ } catch {
1365
+ // The timeout remains the attempt's externally visible failure.
1366
+ }
1367
+ }
492
1368
  lastErr = err; // threw or timed out → retry-eligible
493
1369
  lastEntry = null;
494
1370
  }
@@ -510,24 +1386,27 @@ function _withAttempts(result, attempts) {
510
1386
  }
511
1387
 
512
1388
  /** Build a single-step outcome object from a `runStepWithRetry` result. */
513
- function outcomeFromRetry(recordId, r) {
1389
+ function outcomeFromRetry(recordId, r, admitted = false) {
1390
+ let outcome;
514
1391
  if (r.ok || r.entry) {
515
- return {
1392
+ outcome = {
516
1393
  id: recordId,
517
1394
  status: r.entry.status,
518
1395
  taskId: r.entry.taskId,
519
1396
  result: _withAttempts(r.entry.result, r.attempts),
520
1397
  };
1398
+ } else {
1399
+ outcome = {
1400
+ id: recordId,
1401
+ status: "failed",
1402
+ taskId: null,
1403
+ result: _withAttempts(
1404
+ { summary: `Step threw: ${safeRunErrorMessage(r.error)}` },
1405
+ r.attempts,
1406
+ ),
1407
+ };
521
1408
  }
522
- return {
523
- id: recordId,
524
- status: "failed",
525
- taskId: null,
526
- result: _withAttempts(
527
- { summary: `Step threw: ${r.error.message}` },
528
- r.attempts,
529
- ),
530
- };
1409
+ return admitted ? normalizeAdmittedOutcome(outcome) : outcome;
531
1410
  }
532
1411
 
533
1412
  // ─── while / until loop nodes ──────────────────────────────────────────────────
@@ -598,11 +1477,16 @@ export async function runLoopStep({
598
1477
  cwd,
599
1478
  llmOptions,
600
1479
  resultsById,
1480
+ taskSemaphore,
1481
+ runTask = _deps.runTask,
1482
+ onStepStart,
1483
+ admitted = false,
601
1484
  }) {
602
1485
  const cap = loopIterationCap(step);
603
1486
  let last = null;
604
1487
  let iterations = 0;
605
1488
  let stopReason = "cap";
1489
+ let startAnnounced = false;
606
1490
  for (let iter = 1; iter <= cap; iter++) {
607
1491
  iterations = iter;
608
1492
  const withSelf = substituteLoopVars(step.message, {
@@ -611,8 +1495,22 @@ export async function runLoopStep({
611
1495
  resultsById,
612
1496
  });
613
1497
  const message = substitutePlaceholders(withSelf, resultsById);
614
- const r = await runStepWithRetry({ step, message, cwd, llmOptions });
615
- last = outcomeFromRetry(recordId, r);
1498
+ const r = await runStepWithRetry({
1499
+ step,
1500
+ message,
1501
+ cwd,
1502
+ llmOptions,
1503
+ taskSemaphore,
1504
+ runTask,
1505
+ recordId,
1506
+ onTaskStart: (event) => {
1507
+ if (startAnnounced) return;
1508
+ startAnnounced = true;
1509
+ notifyWorkflowCallback(onStepStart, event, admitted, "step start");
1510
+ },
1511
+ admitted,
1512
+ });
1513
+ last = outcomeFromRetry(recordId, r, admitted);
616
1514
  resultsById.set(recordId, last);
617
1515
  if (last.status === "failed") {
618
1516
  stopReason = "failed";
@@ -639,7 +1537,7 @@ export async function runLoopStep({
639
1537
  break;
640
1538
  }
641
1539
  }
642
- return {
1540
+ const outcome = {
643
1541
  id: recordId,
644
1542
  status: last ? last.status : "skipped",
645
1543
  taskId: last?.taskId ?? null,
@@ -650,6 +1548,7 @@ export async function runLoopStep({
650
1548
  loopStop: stopReason,
651
1549
  },
652
1550
  };
1551
+ return admitted ? normalizeAdmittedOutcome(outcome) : outcome;
653
1552
  }
654
1553
 
655
1554
  // ─── Step node + no-barrier pipeline ──────────────────────────────────────────
@@ -663,7 +1562,16 @@ export async function runLoopStep({
663
1562
  * is not "completed". Used by the no-barrier pipeline scheduler.
664
1563
  */
665
1564
  export async function runStepNode(step, ctx) {
666
- const { resultsById, cwd, llmOptions, onStepStart, onStepComplete } = ctx;
1565
+ const {
1566
+ resultsById,
1567
+ cwd,
1568
+ llmOptions,
1569
+ onStepStart,
1570
+ onStepComplete,
1571
+ taskSemaphore,
1572
+ runTask,
1573
+ admitted = false,
1574
+ } = ctx;
667
1575
  const recordId = step.id;
668
1576
  const single = (status, summary) => {
669
1577
  const o = { id: recordId, status, taskId: null, result: { summary } };
@@ -683,16 +1591,19 @@ export async function runStepNode(step, ctx) {
683
1591
  if (!runThis) return single("skipped", "when-condition false");
684
1592
 
685
1593
  if (isLoopStep(step)) {
686
- if (onStepStart) onStepStart({ stepId: recordId, message: step.message });
687
1594
  const o = await runLoopStep({
688
1595
  step,
689
1596
  recordId,
690
1597
  cwd,
691
1598
  llmOptions,
692
1599
  resultsById,
1600
+ taskSemaphore,
1601
+ runTask,
1602
+ onStepStart,
1603
+ admitted,
693
1604
  });
694
1605
  resultsById.set(recordId, o);
695
- if (onStepComplete) onStepComplete(o);
1606
+ notifyWorkflowCallback(onStepComplete, o, admitted, "step complete");
696
1607
  return {
697
1608
  outcomes: [o],
698
1609
  failed: o.status !== "completed" && o.status !== "skipped",
@@ -712,16 +1623,20 @@ export async function runStepNode(step, ctx) {
712
1623
  const childId = `${recordId}[${k}]`;
713
1624
  const withItem = substituteItem(step.message, item);
714
1625
  const msg = substitutePlaceholders(withItem, resultsById);
715
- if (onStepStart) onStepStart({ stepId: childId, message: msg });
716
1626
  const r = await runStepWithRetry({
717
1627
  step,
718
1628
  message: msg,
719
1629
  cwd,
720
1630
  llmOptions,
1631
+ taskSemaphore,
1632
+ runTask,
1633
+ recordId: childId,
1634
+ onTaskStart: onStepStart,
1635
+ admitted,
721
1636
  });
722
- const co = outcomeFromRetry(childId, r);
1637
+ const co = outcomeFromRetry(childId, r, admitted);
723
1638
  resultsById.set(childId, co);
724
- if (onStepComplete) onStepComplete(co);
1639
+ notifyWorkflowCallback(onStepComplete, co, admitted, "step complete");
725
1640
  return co;
726
1641
  }),
727
1642
  );
@@ -743,11 +1658,20 @@ export async function runStepNode(step, ctx) {
743
1658
  }
744
1659
 
745
1660
  const message = substitutePlaceholders(step.message, resultsById);
746
- if (onStepStart) onStepStart({ stepId: recordId, message });
747
- const r = await runStepWithRetry({ step, message, cwd, llmOptions });
748
- const o = outcomeFromRetry(recordId, r);
1661
+ const r = await runStepWithRetry({
1662
+ step,
1663
+ message,
1664
+ cwd,
1665
+ llmOptions,
1666
+ taskSemaphore,
1667
+ runTask,
1668
+ recordId,
1669
+ onTaskStart: onStepStart,
1670
+ admitted,
1671
+ });
1672
+ const o = outcomeFromRetry(recordId, r, admitted);
749
1673
  resultsById.set(recordId, o);
750
- if (onStepComplete) onStepComplete(o);
1674
+ notifyWorkflowCallback(onStepComplete, o, admitted, "step complete");
751
1675
  return { outcomes: [o], failed: o.status !== "completed" };
752
1676
  }
753
1677
 
@@ -768,6 +1692,9 @@ export async function runPipeline({
768
1692
  llmOptions = {},
769
1693
  onStepStart,
770
1694
  onStepComplete,
1695
+ taskSemaphore,
1696
+ runTask = _deps.runTask,
1697
+ admitted = false,
771
1698
  }) {
772
1699
  const limit = Math.max(1, Math.floor(maxParallel) || 1);
773
1700
  const remainingDeps = new Map(
@@ -784,8 +1711,9 @@ export async function runPipeline({
784
1711
  let anyFailure = false;
785
1712
  let halted = false;
786
1713
  let active = 0;
1714
+ let fatalError = null;
787
1715
 
788
- await new Promise((resolve) => {
1716
+ await new Promise((resolve, reject) => {
789
1717
  function pump() {
790
1718
  if (!halted) {
791
1719
  for (const s of steps) {
@@ -798,6 +1726,10 @@ export async function runPipeline({
798
1726
  }
799
1727
  }
800
1728
  if (active === 0 && (halted || scheduled.size === steps.length)) {
1729
+ if (fatalError) {
1730
+ reject(fatalError);
1731
+ return;
1732
+ }
801
1733
  for (const s of steps) {
802
1734
  if (scheduled.has(s.id)) continue;
803
1735
  const o = {
@@ -823,13 +1755,23 @@ export async function runPipeline({
823
1755
  llmOptions,
824
1756
  onStepStart,
825
1757
  onStepComplete,
1758
+ taskSemaphore,
1759
+ runTask,
1760
+ admitted,
826
1761
  });
827
1762
  } catch (err) {
1763
+ if (err?.code === COWORK_WORKFLOW_RUN_RESULT_INVALID_CODE) {
1764
+ fatalError = err;
1765
+ halted = true;
1766
+ active--;
1767
+ pump();
1768
+ return;
1769
+ }
828
1770
  const o = {
829
1771
  id: step.id,
830
1772
  status: "failed",
831
1773
  taskId: null,
832
- result: { summary: `Step threw: ${err.message}` },
1774
+ result: { summary: `Step threw: ${safeRunErrorMessage(err)}` },
833
1775
  };
834
1776
  resultsById.set(step.id, o);
835
1777
  res = { outcomes: [o], failed: true };
@@ -854,6 +1796,98 @@ export async function runPipeline({
854
1796
 
855
1797
  // ─── Execution ───────────────────────────────────────────────────────────────
856
1798
 
1799
+ function normalizeAdmittedRunRecord(
1800
+ value,
1801
+ runtimeWorkflow,
1802
+ definitionAuthority,
1803
+ runAdmission,
1804
+ ) {
1805
+ const record = snapshotAdmittedCanonicalValue(value, "workflow run record");
1806
+ const expectedFields = new Set([
1807
+ "schema",
1808
+ "workflowId",
1809
+ "workflowName",
1810
+ "definitionSchema",
1811
+ "definitionDigest",
1812
+ "runAdmission",
1813
+ "status",
1814
+ "steps",
1815
+ "startedAt",
1816
+ "finishedAt",
1817
+ ]);
1818
+ if (
1819
+ !record ||
1820
+ typeof record !== "object" ||
1821
+ Array.isArray(record) ||
1822
+ Reflect.ownKeys(record).length !== expectedFields.size ||
1823
+ Reflect.ownKeys(record).some(
1824
+ (field) => typeof field !== "string" || !expectedFields.has(field),
1825
+ ) ||
1826
+ record.schema !== COWORK_WORKFLOW_RUN_RECORD_SCHEMA ||
1827
+ record.workflowId !== runtimeWorkflow.id ||
1828
+ record.workflowName !== runtimeWorkflow.name ||
1829
+ record.definitionSchema !== definitionAuthority.schema ||
1830
+ record.definitionDigest !== definitionAuthority.definitionDigest ||
1831
+ !["completed", "failed", "partial"].includes(record.status) ||
1832
+ !Array.isArray(record.steps) ||
1833
+ record.steps.length === 0 ||
1834
+ record.steps.length > MAX_ADMITTED_RUN_STEPS ||
1835
+ typeof record.startedAt !== "string" ||
1836
+ typeof record.finishedAt !== "string" ||
1837
+ !Number.isFinite(Date.parse(record.startedAt)) ||
1838
+ !Number.isFinite(Date.parse(record.finishedAt)) ||
1839
+ new Date(record.startedAt).toISOString() !== record.startedAt ||
1840
+ new Date(record.finishedAt).toISOString() !== record.finishedAt ||
1841
+ Date.parse(record.finishedAt) < Date.parse(record.startedAt) ||
1842
+ canonicalJson(record.runAdmission, "recordRunAdmission") !==
1843
+ canonicalJson(runAdmission, "admittedRunAdmission")
1844
+ ) {
1845
+ throw runResultError("workflow run record has an invalid schema");
1846
+ }
1847
+
1848
+ const outcomeIds = new Set();
1849
+ for (const [index, outcome] of record.steps.entries()) {
1850
+ normalizeAdmittedOutcome(outcome, `workflow run record step ${index}`);
1851
+ if (outcomeIds.has(outcome.id)) {
1852
+ throw runResultError("workflow run record contains duplicate step ids");
1853
+ }
1854
+ outcomeIds.add(outcome.id);
1855
+ }
1856
+
1857
+ for (const step of runtimeWorkflow.steps) {
1858
+ if (step.forEach === undefined) {
1859
+ if (!outcomeIds.has(step.id)) {
1860
+ throw runResultError("workflow run record is missing a step outcome");
1861
+ }
1862
+ continue;
1863
+ }
1864
+ const childPrefix = `${step.id}[`;
1865
+ if (
1866
+ !outcomeIds.has(step.id) &&
1867
+ ![...outcomeIds].some(
1868
+ (outcomeId) =>
1869
+ outcomeId.startsWith(childPrefix) && outcomeId.endsWith("]"),
1870
+ )
1871
+ ) {
1872
+ throw runResultError("workflow run record is missing a forEach outcome");
1873
+ }
1874
+ }
1875
+
1876
+ const expectedStatus = record.steps.every(
1877
+ (outcome) => outcome.status === "completed",
1878
+ )
1879
+ ? "completed"
1880
+ : record.steps.some((outcome) => outcome.status === "completed")
1881
+ ? "partial"
1882
+ : "failed";
1883
+ if (record.status !== expectedStatus) {
1884
+ throw runResultError(
1885
+ "workflow run record status does not match its step outcomes",
1886
+ );
1887
+ }
1888
+ return record;
1889
+ }
1890
+
857
1891
  /**
858
1892
  * Execute a workflow. The runner for individual tasks must be injected via
859
1893
  * `_deps.runTask` (signature matches `runCoworkTask`).
@@ -872,6 +1906,7 @@ export async function runPipeline({
872
1906
  *
873
1907
  * @param {object} options
874
1908
  * @param {object} options.workflow - Workflow definition
1909
+ * @param {string} [options.definitionDigest] - Exact definition digest to bind
875
1910
  * @param {string} [options.cwd] - Working directory for history
876
1911
  * @param {number} [options.maxParallel] - Max parallel steps (per batch, or
877
1912
  * concurrent step nodes in pipeline mode)
@@ -882,6 +1917,9 @@ export async function runPipeline({
882
1917
  * @param {object} [options.llmOptions] - Forwarded to each task
883
1918
  * @param {function} [options.onStepStart]
884
1919
  * @param {function} [options.onStepComplete]
1920
+ * @param {object} [options.runAdmission] - Secret-free, digest-bound dynamic
1921
+ * workflow admission. Production entry points require this even though the
1922
+ * low-level executor keeps it optional for existing library callers.
885
1923
  * @returns {Promise<{
886
1924
  * workflowId: string,
887
1925
  * status: "completed"|"failed"|"partial",
@@ -893,6 +1931,7 @@ export async function runPipeline({
893
1931
  export async function executeWorkflow(options = {}) {
894
1932
  const {
895
1933
  workflow,
1934
+ definitionDigest: requestedDefinitionDigest,
896
1935
  cwd = process.cwd(),
897
1936
  maxParallel = 4,
898
1937
  continueOnError = false,
@@ -900,17 +1939,55 @@ export async function executeWorkflow(options = {}) {
900
1939
  onStepStart,
901
1940
  onStepComplete,
902
1941
  pipeline,
1942
+ runAdmission,
1943
+ runTask: requestedRunTask,
903
1944
  } = options;
904
1945
 
905
- const { valid, errors } = validateWorkflow(workflow);
1946
+ const definitionAuthority = createWorkflowDefinitionAuthority(workflow);
1947
+ const runtimeWorkflow = definitionAuthority.definition;
1948
+ const { valid, errors } = validateWorkflow(runtimeWorkflow);
906
1949
  if (!valid) throw new Error(`Invalid workflow: ${errors.join("; ")}`);
907
- if (typeof _deps.runTask !== "function") {
1950
+ const numericMaxParallel = Number(maxParallel);
1951
+ const concurrencyLimit =
1952
+ Number.isSafeInteger(numericMaxParallel) && numericMaxParallel > 0
1953
+ ? numericMaxParallel
1954
+ : 1;
1955
+ const usePipeline = pipeline ?? runtimeWorkflow.pipeline ?? false;
1956
+ const runtimeLlmOptions =
1957
+ runAdmission == null ? llmOptions : normalizeAdmittedLlmOptions(llmOptions);
1958
+ if (requestedDefinitionDigest != null) {
1959
+ const normalizedDigest = normalizeWorkflowDefinitionDigest(
1960
+ requestedDefinitionDigest,
1961
+ );
1962
+ if (!normalizedDigest) {
1963
+ throw new Error("workflow definition digest is invalid");
1964
+ }
1965
+ if (normalizedDigest !== definitionAuthority.definitionDigest) {
1966
+ throw new Error("workflow definition digest mismatch");
1967
+ }
1968
+ }
1969
+ const verifiedRunAdmission =
1970
+ runAdmission == null
1971
+ ? null
1972
+ : normalizeWorkflowRunAdmission(
1973
+ runAdmission,
1974
+ definitionAuthority,
1975
+ concurrencyLimit,
1976
+ {
1977
+ cwd,
1978
+ continueOnError,
1979
+ pipeline: usePipeline,
1980
+ llmOptions: runtimeLlmOptions,
1981
+ },
1982
+ );
1983
+ const runtimeRunTask = requestedRunTask ?? _deps.runTask;
1984
+ if (typeof runtimeRunTask !== "function") {
908
1985
  throw new Error(
909
- "cowork-workflow: _deps.runTask is not injected (wire runCoworkTask in CLI before executing)",
1986
+ "cowork-workflow: runTask is not available (wire runCoworkTask before executing)",
910
1987
  );
911
1988
  }
912
1989
 
913
- const usePipeline = pipeline ?? workflow.pipeline ?? false;
1990
+ const taskSemaphore = createWorkflowTaskSemaphore(concurrencyLimit);
914
1991
  const resultsById = new Map();
915
1992
  const startedAt = new Date(_deps.now()).toISOString();
916
1993
  let stepOutcomes;
@@ -918,24 +1995,27 @@ export async function executeWorkflow(options = {}) {
918
1995
 
919
1996
  if (usePipeline) {
920
1997
  ({ stepOutcomes, anyFailure } = await runPipeline({
921
- steps: workflow.steps,
1998
+ steps: runtimeWorkflow.steps,
922
1999
  resultsById,
923
- maxParallel,
2000
+ maxParallel: concurrencyLimit,
924
2001
  continueOnError,
925
2002
  cwd,
926
- llmOptions,
2003
+ llmOptions: runtimeLlmOptions,
927
2004
  onStepStart,
928
2005
  onStepComplete,
2006
+ taskSemaphore,
2007
+ runTask: runtimeRunTask,
2008
+ admitted: verifiedRunAdmission != null,
929
2009
  }));
930
2010
  } else {
931
2011
  stepOutcomes = [];
932
2012
  anyFailure = false;
933
- const batches = planBatches(workflow.steps);
2013
+ const batches = planBatches(runtimeWorkflow.steps);
934
2014
  for (const batch of batches) {
935
2015
  // Respect maxParallel by slicing batch into chunks
936
2016
  const chunks = [];
937
- for (let i = 0; i < batch.length; i += maxParallel) {
938
- chunks.push(batch.slice(i, i + maxParallel));
2017
+ for (let i = 0; i < batch.length; i += concurrencyLimit) {
2018
+ chunks.push(batch.slice(i, i + concurrencyLimit));
939
2019
  }
940
2020
 
941
2021
  for (const chunk of chunks) {
@@ -1040,33 +2120,55 @@ export async function executeWorkflow(options = {}) {
1040
2120
 
1041
2121
  const promises = runnable.map(
1042
2122
  async ({ step, message, recordId, isLoop }) => {
1043
- if (onStepStart) onStepStart({ stepId: recordId, message });
1044
2123
  let outcome;
1045
2124
  if (isLoop) {
1046
2125
  outcome = await runLoopStep({
1047
2126
  step,
1048
2127
  recordId,
1049
2128
  cwd,
1050
- llmOptions,
2129
+ llmOptions: runtimeLlmOptions,
1051
2130
  resultsById,
2131
+ taskSemaphore,
2132
+ runTask: runtimeRunTask,
2133
+ onStepStart,
2134
+ admitted: verifiedRunAdmission != null,
1052
2135
  });
1053
2136
  } else {
1054
2137
  const r = await runStepWithRetry({
1055
2138
  step,
1056
2139
  message,
1057
2140
  cwd,
1058
- llmOptions,
2141
+ llmOptions: runtimeLlmOptions,
2142
+ taskSemaphore,
2143
+ runTask: runtimeRunTask,
2144
+ recordId,
2145
+ onTaskStart: onStepStart,
2146
+ admitted: verifiedRunAdmission != null,
1059
2147
  });
1060
- outcome = outcomeFromRetry(recordId, r);
2148
+ outcome = outcomeFromRetry(
2149
+ recordId,
2150
+ r,
2151
+ verifiedRunAdmission != null,
2152
+ );
1061
2153
  }
1062
2154
  if (outcome.status !== "completed") anyFailure = true;
1063
2155
  resultsById.set(recordId, outcome);
1064
- if (onStepComplete) onStepComplete(outcome);
2156
+ notifyWorkflowCallback(
2157
+ onStepComplete,
2158
+ outcome,
2159
+ verifiedRunAdmission != null,
2160
+ "step complete",
2161
+ );
1065
2162
  return outcome;
1066
2163
  },
1067
2164
  );
1068
2165
 
1069
- const results = await Promise.all(promises);
2166
+ const settledResults = await Promise.allSettled(promises);
2167
+ const rejected = settledResults.find(
2168
+ (settled) => settled.status === "rejected",
2169
+ );
2170
+ if (rejected) throw rejected.reason;
2171
+ const results = settledResults.map((settled) => settled.value);
1070
2172
  stepOutcomes.push(...preOutcomes, ...results);
1071
2173
 
1072
2174
  // Aggregate forEach children into a parent entry so downstream
@@ -1093,8 +2195,6 @@ export async function executeWorkflow(options = {}) {
1093
2195
  });
1094
2196
  }
1095
2197
  }
1096
-
1097
- if (anyFailure && !continueOnError) break;
1098
2198
  }
1099
2199
  }
1100
2200
 
@@ -1107,15 +2207,27 @@ export async function executeWorkflow(options = {}) {
1107
2207
  : "failed";
1108
2208
 
1109
2209
  const record = {
1110
- workflowId: workflow.id,
1111
- workflowName: workflow.name,
2210
+ schema: COWORK_WORKFLOW_RUN_RECORD_SCHEMA,
2211
+ workflowId: runtimeWorkflow.id,
2212
+ workflowName: runtimeWorkflow.name,
2213
+ definitionSchema: definitionAuthority.schema,
2214
+ definitionDigest: definitionAuthority.definitionDigest,
2215
+ ...(verifiedRunAdmission ? { runAdmission: verifiedRunAdmission } : {}),
1112
2216
  status,
1113
2217
  steps: stepOutcomes,
1114
2218
  startedAt,
1115
2219
  finishedAt,
1116
2220
  };
1117
- _appendHistory(cwd, record);
1118
- return record;
2221
+ const finalRecord = verifiedRunAdmission
2222
+ ? normalizeAdmittedRunRecord(
2223
+ record,
2224
+ runtimeWorkflow,
2225
+ definitionAuthority,
2226
+ verifiedRunAdmission,
2227
+ )
2228
+ : record;
2229
+ _appendHistory(cwd, finalRecord);
2230
+ return finalRecord;
1119
2231
  }
1120
2232
 
1121
2233
  function _appendHistory(cwd, record) {
@@ -1127,7 +2239,7 @@ function _appendHistory(cwd, record) {
1127
2239
  JSON.stringify(record) + "\n",
1128
2240
  "utf-8",
1129
2241
  );
1130
- } catch (_e) {
2242
+ } catch {
1131
2243
  // best-effort
1132
2244
  }
1133
2245
  }