@codespar/cli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/README.md +1 -0
  2. package/dist/surface.js +53 -1
  3. package/dist/surface.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/bills-agent/README.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  7. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  8. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  9. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  10. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  11. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  12. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  13. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  14. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  15. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  16. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  17. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  18. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  19. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  20. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  21. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  22. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  23. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  24. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  25. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  26. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  27. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  28. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  29. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  30. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  31. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  32. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  33. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  34. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  35. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  36. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  37. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  38. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  39. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  40. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  41. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  42. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  43. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  44. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  45. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  46. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  63. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  64. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  66. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  67. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  69. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  70. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  71. package/templates/checkout-agent/LICENSE +21 -0
  72. package/templates/checkout-agent/README.md +28 -0
  73. package/templates/checkout-agent/_gitignore +10 -0
  74. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  75. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  76. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  77. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  78. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  79. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  83. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  110. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  111. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  112. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  113. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  158. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  159. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  160. package/templates/checkout-agent/package.json +38 -0
  161. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  162. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  163. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  167. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  169. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  170. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  171. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  172. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  173. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  174. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  175. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  176. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  177. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  178. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  179. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  180. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  181. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  182. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  184. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  185. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  186. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  187. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  188. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  189. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  190. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  191. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  192. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  193. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  195. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  196. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  197. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  198. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  199. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  200. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  201. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  202. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  203. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  204. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  205. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  206. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  208. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  209. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  210. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  211. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  212. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  213. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  216. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  217. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  218. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  219. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  220. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  221. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  222. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  223. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  224. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  269. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  270. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  271. package/templates/checkout-agent/tsconfig.base.json +16 -0
  272. package/templates/checkout-agent/vitest.config.ts +12 -0
  273. package/templates/collections-agent/README.md +6 -1
  274. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  276. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  277. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  278. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  279. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  280. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  281. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  283. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  284. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  285. package/templates/collections-agent/package.json +1 -0
  286. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  287. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  288. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  289. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  290. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  291. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  292. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  293. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  294. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  295. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  296. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  297. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  298. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  299. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  300. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  301. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  302. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  303. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  304. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  305. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  306. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  307. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  308. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  309. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  310. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  311. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  312. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  313. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  314. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  315. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  316. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  317. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  318. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  319. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  320. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  321. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  322. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  339. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  340. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  342. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  343. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  345. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  346. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  347. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  348. package/templates/hello-agent/README.md +1 -1
  349. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  350. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  351. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  352. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  353. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  354. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  355. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  356. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  357. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  358. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  359. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  360. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  361. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  362. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  363. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  364. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  365. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  366. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  367. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  368. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  369. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  370. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  371. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  372. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  373. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  374. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  375. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  376. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  377. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  378. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  379. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  380. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  381. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  382. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  383. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  384. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  385. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  402. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  403. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  405. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  406. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  408. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  409. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  410. package/templates/kits.lock.json +29 -6
  411. package/templates/supplier-payments-agent/README.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  418. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  420. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  424. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  426. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  427. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  428. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  429. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  430. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  431. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  432. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  433. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  435. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  436. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  437. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  438. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  439. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  440. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  441. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  442. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  446. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  447. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  448. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  449. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  452. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  453. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  477. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  478. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  479. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  481. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -12,19 +12,21 @@
12
12
  * meantime is caught at the last gate, which is the only one that matters.
13
13
  */
14
14
  import { checkApprovalArtifact, createApprovalArtifact, type ApprovalSigner } from "./approval.js";
15
- import type { ProofBundle } from "./bundle.js";
15
+ import { maskPayee, type ProofBundle } from "./bundle.js";
16
16
  import { evaluateEscalation, type Escalation } from "./escalate.js";
17
17
  import { CHARGE_CANCELLED, CHARGE_EXPIRED, CHARGE_PAID, PAYMENT_FAILED, PAYMENT_SUCCEEDED, type PublishedEvent } from "./events.js";
18
18
  import type { Guardrails } from "./guardrails.js";
19
- import { itemsHash, sha256Hex } from "./hash.js";
19
+ import { batchAttemptId, itemsHash, sha256Hex } from "./hash.js";
20
20
  import { newId } from "./ids.js";
