@codespar/cli 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/templates/bills-agent/README.md +1 -1
  4. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  5. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  7. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  8. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  9. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  10. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  11. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  12. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  13. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  14. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  15. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  16. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  17. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  18. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  19. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  20. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  21. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  22. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  23. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  24. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  25. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  26. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  27. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  28. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  29. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  30. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  31. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  32. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  33. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  34. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  35. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  36. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  37. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  38. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  39. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  40. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  41. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  42. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  43. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  44. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  45. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  46. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  52. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  53. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  59. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  60. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  61. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  62. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  63. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  64. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  65. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  66. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  67. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  69. package/templates/checkout-agent/LICENSE +21 -0
  70. package/templates/checkout-agent/README.md +28 -0
  71. package/templates/checkout-agent/_gitignore +10 -0
  72. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  73. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  74. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  75. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  76. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  77. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  78. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  79. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  83. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  84. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  108. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  109. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  110. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  111. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  112. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  113. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  141. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  146. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  147. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  156. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  157. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  158. package/templates/checkout-agent/package.json +38 -0
  159. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  160. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  161. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  162. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  163. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  167. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  168. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  169. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  170. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  171. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  172. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  173. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  174. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  175. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  176. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  177. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  178. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  179. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  180. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  181. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  182. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  184. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  185. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  186. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  187. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  188. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  189. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  190. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  191. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  192. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  193. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  195. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  196. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  197. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  198. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  199. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  200. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  201. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  202. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  203. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  204. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  205. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  206. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  208. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  209. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  210. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  211. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  212. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  213. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  216. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  217. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  218. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  219. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  220. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  221. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  222. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  223. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  224. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  260. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  261. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  267. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  268. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  269. package/templates/checkout-agent/tsconfig.base.json +16 -0
  270. package/templates/checkout-agent/vitest.config.ts +12 -0
  271. package/templates/collections-agent/README.md +6 -1
  272. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  273. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  274. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  276. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  277. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  278. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  279. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  280. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  281. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  283. package/templates/collections-agent/package.json +1 -0
  284. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  285. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  286. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  287. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  288. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  289. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  290. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  291. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  292. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  293. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  294. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  295. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  296. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  297. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  298. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  299. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  300. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  301. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  302. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  303. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  304. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  305. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  306. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  307. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  308. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  309. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  310. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  311. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  312. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  313. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  314. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  315. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  316. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  317. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  318. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  319. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  320. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  321. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  322. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  328. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  329. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  335. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  336. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  337. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  338. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  339. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  340. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  341. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  342. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  343. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  345. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  346. package/templates/hello-agent/README.md +1 -1
  347. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  348. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  349. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  350. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  351. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  352. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  353. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  354. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  355. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  356. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  357. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  358. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  359. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  360. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  361. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  362. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  363. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  364. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  365. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  366. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  367. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  368. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  369. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  370. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  371. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  372. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  373. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  374. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  375. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  376. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  377. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  378. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  379. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  380. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  381. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  382. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  383. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  384. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  385. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  391. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  392. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  398. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  399. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  400. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  401. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  402. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  403. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  404. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  405. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  406. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  408. package/templates/kits.lock.json +29 -6
  409. package/templates/supplier-payments-agent/README.md +1 -1
  410. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  411. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  416. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  417. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  418. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  420. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  424. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  425. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  426. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  427. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  428. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  429. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  430. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  431. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  432. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  433. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  435. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  437. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  438. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  439. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  440. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  441. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  442. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  443. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  446. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  447. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  451. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  452. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  453. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  473. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  474. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  477. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  478. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  479. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,1074 @@
