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.
- package/package.json +7 -4
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- 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
|
|
15
|
-
* `.chainlesschain/cowork/workflows/<id>.json`,
|
|
16
|
-
*
|
|
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")
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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
|
|
400
|
-
} catch
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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({
|
|
615
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
1986
|
+
"cowork-workflow: runTask is not available (wire runCoworkTask before executing)",
|
|
910
1987
|
);
|
|
911
1988
|
}
|
|
912
1989
|
|
|
913
|
-
const
|
|
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:
|
|
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(
|
|
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 +=
|
|
938
|
-
chunks.push(batch.slice(i, i +
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
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
|
|
2242
|
+
} catch {
|
|
1131
2243
|
// best-effort
|
|
1132
2244
|
}
|
|
1133
2245
|
}
|