21
21
  import { mandateExpired, payeeAllowed, resolveBeneficiary, windowCap, windowStart, type Mandate } from "./mandate.js";
22
22
  import type { Manifest } from "./manifest.js";
23
- import type { PaymentRail, RailOutcome, RailPayment } from "./rail.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";
24
26
  import type { MandateStatusReport, MandateStatusSource } from "./revocation.js";
25
27
  import { isTerminal, transition, type Execution, type ExecutionState } from "./state-machine.js";
26
28
  import type { StateStore } from "./state/store.js";
27
- import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
29
+ import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionComposition, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
28
30
 
29
31
  export interface ProposedItem {
30
32
  /** An alias from the mandate's named payees, or a raw payee key. */
@@ -61,6 +63,13 @@ export interface Proposal {
61
63
  * it. The core never invents one and never fills one in.
62
64
  */
63
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;
64
73
  }
65
74
 
66
75
  export type DraftResult =
@@ -94,6 +103,14 @@ type Verdict =
94
103
 
95
104
  type Gate = "draft" | "approve" | "execute";
96
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
+
97
114
  /** Open executions reserve the window: a cap is a ceiling on what may be committed, not a balance. */
98
115
  const WINDOW_STATES: ExecutionState[] = ["awaiting_approval", "approved", "executing", "settled"];
99
116
 
@@ -127,6 +144,7 @@ export class ExecutionEngine {
127
144
  const items = proposal.items.map((p) => this.resolveItem(p));
128
145
  const total = items.reduce((sum, i) => sum + i.amount, 0);
129
146
  if (proposal.batch) assertBatchBinding(proposal.batch);
147
+ if (proposal.composition) assertCompositionBinding(proposal.composition);
130
148
  const id = newId("exe");
131
149
  const execution: Execution<"drafted"> = {
132
150
  id,
@@ -140,6 +158,7 @@ export class ExecutionEngine {
140
158
  ...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
141
159
  items_hash: itemsHash(items),
142
160
  ...(proposal.batch ? { batch: proposal.batch } : {}),
161
+ ...(proposal.composition ? { composition: proposal.composition } : {}),
143
162
  mandate: { id: this.deps.mandate.id, version: this.deps.mandate.version },
144
163
  idempotency_key: `idk_${sha256Hex(`${this.deps.runId}:${id}`).slice(0, 32)}`,
145
164
  blocking_reasons: [],
@@ -149,11 +168,58 @@ export class ExecutionEngine {
149
168
  updated_at: now.toISOString(),
150
169
  };
151
170
  this.deps.store.saveExecution(execution);
152
- this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null });
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 } : {}) });
153
172
 
154
173
  return { ok: true, execution: await this.evaluateDraft(execution) };
155
174
  }
156
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
+
157
223
  // ---- deterministic policy ---------------------------------------------
158
224
 