1
+ /**
2
+ * Section 4.6: the model proposes, the code executes. The model's only way
3
+ * in is `draft()`, which creates an execution in `drafted` and nothing else.
4
+ * Every check that decides whether money moves lives here, in
5
+ * deterministic code: mandate status, allowlist, per-transaction and window
6
+ * caps, `escalate_above`, the approval artifact and its `items_hash`.
7
+ *
8
+ * The policy runs THREE times, not once: at draft, when a human approves,
9
+ * and immediately before `executing`, inside the same transaction that
10
+ * writes the outbox row. A list that changed after approval, a window that
11
+ * filled up between approval and execution, or a mandate revoked in the
12
+ * meantime is caught at the last gate, which is the only one that matters.
13
+ */
14
+ import { checkApprovalArtifact, createApprovalArtifact, type ApprovalSigner } from "./approval.js";
15
+ import { maskPayee, type ProofBundle } from "./bundle.js";
16
+ import { evaluateEscalation, type Escalation } from "./escalate.js";
17
+ import { CHARGE_CANCELLED, CHARGE_EXPIRED, CHARGE_PAID, PAYMENT_FAILED, PAYMENT_SUCCEEDED, type PublishedEvent } from "./events.js";
18
+ import type { Guardrails } from "./guardrails.js";
19
+ import { batchAttemptId, itemsHash, sha256Hex } from "./hash.js";
20
+ import { newId } from "./ids.js";
21
+ import { mandateExpired, payeeAllowed, resolveBeneficiary, windowCap, windowStart, type Mandate } from "./mandate.js";
22
+ import type { Manifest } from "./manifest.js";
23
+ import { checkQuote, quoteFromApproval } from "./quote.js";
24
+ import { spendApprovalOf, type PaymentRail, type RailOutcome, type RailPayment, type SealedSpendApproval, type SpendApproval } from "./rail.js";
25
+ import { sameApprovalHash } from "./receipt-chain.js";
26
+ import type { MandateStatusReport, MandateStatusSource } from "./revocation.js";
27
+ import { isTerminal, transition, type Execution, type ExecutionState } from "./state-machine.js";
28
+ import type { StateStore } from "./state/store.js";
29
+ import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionComposition, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
30
+
31
+ export interface ProposedItem {
32
+ /** An alias from the mandate's named payees, or a raw payee key. */
33
+ payee: string;
34
+ amount: number;
35
+ description?: string;
36
+ /** A receivable's due date, `YYYY-MM-DD`. */
37
+ due_date?: string;
38
+ }
39
+
40
+ export interface PolicyExtensionContext {
41
+ gate: "draft" | "approve" | "execute";
42
+ now: Date;
43
+ mode: ApprovalMode;
44
+ mandate: Mandate;
45
+ guardrails: Guardrails;
46
+ }
47
+
48
+ /**
49
+ * The kit's own deterministic check, run by the core at every gate after
50
+ * the mandate's (allowlist, caps, window). It can only refuse; it cannot
51
+ * widen anything. A collections agent uses it for its negotiation envelope.
52
+ */
53
+ export type PolicyExtension = (execution: Execution, ctx: PolicyExtensionContext) => { reason: ExecutionReason; detail: string } | undefined;
54
+
55
+ export interface Proposal {
56
+ items: ProposedItem[];
57
+ /** What the model said the total was. Recorded; never used to pay. */
58
+ claimed_total?: number;
59
+ /**
60
+ * The batch this proposal is one line of, when it is one. The kit computes
61
+ * it once over the whole ordered list before drafting any of it; the core
62
+ * stamps it on the execution and every artifact of that execution carries
63
+ * it. The core never invents one and never fills one in.
64
+ */
65
+ batch?: ExecutionBatch;
66
+ /**
67
+ * What the proposal's amount is composed of, when it is composed of lines
68
+ * (a cart behind one order). The kit computes `compositionHash` over the
69
+ * resolved lines; the core stamps it on the execution and every artifact of
70
+ * it, and never computes, fills in or reads the lines.
71
+ */
72
+ composition?: ExecutionComposition;
73
+ }
74
+
75
+ export type DraftResult =
76
+ | { ok: true; execution: Execution }
77
+ | { ok: false; refused_before_draft: true; reason: ExecutionReason; message: string };
78
+
79
+ export interface EngineDeps {
80
+ store: StateStore;
81
+ rail: PaymentRail;
82
+ status: MandateStatusSource;
83
+ signer: ApprovalSigner;
84
+ manifest: Manifest;
85
+ guardrails: Guardrails;
86
+ mandate: Mandate;
87
+ bundle: ProofBundle;
88
+ mode: ApprovalMode;
89
+ runId: string;
90
+ /** The person the agent acts for. */
91
+ onBehalfOf: string;
92
+ clock?: () => Date;
93
+ /** The kit's own envelope check, run at every gate after the mandate's. */
94
+ policyExtension?: PolicyExtension | undefined;
95
+ }
96
+
97
+ /** What the deterministic policy says about an execution at one of its three gates. */
98
+ type Verdict =
99
+ | { kind: "ok" }
100
+ | { kind: "deny"; reason: ExecutionReason; detail: string }
101
+ | { kind: "block"; reason: ExecutionReason; detail: string }
102
+ | { kind: "escalate"; escalation: Escalation };
103
+
104
+ type Gate = "draft" | "approve" | "execute";
105
+
106
+ /**
107
+ * How many spent generations of one batch line's attempt id a dispatch walks
108
+ * past before it reports the line failed. Each one is a payment the provider
109
+ * already refused for this exact line, so eight is a line that keeps failing,
110
+ * not a line still looking for its id.
111
+ */
112
+ const MAX_ATTEMPT_GENERATION = 8;
113
+
114
+ /** Open executions reserve the window: a cap is a ceiling on what may be committed, not a balance. */
115
+ const WINDOW_STATES: ExecutionState[] = ["awaiting_approval", "approved", "executing", "settled"];
116
+
117
+ export class ExecutionEngine {
118
+ readonly clock: () => Date;
119
+ readonly agentActor: Actor;
120
+
121
+ constructor(private readonly deps: EngineDeps) {
122
+ this.clock = deps.clock ?? (() => new Date());
123
+ this.agentActor = { type: "agent", agent: `${deps.manifest.name}@${deps.manifest.version}`, on_behalf_of: deps.onBehalfOf };
124
+ }
125
+
126
+ get mode(): ApprovalMode {
127
+ return this.deps.mode;
128
+ }
129
+
130
+ get mandate(): Mandate {
131
+ return this.deps.mandate;
132
+ }
133
+
134
+ // ---- the model's only entry point -------------------------------------
135
+
136
+ async draft(proposal: Proposal): Promise<DraftResult> {
137
+ const now = this.clock();
138
+ const gate = await this.mandateGate();
139
+ if (gate) {
140
+ this.record("execution.refused_before_draft", null, { reason: gate.reason, detail: gate.detail });
141
+ return { ok: false, refused_before_draft: true, reason: gate.reason, message: gate.detail };
142
+ }
143
+
144
+ const items = proposal.items.map((p) => this.resolveItem(p));
145
+ const total = items.reduce((sum, i) => sum + i.amount, 0);
146
+ if (proposal.batch) assertBatchBinding(proposal.batch);
147
+ if (proposal.composition) assertCompositionBinding(proposal.composition);
148
+ const id = newId("exe");
149
+ const execution: Execution<"drafted"> = {
150
+ id,
151
+ run_id: this.deps.runId,
152
+ state: "drafted",
153
+ mode: this.deps.mode,
154
+ actor: this.agentActor,
155
+ items,
156
+ total,
157
+ currency: this.deps.mandate.currency,
158
+ ...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
159
+ items_hash: itemsHash(items),
160
+ ...(proposal.batch ? { batch: proposal.batch } : {}),
161
+ ...(proposal.composition ? { composition: proposal.composition } : {}),
162
+ mandate: { id: this.deps.mandate.id, version: this.deps.mandate.version },
163
+ idempotency_key: `idk_${sha256Hex(`${this.deps.runId}:${id}`).slice(0, 32)}`,
164
+ blocking_reasons: [],
165
+ outcomes: [],
166
+ history: [],
167
+ created_at: now.toISOString(),
168
+ updated_at: now.toISOString(),
169
+ };
170
+ this.deps.store.saveExecution(execution);
171
+ this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null, ...(execution.composition ? { composition: execution.composition } : {}) });
172
+
173
+ return { ok: true, execution: await this.evaluateDraft(execution) };
174
+ }
175
+
176
+ /**
177
+ * The proposal behind an OPEN execution changed, and the execution follows
178
+ * it: a customer who changes the cart after the order was approved is
179
+ * changing the order, not opening a second one. The items, the total, the
180
+ * `items_hash` and the composition are replaced; the state, the approval
181
+ * and the idempotency key are NOT.
182
+ *
183
+ * That is the point. No transition happens here and nothing is judged: an
184
+ * `approved` execution keeps the artifact it was approved with, and the
185
+ * last gate (`execute`) compares the two and sends it back to
186
+ * `awaiting_approval` with `items_hash_mismatch` — the same edge a list
187
+ * tampered after approval takes, reached honestly. An `awaiting_approval`
188
+ * one is judged again by whoever decides it, on what it says now.
189
+ *
190
+ * The payees may not change, in number or in order: a restatement changes
191
+ * what is charged or paid, never to whom. Nothing is dispatched by an open
192
+ * execution, so no attempt can exist under the key it keeps.
193
+ */
194
+ restate(executionId: string, proposal: Proposal): Execution {
195
+ const execution = this.mustGet(executionId);
196
+ if (execution.state !== "awaiting_approval" && execution.state !== "approved") throw new Error(`execution ${executionId} is ${execution.state}; only an open execution (awaiting_approval, approved) can be restated`);
197
+ if (proposal.batch) throw new Error("a batch line is not restated; the batch is presented again");
198
+ if (proposal.composition) assertCompositionBinding(proposal.composition);
199
+ const items = proposal.items.map((p) => this.resolveItem(p));
200
+ const before = execution.items.map((i) => i.payee);
201
+ if (items.length !== before.length || items.some((item, i) => item.payee !== before[i])) throw new Error(`execution ${executionId}: a restatement may not change the payees`);
202
+ const total = items.reduce((sum, i) => sum + i.amount, 0);
203
+ const { composition: _composition, model_claimed_total: _claimed, ...rest } = execution;
204
+ const restated: Execution = {
205
+ ...rest,
206
+ items,
207
+ total,
208
+ ...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
209
+ items_hash: itemsHash(items),
210
+ ...(proposal.composition ? { composition: proposal.composition } : {}),
211
+ updated_at: this.clock().toISOString(),
212
+ };
213
+ this.deps.store.saveExecution(restated);
214
+ this.record("execution.restated", execution.id, {
215
+ state: execution.state,
216
+ items_hash: { from: execution.items_hash, to: restated.items_hash },
217
+ total: { from: execution.total, to: total },
218
+ composition: { from: execution.composition ?? null, to: restated.composition ?? null },
219
+ });
220
+ return restated;
221
+ }
222
+
223
+ // ---- deterministic policy ---------------------------------------------
224
+
225
+ /**
226
+ * The one policy, run at every gate. `humanApproved` is true when a person
227
+ * already decided this execution: their decision satisfies `escalate_above`,
228
+ * and only that. Everything else — allowlist, caps, window — is the
229
+ * mandate's and no approval widens it.
230
+ */
231
+ private policy(execution: Execution, now: Date, gate: Gate, humanApproved: boolean): Verdict {
232
+ const { mandate, guardrails, manifest } = this.deps;
233
+
234
+ if (gate === "draft" && execution.model_claimed_total !== undefined && execution.model_claimed_total !== execution.total) {
235
+ const detail = `the model stated ${execution.model_claimed_total}; the core computed ${execution.total} and that is the only number that counts`;
236
+ if (guardrails.model_total_mismatch === "refuse") return { kind: "deny", reason: "model_total_mismatch", detail };
237
+ this.record("execution.model_total_ignored", execution.id, { detail });
238
+ }
239
+
240
+ const blocked = execution.items.filter((i) => !payeeAllowed(mandate, i.payee));
241
+ if (blocked.length > 0) {
242
+ const detail = `payee not named in the signed allowlist: ${blocked.map((i) => i.beneficiary).join(", ")}`;
243
+ // At draft in `human` the person is told; at any later gate, or in `mandate`, it is a refusal. escalate_above never widens the allowlist.
244
+ if (gate === "draft" && this.deps.mode === "human") return { kind: "block", reason: "beneficiary_not_allowed", detail };
245
+ return { kind: "deny", reason: "beneficiary_not_allowed", detail };
246
+ }
247
+
248
+ // A receivable closed `charge_reference_ambiguous` was issued and may still be paid. Another one to the same payee is how a debtor pays twice.
249
+ const unreconciled = this.deps.store
250
+ .listExecutions({ state: "failed", mandate_id: mandate.id })
251
+ .find((e) => e.id !== execution.id && e.reason === "charge_reference_ambiguous" && e.items.some((i) => execution.items.some((mine) => mine.payee === i.payee)));
252
+ if (unreconciled) {
253
+ return { kind: "deny", reason: "charge_reference_ambiguous", detail: `execution ${unreconciled.id} issued a receivable to this payee whose reference turned ambiguous; reconcile it by the charge id before issuing another` };
254
+ }
255
+
256
+ const over = execution.items.find((i) => i.amount > mandate.per_tx_cap_minor);
257
+ if (over) return { kind: "deny", reason: "per_tx_cap_exceeded", detail: `${over.beneficiary}: ${over.amount} is above the per-payment cap ${mandate.per_tx_cap_minor}` };
258
+
259
+ const committed = this.committedInWindow(now, execution.id);
260
+ if (committed + execution.total > windowCap(mandate)) {
261
+ return {
262
+ kind: "deny",
263
+ reason: "window_cap_exceeded",
264
+ detail: `${committed} already committed this ${mandate.periodic_cap?.window ?? "month"} (settled, in flight and awaiting a decision) plus ${execution.total} exceeds the cap ${windowCap(mandate)}`,
265
+ };
266
+ }
267
+
268
+ // The kit's envelope, at every gate: a person's yes does not widen it either.
269
+ const outside = this.deps.policyExtension?.(execution, { gate, now, mode: this.deps.mode, mandate, guardrails });
270
+ if (outside) return { kind: "deny", reason: outside.reason, detail: outside.detail };
271
+
272
+ if (this.deps.mode === "mandate" && !humanApproved) {
273
+ const escalation = evaluateEscalation(manifest.escalate_above, guardrails, execution.items, {
274
+ now,
275
+ timezone: guardrails.timezone,
276
+ knownPayees: this.knownPayees(),
277
+ recentByPayee: this.recentByPayee(now, execution.id),
278
+ });
279
+ if (escalation) {
280
+ if (escalation.trigger === "outside_hours" && guardrails.outside_hours_action === "refuse") return { kind: "deny", reason: "outside_hours", detail: escalation.detail };
281
+ return { kind: "escalate", escalation };
282
+ }
283
+ }
284
+
285
+ return { kind: "ok" };
286
+ }
287
+
288
+ private async evaluateDraft(execution: Execution<"drafted">): Promise<Execution> {
289
+ const now = this.clock();
290
+ const at = now.toISOString();
291
+ const gate = await this.mandateGate();
292
+ if (gate) return this.persist(transition(execution, gate.to, { at, actor: this.agentActor, reason: gate.reason, detail: gate.detail }));
293
+
294
+ const verdict = this.policy(execution, now, "draft", false);
295
+ switch (verdict.kind) {
296
+ case "deny":
297
+ return this.persist(transition(execution, "denied", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
298
+ case "block":
299
+ return this.persist(transition({ ...execution, blocking_reasons: [verdict.reason] }, "awaiting_approval", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
300
+ case "escalate":
301
+ return this.persist(transition({ ...execution, escalation: verdict.escalation }, "awaiting_approval", { at, actor: this.agentActor, reason: "escalated", detail: `${verdict.escalation.trigger}: ${verdict.escalation.detail}` }));
302
+ case "ok":
303
+ break;
304
+ }
305
+ if (this.deps.mode === "human") return this.persist(transition(execution, "awaiting_approval", { at, actor: this.agentActor }));
306
+
307
+ // The allowance covers it: the approver is the mandate, and the artifact says so.
308
+ const artifact = createApprovalArtifact(this.deps.signer, {
309
+ execution,
310
+ approver: { type: "mandate", id: this.deps.mandate.id },
311
+ actor: this.agentActor,
312
+ now,
313
+ ttlMs: this.deps.guardrails.approval_ttl_minutes * 60_000,
314
+ });
315
+ this.storeApproval(artifact);
316
+ return this.persist(transition({ ...execution, approval_id: artifact.approval_id }, "approved", { at, actor: this.agentActor, detail: "within the signed allowance" }));
317
+ }
318
+
319
+ // ---- the human's entry points -----------------------------------------
320
+
321
+ approve(executionId: string, approver: { id: string; channel: string }): Execution {
322
+ const execution = this.mustGet(executionId);
323
+ if (execution.state !== "awaiting_approval") throw new Error(`execution ${executionId} is ${execution.state}, not awaiting_approval`);
324
+ const now = this.clock();
325
+ const at = now.toISOString();
326
+ const humanActor: Actor = { type: "human", id: approver.id, channel: approver.channel };
327
+ const awaiting = execution as Execution<"awaiting_approval">;
328
+ if (execution.blocking_reasons.length > 0) {
329
+ const reason = execution.blocking_reasons[0] as ExecutionReason;
330
+ return this.persist(transition(awaiting, "denied", { at, actor: humanActor, reason, detail: `a human said yes, but ${reason} cannot be approved: the mandate does not authorize it` }));
331
+ }
332
+ // A yes does not skip the policy: the list may have changed since it was presented (4.2), or the window may have filled.
333
+ const verdict = this.policy(execution, now, "approve", true);
334
+ if (verdict.kind === "deny") return this.persist(transition(awaiting, "denied", { at, actor: humanActor, reason: verdict.reason, detail: `a human said yes, but ${verdict.detail}` }));
335
+
336
+ const artifact = createApprovalArtifact(this.deps.signer, {
337
+ execution,
338
+ approver: { type: "person", id: approver.id, channel: approver.channel },
339
+ actor: humanActor,
340
+ now,
341
+ ttlMs: this.deps.guardrails.approval_ttl_minutes * 60_000,
342
+ ...(execution.escalation ? { escalation: execution.escalation } : {}),
343
+ });
344
+ this.storeApproval(artifact);
345
+ return this.persist(transition({ ...awaiting, approval_id: artifact.approval_id }, "approved", { at, actor: humanActor }));
346
+ }
347
+
348
+ deny(executionId: string, approver: { id: string; channel: string }, detail = "denied by the approver"): Execution {
349
+ const execution = this.mustGet(executionId);
350
+ if (execution.state !== "awaiting_approval") throw new Error(`execution ${executionId} is ${execution.state}, not awaiting_approval`);
351
+ const humanActor: Actor = { type: "human", id: approver.id, channel: approver.channel };
352
+ return this.persist(transition(execution as Execution<"awaiting_approval">, "denied", { at: this.clock().toISOString(), actor: humanActor, reason: "denied_by_approver", detail }));
353
+ }
354
+
355
+ // ---- the only path to `executing` -------------------------------------
356
+
357
+ async execute(executionId: string): Promise<Execution> {
358
+ const execution = this.mustGet(executionId);
359
+ if (execution.state !== "approved") throw new Error(`execution ${executionId} is ${execution.state}, not approved`);
360
+ const now = this.clock();
361
+ const at = now.toISOString();
362
+ const approved = execution as Execution<"approved">;
363
+
364
+ // 4.7: the artifact may predate a revocation. Ask the source, every time.
365
+ const gate = await this.mandateGate();
366
+ if (gate) return this.persist(transition(approved, gate.to, { at, actor: this.agentActor, reason: gate.reason, detail: gate.detail }));
367
+
368
+ const artifact = execution.approval_id ? this.deps.store.getApproval(execution.approval_id) : undefined;
369
+ if (!artifact) throw new Error(`execution ${executionId} is approved without an approval artifact`);
370
+
371
+ // Everything below is one transaction: the last policy run, the artifact check, the outbox row and the state change land together or not at all.
372
+ const payments = this.paymentsFor(execution, artifact);
373
+ const decided = this.deps.store.transaction((): Execution => {
374
+ // The approval was given under one mandate; a re-signed one (new version) is a new authorization, and the person decides again under it.
375
+ const current = { id: this.deps.mandate.id, version: this.deps.mandate.version };
376
+ if (execution.mandate.id !== current.id || execution.mandate.version !== current.version) {
377
+ return this.persist(transition({ ...this.withoutApproval(approved), mandate: current }, "awaiting_approval", { at, actor: this.agentActor, reason: "mandate_changed", detail: `approved under mandate ${execution.mandate.id} v${execution.mandate.version}; the mandate is now ${current.id} v${current.version}` }));
378
+ }
379
+ // 4.2: recompute the hash of what is about to be executed and compare.
380
+ const check = checkApprovalArtifact(this.deps.signer, artifact, execution, now);
381
+ if (!check.ok) {
382
+ if (check.problem === "expired") {
383
+ return this.persist(transition(approved, "expired", { at, actor: this.agentActor, reason: "approval_expired", detail: `approval ${artifact.approval_id} expired at ${artifact.expires_at}` }));
384
+ }
385
+ return this.persist(transition(this.withoutApproval(approved), "awaiting_approval", { at, actor: this.agentActor, reason: "items_hash_mismatch", detail: `approval ${artifact.approval_id} does not match what would be executed (${check.problem})` }));
386
+ }
387
+ // The API only records a quote that disagrees with the spend, and pays anyway. The kit does not send one.
388
+ const unquoted = payments.map(checkQuote).find((q) => !q.ok);
389
+ if (unquoted && !unquoted.ok) return this.persist(transition(approved, "denied", { at, actor: this.agentActor, reason: "quote_mismatch", detail: `${unquoted.code}: ${unquoted.detail}; nothing was sent` }));
390
+
391
+ const verdict = this.policy(execution, now, "execute", artifact.approver.type === "person");
392
+ if (verdict.kind === "deny" || verdict.kind === "block") {
393
+ return this.persist(transition(approved, "denied", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
394
+ }
395
+ if (verdict.kind === "escalate") {
396
+ return this.persist(transition({ ...this.withoutApproval(approved), escalation: verdict.escalation }, "awaiting_approval", { at, actor: this.agentActor, reason: "escalated", detail: `${verdict.escalation.trigger}: ${verdict.escalation.detail}` }));
397
+ }
398
+
399
+ this.deps.store.putOutbox({
400
+ idempotency_key: execution.idempotency_key,
401
+ execution_id: execution.id,
402
+ kind: "rail.pay",
403
+ payload: { attempts: payments.map((p) => p.attempt_id), items: execution.items },
404
+ status: "pending",
405
+ response: undefined,
406
+ created_at: at,
407
+ });
408
+ return this.persist(transition(approved, "executing", { at, actor: this.agentActor, detail: `idempotency_key ${execution.idempotency_key}` }));
409
+ });
410
+ if (decided.state !== "executing") return decided;
411
+
412
+ return this.dispatch(decided as Execution<"executing">, payments);
413
+ }
414
+
415
+ /**
416
+ * Sends the attempts of an `executing` execution to the rail. The outbox row
417
+ * flips to `sent` BEFORE the first call, so a crash after that point is
418
+ * reconciled and never re-sent.
419
+ */
420
+ private async dispatch(execution: Execution<"executing">, payments: RailPayment[]): Promise<Execution> {
421
+ const outcomes: ItemOutcome[] = [...execution.outcomes];
422
+ const unknown: string[] = [];
423
+ const sealMismatches: string[] = [];
424
+ this.deps.store.updateOutbox(execution.idempotency_key, "sent", undefined, this.clock().toISOString());
425
+
426
+ const generations: Record<number, number> = { ...execution.attempt_generations };
427
+ for (const [index, presented] of payments.entries()) {
428
+ if (outcomes.some((o) => o.index === index)) continue;
429
+ let payment = presented;
430
+ // The dispatch line is the request. `idempotency_key` is what makes a retry the same payment, so the bundle names it next to the attempt.
431
+ this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
432
+ let outcome = await this.deps.rail.pay(payment);
433
+ this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
434
+ // A batch line whose derived id the rail holds as failed-with-no-money
435
+ // moves to the next generation of it, and only on that answer (see
436
+ // `batchAttemptId`). The generation is saved before the next id goes
437
+ // out, so a crash or an unknown answer is reconciled against the id
438
+ // that was actually sent.
439
+ while (execution.batch && outcome.status === "failed" && outcome.spent && (generations[index] ?? 0) < MAX_ATTEMPT_GENERATION) {
440
+ const generation = (generations[index] ?? 0) + 1;
441
+ const next = batchAttemptId(execution.mandate.id, execution.batch, index, generation);
442
+ this.record("rail.attempt_spent", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, next_attempt_id: next, generation });
443
+ generations[index] = generation;
444
+ this.deps.store.saveExecution({ ...execution, outcomes, attempt_generations: { ...generations }, updated_at: this.clock().toISOString() });
445
+ payment = { ...payment, attempt_id: next };
446
+ this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
447
+ outcome = await this.deps.rail.pay(payment);
448
+ this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
449
+ }
450
+ if (outcome.status === "uncertain") {
451
+ this.record("rail.uncertain", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, message: outcome.message });
452
+ // No outcome is recorded because there is none. The siblings are still
453
+ // sent: an unknown answer about THIS payee says nothing about the next
454
+ // one, and the execution stays open until reconciliation settles it.
455
+ unknown.push(`attempt ${payment.attempt_id}: ${outcome.code} — outcome unknown, kept for reconciliation`);
456
+ continue;
457
+ }
458
+ if (outcome.status === "failed") {
459
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}`, ...(outcome.held ? { held: outcome.held } : {}) });
460
+ continue;
461
+ }
462
+ if (outcome.status === "accepted") {
463
+ // A receivable was issued; the payer decides the rest. Nothing settles here.
464
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "accepted", transaction_id: outcome.transaction_id, instrument: outcome.instrument });
465
+ this.recordInstrument(execution.id, payment.attempt_id, outcome.transaction_id, outcome.instrument);
466
+ continue;
467
+ }
468
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}), ...(outcome.replayed ? { replayed: true as const } : {}) });
469
+ const mismatch = await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
470
+ if (mismatch) sealMismatches.push(mismatch);
471
+ }
472
+ const closed = this.close({ ...execution, outcomes, ...(Object.keys(generations).length > 0 ? { attempt_generations: generations } : {}) }, payments.length, unknown.join("; ") || undefined);
473
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
474
+ return closed;
475
+ }
476
+
477
+ /**
478
+ * Closes an `executing` execution from its recorded outcomes: stays while a
479
+ * receivable is accepted and unpaid, stays while any attempt has no outcome
480
+ * at all, failed if any attempt failed, settled when every attempt settled.
481
+ *
482
+ * The order matters. An execution does not close while one of its attempts
483
+ * is unknown, even when another one failed: "some of this moved and one of
484
+ * them we cannot see" is not a closed outcome, and closing it as `failed`
485
+ * would report a batch as finished while a payment may still be in flight.
486
+ */
487
+ private close(execution: Execution<"executing">, attempts: number, unknownDetail?: string): Execution {
488
+ const at = this.clock().toISOString();
489
+ const updated: Execution<"executing"> = { ...execution, updated_at: at };
490
+ const accepted = execution.outcomes.filter((o) => o.status === "accepted");
491
+ if (accepted.length > 0) {
492
+ return this.leaveAwaiting(updated, `${accepted.length} receivable(s) issued and unpaid: ${accepted.map((o) => o.transaction_id ?? o.attempt_id).join(", ")}`);
493
+ }
494
+ if (execution.outcomes.length < attempts) {
495
+ return this.leaveUnresolved(updated, unknownDetail ?? `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
496
+ }
497
+ // An ambiguous receivable outranks any other failure: it is the one outcome that must be reconciled rather than issued again.
498
+ const failed = execution.outcomes.find((o) => o.status === "failed" && o.code === "charge_reference_ambiguous") ?? execution.outcomes.find((o) => o.status === "failed");
499
+ if (failed) {
500
+ this.deps.store.updateOutbox(execution.idempotency_key, "failed", execution.outcomes, at);
501
+ // `org_paused` is the API refusing the spend itself: the kill switch was pressed after the gate read the status. Nothing moved.
502
+ const reason: ExecutionReason =
503
+ failed.code === "charge_expired" || failed.code === "charge_cancelled" || failed.code === "org_paused" || failed.code === "charge_reference_ambiguous" ? failed.code : "rail_failed";
504
+ return this.persist(transition(updated, "failed", { at, actor: this.agentActor, reason, detail: failed.error ?? "rail refused" }));
505
+ }
506
+ if (execution.outcomes.filter((o) => o.status === "settled").length === attempts) {
507
+ this.deps.store.updateOutbox(execution.idempotency_key, "done", execution.outcomes, at);
508
+ return this.persist(transition(updated, "settled", { at, actor: this.agentActor }));
509
+ }
510
+ return this.leaveUnresolved(updated, `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
511
+ }
512
+
513
+ private leaveUnresolved(execution: Execution<"executing">, detail: string): Execution {
514
+ const kept: Execution<"executing"> = { ...execution, reason: "rail_uncertain", detail, updated_at: this.clock().toISOString() };
515
+ this.deps.store.saveExecution(kept);
516
+ this.record("execution.uncertain", execution.id, { detail });
517
+ return kept;
518
+ }
519
+
520
+ /** A receivable is out and its payer has not acted. Not uncertain: the rail knows the attempt; the outcome is simply not yet decided. */
521
+ private leaveAwaiting(execution: Execution<"executing">, detail: string): Execution {
522
+ const kept: Execution<"executing"> = { ...execution, reason: "awaiting_settlement", detail, updated_at: this.clock().toISOString() };
523
+ this.deps.store.saveExecution(kept);
524
+ // Deduplicated on what was observed, so a poll every few seconds does not fill the log with the same line.
525
+ const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: execution.id, event_id: `awaiting:${execution.id}:${sha256Hex(detail).slice(0, 16)}`, type: "execution.awaiting_settlement", payload: { detail }, at: kept.updated_at });
526
+ if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
527
+ return kept;
528
+ }
529
+
530
+ /** The instrument the payer is shown, logged once per observed state (registration flips `payable`; the log says when). */
531
+ private recordInstrument(executionId: string, attemptId: string, transactionId: string, instrument: ItemOutcome["instrument"]): void {
532
+ if (!instrument) return;
533
+ const stored = this.deps.store.appendEvent({
534
+ run_id: this.deps.runId,
535
+ execution_id: executionId,
536
+ event_id: `instrument:${attemptId}:${instrument.status}:${instrument.payable ? "payable" : "not-payable"}`,
537
+ type: "charge.instrument",
538
+ payload: { attempt_id: attemptId, charge_id: transactionId, status: instrument.status, payable: instrument.payable, has_pix: instrument.pix_copy_paste !== null, has_boleto: instrument.boleto_bank_line !== null, due_date: instrument.due_date },
539
+ at: this.clock().toISOString(),
540
+ });
541
+ if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
542
+ }
543
+
544
+ /** Records a settled attempt and fetches its receipt; answers the seal mismatch, if the receipt disagrees with what was paid. */
545
+ private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<string | undefined> {
546
+ // The rail's answer is the settlement event the API publishes (section 4.3): `commerce.payment.succeeded` for a payout, `commerce.charge.paid` for a receivable. Keyed by attempt so a replay is a no-op.
547
+ const appended = this.deps.store.appendEvent({
548
+ run_id: this.deps.runId,
549
+ execution_id: execution.id,
550
+ event_id: `succeeded:${payment.attempt_id}`,
551
+ type: eventType,
552
+ payload: { attempt_id: payment.attempt_id, amount: payment.amount_minor, payee: payment.payee, receipt_id: receiptId, actor: this.agentActor },
553
+ at: this.clock().toISOString(),
554
+ });
555
+ if (appended) this.deps.bundle.event({ ...appended, actor: this.agentActor, item_index: index });
556
+ return receiptId ? this.saveReceipt(execution.id, receiptId, payment) : undefined;
557
+ }
558
+
559
+ /**
560
+ * The bundle's copy is the receipt as it was sealed, payee included. A
561
+ * payment receipt whose sealed payee is not the payee this attempt paid —
562
+ * including one that sealed no payee — is recorded and answered as a
563
+ * mismatch, which the caller raises once the execution's outcome is saved:
564
+ * the money is where the rail says it is, and it is the EVIDENCE that
565
+ * disagrees. The same holds for the approval link (§3): a receipt that
566
+ * seals other hashes than the ones this attempt sent, or none, does not
567
+ * say this payment was made against the approved list. A rail that cannot
568
+ * report the link leaves it `undefined`, and nothing is compared. A paid
569
+ * charge carries no seal, so there is nothing to compare at all.
570
+ */
571
+ private async saveReceipt(executionId: string, receiptId: string, payment: RailPayment): Promise<string | undefined> {
572
+ const receipt = await this.deps.rail.receipt(receiptId, this.agentActor);
573
+ if (!receipt) return undefined;
574
+ const path = this.deps.bundle.receipt(receipt, { approval_id: this.deps.store.getExecution(executionId)?.approval_id });
575
+ this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
576
+ if (receipt.kind === "charge") return undefined;
577
+ const mismatches: string[] = [];
578
+ if (receipt.payment.payee !== payment.payee) {
579
+ const sealed = receipt.payment.payee === null ? null : maskPayee(receipt.payment.payee);
580
+ this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_payee: sealed, paid_payee: maskPayee(payment.payee) });
581
+ mismatches.push(`receipt ${receiptId} seals payee ${sealed ?? "none"}; attempt ${payment.attempt_id} paid ${maskPayee(payment.payee)}`);
582
+ }
583
+ if (receipt.approval !== undefined && payment.approval && !sealsApproval(receipt.approval, payment.approval)) {
584
+ this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_approval: receipt.approval, sent_approval: payment.approval });
585
+ mismatches.push(`receipt ${receiptId} seals approval ${receipt.approval ? receipt.approval.items_hash : "none"}; attempt ${payment.attempt_id} sent ${payment.approval.items_hash}`);
586
+ }
587
+ return mismatches.length > 0 ? mismatches.join("; ") : undefined;
588
+ }
589
+
590
+
591
+ /**
592
+ * Fetches into the bundle the receipts of settled attempts the bundle does
593
+ * not hold yet (a settlement that arrived by event names a receipt id and
594
+ * nothing else). Read-only on the rail; never dispatches.
595
+ */
596
+ async collectReceipts(executionId: string): Promise<string[]> {
597
+ const execution = this.mustGet(executionId);
598
+ const held = new Set(this.deps.bundle.listReceipts().map((f) => f.replace(/\.json$/, "")));
599
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
600
+ const fetched: string[] = [];
601
+ const sealMismatches: string[] = [];
602
+ for (const outcome of execution.outcomes) {
603
+ const payment = payments[outcome.index];
604
+ if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id) || !payment) continue;
605
+ const mismatch = await this.saveReceipt(execution.id, outcome.receipt_id, payment);
606
+ if (mismatch) sealMismatches.push(mismatch);
607
+ fetched.push(outcome.receipt_id);
608
+ }
609
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
610
+ return fetched;
611
+ }
612
+
613
+ // ---- section 10: resume and reconcile ---------------------------------
614
+
615
+ /**
616
+ * An execution left in `executing` is reconciled, NEVER dispatched. Each
617
+ * attempt without an outcome is looked up on the rail; a recorded settled
618
+ * or failed answer is taken, and anything else (in flight, uncertain,
619
+ * unknown) leaves the execution in `executing` with an `execution.uncertain`
620
+ * event, for a human. "Unknown" is not "never sent": on the real rail a
621
+ * receipt is indexed seconds after settlement (~43 s measured on Celcoin).
622
+ */
623
+ async reconcile(executionId: string): Promise<Execution> {
624
+ const execution = this.mustGet(executionId);
625
+ if (execution.state !== "executing") return execution;
626
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
627
+ const outbox = this.deps.store.getOutbox(execution.idempotency_key);
628
+ if (outbox?.status === "pending") {
629
+ return this.leaveUnresolved(execution as Execution<"executing">, "outbox pending: nothing was ever sent; `resume` dispatches it, reconcile does not");
630
+ }
631
+
632
+ const outcomes: ItemOutcome[] = [...execution.outcomes];
633
+ const sealMismatches: string[] = [];
634
+ let unresolved: string | undefined;
635
+ for (const [index, payment] of payments.entries()) {
636
+ const prior = outcomes.find((o) => o.index === index);
637
+ // Settled and failed are final. An accepted receivable is looked at again: its payer may have acted.
638
+ if (prior && prior.status !== "accepted") continue;
639
+ const seen = await this.deps.rail.lookup(payment.attempt_id, payment, prior?.transaction_id);
640
+ const found = seen ? seen.status : "absent";
641
+ const stored = this.deps.store.appendEvent({
642
+ run_id: this.deps.runId,
643
+ execution_id: execution.id,
644
+ // A poll that sees the same thing twice logs it once; a change in what it sees is a new line.
645
+ ...(prior ? { event_id: `reconcile:${payment.attempt_id}:${found}:${seen && seen.status === "accepted" ? `${seen.instrument.status}:${seen.instrument.payable}` : ""}` } : {}),
646
+ type: "rail.reconcile",
647
+ payload: { attempt_id: payment.attempt_id, found },
648
+ at: this.clock().toISOString(),
649
+ });
650
+ if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
651
+ if (!seen || seen.status === "uncertain" || seen.status === "in_flight") {
652
+ if (prior) continue; // the receivable is known to exist; the rail just did not answer this time
653
+ // One attempt the rail cannot answer for does not stop us learning
654
+ // about the others. A lookup moves no money, so there is nothing to
655
+ // be careful about here, and stopping would mean a later attempt's
656
+ // settlement is never recorded at all.
657
+ unresolved ??= `attempt ${payment.attempt_id}: ${seen ? seen.status : "unknown to the rail"}; a human decides, nothing is re-sent`;
658
+ continue;
659
+ }
660
+ const replace = (outcome: ItemOutcome) => {
661
+ const at = outcomes.findIndex((o) => o.index === index);
662
+ if (at >= 0) outcomes[at] = outcome;
663
+ else outcomes.push(outcome);
664
+ };
665
+ if (seen.status === "accepted") {
666
+ replace({ index, attempt_id: payment.attempt_id, status: "accepted", transaction_id: seen.transaction_id, instrument: seen.instrument });
667
+ this.recordInstrument(execution.id, payment.attempt_id, seen.transaction_id, seen.instrument);
668
+ continue;
669
+ }
670
+ if (seen.status === "failed") {
671
+ replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}`, ...(seen.held ? { held: seen.held } : {}) });
672
+ continue;
673
+ }
674
+ replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}), ...(seen.replayed ? { replayed: true as const } : {}) });
675
+ const mismatch = await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
676
+ if (mismatch) sealMismatches.push(mismatch);
677
+ }
678
+ const updated: Execution<"executing"> = { ...(execution as Execution<"executing">), outcomes };
679
+ const reconciled = unresolved ? this.leaveUnresolved(updated, unresolved) : this.close(updated, payments.length);
680
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
681
+ return reconciled;
682
+ }
683
+
684
+ /**
685
+ * `resume` only: dispatches the attempts of an `executing` execution whose
686
+ * outbox row is still `pending`. `pending` flips to `sent` before the first
687
+ * rail call, so a `pending` row proves nothing ever left; anything `sent`
688
+ * goes through `reconcile()` instead and is never re-sent.
689
+ */
690
+ async resumePending(executionId: string): Promise<Execution> {
691
+ const execution = this.mustGet(executionId);
692
+ if (execution.state !== "executing") return execution;
693
+ const outbox = this.deps.store.getOutbox(execution.idempotency_key);
694
+ if (outbox?.status !== "pending") return this.reconcile(executionId);
695
+ this.record("rail.resume", execution.id, { detail: "outbox pending: nothing was ever sent; dispatching once under the same attempt ids" });
696
+ return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution, this.approvalOf(execution)));
697
+ }
698
+
699
+ /** Time-based exits: an open execution past its approval TTL closes as `expired`. */
700
+ expireStale(): Execution[] {
701
+ const now = this.clock();
702
+ const ttl = this.deps.guardrails.approval_ttl_minutes * 60_000;
703
+ const out: Execution[] = [];
704
+ for (const execution of this.deps.store.listExecutions({ state: ["awaiting_approval", "approved"] })) {
705
+ const opened = new Date(execution.updated_at).getTime();
706
+ if (now.getTime() - opened <= ttl) continue;
707
+ out.push(this.persist(transition(execution as Execution<"awaiting_approval">, "expired", { at: now.toISOString(), actor: this.agentActor, reason: "approval_expired", detail: `no decision within ${this.deps.guardrails.approval_ttl_minutes} minutes` })));
708
+ }
709
+ return out;
710
+ }
711
+
712
+ /**
713
+ * An event arriving from outside (a trigger, a webhook): deduplicated by
714
+ * id, applied to the ONE attempt it names, and the execution closes only
715
+ * when every attempt has its outcome. `paid` before `created` is fine;
716
+ * `paid` twice settles once.
717
+ */
718
+ ingestExternalEvent(event: { event_id: string; type: string; attempt_id?: string; transaction_id?: string; at?: string }): { applied: boolean; reason: string } {
719
+ const located = this.locateAttempt(event);
720
+ if (!located) return { applied: false, reason: event.attempt_id || event.transaction_id ? "unknown attempt" : "event names no attempt_id and no transaction_id" };
721
+ const { row, attemptId } = located;
722
+ const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: row.execution_id, event_id: event.event_id, type: event.type, payload: { ...event, attempt_id: attemptId }, at: event.at ?? this.clock().toISOString() });
723
+ if (!stored) return { applied: false, reason: "duplicate event id" };
724
+ this.deps.bundle.event({ ...stored, actor: this.agentActor });
725
+ const execution = this.mustGet(row.execution_id);
726
+ if (isTerminal(execution.state)) return { applied: false, reason: `execution already ${execution.state}` };
727
+ if (execution.state !== "executing") return { applied: false, reason: `execution is ${execution.state}; the rail event cannot move it` };
728
+ const attempts = (row.payload as { attempts: string[] }).attempts;
729
+ const index = attempts.indexOf(attemptId);
730
+ const prior = execution.outcomes.find((o) => o.index === index);
731
+ // Settled and failed are final. An accepted receivable is exactly what a `commerce.charge.*` event decides.
732
+ if (prior && prior.status !== "accepted") return { applied: false, reason: "attempt already has an outcome" };
733
+
734
+ const carry = prior ? { ...(prior.transaction_id ? { transaction_id: prior.transaction_id } : {}) } : {};
735
+ let outcome: ItemOutcome;
736
+ switch (event.type) {
737
+ case PAYMENT_SUCCEEDED:
738
+ outcome = { index, attempt_id: attemptId, status: "settled", ...carry };
739
+ break;
740
+ case CHARGE_PAID:
741
+ // The paid receivable is its own record on the API; `collectReceipts()` fetches it into the bundle.
742
+ outcome = { index, attempt_id: attemptId, status: "settled", ...carry, ...(prior?.transaction_id ? { receipt_id: prior.transaction_id } : {}) };
743
+ break;
744
+ case PAYMENT_FAILED:
745
+ outcome = { index, attempt_id: attemptId, status: "failed", code: "rail_failed", error: `event ${event.event_id}` };
746
+ break;
747
+ case CHARGE_EXPIRED:
748
+ outcome = { index, attempt_id: attemptId, status: "failed", code: "charge_expired", error: `charge_expired: event ${event.event_id}`, ...carry };
749
+ break;
750
+ case CHARGE_CANCELLED:
751
+ outcome = { index, attempt_id: attemptId, status: "failed", code: "charge_cancelled", error: `charge_cancelled: event ${event.event_id}`, ...carry };
752
+ break;
753
+ default:
754
+ return { applied: false, reason: `event type ${event.type} moves nothing` };
755
+ }
756
+
757
+ const outcomes = execution.outcomes.filter((o) => o.index !== index).concat(outcome).sort((a, b) => a.index - b.index);
758
+ const closed = this.close({ ...(execution as Execution<"executing">), outcomes }, attempts.length);
759
+ return { applied: true, reason: closed.state === "executing" ? `attempt recorded; ${closed.detail}` : closed.state };
760
+ }
761
+
762
+ /** An event names our attempt id, or the rail's transaction id we recorded when the attempt was accepted. */
763
+ private locateAttempt(event: { attempt_id?: string; transaction_id?: string }): { row: ReturnType<StateStore["listOutbox"]>[number]; attemptId: string } | undefined {
764
+ const rows = this.deps.store.listOutbox();
765
+ if (event.attempt_id) {
766
+ const attemptId = event.attempt_id;
767
+ const row = rows.find((o) => ((o.payload as { attempts?: string[] }).attempts ?? []).includes(attemptId));
768
+ return row ? { row, attemptId } : undefined;
769
+ }
770
+ if (event.transaction_id) {
771
+ for (const row of rows) {
772
+ const execution = this.deps.store.getExecution(row.execution_id);
773
+ const hit = execution?.outcomes.find((o) => o.transaction_id === event.transaction_id);
774
+ if (hit) return { row, attemptId: hit.attempt_id };
775
+ }
776
+ }
777
+ return undefined;
778
+ }
779
+
780
+ // ---- the channel's small writes -----------------------------------------
781
+
782
+ /** A line in the event log written by the channel (a message to the person, a sandbox payer call). Never a transition. */
783
+ note(type: string, executionId: string | null, payload: Record<string, unknown>): void {
784
+ this.record(type, executionId, payload);
785
+ }
786
+
787
+ /**
788
+ * Once-only presentation: true the first time a given attempt's instrument
789
+ * is marked shown, false afterwards, across restarts (it is a cursor in
790
+ * state.db, not memory). The debtor sees each QR once.
791
+ */
792
+ markShown(executionId: string, attemptId: string): boolean {
793
+ const key = `shown:${executionId}:${attemptId}`;
794
+ if (this.deps.store.getCursor(key)) return false;
795
+ this.deps.store.setCursor(key, this.clock().toISOString());
796
+ return true;
797
+ }
798
+
799
+ /** Once-only message per execution outcome, same mechanism. */
800
+ markTold(executionId: string, outcome: string): boolean {
801
+ const key = `told:${executionId}:${outcome}`;
802
+ if (this.deps.store.getCursor(key)) return false;
803
+ this.deps.store.setCursor(key, this.clock().toISOString());
804
+ return true;
805
+ }
806
+
807
+ /**
808
+ * A durable claim for work whose unit is bigger than one execution: a batch
809
+ * line, a scheduled instruction. `claimed` reads which execution covers a
810
+ * key, `claim` records it. Same cursor table as `markShown`, which is this
811
+ * with a boolean answer.
812
+ *
813
+ * It exists because a tool handler's only durable surface is this engine,
814
+ * and a batch that must not pay the same payee twice across runs has to
815
+ * remember which execution already covers each line. What a held claim
816
+ * MEANS is the kit's to decide: the engine records the pairing and reads
817
+ * nothing into it, because "settled, so skip" and "denied, so retry" are
818
+ * the batch's rules and not the state machine's.
819
+ */
820
+ claimed(key: string): string | undefined {
821
+ return this.deps.store.getCursor(`claim:${key}`);
822
+ }
823
+
824
+ claim(key: string, executionId: string): void {
825
+ this.deps.store.setCursor(`claim:${key}`, executionId);
826
+ }
827
+
828
+ // ---- reads --------------------------------------------------------------
829
+
830
+ get(executionId: string): Execution | undefined {
831
+ return this.deps.store.getExecution(executionId);
832
+ }
833
+
834
+ list(filter: { state?: ExecutionState | ExecutionState[] } = {}): Execution[] {
835
+ return this.deps.store.listExecutions({ ...filter, mandate_id: this.deps.mandate.id });
836
+ }
837
+
838
+ /**
839
+ * The items `draft()` would build from these proposals, resolved against
840
+ * the mandate and WITHOUT the validation that refuses one. It exists for a
841
+ * caller that must hash a whole list before it drafts any of it — a batch
842
+ * computing its `batch_hash` — and it does not throw on purpose: a line the
843
+ * core will refuse is still a line of the presented list, and dropping it
844
+ * from the hash would make a batch holding a broken line hash the same as
845
+ * the shorter batch without it, which is the hole `batch_hash` closes.
846
+ *
847
+ * It resolves nothing the draft will not resolve identically, which is what
848
+ * makes the hash computed here the hash the drafts below carry.
849
+ */
850
+ preview(items: readonly ProposedItem[]): ExecutionItem[] {
851
+ return items.map((p) => this.previewItem(p));
852
+ }
853
+
854
+ // ---- helpers ------------------------------------------------------------
855
+
856
+ /**
857
+ * Section 4.7, fail-closed: `executing` is reached on `active` and on
858
+ * nothing else. A source that cannot answer (a failed read, a status this
859
+ * code does not know, an exception) is `mandate_status_unavailable`, never
860
+ * "assume active". An expired mandate closes the execution as `expired`;
861
+ * the other refusals close it as `denied`.
862
+ */
863
+ private async mandateGate(): Promise<{ reason: ExecutionReason; detail: string; to: "denied" | "expired" } | undefined> {
864
+ const { mandate } = this.deps;
865
+ let report: MandateStatusReport;
866
+ try {
867
+ report = await this.deps.status.check(mandate.id);
868
+ } catch (err) {
869
+ return { to: "denied", reason: "mandate_status_unavailable", detail: `the status of mandate ${mandate.id} could not be read (${err instanceof Error ? err.message : String(err)}); nothing executes until it can` };
870
+ }
871
+ if (report.org_paused) return { to: "denied", reason: "org_paused", detail: "the organization paused every mandate (kill switch)" };
872
+ switch (report.status) {
873
+ case "revoked":
874
+ return { to: "denied", reason: "mandate_revoked", detail: `mandate ${mandate.id} was revoked (source: ${report.source})` };
875
+ case "paused":
876
+ return { to: "denied", reason: "mandate_paused", detail: `mandate ${mandate.id} is paused (source: ${report.source})` };
877
+ case "expired":
878
+ return { to: "expired", reason: "mandate_expired", detail: `mandate ${mandate.id} expired at ${mandate.expires_at} (source: ${report.source})` };
879
+ case "active":
880
+ if (mandateExpired(mandate, this.clock())) return { to: "expired", reason: "mandate_expired", detail: `mandate ${mandate.id} expired at ${mandate.expires_at}` };
881
+ return undefined;
882
+ default:
883
+ return { to: "denied", reason: "mandate_status_unavailable", detail: `the status of mandate ${mandate.id} could not be read (${report.detail ?? "no detail"}); nothing executes until it can` };
884
+ }
885
+ }
886
+
887
+ private resolveItem(proposed: ProposedItem): ExecutionItem {
888
+ const amount = Math.trunc(proposed.amount);
889
+ if (!Number.isFinite(amount) || amount <= 0) throw new Error(`amount must be a positive integer in minor units, got ${proposed.amount}`);
890
+ if (proposed.due_date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(proposed.due_date)) throw new Error(`due_date must be YYYY-MM-DD, got ${proposed.due_date}`);
891
+ return this.previewItem(proposed);
892
+ }
893
+
894
+ private previewItem(proposed: ProposedItem): ExecutionItem {
895
+ const known = resolveBeneficiary(this.deps.mandate, proposed.payee);
896
+ return {
897
+ ...(known ? { alias: known.alias } : {}),
898
+ beneficiary: known?.name ?? proposed.payee,
899
+ payee: known?.payee ?? proposed.payee,
900
+ amount: Math.trunc(proposed.amount),
901
+ currency: this.deps.mandate.currency,
902
+ ...(proposed.description ? { description: proposed.description } : {}),
903
+ ...(proposed.due_date ? { due_date: proposed.due_date } : {}),
904
+ };
905
+ }
906
+
907
+ /**
908
+ * The attempts of an execution. Each carries the quote of the SAME line of
909
+ * the approval artifact — what was approved, not what the execution says
910
+ * now — so a line that drifted after approval is a quote that disagrees,
911
+ * refused before the call. Each also carries the artifact's hashes, which
912
+ * the API seals into the receipt (OPEN_QUESTIONS §3): the artifact is fixed
913
+ * per execution, so a reconcile presents the same approval the first
914
+ * dispatch did, and a batch line's hashes are the same on every machine.
915
+ */
916
+ private paymentsFor(execution: Execution, artifact: ApprovalArtifact): RailPayment[] {
917
+ return execution.items.map((item, index) => {
918
+ // A batch line is the same attempt, with the same quote, on every run of the same list, wherever it runs (§39c); anything else is its execution's own.
919
+ const quote = quoteFromApproval(artifact, index, this.deps.mandate.purpose, { stable: execution.batch !== undefined });
920
+ return {
921
+ attempt_id: execution.batch
922
+ ? batchAttemptId(execution.mandate.id, execution.batch, index, execution.attempt_generations?.[index] ?? 0)
923
+ : `att_${execution.idempotency_key.slice(4)}_${index}`,
924
+ mandate_id: execution.mandate.id,
925
+ amount_minor: item.amount,
926
+ currency: item.currency,
927
+ payee: item.payee,
928
+ beneficiary: item.beneficiary,
929
+ purpose: this.deps.mandate.purpose,
930
+ agent_id: this.deps.mandate.agent_id,
931
+ consumer_id: this.deps.mandate.consumer_id,
932
+ ...(item.description ? { description: item.description } : {}),
933
+ ...(item.due_date ? { due_date: item.due_date } : {}),
934
+ ...(quote ? { quote } : {}),
935
+ approval: spendApprovalOf(artifact),
936
+ actor: this.agentActor,
937
+ };
938
+ });
939
+ }
940
+
941
+ private approvalOf(execution: Execution): ApprovalArtifact {
942
+ const artifact = execution.approval_id ? this.deps.store.getApproval(execution.approval_id) : undefined;
943
+ if (!artifact) throw new Error(`execution ${execution.id} is ${execution.state} without an approval artifact`);
944
+ return artifact;
945
+ }
946
+
947
+ private withoutApproval(execution: Execution<"approved">): Execution<"approved"> {
948
+ const { approval_id: _stale, ...rest } = execution;
949
+ return rest as Execution<"approved">;
950
+ }
951
+
952
+ /** Settled, in flight or awaiting a decision under this mandate inside the cap window, excluding the execution being judged. */
953
+ private committedInWindow(now: Date, excludeId: string): number {
954
+ const start = windowStart(this.deps.mandate, now).getTime();
955
+ return this.list({ state: WINDOW_STATES })
956
+ .filter((e) => e.id !== excludeId && new Date(e.created_at).getTime() >= start)
957
+ .reduce((sum, e) => sum + e.total, 0);
958
+ }
959
+
960
+ private knownPayees(): Set<string> {
961
+ const out = new Set<string>();
962
+ for (const e of this.list({ state: "settled" })) for (const i of e.items) out.add(i.payee);
963
+ return out;
964
+ }
965
+
966
+ /**
967
+ * What the agent ran ALONE per payee inside the velocity window: mandate-mode
968
+ * executions the allowance approved without a human. A payment a human
969
+ * approved is not fractioning, so it does not count against the threshold.
970
+ */
971
+ private recentByPayee(now: Date, excludeId: string): Map<string, number> {
972
+ const out = new Map<string, number>();
973
+ const hours = this.deps.guardrails.velocity?.window_hours;
974
+ if (!hours) return out;
975
+ const since = now.getTime() - hours * 3_600_000;
976
+ for (const e of this.list({ state: ["approved", "executing", "settled"] })) {
977
+ if (e.id === excludeId || e.mode !== "mandate" || e.escalation !== undefined) continue;
978
+ if (new Date(e.created_at).getTime() < since) continue;
979
+ for (const i of e.items) out.set(i.payee, (out.get(i.payee) ?? 0) + i.amount);
980
+ }
981
+ return out;
982
+ }
983
+
984
+ private storeApproval(artifact: ApprovalArtifact): void {
985
+ this.deps.store.saveApproval(artifact);
986
+ this.deps.bundle.approval(artifact);
987
+ this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, ...(artifact.composition ? { composition: artifact.composition } : {}), escalation: artifact.escalation ?? null });
988
+ }
989
+
990
+ private persist<S extends ExecutionState>(execution: Execution<S>): Execution<S> {
991
+ this.deps.store.saveExecution(execution);
992
+ const last = execution.history[execution.history.length - 1];
993
+ if (last) {
994
+ const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: execution.id, type: "execution.transition", payload: last, at: last.at });
995
+ if (stored) this.deps.bundle.event({ ...stored, actor: last.actor });
996
+ }
997
+ return execution;
998
+ }
999
+
1000
+ private record(type: string, executionId: string | null, payload: Record<string, unknown>): void {
1001
+ const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: executionId, type, payload, at: this.clock().toISOString() });
1002
+ if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
1003
+ }
1004
+
1005
+ private mustGet(executionId: string): Execution {
1006
+ const execution = this.deps.store.getExecution(executionId);
1007
+ if (!execution) throw new Error(`unknown execution ${executionId}`);
1008
+ return execution;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * A batch binding is the kit's to compute and the core's to refuse when it
1014
+ * cannot be true. `index` outside `count`, or a `count` of zero, would let a
1015
+ * bundle say "line 5 of 3" and make the "N of M" an artifact reader sees
1016
+ * meaningless, so it is rejected where the execution is minted rather than
1017
+ * carried into the signature.
1018
+ */
1019
+ function assertBatchBinding(batch: ExecutionBatch): void {
1020
+ if (!batch.ref.trim()) throw new Error("batch.ref must name the batch");
1021
+ if (!batch.batch_hash.trim()) throw new Error("batch.batch_hash must be the hash of the presented lines");
1022
+ if (!Number.isInteger(batch.count) || batch.count < 1) throw new Error(`batch.count must be a positive integer, got ${batch.count}`);
1023
+ if (!Number.isInteger(batch.index) || batch.index < 0 || batch.index >= batch.count) {
1024
+ throw new Error(`batch.index must be a position inside the batch (0..${batch.count - 1}), got ${batch.index}`);
1025
+ }
1026
+ }
1027
+
1028
+ /** A composition binding is the kit's to compute and the core's to refuse when it cannot be true. */
1029
+ function assertCompositionBinding(composition: ExecutionComposition): void {
1030
+ if (!composition.ref.trim()) throw new Error("composition.ref must name what was composed");
1031
+ if (!/^sha256:[0-9a-f]{64}$/.test(composition.composition_hash)) throw new Error("composition.composition_hash must be the compositionHash of the resolved lines");
1032
+ if (!Number.isInteger(composition.line_count) || composition.line_count < 1) throw new Error(`composition.line_count must be a positive integer, got ${composition.line_count}`);
1033
+ }
1034
+
1035
+ /**
1036
+ * What the rail answered about one attempt, as the bundle records it: the
1037
+ * identifiers a reader follows the money by, and never `raw`, which carries
1038
+ * whatever the provider chose to echo back.
1039
+ */
1040
+ function railAnswer(outcome: RailOutcome): Record<string, unknown> {
1041
+ switch (outcome.status) {
1042
+ case "accepted":
1043
+ return { transaction_id: outcome.transaction_id, sandbox: outcome.sandbox };
1044
+ case "settled":
1045
+ return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox, ...(outcome.replayed ? { idempotent_replay: true } : {}) };
1046
+ case "failed":
1047
+ return { code: outcome.code, message: outcome.message, ...(outcome.spent ? { spent: true } : {}), ...(outcome.held ? { held: outcome.held } : {}) };
1048
+ default:
1049
+ return { code: outcome.code, message: outcome.message };
1050
+ }
1051
+ }
1052
+
1053
+ /** The approval link a receipt sealed is the one a spend sent: the same hashes, in either spelling of the `sha256:` prefix. */
1054
+ function sealsApproval(sealed: SealedSpendApproval | null, sent: SpendApproval): boolean {
1055
+ return sealed !== null && sameApprovalHash(sealed.items_hash, sent.items_hash) && sameApprovalHash(sealed.batch_hash, sent.batch_hash ?? null);
1056
+ }
1057
+
1058
+ /**
1059
+ * A payment settled and the receipt CodeSpar sealed for it names a different
1060
+ * payee, or none, or seals another approval than the one the spend sent.
1061
+ * Raised after the execution's outcome is saved, never instead of it: the
1062
+ * rail's answer is the record of where the money went, and this error is
1063
+ * about the evidence. Not a warning, because a receipt that does not name
1064
+ * what was paid proves nothing about that payment.
1065
+ */
1066
+ export class ReceiptSealMismatchError extends Error {
1067
+ constructor(
1068
+ readonly executionId: string,
1069
+ readonly mismatches: string[],
1070
+ ) {
1071
+ super(`execution ${executionId}: the sealed receipt does not seal what was paid — ${mismatches.join("; ")}`);
1072
+ this.name = "ReceiptSealMismatchError";
1073
+ }
1074
+ }