chainlesschain 0.162.187 → 0.162.189
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-CETaO3s8.js → AIOps-DbZft8w0.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CAe7E9TW.js → ActionButton-DoOZpwfN.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BGr2mzwO.js → Analytics-B0J0KLTW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BEoY-NcT.js → AppLayout-D9eIR0qq.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-DUZivCgt.js → Artifacts-BnZXEVeo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-C3yBs68k.js → Audit-X1ce4Bjn.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C9KwiY3O.js → BackgroundAgents-BvWR-y5v.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BoSWD_C8.js → Backup-BublSGEd.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Dp09GkxL.js → BaseInput-CR_77Hso.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5t9d-Ja0.js → Chat-DixW52tq.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DmVku4GH.js → ChatBubbleRenderer-DDHDpBBA.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-8QJFlkqI.js → Checkbox-CCv1C87K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BJVFQ3lL.js → Codegen-B03Cc3dd.js} +1 -1
- package/src/assets/web-panel/assets/{Col-6hdLD0oB.js → Col-CA06-6pU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B8opo0uY.js → Community-Bj881J52.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dv8HudiN.js → Compact-UZLTPoaL.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DhxuCS5N.js → Compliance-kGwzRb_0.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Dggy6VIL.js → Cowork-KundoYM8.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DSmDqJTT.js → Cron-BYZyoqDe.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-7bZq0j5f.js → Crosschain-BhrrppBX.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BWY4kh-H.js → DID-CdC6EguZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-bOzPZxlu.js → Dashboard-CcQx3J4y.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BTFJHqQu.js → Dropdown-BpijtEzQ.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BC3MOdtT.js → EmailListRenderer-DfZjeZTe.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CFHAdTTY.js → FamilyGuardDashboard-BFYSf69C.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DZ_4aF_n.js → Federation-Okdi3pJm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BZje6UTj.js → FormItemContext-mH-HebKW.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D7QfCvjN.js → GenericCardRenderer-xivV69x7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DuG7qANG.js → Git-1rh_YBwg.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BDmYAgTh.js → Governance-eAXkNxdY.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DSXAfc1Q.js → Inference-CS6dMVTN.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DusleniP.js → KnowledgeGraph-Bq_TnkvD.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CpImmA8p.js → Logs-KchHtiHt.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BZ6LJBkk.js → MarkdownRenderer-TabqHW9B.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DXS8CXkE.js → Marketplace-CsmGcX5X.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D151UfUI.js → McpTools-BaPFnnyz.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DKDxnoA1.js → Memory-2m-sUUf7.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BFZ95iKM.js → MobileBridge-D5nVZBRu.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CDAR-grF.js → MobileProjects-Bo8MLZvT.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-OGJ95xJs.js → Mtc-3GDvamGa.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-BmGTo1XC.js → MtcAudit-D2zGuZCH.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-XumJRqOa.js → Multisig-BBVQ4t6t.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DHhhRZed.js → NLProgramming-BIOCyOGp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B9rGxNNr.js → Notes-DVwBGpzc.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DClLbMYV.js → NotificationSettings-BDnHfIfX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-I8OohT7N.js → OrderTableRenderer-C7Ek20ag.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-M6WzaYVa.js → Organization-D3SRKaa7.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cb3B5KtM.js → Overflow-C2B8ahyn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BGk57kya.js → P2P-2iwQzG65.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CXcKyPqK.js → PdhVaultBrowser-CoPiWqMN.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-hsfJ-Y0e.js → Permissions-yV5XNXo5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BSBii4js.js → PersonalDataHub-uCDJhMUF.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-BNw52rey.js → Pipeline-hN7ucMOA.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DUpfue4E.js → Privacy-CgU0ptN4.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C68NsP3h.js → ProjectInit-CfYPiT6j.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BJ2It9v4.js → ProjectSettings-B0A2C4nw.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BE5C0ajn.js +1 -0
- package/src/assets/web-panel/assets/{Providers-EnU4VWwS.js → Providers-CstQPf8W.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BRt5-B_I.js → QrScannerModal-Co3z_ynk.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Jd5L9UL2.js → QuickAsk-DMTS6pQ6.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNDhoVIu.js → Recommend-ETw4odUa.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D1wQPUNv.js → Reputation-BTVqQgKq.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DnjRegul.js → Row-CBKKwwtO.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C_T4I-nF.js → RssFeed-BHUQ438I.js} +2 -2
- package/src/assets/web-panel/assets/{Search-C4lrxlLh.js → Search-B4NqeUsF.js} +1 -1
- package/src/assets/web-panel/assets/{Security-ByYT-VzP.js → Security-BRjoYG5_.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BHpyEqB5.js → Services-CxQzu3p3.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C0I23vG-.js → Skeleton-BKzwwmWO.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DGZXz22K.js → Skills-DrsFcgth.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DL_urP9A.js → Sla-D21tmXCH.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-6x-hUNQ_.js → SpeechSettings-CjyjLfp8.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Crk5TCXq.js → SyncSettings-Cu45-tnx.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-rZGq5PZs.js → Tasks-oMcILYOj.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DT5el8Q8.js → Templates-Bh-Zcr5R.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-mlgeyMK8.js → Tenant-68ns63aI.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C8HEA-lM.js → Terminal-BLJkFykE.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DvlqBayB.js → TimelineRenderer-xcrJLq-O.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C7lDcNmT.js → Tokens-C6PpFJtf.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CM8nCnIJ.js → Trigger-BaKA7tdM.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BQDLUQ9x.js → Trust-BzhA57Jc.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CVqM0OFE.js → UkeySign-4_U3JkIR.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BvAcQpzT.js → VideoEditing-162AANxX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-zFVFaAZ2.js → Wallet-B1viuql_.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Cc522ums.js → WebAuthn-ZVWazhCy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Bg_wxpDo.js → WorkflowEditor-tzdyUk-1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-jg0QwC8K.js → chat-DmrHNuwq.js} +1 -1
- package/src/assets/web-panel/assets/{colors-ALzYz1gO.js → colors-DAJ_M-PK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DXHYUoqi.js → compact-item-uIzmYwLs.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BsGsBM5m.js → createContext-D_ZmuLwb.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BccMLzxA.js → hasIn-CDt_shTL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cg9cOVe9.js → index-BAvSmJXW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTTeAiOP.js → index-BDF6EQBs.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3k3ZwkQ.js → index-BHvlm0In.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ1ORdp-.js → index-BRa4kJ_S.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcJzphrP.js → index-BTCXf1t-.js} +1 -1
- package/src/assets/web-panel/assets/{index-rIy96PeM.js → index-Bbxjfstq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSFESurl.js → index-Bx4d6c1k.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cmg_Npx5.js → index-ByzGvpR9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahDve8M.js → index-C16fYKFD.js} +1 -1
- package/src/assets/web-panel/assets/{index-2VstcImq.js → index-C5WcdJH2.js} +3 -3
- package/src/assets/web-panel/assets/{index-C3Rf8wIM.js → index-CG0lY4h3.js} +1 -1
- package/src/assets/web-panel/assets/{index-uzykuL_R.js → index-CKyd472S.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBVIFX-x.js → index-CaelyMgF.js} +1 -1
- package/src/assets/web-panel/assets/{index-TeCof1s3.js → index-CbD_WW_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfF0OGKU.js → index-CcZ9xJI9.js} +1 -1
- package/src/assets/web-panel/assets/{index-oWltafBn.js → index-CeUpKjvi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWLWBgfe.js → index-Cvak6Oe2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0AbXsDy.js → index-CzXqvMuP.js} +1 -1
- package/src/assets/web-panel/assets/index-D-Swwqld.js +1 -0
- package/src/assets/web-panel/assets/{index-BfV8RDlg.js → index-D4n1lGTB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxqiu9-F.js → index-D9OE8B3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdCmJ_bG.js → index-DBFmcaCQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgjQ-kdT.js → index-DdUMcwRB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBbbr1YB.js → index-DeGBoLQA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmk7dD-T.js → index-DfSSy9uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-JS2LXdd0.js → index-DqDSF2RW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9oV2iRR.js → index-DvFhhe6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvsII10i.js → index-DwfvPe-j.js} +1 -1
- package/src/assets/web-panel/assets/index-FMZAMKy8.js +1 -0
- package/src/assets/web-panel/assets/{index-CrZHUsOZ.js → index-FyplHuNt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLzPzN75.js → index-GcKOHD_c.js} +1 -1
- package/src/assets/web-panel/assets/{index-koT8Q86Z.js → index-OzTxFIJP.js} +1 -1
- package/src/assets/web-panel/assets/{index-08diKcTm.js → index-QsgfSZrV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJg6Wsjc.js → index-Z8P4BexE.js} +1 -1
- package/src/assets/web-panel/assets/{index-srqW5_-X.js → index-clSX4PEc.js} +1 -1
- package/src/assets/web-panel/assets/{index-mIQUoUx1.js → index-ifxmu8SY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz_s25m3.js → index-rS3gl_68.js} +1 -1
- package/src/assets/web-panel/assets/{index-UQXedWQz.js → index-rXdwUeE3.js} +1 -1
- package/src/assets/web-panel/assets/{index-rcwgNMc0.js → index-saf3VNeP.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-xafkqrW3.js → initDefaultProps-pmfaD-aK.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BrtyQXLk.js → motion-CaQ2CpYF.js} +1 -1
- package/src/assets/web-panel/assets/{move-BunUyg59.js → move-CkKTqNbI.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BPwjBeuE.js → mtc-parser-Dd4xsgRI.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BpyqZlNO.js → omit-BFuA5YEs.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDrV0XjE.js → pickAttrs-DDfxOAJc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-1A18Pcl2.js → placementArrow-Be595JCp.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B8Fnlmso.js → responsiveObserve-7byljUM3.js} +1 -1
- package/src/assets/web-panel/assets/{slide-x-Fwb2Zm.js → slide-CTWfOTaY.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-aAG5yJl_.js → statusUtils-CNZOuIxM.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BRQSTMRL.js → styleChecker-DwOcBV78.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D5X_KozY.js → useFlexGapSupport-B6MNwAO9.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DN_YIjP-.js → useFs-CzyQjH9r.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BizxAvjw.js → usePersonalDataHub-Dxh3KiVA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B5oAxN93.js → vnode-DVfZiS5N.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DJhvyzKv.js → zoom-BYme7fbY.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +22 -0
- package/src/commands/auto-mode.js +47 -0
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint.js +3 -0
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/data/auto-mode-safety-eval-v1.json +3294 -0
- package/src/data/changelog.json +26 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +319 -22
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/auto-mode-safety-classifier.js +2034 -0
- package/src/lib/auto-mode-safety-eval.js +1022 -0
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/process-execution-broker/index.js +438 -5
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- 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 +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +3556 -0
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +140 -23
- package/src/repl/agent-repl.js +19 -1
- package/src/runtime/agent-core.js +366 -122
- package/src/runtime/headless-runner.js +12 -0
- package/src/runtime/headless-stream.js +3 -0
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-BII41Mel.js +0 -5
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +0 -1
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +0 -1
- package/src/assets/web-panel/assets/index-C97f71oi.js +0 -1
|
@@ -0,0 +1,1501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable adjudication for ambiguous Agent Team side effects.
|
|
3
|
+
*
|
|
4
|
+
* A resumed real task may have executed an external side effect even though its
|
|
5
|
+
* registry snapshot still contains a dangling lease. The coordinator must not
|
|
6
|
+
* guess whether to replay it. This store records a human/operator decision
|
|
7
|
+
* against the exact recovery evidence and exposes a one-shot claim protocol:
|
|
8
|
+
*
|
|
9
|
+
* case.open -> case.decide -> case.claim -> case.apply
|
|
10
|
+
*
|
|
11
|
+
* `retry` claims authorize exactly one new execution. `accept` and `cancel`
|
|
12
|
+
* claims authorize only local task settlement. If a process dies after claiming
|
|
13
|
+
* a retry, recovery remains fail-closed because the claim cannot be issued
|
|
14
|
+
* again. A claimed accept/cancel is reported as replay-safe settlement work.
|
|
15
|
+
*
|
|
16
|
+
* The file is a logically append-only, hash-chained event log stored as one
|
|
17
|
+
* atomically replaced JSON document. Replaying it validates exact schemas,
|
|
18
|
+
* sequence numbers, transition legality, and every digest. External team state
|
|
19
|
+
* should persist the returned cursor; supplying it as `anchor` proves that the
|
|
20
|
+
* cursor is still an exact prefix and detects a valid-but-older rollback or a
|
|
21
|
+
* competing branch.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
27
|
+
import { TextDecoder } from "node:util";
|
|
28
|
+
import { withFileLock } from "../with-file-lock.js";
|
|
29
|
+
import {
|
|
30
|
+
sameFileStatIdentity,
|
|
31
|
+
samePathHandleFileIdentity,
|
|
32
|
+
SecureFileIdentityError,
|
|
33
|
+
withTrustedFileParentSync,
|
|
34
|
+
} from "../secure-file-identity.js";
|
|
35
|
+
|
|
36
|
+
export const TEAM_ADJUDICATION_SCHEMA_VERSION = 1;
|
|
37
|
+
export const TEAM_ADJUDICATION_DECISIONS = Object.freeze({
|
|
38
|
+
RETRY: "retry",
|
|
39
|
+
ACCEPT: "accept",
|
|
40
|
+
CANCEL: "cancel",
|
|
41
|
+
});
|
|
42
|
+
export const TEAM_ADJUDICATION_ACTIONS = Object.freeze({
|
|
43
|
+
retry: "retry_task_once",
|
|
44
|
+
accept: "complete_task",
|
|
45
|
+
cancel: "cancel_task",
|
|
46
|
+
});
|
|
47
|
+
export const TEAM_ADJUDICATION_ERROR_CODES = Object.freeze({
|
|
48
|
+
INVALID: "TEAM_ADJUDICATION_INVALID",
|
|
49
|
+
UNSAFE_PATH: "TEAM_ADJUDICATION_UNSAFE_PATH",
|
|
50
|
+
LOCK_UNAVAILABLE: "TEAM_ADJUDICATION_LOCK_UNAVAILABLE",
|
|
51
|
+
READ_FAILED: "TEAM_ADJUDICATION_READ_FAILED",
|
|
52
|
+
WRITE_FAILED: "TEAM_ADJUDICATION_WRITE_FAILED",
|
|
53
|
+
CORRUPT: "TEAM_ADJUDICATION_CORRUPT",
|
|
54
|
+
BINDING_MISMATCH: "TEAM_ADJUDICATION_BINDING_MISMATCH",
|
|
55
|
+
NOT_FOUND: "TEAM_ADJUDICATION_NOT_FOUND",
|
|
56
|
+
CONFLICT: "TEAM_ADJUDICATION_CONFLICT",
|
|
57
|
+
STALE: "TEAM_ADJUDICATION_STALE",
|
|
58
|
+
ROLLBACK: "TEAM_ADJUDICATION_ROLLBACK",
|
|
59
|
+
ALREADY_CLAIMED: "TEAM_ADJUDICATION_ALREADY_CLAIMED",
|
|
60
|
+
LIMIT: "TEAM_ADJUDICATION_LIMIT",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const MAX_TEAM_ADJUDICATION_EVENTS = 50_000;
|
|
64
|
+
export const MAX_TEAM_ADJUDICATION_BYTES = 64 * 1024 * 1024;
|
|
65
|
+
|
|
66
|
+
const STORE_SCHEMA = "cc.team-side-effect-adjudication";
|
|
67
|
+
const EVENT_VERSION = 1;
|
|
68
|
+
const DIGEST_PATTERN = /^(?:sha256:)?[a-f0-9]{64}$/;
|
|
69
|
+
const EVENT_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
70
|
+
const CASE_ID_PATTERN = /^tadj_[a-f0-9]{64}$/;
|
|
71
|
+
const CLAIM_ID_PATTERN =
|
|
72
|
+
/^tadj_claim_[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,126}[a-zA-Z0-9])?$/;
|
|
73
|
+
const DECISIONS = new Set(Object.values(TEAM_ADJUDICATION_DECISIONS));
|
|
74
|
+
const HEADER_KEYS = new Set([
|
|
75
|
+
"schema",
|
|
76
|
+
"schemaVersion",
|
|
77
|
+
"statePath",
|
|
78
|
+
"statePathDigest",
|
|
79
|
+
"collaborationRunId",
|
|
80
|
+
"createdAt",
|
|
81
|
+
"bindingDigest",
|
|
82
|
+
"events",
|
|
83
|
+
]);
|
|
84
|
+
const COMMON_EVENT_KEYS = [
|
|
85
|
+
"version",
|
|
86
|
+
"sequence",
|
|
87
|
+
"type",
|
|
88
|
+
"at",
|
|
89
|
+
"previousDigest",
|
|
90
|
+
"digest",
|
|
91
|
+
"bindingDigest",
|
|
92
|
+
"collaborationRunId",
|
|
93
|
+
"statePathDigest",
|
|
94
|
+
"caseId",
|
|
95
|
+
];
|
|
96
|
+
const EVENT_KEYS = Object.freeze({
|
|
97
|
+
"case.open": new Set([
|
|
98
|
+
...COMMON_EVENT_KEYS,
|
|
99
|
+
"taskKey",
|
|
100
|
+
"registryDigest",
|
|
101
|
+
"sideEffectDigest",
|
|
102
|
+
]),
|
|
103
|
+
"case.decide": new Set([
|
|
104
|
+
...COMMON_EVENT_KEYS,
|
|
105
|
+
"decision",
|
|
106
|
+
"authority",
|
|
107
|
+
"reasonDigest",
|
|
108
|
+
]),
|
|
109
|
+
"case.claim": new Set([
|
|
110
|
+
...COMMON_EVENT_KEYS,
|
|
111
|
+
"decisionDigest",
|
|
112
|
+
"claimId",
|
|
113
|
+
"consumer",
|
|
114
|
+
]),
|
|
115
|
+
"case.apply": new Set([...COMMON_EVENT_KEYS, "claimDigest", "outcomeDigest"]),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export class TeamAdjudicationError extends Error {
|
|
119
|
+
constructor(code, message, details = {}, cause = null) {
|
|
120
|
+
super(message, cause ? { cause } : undefined);
|
|
121
|
+
this.name = "TeamAdjudicationError";
|
|
122
|
+
this.code = code;
|
|
123
|
+
for (const [key, value] of Object.entries(details)) {
|
|
124
|
+
if (value !== undefined && value !== null) this[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function adjudicationError(code, message, details = {}, cause = null) {
|
|
130
|
+
return new TeamAdjudicationError(code, message, details, cause);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isAdjudicationError(error) {
|
|
134
|
+
return Object.values(TEAM_ADJUDICATION_ERROR_CODES).includes(error?.code);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertExactKeys(value, allowed, label) {
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
139
|
+
throw adjudicationError(
|
|
140
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
141
|
+
`Invalid team adjudication ${label}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const actual = Object.keys(value);
|
|
145
|
+
if (
|
|
146
|
+
actual.length !== allowed.size ||
|
|
147
|
+
actual.some((key) => !allowed.has(key))
|
|
148
|
+
) {
|
|
149
|
+
throw adjudicationError(
|
|
150
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
151
|
+
`Unexpected or missing field in team adjudication ${label}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function canonicalJson(value, seen = new Set()) {
|
|
157
|
+
if (value === null) return "null";
|
|
158
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
159
|
+
return JSON.stringify(value);
|
|
160
|
+
}
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
if (!Number.isFinite(value)) {
|
|
163
|
+
throw adjudicationError(
|
|
164
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
165
|
+
"Evidence values must contain only finite numbers",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
if (seen.has(value)) {
|
|
172
|
+
throw adjudicationError(
|
|
173
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
174
|
+
"Evidence values must not contain cycles",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
seen.add(value);
|
|
178
|
+
const encoded = `[${value
|
|
179
|
+
.map((item) => {
|
|
180
|
+
if (item === undefined || typeof item === "function") {
|
|
181
|
+
throw adjudicationError(
|
|
182
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
183
|
+
"Evidence arrays must not contain unsupported values",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return canonicalJson(item, seen);
|
|
187
|
+
})
|
|
188
|
+
.join(",")}]`;
|
|
189
|
+
seen.delete(value);
|
|
190
|
+
return encoded;
|
|
191
|
+
}
|
|
192
|
+
if (value && typeof value === "object") {
|
|
193
|
+
if (seen.has(value)) {
|
|
194
|
+
throw adjudicationError(
|
|
195
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
196
|
+
"Evidence values must not contain cycles",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const prototype = Object.getPrototypeOf(value);
|
|
200
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
201
|
+
throw adjudicationError(
|
|
202
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
203
|
+
"Evidence values must be plain JSON objects",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
seen.add(value);
|
|
207
|
+
const entries = [];
|
|
208
|
+
for (const key of Object.keys(value).sort()) {
|
|
209
|
+
const item = value[key];
|
|
210
|
+
if (item === undefined || typeof item === "function") {
|
|
211
|
+
throw adjudicationError(
|
|
212
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
213
|
+
"Evidence objects must not contain unsupported values",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
entries.push(`${JSON.stringify(key)}:${canonicalJson(item, seen)}`);
|
|
217
|
+
}
|
|
218
|
+
seen.delete(value);
|
|
219
|
+
return `{${entries.join(",")}}`;
|
|
220
|
+
}
|
|
221
|
+
throw adjudicationError(
|
|
222
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
223
|
+
"Evidence must be a JSON-compatible value",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sha256(domain, value) {
|
|
228
|
+
return `sha256:${createHash("sha256")
|
|
229
|
+
.update(domain, "utf8")
|
|
230
|
+
.update("\0", "utf8")
|
|
231
|
+
.update(value, "utf8")
|
|
232
|
+
.digest("hex")}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Produce a deterministic digest for a registry snapshot, side-effect ledger,
|
|
237
|
+
* reason, or post-application outcome without persisting the raw evidence.
|
|
238
|
+
*/
|
|
239
|
+
export function computeTeamAdjudicationEvidenceDigest(value) {
|
|
240
|
+
return sha256("cc-team-adjudication-evidence-v1", canonicalJson(value));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeDigest(value, fieldName, { nullable = false } = {}) {
|
|
244
|
+
if (nullable && (value === null || value === undefined || value === "")) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const raw = String(value ?? "")
|
|
248
|
+
.trim()
|
|
249
|
+
.toLowerCase();
|
|
250
|
+
if (!DIGEST_PATTERN.test(raw)) {
|
|
251
|
+
throw adjudicationError(
|
|
252
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
253
|
+
`${fieldName} must be a SHA-256 digest`,
|
|
254
|
+
{ field: fieldName },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return raw.startsWith("sha256:") ? raw : `sha256:${raw}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function requiredLabel(value, fieldName, max = 256) {
|
|
261
|
+
if (typeof value !== "string") {
|
|
262
|
+
throw adjudicationError(
|
|
263
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
264
|
+
`${fieldName} must be a non-empty string`,
|
|
265
|
+
{ field: fieldName },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const normalized = value.trim();
|
|
269
|
+
if (
|
|
270
|
+
!normalized ||
|
|
271
|
+
Buffer.byteLength(normalized, "utf8") > max ||
|
|
272
|
+
/\p{Cc}/u.test(normalized)
|
|
273
|
+
) {
|
|
274
|
+
throw adjudicationError(
|
|
275
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
276
|
+
`${fieldName} must be a bounded string without control characters`,
|
|
277
|
+
{ field: fieldName },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return normalized;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeRunId(value) {
|
|
284
|
+
const runId = requiredLabel(value, "collaborationRunId", 160);
|
|
285
|
+
if (!/^(team|batch)-[a-zA-Z0-9._-]+$/.test(runId)) {
|
|
286
|
+
throw adjudicationError(
|
|
287
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
288
|
+
"collaborationRunId is not a valid team or batch run id",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return runId;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function normalizeDecision(value) {
|
|
295
|
+
const decision = String(value ?? "")
|
|
296
|
+
.trim()
|
|
297
|
+
.toLowerCase();
|
|
298
|
+
if (!DECISIONS.has(decision)) {
|
|
299
|
+
throw adjudicationError(
|
|
300
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
301
|
+
"decision must be retry, accept, or cancel",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return decision;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeFiniteTime(value, fieldName) {
|
|
308
|
+
const number = Number(value);
|
|
309
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
310
|
+
throw adjudicationError(
|
|
311
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
312
|
+
`${fieldName} must be a non-negative finite timestamp`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function pathIdentity(value, platform = process.platform) {
|
|
319
|
+
const normalized = path.normalize(value);
|
|
320
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function unsafePath(message, filePath, cause = null) {
|
|
324
|
+
return adjudicationError(
|
|
325
|
+
TEAM_ADJUDICATION_ERROR_CODES.UNSAFE_PATH,
|
|
326
|
+
message,
|
|
327
|
+
{ filePath },
|
|
328
|
+
cause,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function lstatOrNull(runtimeFs, filePath, options = undefined) {
|
|
333
|
+
try {
|
|
334
|
+
return runtimeFs.lstatSync(filePath, options);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error?.code === "ENOENT") return null;
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function assertRegularSingleLink(runtimeFs, filePath, options = {}) {
|
|
342
|
+
const {
|
|
343
|
+
allowMissing = false,
|
|
344
|
+
bigint = false,
|
|
345
|
+
requireMode0600 = false,
|
|
346
|
+
platform = process.platform,
|
|
347
|
+
} = options;
|
|
348
|
+
let entry;
|
|
349
|
+
try {
|
|
350
|
+
entry = lstatOrNull(
|
|
351
|
+
runtimeFs,
|
|
352
|
+
filePath,
|
|
353
|
+
bigint ? { bigint: true } : undefined,
|
|
354
|
+
);
|
|
355
|
+
} catch (cause) {
|
|
356
|
+
throw unsafePath(
|
|
357
|
+
`Could not inspect team adjudication path: ${filePath}`,
|
|
358
|
+
filePath,
|
|
359
|
+
cause,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (entry === null) {
|
|
363
|
+
if (allowMissing) return null;
|
|
364
|
+
throw unsafePath(
|
|
365
|
+
`Required team adjudication authority file is missing: ${filePath}`,
|
|
366
|
+
filePath,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (entry.isSymbolicLink() || !entry.isFile() || Number(entry.nlink) !== 1) {
|
|
370
|
+
throw unsafePath(
|
|
371
|
+
`Team adjudication authority must be a regular, non-symlink, single-link file: ${filePath}`,
|
|
372
|
+
filePath,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
requireMode0600 &&
|
|
377
|
+
platform !== "win32" &&
|
|
378
|
+
(Number(entry.mode) & 0o777) !== 0o600
|
|
379
|
+
) {
|
|
380
|
+
throw unsafePath(
|
|
381
|
+
`Team adjudication authority must have mode 0600: ${filePath}`,
|
|
382
|
+
filePath,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return entry;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function canonicalAuthorityPath(runtimeFs, input, label, { mustExist }) {
|
|
389
|
+
const raw = requiredLabel(input, label, 4096);
|
|
390
|
+
const requested = path.resolve(raw);
|
|
391
|
+
const parent = path.dirname(requested);
|
|
392
|
+
let canonicalParent;
|
|
393
|
+
try {
|
|
394
|
+
canonicalParent = runtimeFs.realpathSync.native(parent);
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
throw unsafePath(
|
|
397
|
+
`Could not canonicalize ${label} parent: ${parent}`,
|
|
398
|
+
requested,
|
|
399
|
+
cause,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const target = path.join(canonicalParent, path.basename(requested));
|
|
403
|
+
const entry = assertRegularSingleLink(runtimeFs, target, {
|
|
404
|
+
allowMissing: !mustExist,
|
|
405
|
+
});
|
|
406
|
+
if (!entry) return target;
|
|
407
|
+
let canonicalTarget;
|
|
408
|
+
try {
|
|
409
|
+
canonicalTarget = runtimeFs.realpathSync.native(target);
|
|
410
|
+
} catch (cause) {
|
|
411
|
+
throw unsafePath(
|
|
412
|
+
`Could not canonicalize ${label}: ${target}`,
|
|
413
|
+
target,
|
|
414
|
+
cause,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (pathIdentity(canonicalTarget) !== pathIdentity(target)) {
|
|
418
|
+
throw unsafePath(
|
|
419
|
+
`${label} resolves through an unsafe alias: ${target}`,
|
|
420
|
+
target,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return canonicalTarget;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function bindingHeader({
|
|
427
|
+
statePath,
|
|
428
|
+
statePathDigest,
|
|
429
|
+
collaborationRunId,
|
|
430
|
+
createdAt,
|
|
431
|
+
}) {
|
|
432
|
+
return {
|
|
433
|
+
schema: STORE_SCHEMA,
|
|
434
|
+
schemaVersion: TEAM_ADJUDICATION_SCHEMA_VERSION,
|
|
435
|
+
statePath,
|
|
436
|
+
statePathDigest,
|
|
437
|
+
collaborationRunId,
|
|
438
|
+
createdAt,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function computeBindingDigest(header) {
|
|
443
|
+
return sha256(
|
|
444
|
+
"cc-team-adjudication-binding-v1",
|
|
445
|
+
canonicalJson(bindingHeader(header)),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function caseBinding(store, input) {
|
|
450
|
+
const taskKey = requiredLabel(input?.taskKey, "taskKey", 256);
|
|
451
|
+
const registryDigest = normalizeDigest(
|
|
452
|
+
input?.registryDigest,
|
|
453
|
+
"registryDigest",
|
|
454
|
+
);
|
|
455
|
+
const sideEffectDigest = normalizeDigest(
|
|
456
|
+
input?.sideEffectDigest,
|
|
457
|
+
"sideEffectDigest",
|
|
458
|
+
);
|
|
459
|
+
const material = {
|
|
460
|
+
statePathDigest: store.statePathDigest,
|
|
461
|
+
collaborationRunId: store.collaborationRunId,
|
|
462
|
+
taskKey,
|
|
463
|
+
registryDigest,
|
|
464
|
+
sideEffectDigest,
|
|
465
|
+
};
|
|
466
|
+
const caseId = `tadj_${sha256(
|
|
467
|
+
"cc-team-adjudication-case-v1",
|
|
468
|
+
canonicalJson(material),
|
|
469
|
+
).slice("sha256:".length)}`;
|
|
470
|
+
return {
|
|
471
|
+
caseId,
|
|
472
|
+
taskKey,
|
|
473
|
+
registryDigest,
|
|
474
|
+
sideEffectDigest,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function computeEventDigest(event) {
|
|
479
|
+
const payload = { ...event };
|
|
480
|
+
delete payload.digest;
|
|
481
|
+
return sha256("cc-team-adjudication-event-v1", canonicalJson(payload));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function eventEnvelope(runtime, type, caseId, at) {
|
|
485
|
+
return {
|
|
486
|
+
version: EVENT_VERSION,
|
|
487
|
+
sequence: runtime.lastSequence + 1,
|
|
488
|
+
type,
|
|
489
|
+
at: Math.max(at, runtime.lastAt),
|
|
490
|
+
previousDigest: runtime.headDigest,
|
|
491
|
+
digest: null,
|
|
492
|
+
bindingDigest: runtime.document.bindingDigest,
|
|
493
|
+
collaborationRunId: runtime.document.collaborationRunId,
|
|
494
|
+
statePathDigest: runtime.document.statePathDigest,
|
|
495
|
+
caseId,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildEvent(runtime, type, caseId, at, fields) {
|
|
500
|
+
const event = {
|
|
501
|
+
...eventEnvelope(runtime, type, caseId, at),
|
|
502
|
+
...fields,
|
|
503
|
+
};
|
|
504
|
+
event.digest = computeEventDigest(event);
|
|
505
|
+
return event;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function assertEventEnvelope(runtime, event) {
|
|
509
|
+
const allowed = EVENT_KEYS[event?.type];
|
|
510
|
+
if (!allowed) {
|
|
511
|
+
throw adjudicationError(
|
|
512
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
513
|
+
"Team adjudication contains an unsupported event type",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
assertExactKeys(event, allowed, "event");
|
|
517
|
+
if (
|
|
518
|
+
event.version !== EVENT_VERSION ||
|
|
519
|
+
!Number.isSafeInteger(event.sequence) ||
|
|
520
|
+
event.sequence !== runtime.lastSequence + 1 ||
|
|
521
|
+
!Number.isFinite(event.at) ||
|
|
522
|
+
event.at < runtime.lastAt ||
|
|
523
|
+
event.previousDigest !== runtime.headDigest ||
|
|
524
|
+
event.bindingDigest !== runtime.document.bindingDigest ||
|
|
525
|
+
event.collaborationRunId !== runtime.document.collaborationRunId ||
|
|
526
|
+
event.statePathDigest !== runtime.document.statePathDigest ||
|
|
527
|
+
!CASE_ID_PATTERN.test(event.caseId || "") ||
|
|
528
|
+
!EVENT_DIGEST_PATTERN.test(event.digest || "") ||
|
|
529
|
+
computeEventDigest(event) !== event.digest
|
|
530
|
+
) {
|
|
531
|
+
throw adjudicationError(
|
|
532
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
533
|
+
`Invalid team adjudication event at sequence ${String(event.sequence)}`,
|
|
534
|
+
{ sequence: event.sequence },
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function replayEvent(runtime, event) {
|
|
540
|
+
assertEventEnvelope(runtime, event);
|
|
541
|
+
let adjudicationCase = runtime.cases.get(event.caseId);
|
|
542
|
+
|
|
543
|
+
if (event.type === "case.open") {
|
|
544
|
+
const binding = caseBinding(runtime.store, event);
|
|
545
|
+
if (
|
|
546
|
+
binding.caseId !== event.caseId ||
|
|
547
|
+
binding.taskKey !== event.taskKey ||
|
|
548
|
+
binding.registryDigest !== event.registryDigest ||
|
|
549
|
+
binding.sideEffectDigest !== event.sideEffectDigest ||
|
|
550
|
+
adjudicationCase
|
|
551
|
+
) {
|
|
552
|
+
throw adjudicationError(
|
|
553
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
554
|
+
"Invalid or duplicate team adjudication case binding",
|
|
555
|
+
{ caseId: event.caseId },
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const prior = runtime.byTask.get(event.taskKey) || [];
|
|
559
|
+
const latest = prior[prior.length - 1] || null;
|
|
560
|
+
if (
|
|
561
|
+
latest &&
|
|
562
|
+
(!latest.application ||
|
|
563
|
+
latest.decision?.value !== TEAM_ADJUDICATION_DECISIONS.RETRY)
|
|
564
|
+
) {
|
|
565
|
+
throw adjudicationError(
|
|
566
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
567
|
+
"Team adjudication log forks an active or terminal task case",
|
|
568
|
+
{ caseId: event.caseId, taskKey: event.taskKey },
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
adjudicationCase = {
|
|
572
|
+
binding,
|
|
573
|
+
opened: event,
|
|
574
|
+
decision: null,
|
|
575
|
+
claim: null,
|
|
576
|
+
application: null,
|
|
577
|
+
};
|
|
578
|
+
runtime.cases.set(event.caseId, adjudicationCase);
|
|
579
|
+
prior.push(adjudicationCase);
|
|
580
|
+
runtime.byTask.set(event.taskKey, prior);
|
|
581
|
+
} else {
|
|
582
|
+
if (!adjudicationCase) {
|
|
583
|
+
throw adjudicationError(
|
|
584
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
585
|
+
"Team adjudication event refers to an unknown case",
|
|
586
|
+
{ caseId: event.caseId },
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (event.type === "case.decide") {
|
|
590
|
+
if (
|
|
591
|
+
adjudicationCase.decision ||
|
|
592
|
+
!DECISIONS.has(event.decision) ||
|
|
593
|
+
requiredLabel(event.authority, "authority", 256) !== event.authority ||
|
|
594
|
+
normalizeDigest(event.reasonDigest, "reasonDigest", {
|
|
595
|
+
nullable: true,
|
|
596
|
+
}) !== event.reasonDigest
|
|
597
|
+
) {
|
|
598
|
+
throw adjudicationError(
|
|
599
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
600
|
+
"Invalid or duplicate team adjudication decision",
|
|
601
|
+
{ caseId: event.caseId },
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
adjudicationCase.decision = {
|
|
605
|
+
value: event.decision,
|
|
606
|
+
authority: event.authority,
|
|
607
|
+
reasonDigest: event.reasonDigest,
|
|
608
|
+
event,
|
|
609
|
+
};
|
|
610
|
+
} else if (event.type === "case.claim") {
|
|
611
|
+
if (
|
|
612
|
+
!adjudicationCase.decision ||
|
|
613
|
+
adjudicationCase.claim ||
|
|
614
|
+
event.decisionDigest !== adjudicationCase.decision.event.digest ||
|
|
615
|
+
!CLAIM_ID_PATTERN.test(event.claimId || "") ||
|
|
616
|
+
requiredLabel(event.consumer, "consumer", 256) !== event.consumer
|
|
617
|
+
) {
|
|
618
|
+
throw adjudicationError(
|
|
619
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
620
|
+
"Invalid or duplicate team adjudication claim",
|
|
621
|
+
{ caseId: event.caseId },
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
adjudicationCase.claim = {
|
|
625
|
+
claimId: event.claimId,
|
|
626
|
+
consumer: event.consumer,
|
|
627
|
+
decisionDigest: event.decisionDigest,
|
|
628
|
+
event,
|
|
629
|
+
};
|
|
630
|
+
} else {
|
|
631
|
+
if (
|
|
632
|
+
!adjudicationCase.claim ||
|
|
633
|
+
adjudicationCase.application ||
|
|
634
|
+
event.claimDigest !== adjudicationCase.claim.event.digest ||
|
|
635
|
+
normalizeDigest(event.outcomeDigest, "outcomeDigest") !==
|
|
636
|
+
event.outcomeDigest
|
|
637
|
+
) {
|
|
638
|
+
throw adjudicationError(
|
|
639
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
640
|
+
"Invalid or duplicate team adjudication application",
|
|
641
|
+
{ caseId: event.caseId },
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
adjudicationCase.application = {
|
|
645
|
+
claimDigest: event.claimDigest,
|
|
646
|
+
outcomeDigest: event.outcomeDigest,
|
|
647
|
+
event,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
runtime.lastSequence = event.sequence;
|
|
653
|
+
runtime.lastAt = event.at;
|
|
654
|
+
runtime.headDigest = event.digest;
|
|
655
|
+
runtime.digestBySequence.set(event.sequence, event.digest);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function recoveryFor(adjudicationCase) {
|
|
659
|
+
if (!adjudicationCase.decision) {
|
|
660
|
+
return {
|
|
661
|
+
state: "adjudication_required",
|
|
662
|
+
action: null,
|
|
663
|
+
replaySafe: false,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const action = TEAM_ADJUDICATION_ACTIONS[adjudicationCase.decision.value];
|
|
667
|
+
if (!adjudicationCase.claim) {
|
|
668
|
+
return { state: "decision_ready", action, replaySafe: false };
|
|
669
|
+
}
|
|
670
|
+
if (!adjudicationCase.application) {
|
|
671
|
+
if (adjudicationCase.decision.value === TEAM_ADJUDICATION_DECISIONS.RETRY) {
|
|
672
|
+
return {
|
|
673
|
+
state: "retry_outcome_unknown",
|
|
674
|
+
action: null,
|
|
675
|
+
replaySafe: false,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
state: "settlement_recovery_required",
|
|
680
|
+
action,
|
|
681
|
+
replaySafe: true,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return { state: "complete", action: null, replaySafe: false };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function caseView(adjudicationCase) {
|
|
688
|
+
const revision = adjudicationCase.application
|
|
689
|
+
? 4
|
|
690
|
+
: adjudicationCase.claim
|
|
691
|
+
? 3
|
|
692
|
+
: adjudicationCase.decision
|
|
693
|
+
? 2
|
|
694
|
+
: 1;
|
|
695
|
+
const status = adjudicationCase.application
|
|
696
|
+
? "applied"
|
|
697
|
+
: adjudicationCase.claim
|
|
698
|
+
? "claimed"
|
|
699
|
+
: adjudicationCase.decision
|
|
700
|
+
? "authorized"
|
|
701
|
+
: "pending";
|
|
702
|
+
return {
|
|
703
|
+
...adjudicationCase.binding,
|
|
704
|
+
status,
|
|
705
|
+
revision,
|
|
706
|
+
openedAt: adjudicationCase.opened.at,
|
|
707
|
+
openDigest: adjudicationCase.opened.digest,
|
|
708
|
+
decision: adjudicationCase.decision
|
|
709
|
+
? {
|
|
710
|
+
value: adjudicationCase.decision.value,
|
|
711
|
+
authority: adjudicationCase.decision.authority,
|
|
712
|
+
reasonDigest: adjudicationCase.decision.reasonDigest,
|
|
713
|
+
decidedAt: adjudicationCase.decision.event.at,
|
|
714
|
+
decisionDigest: adjudicationCase.decision.event.digest,
|
|
715
|
+
}
|
|
716
|
+
: null,
|
|
717
|
+
claim: adjudicationCase.claim
|
|
718
|
+
? {
|
|
719
|
+
claimId: adjudicationCase.claim.claimId,
|
|
720
|
+
consumer: adjudicationCase.claim.consumer,
|
|
721
|
+
claimedAt: adjudicationCase.claim.event.at,
|
|
722
|
+
claimDigest: adjudicationCase.claim.event.digest,
|
|
723
|
+
}
|
|
724
|
+
: null,
|
|
725
|
+
application: adjudicationCase.application
|
|
726
|
+
? {
|
|
727
|
+
outcomeDigest: adjudicationCase.application.outcomeDigest,
|
|
728
|
+
appliedAt: adjudicationCase.application.event.at,
|
|
729
|
+
applicationDigest: adjudicationCase.application.event.digest,
|
|
730
|
+
}
|
|
731
|
+
: null,
|
|
732
|
+
recovery: recoveryFor(adjudicationCase),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function cursorFromRuntime(runtime) {
|
|
737
|
+
return {
|
|
738
|
+
version: TEAM_ADJUDICATION_SCHEMA_VERSION,
|
|
739
|
+
statePathDigest: runtime.store.statePathDigest,
|
|
740
|
+
collaborationRunId: runtime.store.collaborationRunId,
|
|
741
|
+
lastSequence: runtime.lastSequence,
|
|
742
|
+
headDigest: runtime.headDigest,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function emptyRuntime(store) {
|
|
747
|
+
return {
|
|
748
|
+
store,
|
|
749
|
+
document: null,
|
|
750
|
+
cases: new Map(),
|
|
751
|
+
byTask: new Map(),
|
|
752
|
+
digestBySequence: new Map(),
|
|
753
|
+
lastSequence: 0,
|
|
754
|
+
lastAt: 0,
|
|
755
|
+
headDigest: null,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function replayDocument(store, document) {
|
|
760
|
+
assertExactKeys(document, HEADER_KEYS, "document");
|
|
761
|
+
if (
|
|
762
|
+
document.schema !== STORE_SCHEMA ||
|
|
763
|
+
document.schemaVersion !== TEAM_ADJUDICATION_SCHEMA_VERSION ||
|
|
764
|
+
document.statePath !== store.statePath ||
|
|
765
|
+
document.statePathDigest !== store.statePathDigest ||
|
|
766
|
+
document.collaborationRunId !== store.collaborationRunId ||
|
|
767
|
+
!Number.isFinite(document.createdAt) ||
|
|
768
|
+
document.createdAt < 0 ||
|
|
769
|
+
!EVENT_DIGEST_PATTERN.test(document.bindingDigest || "") ||
|
|
770
|
+
computeBindingDigest(document) !== document.bindingDigest ||
|
|
771
|
+
!Array.isArray(document.events)
|
|
772
|
+
) {
|
|
773
|
+
throw adjudicationError(
|
|
774
|
+
TEAM_ADJUDICATION_ERROR_CODES.BINDING_MISMATCH,
|
|
775
|
+
"Team adjudication document does not match its state/run binding",
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (document.events.length > store.maxEvents) {
|
|
779
|
+
throw adjudicationError(
|
|
780
|
+
TEAM_ADJUDICATION_ERROR_CODES.LIMIT,
|
|
781
|
+
`Team adjudication event limit exceeded (${store.maxEvents})`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (document.events.length === 0) {
|
|
785
|
+
throw adjudicationError(
|
|
786
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
787
|
+
"A persisted team adjudication document cannot have an empty log",
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const runtime = emptyRuntime(store);
|
|
792
|
+
runtime.document = document;
|
|
793
|
+
runtime.lastAt = document.createdAt;
|
|
794
|
+
for (const event of document.events) replayEvent(runtime, event);
|
|
795
|
+
return runtime;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function validateCursorIdentity(store, cursor) {
|
|
799
|
+
if (
|
|
800
|
+
!cursor ||
|
|
801
|
+
cursor.version !== TEAM_ADJUDICATION_SCHEMA_VERSION ||
|
|
802
|
+
cursor.statePathDigest !== store.statePathDigest ||
|
|
803
|
+
cursor.collaborationRunId !== store.collaborationRunId ||
|
|
804
|
+
!Number.isSafeInteger(cursor.lastSequence) ||
|
|
805
|
+
cursor.lastSequence < 0 ||
|
|
806
|
+
(cursor.lastSequence === 0
|
|
807
|
+
? cursor.headDigest !== null
|
|
808
|
+
: !EVENT_DIGEST_PATTERN.test(cursor.headDigest || ""))
|
|
809
|
+
) {
|
|
810
|
+
throw adjudicationError(
|
|
811
|
+
TEAM_ADJUDICATION_ERROR_CODES.ROLLBACK,
|
|
812
|
+
"Invalid or mismatched team adjudication recovery cursor",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function validateAnchor(runtime, anchor) {
|
|
818
|
+
if (anchor == null) return;
|
|
819
|
+
validateCursorIdentity(runtime.store, anchor);
|
|
820
|
+
const matches =
|
|
821
|
+
anchor.lastSequence === 0
|
|
822
|
+
? anchor.headDigest === null
|
|
823
|
+
: runtime.digestBySequence.get(anchor.lastSequence) === anchor.headDigest;
|
|
824
|
+
if (anchor.lastSequence > runtime.lastSequence || !matches) {
|
|
825
|
+
throw adjudicationError(
|
|
826
|
+
TEAM_ADJUDICATION_ERROR_CODES.ROLLBACK,
|
|
827
|
+
"Team adjudication log rolled back or diverged from its recovery cursor",
|
|
828
|
+
{
|
|
829
|
+
expectedSequence: anchor.lastSequence,
|
|
830
|
+
actualSequence: runtime.lastSequence,
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function validateExpectedCursor(runtime, expectedCursor) {
|
|
837
|
+
if (expectedCursor == null) return;
|
|
838
|
+
validateCursorIdentity(runtime.store, expectedCursor);
|
|
839
|
+
if (
|
|
840
|
+
expectedCursor.lastSequence !== runtime.lastSequence ||
|
|
841
|
+
expectedCursor.headDigest !== runtime.headDigest
|
|
842
|
+
) {
|
|
843
|
+
throw adjudicationError(
|
|
844
|
+
TEAM_ADJUDICATION_ERROR_CODES.STALE,
|
|
845
|
+
"Team adjudication mutation was based on a stale cursor",
|
|
846
|
+
{
|
|
847
|
+
expectedSequence: expectedCursor.lastSequence,
|
|
848
|
+
actualSequence: runtime.lastSequence,
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function validateRevision(adjudicationCase, expectedRevision) {
|
|
855
|
+
const current = caseView(adjudicationCase).revision;
|
|
856
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision !== current) {
|
|
857
|
+
throw adjudicationError(
|
|
858
|
+
TEAM_ADJUDICATION_ERROR_CODES.STALE,
|
|
859
|
+
`Team adjudication case revision changed (expected ${String(
|
|
860
|
+
expectedRevision,
|
|
861
|
+
)}, actual ${current})`,
|
|
862
|
+
{
|
|
863
|
+
caseId: adjudicationCase.binding.caseId,
|
|
864
|
+
expectedRevision,
|
|
865
|
+
actualRevision: current,
|
|
866
|
+
},
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function appendEvent(runtime, event) {
|
|
872
|
+
replayEvent(runtime, event);
|
|
873
|
+
runtime.document.events.push(event);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Durable store/control protocol shared by the CLI coordinator and IDE-facing
|
|
878
|
+
* adapters. All methods are synchronous so one strict cross-process lock covers
|
|
879
|
+
* each complete read/verify/mutate/atomic-write transaction.
|
|
880
|
+
*/
|
|
881
|
+
export class TeamAdjudicationStore {
|
|
882
|
+
constructor({
|
|
883
|
+
statePath,
|
|
884
|
+
collaborationRunId,
|
|
885
|
+
filePath = null,
|
|
886
|
+
now = () => Date.now(),
|
|
887
|
+
lockTimeoutMs = 2000,
|
|
888
|
+
lockStaleMs = 30_000,
|
|
889
|
+
maxEvents = MAX_TEAM_ADJUDICATION_EVENTS,
|
|
890
|
+
maxBytes = MAX_TEAM_ADJUDICATION_BYTES,
|
|
891
|
+
_fs = fs,
|
|
892
|
+
_lock = withFileLock,
|
|
893
|
+
_randomUUID = randomUUID,
|
|
894
|
+
_platform = process.platform,
|
|
895
|
+
} = {}) {
|
|
896
|
+
this._fs = _fs;
|
|
897
|
+
this._lock = _lock;
|
|
898
|
+
this._randomUUID = _randomUUID;
|
|
899
|
+
this._platform = _platform;
|
|
900
|
+
this._now = typeof now === "function" ? now : () => Number(now);
|
|
901
|
+
this.lockTimeoutMs = Math.max(0, Number(lockTimeoutMs) || 0);
|
|
902
|
+
this.lockStaleMs = Math.max(1, Number(lockStaleMs) || 30_000);
|
|
903
|
+
this.maxEvents = Math.max(
|
|
904
|
+
1,
|
|
905
|
+
Math.min(
|
|
906
|
+
MAX_TEAM_ADJUDICATION_EVENTS,
|
|
907
|
+
Math.floor(Number(maxEvents) || MAX_TEAM_ADJUDICATION_EVENTS),
|
|
908
|
+
),
|
|
909
|
+
);
|
|
910
|
+
this.maxBytes = Math.max(
|
|
911
|
+
1024,
|
|
912
|
+
Math.min(
|
|
913
|
+
MAX_TEAM_ADJUDICATION_BYTES,
|
|
914
|
+
Math.floor(Number(maxBytes) || MAX_TEAM_ADJUDICATION_BYTES),
|
|
915
|
+
),
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
this.statePath = canonicalAuthorityPath(this._fs, statePath, "statePath", {
|
|
919
|
+
mustExist: true,
|
|
920
|
+
});
|
|
921
|
+
this.collaborationRunId = normalizeRunId(collaborationRunId);
|
|
922
|
+
this.statePathDigest = sha256(
|
|
923
|
+
"cc-team-adjudication-state-path-v1",
|
|
924
|
+
pathIdentity(this.statePath, this._platform),
|
|
925
|
+
);
|
|
926
|
+
const requestedFile =
|
|
927
|
+
filePath == null ? `${this.statePath}.adjudication.json` : filePath;
|
|
928
|
+
this.filePath = canonicalAuthorityPath(
|
|
929
|
+
this._fs,
|
|
930
|
+
requestedFile,
|
|
931
|
+
"filePath",
|
|
932
|
+
{ mustExist: false },
|
|
933
|
+
);
|
|
934
|
+
this.lockPath = `${this.filePath}.lock`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
_assertAuthorityPaths() {
|
|
938
|
+
const currentStatePath = canonicalAuthorityPath(
|
|
939
|
+
this._fs,
|
|
940
|
+
this.statePath,
|
|
941
|
+
"statePath",
|
|
942
|
+
{ mustExist: true },
|
|
943
|
+
);
|
|
944
|
+
if (
|
|
945
|
+
pathIdentity(currentStatePath, this._platform) !==
|
|
946
|
+
pathIdentity(this.statePath, this._platform)
|
|
947
|
+
) {
|
|
948
|
+
throw unsafePath(
|
|
949
|
+
"Team state canonical path changed after adjudication binding",
|
|
950
|
+
this.statePath,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const currentFile = canonicalAuthorityPath(
|
|
954
|
+
this._fs,
|
|
955
|
+
this.filePath,
|
|
956
|
+
"filePath",
|
|
957
|
+
{ mustExist: false },
|
|
958
|
+
);
|
|
959
|
+
if (
|
|
960
|
+
pathIdentity(currentFile, this._platform) !==
|
|
961
|
+
pathIdentity(this.filePath, this._platform)
|
|
962
|
+
) {
|
|
963
|
+
throw unsafePath(
|
|
964
|
+
"Team adjudication canonical path changed",
|
|
965
|
+
this.filePath,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const adjudicationEntry = assertRegularSingleLink(this._fs, this.filePath, {
|
|
969
|
+
allowMissing: true,
|
|
970
|
+
requireMode0600: true,
|
|
971
|
+
platform: this._platform,
|
|
972
|
+
});
|
|
973
|
+
const lockEntry = lstatOrNull(this._fs, this.lockPath);
|
|
974
|
+
if (lockEntry && (lockEntry.isSymbolicLink() || !lockEntry.isDirectory())) {
|
|
975
|
+
throw unsafePath(
|
|
976
|
+
"Team adjudication lock path must be a non-symlink directory",
|
|
977
|
+
this.lockPath,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return adjudicationEntry;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
_secureRead() {
|
|
984
|
+
try {
|
|
985
|
+
return withTrustedFileParentSync(
|
|
986
|
+
this._fs,
|
|
987
|
+
this.filePath,
|
|
988
|
+
({ canonicalPath, parentDevice }) => {
|
|
989
|
+
const before = assertRegularSingleLink(this._fs, canonicalPath, {
|
|
990
|
+
bigint: true,
|
|
991
|
+
requireMode0600: true,
|
|
992
|
+
platform: this._platform,
|
|
993
|
+
});
|
|
994
|
+
if (Number(before.size) <= 0 || Number(before.size) > this.maxBytes) {
|
|
995
|
+
throw adjudicationError(
|
|
996
|
+
TEAM_ADJUDICATION_ERROR_CODES.LIMIT,
|
|
997
|
+
`Team adjudication file must be between 1 and ${this.maxBytes} bytes`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
let descriptor = null;
|
|
1001
|
+
try {
|
|
1002
|
+
const flags =
|
|
1003
|
+
Number(this._fs.constants?.O_RDONLY ?? fs.constants.O_RDONLY) |
|
|
1004
|
+
Number(
|
|
1005
|
+
this._fs.constants?.O_NOFOLLOW ?? fs.constants.O_NOFOLLOW ?? 0,
|
|
1006
|
+
);
|
|
1007
|
+
descriptor = this._fs.openSync(canonicalPath, flags);
|
|
1008
|
+
const opened = this._fs.fstatSync(descriptor, { bigint: true });
|
|
1009
|
+
if (
|
|
1010
|
+
!opened.isFile() ||
|
|
1011
|
+
Number(opened.nlink) !== 1 ||
|
|
1012
|
+
!samePathHandleFileIdentity(before, opened, parentDevice)
|
|
1013
|
+
) {
|
|
1014
|
+
throw unsafePath(
|
|
1015
|
+
"Team adjudication file identity changed while opening",
|
|
1016
|
+
this.filePath,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const bytes = this._fs.readFileSync(descriptor);
|
|
1020
|
+
const after = this._fs.fstatSync(descriptor, { bigint: true });
|
|
1021
|
+
if (
|
|
1022
|
+
!sameFileStatIdentity(opened, after) ||
|
|
1023
|
+
Number(after.size) !== bytes.length
|
|
1024
|
+
) {
|
|
1025
|
+
throw unsafePath(
|
|
1026
|
+
"Team adjudication file changed while reading",
|
|
1027
|
+
this.filePath,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return bytes;
|
|
1031
|
+
} finally {
|
|
1032
|
+
if (descriptor !== null) {
|
|
1033
|
+
try {
|
|
1034
|
+
this._fs.closeSync(descriptor);
|
|
1035
|
+
} catch {
|
|
1036
|
+
// The authoritative read error, if any, is reported above.
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
);
|
|
1042
|
+
} catch (cause) {
|
|
1043
|
+
if (isAdjudicationError(cause)) throw cause;
|
|
1044
|
+
if (cause instanceof SecureFileIdentityError) {
|
|
1045
|
+
throw unsafePath(
|
|
1046
|
+
"Team adjudication parent identity is unsafe",
|
|
1047
|
+
this.filePath,
|
|
1048
|
+
cause,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
throw adjudicationError(
|
|
1052
|
+
TEAM_ADJUDICATION_ERROR_CODES.READ_FAILED,
|
|
1053
|
+
"Could not securely read team adjudication state",
|
|
1054
|
+
{ filePath: this.filePath },
|
|
1055
|
+
cause,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
_decodeDocument(bytes) {
|
|
1061
|
+
let text;
|
|
1062
|
+
try {
|
|
1063
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1064
|
+
} catch (cause) {
|
|
1065
|
+
throw adjudicationError(
|
|
1066
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
1067
|
+
"Team adjudication state is not valid UTF-8",
|
|
1068
|
+
{},
|
|
1069
|
+
cause,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const parsed = JSON.parse(text);
|
|
1074
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1075
|
+
throw new TypeError("not an object");
|
|
1076
|
+
}
|
|
1077
|
+
return parsed;
|
|
1078
|
+
} catch (cause) {
|
|
1079
|
+
throw adjudicationError(
|
|
1080
|
+
TEAM_ADJUDICATION_ERROR_CODES.CORRUPT,
|
|
1081
|
+
"Team adjudication state is not valid JSON",
|
|
1082
|
+
{},
|
|
1083
|
+
cause,
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
_readRuntime() {
|
|
1089
|
+
const entry = this._assertAuthorityPaths();
|
|
1090
|
+
if (!entry) return emptyRuntime(this);
|
|
1091
|
+
return replayDocument(this, this._decodeDocument(this._secureRead()));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
_newRuntime(at) {
|
|
1095
|
+
const document = {
|
|
1096
|
+
...bindingHeader({
|
|
1097
|
+
statePath: this.statePath,
|
|
1098
|
+
statePathDigest: this.statePathDigest,
|
|
1099
|
+
collaborationRunId: this.collaborationRunId,
|
|
1100
|
+
createdAt: at,
|
|
1101
|
+
}),
|
|
1102
|
+
bindingDigest: null,
|
|
1103
|
+
events: [],
|
|
1104
|
+
};
|
|
1105
|
+
document.bindingDigest = computeBindingDigest(document);
|
|
1106
|
+
const runtime = emptyRuntime(this);
|
|
1107
|
+
runtime.document = document;
|
|
1108
|
+
runtime.lastAt = at;
|
|
1109
|
+
return runtime;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
_atomicWrite(document) {
|
|
1113
|
+
const serialized = `${JSON.stringify(document, null, 2)}\n`;
|
|
1114
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
1115
|
+
if (bytes > this.maxBytes) {
|
|
1116
|
+
throw adjudicationError(
|
|
1117
|
+
TEAM_ADJUDICATION_ERROR_CODES.LIMIT,
|
|
1118
|
+
`Team adjudication file cannot exceed ${this.maxBytes} bytes`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Security precondition: the canonical parent and its ancestors cannot be
|
|
1123
|
+
// renamed by an untrusted concurrent writer during this callback. Node's
|
|
1124
|
+
// path-based primitives are not an openat-style authority boundary.
|
|
1125
|
+
try {
|
|
1126
|
+
return withTrustedFileParentSync(
|
|
1127
|
+
this._fs,
|
|
1128
|
+
this.filePath,
|
|
1129
|
+
({ canonicalPath, parentPath, parentDescriptor }) => {
|
|
1130
|
+
const token = String(this._randomUUID()).replace(
|
|
1131
|
+
/[^a-zA-Z0-9._-]/g,
|
|
1132
|
+
"-",
|
|
1133
|
+
);
|
|
1134
|
+
const temporaryPath = path.join(
|
|
1135
|
+
parentPath,
|
|
1136
|
+
`.${path.basename(canonicalPath)}.${process.pid}.${token}.tmp`,
|
|
1137
|
+
);
|
|
1138
|
+
let descriptor = null;
|
|
1139
|
+
let renamed = false;
|
|
1140
|
+
let temporaryCreated = false;
|
|
1141
|
+
const flags =
|
|
1142
|
+
Number(this._fs.constants?.O_WRONLY ?? fs.constants.O_WRONLY) |
|
|
1143
|
+
Number(this._fs.constants?.O_CREAT ?? fs.constants.O_CREAT) |
|
|
1144
|
+
Number(this._fs.constants?.O_EXCL ?? fs.constants.O_EXCL) |
|
|
1145
|
+
Number(
|
|
1146
|
+
this._fs.constants?.O_NOFOLLOW ?? fs.constants.O_NOFOLLOW ?? 0,
|
|
1147
|
+
);
|
|
1148
|
+
try {
|
|
1149
|
+
descriptor = this._fs.openSync(temporaryPath, flags, 0o600);
|
|
1150
|
+
temporaryCreated = true;
|
|
1151
|
+
this._fs.writeFileSync(descriptor, serialized, "utf8");
|
|
1152
|
+
this._fs.fsyncSync(descriptor);
|
|
1153
|
+
const temporaryStat = this._fs.fstatSync(descriptor);
|
|
1154
|
+
if (!temporaryStat.isFile() || Number(temporaryStat.nlink) !== 1) {
|
|
1155
|
+
throw unsafePath(
|
|
1156
|
+
"Atomic team adjudication temporary file is not private",
|
|
1157
|
+
temporaryPath,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
this._fs.closeSync(descriptor);
|
|
1161
|
+
descriptor = null;
|
|
1162
|
+
|
|
1163
|
+
this._assertAuthorityPaths();
|
|
1164
|
+
this._fs.renameSync(temporaryPath, canonicalPath);
|
|
1165
|
+
renamed = true;
|
|
1166
|
+
this._fs.chmodSync(canonicalPath, 0o600);
|
|
1167
|
+
const persisted = this._secureRead();
|
|
1168
|
+
if (!persisted.equals(Buffer.from(serialized, "utf8"))) {
|
|
1169
|
+
throw adjudicationError(
|
|
1170
|
+
TEAM_ADJUDICATION_ERROR_CODES.WRITE_FAILED,
|
|
1171
|
+
"Atomic team adjudication write verification failed",
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
if (this._platform !== "win32") {
|
|
1175
|
+
this._fs.fsyncSync(parentDescriptor);
|
|
1176
|
+
}
|
|
1177
|
+
} finally {
|
|
1178
|
+
if (descriptor !== null) {
|
|
1179
|
+
try {
|
|
1180
|
+
this._fs.closeSync(descriptor);
|
|
1181
|
+
} catch {
|
|
1182
|
+
// Best-effort descriptor cleanup.
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (!renamed && temporaryCreated) {
|
|
1186
|
+
try {
|
|
1187
|
+
this._fs.unlinkSync(temporaryPath);
|
|
1188
|
+
} catch {
|
|
1189
|
+
// Clean only this attempt's canonical temporary path.
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
} catch (cause) {
|
|
1196
|
+
if (cause instanceof SecureFileIdentityError) {
|
|
1197
|
+
throw unsafePath(
|
|
1198
|
+
"Team adjudication parent identity is unsafe",
|
|
1199
|
+
this.filePath,
|
|
1200
|
+
cause,
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
if (isAdjudicationError(cause)) throw cause;
|
|
1204
|
+
throw adjudicationError(
|
|
1205
|
+
TEAM_ADJUDICATION_ERROR_CODES.WRITE_FAILED,
|
|
1206
|
+
"Could not atomically persist team adjudication state",
|
|
1207
|
+
{ filePath: this.filePath },
|
|
1208
|
+
cause,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
_withLock(operation, callback) {
|
|
1214
|
+
this._assertAuthorityPaths();
|
|
1215
|
+
try {
|
|
1216
|
+
return this._lock(
|
|
1217
|
+
this.filePath,
|
|
1218
|
+
() => {
|
|
1219
|
+
this._assertAuthorityPaths();
|
|
1220
|
+
return callback(this._readRuntime());
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
timeoutMs: this.lockTimeoutMs,
|
|
1224
|
+
staleMs: this.lockStaleMs,
|
|
1225
|
+
failIfUnavailable: true,
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1228
|
+
} catch (cause) {
|
|
1229
|
+
if (isAdjudicationError(cause)) throw cause;
|
|
1230
|
+
if (
|
|
1231
|
+
cause?.code === "STATE_LOCK_UNAVAILABLE" ||
|
|
1232
|
+
cause?.code === "STATE_LOCK_OWNERSHIP_LOST"
|
|
1233
|
+
) {
|
|
1234
|
+
throw adjudicationError(
|
|
1235
|
+
TEAM_ADJUDICATION_ERROR_CODES.LOCK_UNAVAILABLE,
|
|
1236
|
+
`Could not hold the strict team adjudication lock for ${operation}`,
|
|
1237
|
+
{ filePath: this.filePath },
|
|
1238
|
+
cause,
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
throw adjudicationError(
|
|
1242
|
+
TEAM_ADJUDICATION_ERROR_CODES.WRITE_FAILED,
|
|
1243
|
+
`Team adjudication ${operation} failed closed`,
|
|
1244
|
+
{ filePath: this.filePath },
|
|
1245
|
+
cause,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
_result(runtime, adjudicationCase, extra = {}) {
|
|
1251
|
+
return {
|
|
1252
|
+
case: caseView(adjudicationCase),
|
|
1253
|
+
cursor: cursorFromRuntime(runtime),
|
|
1254
|
+
...extra,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Read and verify the complete log, optionally proving a recovery prefix. */
|
|
1259
|
+
read({ anchor = null } = {}) {
|
|
1260
|
+
return this._withLock("read", (runtime) => {
|
|
1261
|
+
validateAnchor(runtime, anchor);
|
|
1262
|
+
return {
|
|
1263
|
+
statePath: this.statePath,
|
|
1264
|
+
statePathDigest: this.statePathDigest,
|
|
1265
|
+
collaborationRunId: this.collaborationRunId,
|
|
1266
|
+
cases: [...runtime.cases.values()].map(caseView),
|
|
1267
|
+
cursor: cursorFromRuntime(runtime),
|
|
1268
|
+
};
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
getCase(input, { anchor = null } = {}) {
|
|
1273
|
+
const binding = caseBinding(this, input);
|
|
1274
|
+
return this._withLock("get", (runtime) => {
|
|
1275
|
+
validateAnchor(runtime, anchor);
|
|
1276
|
+
const adjudicationCase = runtime.cases.get(binding.caseId);
|
|
1277
|
+
return adjudicationCase ? caseView(adjudicationCase) : null;
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** Open one case against immutable registry + side-effect evidence digests. */
|
|
1282
|
+
openCase(input, { anchor = null, expectedCursor = null } = {}) {
|
|
1283
|
+
const binding = caseBinding(this, input);
|
|
1284
|
+
return this._withLock("open", (loaded) => {
|
|
1285
|
+
validateAnchor(loaded, anchor);
|
|
1286
|
+
const existing = loaded.cases.get(binding.caseId);
|
|
1287
|
+
if (existing) {
|
|
1288
|
+
return this._result(loaded, existing, { duplicate: true });
|
|
1289
|
+
}
|
|
1290
|
+
validateExpectedCursor(loaded, expectedCursor);
|
|
1291
|
+
|
|
1292
|
+
const prior = loaded.byTask.get(binding.taskKey) || [];
|
|
1293
|
+
const latest = prior[prior.length - 1] || null;
|
|
1294
|
+
if (
|
|
1295
|
+
latest &&
|
|
1296
|
+
(!latest.application ||
|
|
1297
|
+
latest.decision?.value !== TEAM_ADJUDICATION_DECISIONS.RETRY)
|
|
1298
|
+
) {
|
|
1299
|
+
throw adjudicationError(
|
|
1300
|
+
TEAM_ADJUDICATION_ERROR_CODES.CONFLICT,
|
|
1301
|
+
`Task "${binding.taskKey}" already has an active or terminal adjudication`,
|
|
1302
|
+
{ taskKey: binding.taskKey, caseId: latest.binding.caseId },
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const at = normalizeFiniteTime(this._now(), "clock");
|
|
1307
|
+
const runtime = loaded.document ? loaded : this._newRuntime(at);
|
|
1308
|
+
const event = buildEvent(runtime, "case.open", binding.caseId, at, {
|
|
1309
|
+
taskKey: binding.taskKey,
|
|
1310
|
+
registryDigest: binding.registryDigest,
|
|
1311
|
+
sideEffectDigest: binding.sideEffectDigest,
|
|
1312
|
+
});
|
|
1313
|
+
appendEvent(runtime, event);
|
|
1314
|
+
this._atomicWrite(runtime.document);
|
|
1315
|
+
return this._result(runtime, runtime.cases.get(binding.caseId), {
|
|
1316
|
+
duplicate: false,
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Record the sole decision for a case. An exact retry of the same API request
|
|
1323
|
+
* is idempotent; any different authority, reason digest, or decision conflicts.
|
|
1324
|
+
*/
|
|
1325
|
+
decideCase(input, { anchor = null, expectedCursor = null } = {}) {
|
|
1326
|
+
const binding = caseBinding(this, input);
|
|
1327
|
+
const decision = normalizeDecision(input?.decision);
|
|
1328
|
+
const authority = requiredLabel(input?.authority, "authority", 256);
|
|
1329
|
+
const reasonDigest = normalizeDigest(input?.reasonDigest, "reasonDigest", {
|
|
1330
|
+
nullable: true,
|
|
1331
|
+
});
|
|
1332
|
+
return this._withLock("decide", (runtime) => {
|
|
1333
|
+
validateAnchor(runtime, anchor);
|
|
1334
|
+
const adjudicationCase = runtime.cases.get(binding.caseId);
|
|
1335
|
+
if (!adjudicationCase) {
|
|
1336
|
+
throw adjudicationError(
|
|
1337
|
+
TEAM_ADJUDICATION_ERROR_CODES.NOT_FOUND,
|
|
1338
|
+
"Team adjudication case was not found",
|
|
1339
|
+
{ caseId: binding.caseId },
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
if (adjudicationCase.decision) {
|
|
1343
|
+
const same =
|
|
1344
|
+
adjudicationCase.decision.value === decision &&
|
|
1345
|
+
adjudicationCase.decision.authority === authority &&
|
|
1346
|
+
adjudicationCase.decision.reasonDigest === reasonDigest;
|
|
1347
|
+
if (!same) {
|
|
1348
|
+
throw adjudicationError(
|
|
1349
|
+
TEAM_ADJUDICATION_ERROR_CODES.CONFLICT,
|
|
1350
|
+
"Team adjudication case already has a conflicting decision",
|
|
1351
|
+
{ caseId: binding.caseId },
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
return this._result(runtime, adjudicationCase, { duplicate: true });
|
|
1355
|
+
}
|
|
1356
|
+
validateRevision(adjudicationCase, input?.expectedRevision);
|
|
1357
|
+
validateExpectedCursor(runtime, expectedCursor);
|
|
1358
|
+
const event = buildEvent(
|
|
1359
|
+
runtime,
|
|
1360
|
+
"case.decide",
|
|
1361
|
+
binding.caseId,
|
|
1362
|
+
normalizeFiniteTime(this._now(), "clock"),
|
|
1363
|
+
{ decision, authority, reasonDigest },
|
|
1364
|
+
);
|
|
1365
|
+
appendEvent(runtime, event);
|
|
1366
|
+
this._atomicWrite(runtime.document);
|
|
1367
|
+
return this._result(runtime, adjudicationCase, { duplicate: false });
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Consume a decision once. Only a fresh claim returns an `authorization`;
|
|
1373
|
+
* duplicate calls return `authorization:null`, so retry cannot be replayed.
|
|
1374
|
+
*/
|
|
1375
|
+
claimDecision(input, { anchor = null, expectedCursor = null } = {}) {
|
|
1376
|
+
const binding = caseBinding(this, input);
|
|
1377
|
+
const decisionDigest = normalizeDigest(
|
|
1378
|
+
input?.decisionDigest,
|
|
1379
|
+
"decisionDigest",
|
|
1380
|
+
);
|
|
1381
|
+
const consumer = requiredLabel(input?.consumer, "consumer", 256);
|
|
1382
|
+
return this._withLock("claim", (runtime) => {
|
|
1383
|
+
validateAnchor(runtime, anchor);
|
|
1384
|
+
const adjudicationCase = runtime.cases.get(binding.caseId);
|
|
1385
|
+
if (!adjudicationCase?.decision) {
|
|
1386
|
+
throw adjudicationError(
|
|
1387
|
+
TEAM_ADJUDICATION_ERROR_CODES.NOT_FOUND,
|
|
1388
|
+
"A decided team adjudication case was not found",
|
|
1389
|
+
{ caseId: binding.caseId },
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (adjudicationCase.claim) {
|
|
1393
|
+
const same =
|
|
1394
|
+
adjudicationCase.claim.decisionDigest === decisionDigest &&
|
|
1395
|
+
adjudicationCase.claim.consumer === consumer;
|
|
1396
|
+
if (!same) {
|
|
1397
|
+
throw adjudicationError(
|
|
1398
|
+
TEAM_ADJUDICATION_ERROR_CODES.ALREADY_CLAIMED,
|
|
1399
|
+
"Team adjudication decision was already claimed",
|
|
1400
|
+
{ caseId: binding.caseId },
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
return this._result(runtime, adjudicationCase, {
|
|
1404
|
+
duplicate: true,
|
|
1405
|
+
authorization: null,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
if (adjudicationCase.decision.event.digest !== decisionDigest) {
|
|
1409
|
+
throw adjudicationError(
|
|
1410
|
+
TEAM_ADJUDICATION_ERROR_CODES.BINDING_MISMATCH,
|
|
1411
|
+
"Decision digest does not bind the current adjudication decision",
|
|
1412
|
+
{ caseId: binding.caseId },
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
validateRevision(adjudicationCase, input?.expectedRevision);
|
|
1416
|
+
validateExpectedCursor(runtime, expectedCursor);
|
|
1417
|
+
const claimId = `tadj_claim_${String(this._randomUUID())}`;
|
|
1418
|
+
if (!CLAIM_ID_PATTERN.test(claimId)) {
|
|
1419
|
+
throw adjudicationError(
|
|
1420
|
+
TEAM_ADJUDICATION_ERROR_CODES.INVALID,
|
|
1421
|
+
"Generated adjudication claim id is invalid",
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
const event = buildEvent(
|
|
1425
|
+
runtime,
|
|
1426
|
+
"case.claim",
|
|
1427
|
+
binding.caseId,
|
|
1428
|
+
normalizeFiniteTime(this._now(), "clock"),
|
|
1429
|
+
{ decisionDigest, claimId, consumer },
|
|
1430
|
+
);
|
|
1431
|
+
appendEvent(runtime, event);
|
|
1432
|
+
this._atomicWrite(runtime.document);
|
|
1433
|
+
return this._result(runtime, adjudicationCase, {
|
|
1434
|
+
duplicate: false,
|
|
1435
|
+
authorization: {
|
|
1436
|
+
caseId: binding.caseId,
|
|
1437
|
+
claimId,
|
|
1438
|
+
claimDigest: event.digest,
|
|
1439
|
+
decisionDigest,
|
|
1440
|
+
action: TEAM_ADJUDICATION_ACTIONS[adjudicationCase.decision.value],
|
|
1441
|
+
},
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Confirm that the claimed action and the corresponding team-state update
|
|
1448
|
+
* were durably applied. The outcome is represented only by a digest.
|
|
1449
|
+
*/
|
|
1450
|
+
completeCase(input, { anchor = null, expectedCursor = null } = {}) {
|
|
1451
|
+
const binding = caseBinding(this, input);
|
|
1452
|
+
const claimDigest = normalizeDigest(input?.claimDigest, "claimDigest");
|
|
1453
|
+
const outcomeDigest = normalizeDigest(
|
|
1454
|
+
input?.outcomeDigest,
|
|
1455
|
+
"outcomeDigest",
|
|
1456
|
+
);
|
|
1457
|
+
return this._withLock("apply", (runtime) => {
|
|
1458
|
+
validateAnchor(runtime, anchor);
|
|
1459
|
+
const adjudicationCase = runtime.cases.get(binding.caseId);
|
|
1460
|
+
if (!adjudicationCase?.claim) {
|
|
1461
|
+
throw adjudicationError(
|
|
1462
|
+
TEAM_ADJUDICATION_ERROR_CODES.NOT_FOUND,
|
|
1463
|
+
"A claimed team adjudication case was not found",
|
|
1464
|
+
{ caseId: binding.caseId },
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
if (adjudicationCase.application) {
|
|
1468
|
+
const same =
|
|
1469
|
+
adjudicationCase.application.claimDigest === claimDigest &&
|
|
1470
|
+
adjudicationCase.application.outcomeDigest === outcomeDigest;
|
|
1471
|
+
if (!same) {
|
|
1472
|
+
throw adjudicationError(
|
|
1473
|
+
TEAM_ADJUDICATION_ERROR_CODES.CONFLICT,
|
|
1474
|
+
"Team adjudication case already has a conflicting application",
|
|
1475
|
+
{ caseId: binding.caseId },
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
return this._result(runtime, adjudicationCase, { duplicate: true });
|
|
1479
|
+
}
|
|
1480
|
+
if (adjudicationCase.claim.event.digest !== claimDigest) {
|
|
1481
|
+
throw adjudicationError(
|
|
1482
|
+
TEAM_ADJUDICATION_ERROR_CODES.BINDING_MISMATCH,
|
|
1483
|
+
"Claim digest does not bind the current adjudication claim",
|
|
1484
|
+
{ caseId: binding.caseId },
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
validateRevision(adjudicationCase, input?.expectedRevision);
|
|
1488
|
+
validateExpectedCursor(runtime, expectedCursor);
|
|
1489
|
+
const event = buildEvent(
|
|
1490
|
+
runtime,
|
|
1491
|
+
"case.apply",
|
|
1492
|
+
binding.caseId,
|
|
1493
|
+
normalizeFiniteTime(this._now(), "clock"),
|
|
1494
|
+
{ claimDigest, outcomeDigest },
|
|
1495
|
+
);
|
|
1496
|
+
appendEvent(runtime, event);
|
|
1497
|
+
this._atomicWrite(runtime.document);
|
|
1498
|
+
return this._result(runtime, adjudicationCase, { duplicate: false });
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|