159
225
  /**
@@ -179,6 +245,14 @@ export class ExecutionEngine {
179
245
  return { kind: "deny", reason: "beneficiary_not_allowed", detail };
180
246
  }
181
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
+
182
256
  const over = execution.items.find((i) => i.amount > mandate.per_tx_cap_minor);
183
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}` };
184
258
 
@@ -295,7 +369,7 @@ export class ExecutionEngine {
295
369
  if (!artifact) throw new Error(`execution ${executionId} is approved without an approval artifact`);
296
370
 
297
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.
298
- const payments = this.paymentsFor(execution);
372
+ const payments = this.paymentsFor(execution, artifact);
299
373
  const decided = this.deps.store.transaction((): Execution => {
300
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.
301
375
  const current = { id: this.deps.mandate.id, version: this.deps.mandate.version };
@@ -310,6 +384,9 @@ export class ExecutionEngine {
310
384
  }
311
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})` }));
312
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` }));
313
390
 
314
391
  const verdict = this.policy(execution, now, "execute", artifact.approver.type === "person");
315
392
  if (verdict.kind === "deny" || verdict.kind === "block") {
@@ -343,14 +420,33 @@ export class ExecutionEngine {
343
420
  private async dispatch(execution: Execution<"executing">, payments: RailPayment[]): Promise<Execution> {
344
421
  const outcomes: ItemOutcome[] = [...execution.outcomes];
345
422
  const unknown: string[] = [];
423
+ const sealMismatches: string[] = [];
346
424
  this.deps.store.updateOutbox(execution.idempotency_key, "sent", undefined, this.clock().toISOString());
347
425
 
348
- for (const [index, payment] of payments.entries()) {
426
+ const generations: Record<number, number> = { ...execution.attempt_generations };
427
+ for (const [index, presented] of payments.entries()) {
349
428
  if (outcomes.some((o) => o.index === index)) continue;
429
+ let payment = presented;
350
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.
351
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 });
352
- const outcome = await this.deps.rail.pay(payment);
432
+ let outcome = await this.deps.rail.pay(payment);
353
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
+ }
354
450
  if (outcome.status === "uncertain") {
355
451
  this.record("rail.uncertain", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, message: outcome.message });
356
452
  // No outcome is recorded because there is none. The siblings are still
@@ -360,7 +456,7 @@ export class ExecutionEngine {
360
456
  continue;
361
457
  }
362
458
  if (outcome.status === "failed") {
363
- outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}` });
459
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}`, ...(outcome.held ? { held: outcome.held } : {}) });
364
460
  continue;
365
461
  }
366
462
  if (outcome.status === "accepted") {
@@ -369,10 +465,13 @@ export class ExecutionEngine {
369
465
  this.recordInstrument(execution.id, payment.attempt_id, outcome.transaction_id, outcome.instrument);
370
466
  continue;
371
467
  }
372
- outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}) });
373
- await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
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);
374
471
  }
375
- return this.close({ ...execution, outcomes }, payments.length, unknown.join("; ") || undefined);
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;
376
475
  }
377
476
 
378
477
  /**
@@ -395,10 +494,13 @@ export class ExecutionEngine {
395
494
  if (execution.outcomes.length < attempts) {
396
495
  return this.leaveUnresolved(updated, unknownDetail ?? `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
397
496
  }
398
- const failed = execution.outcomes.find((o) => o.status === "failed");
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");
399
499
  if (failed) {
400
500
  this.deps.store.updateOutbox(execution.idempotency_key, "failed", execution.outcomes, at);
401
- const reason: ExecutionReason = failed.code === "charge_expired" || failed.code === "charge_cancelled" ? failed.code : "rail_failed";
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";
402
504
  return this.persist(transition(updated, "failed", { at, actor: this.agentActor, reason, detail: failed.error ?? "rail refused" }));
403
505
  }
404
506
  if (execution.outcomes.filter((o) => o.status === "settled").length === attempts) {
@@ -439,7 +541,8 @@ export class ExecutionEngine {
439
541
  if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
440
542
  }
441
543
 
442
- private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<void> {
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> {
443
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.
444
547
  const appended = this.deps.store.appendEvent({
445
548
  run_id: this.deps.runId,
@@ -450,15 +553,38 @@ export class ExecutionEngine {
450
553
  at: this.clock().toISOString(),
451
554
  });
452
555
  if (appended) this.deps.bundle.event({ ...appended, actor: this.agentActor, item_index: index });
453
- if (receiptId) await this.saveReceipt(execution.id, receiptId);
556
+ return receiptId ? this.saveReceipt(execution.id, receiptId, payment) : undefined;
454
557
  }
455
558
 
456
- private async saveReceipt(executionId: string, receiptId: string): Promise<void> {
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> {
457
572
  const receipt = await this.deps.rail.receipt(receiptId, this.agentActor);
458
- if (receipt) {
459
- const path = this.deps.bundle.receipt(receipt);
460
- this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
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)}`);
461
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;
462
588
  }
463
589
 
464
590
 
@@ -470,12 +596,17 @@ export class ExecutionEngine {
470
596
  async collectReceipts(executionId: string): Promise<string[]> {
471
597
  const execution = this.mustGet(executionId);
472
598
  const held = new Set(this.deps.bundle.listReceipts().map((f) => f.replace(/\.json$/, "")));
599
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
473
600
  const fetched: string[] = [];
601
+ const sealMismatches: string[] = [];
474
602
  for (const outcome of execution.outcomes) {
475
- if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id)) continue;
476
- await this.saveReceipt(execution.id, outcome.receipt_id);
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);
477
607
  fetched.push(outcome.receipt_id);
478
608
  }
609
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
479
610
  return fetched;
480
611
  }
481
612
 
@@ -492,13 +623,14 @@ export class ExecutionEngine {
492
623
  async reconcile(executionId: string): Promise<Execution> {
493
624
  const execution = this.mustGet(executionId);
494
625
  if (execution.state !== "executing") return execution;
495
- const payments = this.paymentsFor(execution);
626
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
496
627
  const outbox = this.deps.store.getOutbox(execution.idempotency_key);
497
628
  if (outbox?.status === "pending") {
498
629
  return this.leaveUnresolved(execution as Execution<"executing">, "outbox pending: nothing was ever sent; `resume` dispatches it, reconcile does not");
499
630
  }
500
631
 
501
632
  const outcomes: ItemOutcome[] = [...execution.outcomes];
633
+ const sealMismatches: string[] = [];
502
634
  let unresolved: string | undefined;
503
635
  for (const [index, payment] of payments.entries()) {
504
636
  const prior = outcomes.find((o) => o.index === index);
@@ -536,15 +668,17 @@ export class ExecutionEngine {
536
668
  continue;
537
669
  }
538
670
  if (seen.status === "failed") {
539
- replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}` });
671
+ replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}`, ...(seen.held ? { held: seen.held } : {}) });
540
672
  continue;
541
673
  }
542
- replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}) });
543
- await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
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);
544
677
  }
545
678
  const updated: Execution<"executing"> = { ...(execution as Execution<"executing">), outcomes };
546
- if (unresolved) return this.leaveUnresolved(updated, unresolved);
547
- return this.close(updated, payments.length);
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;
548
682
  }
549
683
 
550
684
  /**
@@ -559,7 +693,7 @@ export class ExecutionEngine {
559
693
  const outbox = this.deps.store.getOutbox(execution.idempotency_key);
560
694
  if (outbox?.status !== "pending") return this.reconcile(executionId);
561
695
  this.record("rail.resume", execution.id, { detail: "outbox pending: nothing was ever sent; dispatching once under the same attempt ids" });
562
- return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution));
696
+ return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution, this.approvalOf(execution)));
563
697
  }
564
698
 
565
699
  /** Time-based exits: an open execution past its approval TTL closes as `expired`. */
@@ -770,21 +904,44 @@ export class ExecutionEngine {
770
904
  };
771
905
  }
772
906
 
773
- private paymentsFor(execution: Execution): RailPayment[] {
774
- return execution.items.map((item, index) => ({
775
- attempt_id: `att_${execution.idempotency_key.slice(4)}_${index}`,
776
- mandate_id: execution.mandate.id,
777
- amount_minor: item.amount,
778
- currency: item.currency,
779
- payee: item.payee,
780
- beneficiary: item.beneficiary,
781
- purpose: this.deps.mandate.purpose,
782
- agent_id: this.deps.mandate.agent_id,
783
- consumer_id: this.deps.mandate.consumer_id,
784
- ...(item.description ? { description: item.description } : {}),
785
- ...(item.due_date ? { due_date: item.due_date } : {}),
786
- actor: this.agentActor,
787
- }));
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;
788
945
  }
789
946
 
790
947
  private withoutApproval(execution: Execution<"approved">): Execution<"approved"> {
@@ -827,7 +984,7 @@ export class ExecutionEngine {
827
984
  private storeApproval(artifact: ApprovalArtifact): void {
828
985
  this.deps.store.saveApproval(artifact);
829
986
  this.deps.bundle.approval(artifact);
830
- this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, escalation: artifact.escalation ?? null });
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 });
831
988
  }
832
989
 
833
990
  private persist<S extends ExecutionState>(execution: Execution<S>): Execution<S> {
@@ -868,6 +1025,13 @@ function assertBatchBinding(batch: ExecutionBatch): void {
868
1025
  }
869
1026
  }
870
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
+
871
1035
  /**
872
1036
  * What the rail answered about one attempt, as the bundle records it: the
873
1037
  * identifiers a reader follows the money by, and never `raw`, which carries
@@ -878,8 +1042,33 @@ function railAnswer(outcome: RailOutcome): Record<string, unknown> {
878
1042
  case "accepted":
879
1043
  return { transaction_id: outcome.transaction_id, sandbox: outcome.sandbox };
880
1044
  case "settled":
881
- return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox };
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 } : {}) };
882
1048
  default:
883
1049
  return { code: outcome.code, message: outcome.message };
884
1050
  }
885
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
- import type { ExecutionItem } from "./types.js";
2
+ import type { CompositionLine, ExecutionBatch, ExecutionItem } from "./types.js";
3
3
 
4
4
  /** JSON with keys sorted at every level, so the same value always hashes the same. */
5
5
  export function canonicalJson(value: unknown): string {
@@ -52,6 +52,62 @@ export function batchHash(lines: readonly ExecutionItem[]): string {
52
52
  return itemsHash(lines);
53
53
  }
54
54
 
55
+ /**
56
+ * The hash of what an execution's amount is composed of (a cart's resolved
57
+ * lines, in order). The same canonicalisation as `itemsHash` — sorted-key
58
+ * JSON, SHA-256, the `sha256:` prefix — over the fields that decide a
59
+ * composition, and deliberately not `itemsHash` itself: its fields are the
60
+ * ones that decide where money goes (payee, amount, currency, due date), and
61
+ * none of them sees a quantity or a unit price. Ten units at a 10% discount
62
+ * and nine at list price are the same line amount; they are not the same
63
+ * sale. Order is part of it, like a batch.
64
+ */
65
+ export function compositionHash(lines: readonly CompositionLine[]): string {
66
+ const canonical = lines.map((line) => ({
67
+ ref: line.ref,
68
+ quantity: line.quantity,
69
+ unit_amount: line.unit_amount,
70
+ amount: line.amount,
71
+ currency: line.currency,
72
+ }));
73
+ return `sha256:${sha256Hex(canonicalJson(canonical))}`;
74
+ }
75
+
76
+ /**
77
+ * The `attempt_id` of an item of a batch line, derived from WHAT is being
78
+ * paid rather than from the execution that happens to pay it:
79
+ *
80
+ * ska_ + hex(sha256(mandate_id | batch_hash | line index | item index))
81
+ *
82
+ * An execution's own attempt ids come from its `idempotency_key`, which comes
83
+ * from a random execution id, so a second run of the same batch on a machine
84
+ * that has lost `.codespar/state.db` (or never had it) would present fresh ids
85
+ * and the API would take each line as a new payment (OPEN_QUESTIONS §39c).
86
+ * Derived this way, the same line of the same list under the same mandate is
87
+ * the same attempt on any machine, and the API answers a repeat of it from its
88
+ * record of that attempt (ent#1671): the original body when it settled, a 409
89
+ * while it is in flight or pinned, never a second dispatch.
90
+ *
91
+ * Every input decides the payment: the mandate is whose money, the hash is the
92
+ * exact ordered list (payee, amount, currency of every line), the index is
93
+ * which line of it. A list that changes in any line is a different hash and so
94
+ * different ids. `ska_` names the namespace (starter-kit attempt) and 68
95
+ * characters sit inside the API's 128.
96
+ *
97
+ * `generation` exists because the API never runs an attempt id twice: once an
98
+ * attempt FAILED and moved no money, a repeat of it answers
99
+ * `psp_attempt_conflict` for good. A line whose payment the provider refused
100
+ * must still be payable by a later run of the same list, so it moves to the
101
+ * next generation, and only on that answer: the server saying the id is
102
+ * spent is the one fact every machine reads the same way, so every machine
103
+ * walks the same sequence and none of them finds a "free" id past one that
104
+ * paid. Generation 0 hashes exactly as above.
105
+ */
106
+ export function batchAttemptId(mandateId: string, batch: Pick<ExecutionBatch, "batch_hash" | "index">, item: number, generation = 0): string {
107
+ const base = `${mandateId}|${batch.batch_hash}|${batch.index}|${item}`;
108
+ return `ska_${sha256Hex(generation === 0 ? base : `${base}|${generation}`)}`;
109
+ }
110
+
55
111
  export function hmacSha256Hex(key: Buffer, payload: string): string {
56
112
  return createHmac("sha256", key).update(payload).digest("hex");
57
113
  }
@@ -12,7 +12,9 @@ export * from "./clock.js";
12
12
  export * from "./secrets.js";
13
13
  export * from "./revocation.js";
14
14
  export * from "./rail.js";
15
+ export * from "./quote.js";
15
16
  export * from "./receipt-verification.js";
17
+ export * from "./receipt-chain.js";
16
18
  export * from "./tools.js";
17
19
  export * from "./bundle.js";
18
20
  export * from "./engine.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The offer a spend presents to the API (`SpendQuote`): seller, resource,
3
+ * price and payee. When a spend carries one, the API binds it into the
4
+ * receipt's chain between the mandate and the payment, so the sealed receipt
5
+ * names the payee; without one the chain carries no payee at all, and
6
+ * `GET /v1/consumers/receipts/{id}` answers `quote: null` (OPEN_QUESTIONS §18).
7
+ *
8
+ * The quote is built from the approval artifact's items, the list its
9
+ * `items_hash` covers, and from nothing the model says after approval. The
10
+ * API compares the quote to what settled in OBSERVE mode only: a divergence is
11
+ * recorded on the receipt and the money still moves. So the kit refuses a
12
+ * divergence itself, before the call.
13
+ */
14
+ import type { ApprovalArtifact } from "./types.js";
15
+
16
+ export interface SpendQuote {
17
+ seller: string;
18
+ resource: string;
19
+ price_minor: number;
20
+ payee: string;
21
+ /**
22
+ * When the offer was approved: the artifact's `approved_at`. Absent on a
23
+ * batch line, whose quote must be the same on every run of the list: the
24
+ * API compares the quote's digest on a repeat of an `attempt_id`, so an
25
+ * approval time in it would make the same line a different payment on
26
+ * every machine (OPEN_QUESTIONS §39c). The time is still in the artifact.
27
+ */
28
+ at?: string;
29
+ }
30
+
31
+ /**
32
+ * The quote for line `index` of what was approved, or undefined when the
33
+ * artifact has no such line. `stable` leaves the approval time out, for a
34
+ * batch line (see `SpendQuote.at`).
35
+ */
36
+ export function quoteFromApproval(artifact: ApprovalArtifact, index: number, purpose: string, options: { stable?: boolean } = {}): SpendQuote | undefined {
37
+ const item = artifact.items[index];
38
+ if (!item) return undefined;
39
+ const quote = { seller: item.beneficiary, resource: item.description ?? purpose, price_minor: item.amount, payee: item.payee };
40
+ return options.stable ? quote : { ...quote, at: artifact.approved_at };
41
+ }
42
+
43
+ /** The quote a spend presents, or why it may not go out: none, or one that does not name exactly the amount and the payee it pays. */
44
+ export function checkQuote(payment: { amount_minor: number; payee: string; quote?: SpendQuote | undefined }): { ok: true; quote: SpendQuote } | { ok: false; code: "quote_missing" | "quote_mismatch"; detail: string } {
45
+ const { quote } = payment;
46
+ if (!quote) return { ok: false, code: "quote_missing", detail: "a spend carries the quote of what was approved, or it does not go out" };
47
+ if (quote.price_minor !== payment.amount_minor) return { ok: false, code: "quote_mismatch", detail: `the approved price ${quote.price_minor} is not the amount ${payment.amount_minor} about to be spent` };
48
+ if (quote.payee !== payment.payee) return { ok: false, code: "quote_mismatch", detail: "the approved payee is not the payee about to be paid" };
49
+ return { ok: true, quote };
50
+ }