@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
@@ -15,7 +15,7 @@ export type MandateStatus = "active" | "paused" | "revoked" | "expired";
15
15
  export interface MandateStatusReport {
16
16
  mandate_id: string;
17
17
  status: MandateStatus | "unknown";
18
- /** True when the organization pressed the kill switch (`org pauseAll`). The API has no such read yet; only the stub can say it. */
18
+ /** True when the organization pressed the kill switch (ent#1648). The API says it as `org_paused` on `GET /v1/mandates/{id}`; the stub says it from `pauseAll`. */
19
19
  org_paused: boolean;
20
20
  checked_at: string;
21
21
  /** Which source answered, so the bundle says whether the check was real. */
@@ -6,7 +6,7 @@
6
6
  * does not compile; the runtime check below refuses the same pair when the
7
7
  * types were bypassed (a cast, a row read back from SQLite).
8
8
  */
9
- import type { Actor, ApprovalMode, EscalationTrigger, ExecutionBatch, ExecutionItem, ExecutionReason, ItemOutcome, MandateRef } from "./types.js";
9
+ import type { Actor, ApprovalMode, EscalationTrigger, ExecutionBatch, ExecutionComposition, ExecutionItem, ExecutionReason, ItemOutcome, MandateRef } from "./types.js";
10
10
 
11
11
  export type ExecutionState =
12
12
  | "drafted"
@@ -78,6 +78,15 @@ export interface Execution<S extends ExecutionState = ExecutionState> {
78
78
  items_hash: string;
79
79
  /** Present when this execution is one line of a batch: the set it was presented inside, and where in it. */
80
80
  batch?: ExecutionBatch;
81
+ /** Present when this execution's amount is composed of lines (a cart): the lines' hash, not the lines. */
82
+ composition?: ExecutionComposition;
83
+ /**
84
+ * A batch line's attempt generation per item index, when it is not 0: how
85
+ * many of the line's derived attempt ids the rail answered as spent before
86
+ * the one this execution presented (`batchAttemptId`). Saved BEFORE the
87
+ * next id is presented, so a reconcile looks up the id that was sent.
88
+ */
89
+ attempt_generations?: Record<number, number>;
81
90
  mandate: MandateRef;
82
91
  /** Written once, before the rail is called. Retrying reuses it. */
83
92
  idempotency_key: string;
@@ -8,8 +8,9 @@
8
8
  * a run with a test key gets instead; this file is never consulted on that
9
9
  * path.
10
10
  *
11
- * It also holds the organization kill switch (`pauseAll`), which the API
12
- * does not expose yet, so `org_paused` can be exercised locally.
11
+ * It also holds the organization kill switch (`pauseAll`), so `org_paused`
12
+ * can be exercised locally the way `GET /v1/mandates/{id}` reports it
13
+ * (ent#1648): a flag next to the mandate's own status, which it leaves alone.
13
14
  */
14
15
  import type { MandateStatusSource, MandateStatus, MandateStatusReport } from "../revocation.js";
15
16
  import type { StateStore } from "../state/store.js";
@@ -46,7 +47,7 @@ export class LocalMandateStatusStub implements MandateStatusSource {
46
47
  this.store.stubSetMandateStatus(mandateId, "active", null, this.clock().toISOString());
47
48
  }
48
49
 
49
- /** The organization kill switch, locally: the API has no `org pauseAll` yet. */
50
+ /** The organization kill switch, locally. Against the API it is pressed with `POST /v1/orgs/{orgId}/pause`, which the kit never calls. */
50
51
  pauseAll(): void {
51
52
  this.store.stubSetOrgPaused(STUB_ORG_ID, true, this.clock().toISOString());
52
53
  }
@@ -3,11 +3,16 @@
3
3
  * scenarios, the adversarial suite and the restart test run offline. Every
4
4
  * attempt is persisted in state.db before the outcome is returned, which is
5
5
  * what lets a process killed right after "the money left" be reconciled on
6
- * `resume` instead of paying again. Receipts here are HMAC-shaped only in
6
+ * `resume` instead of paying again. A repeat of an attempt id is answered the
7
+ * way the deployed API answers it (ent#1671): replayed when it settled,
8
+ * in flight while the provider is on it, spent when it failed, and refused
9
+ * when the id arrives with a different payment. A stub that simply replayed
10
+ * whatever it held would let a suite pass that the API fails. Receipts here are HMAC-shaped only in
7
11
  * form; nothing about them is verifiable by anyone.
8
12
  */
9
- import { sha256Hex } from "../hash.js";
10
- import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
13
+ import { canonicalJson, sha256Hex } from "../hash.js";
14
+ import { checkQuote } from "../quote.js";
15
+ import { checkSpendApproval, type PaymentRail, type RailLookup, type RailOutcome, type RailPayment, type RailReceipt } from "../rail.js";
11
16
  import type { StateStore } from "../state/store.js";
12
17
  import type { Actor } from "../types.js";
13
18
 
@@ -29,6 +34,34 @@ export interface StubRailOptions {
29
34
  afterDispatch?: (attemptId: string) => void;
30
35
  /** Attempt ids whose first lookup answers `in_flight` and whose second lookup finds the provider finished (settled), with no pay() in between. */
31
36
  inFlightThenSettled?: string[];
37
+ /**
38
+ * Where the "provider" keeps its books, when that is not the agent's own
39
+ * state file. The real rail is a server two machines share; a test that
40
+ * runs the same batch from two state files points both at one ledger.
41
+ */
42
+ ledger?: StateStore;
43
+ }
44
+
45
+ /** What the stub's books hold for an attempt the "provider" took and has not finished. */
46
+ const IN_FLIGHT = { status: "in_flight" } as const;
47
+
48
+ type Recorded = RailOutcome | typeof IN_FLIGHT;
49
+
50
+ /**
51
+ * The fields a repeat of an attempt id is compared on, the stub's share of
52
+ * the API's attempt tuple (ent#1671): whose money, how much, to whom, the
53
+ * digest of the quote and the approval the spend carried (ent#1670). Rail
54
+ * and instrument are the API's and have no stub counterpart.
55
+ */
56
+ function mismatchedFields(recorded: Omit<RailPayment, "actor">, asked: Omit<RailPayment, "actor">): string[] {
57
+ const out: string[] = [];
58
+ if (recorded.mandate_id !== asked.mandate_id) out.push("mandate");
59
+ if (recorded.amount_minor !== asked.amount_minor) out.push("amount_minor");
60
+ if (recorded.currency !== asked.currency) out.push("currency");
61
+ if (recorded.payee !== asked.payee) out.push("payee");
62
+ if (canonicalJson(recorded.quote ?? null) !== canonicalJson(asked.quote ?? null)) out.push("quote");
63
+ if (canonicalJson(recorded.approval ?? null) !== canonicalJson(asked.approval ?? null)) out.push("approval");
64
+ return out;
32
65
  }
33
66
 
34
67
  export class StubRail implements PaymentRail {
@@ -36,18 +69,19 @@ export class StubRail implements PaymentRail {
36
69
  private readonly clock: () => Date;
37
70
  private readonly uncertainPending: Set<string>;
38
71
 
72
+ private readonly store: StateStore;
73
+
39
74
  constructor(
40
- private readonly store: StateStore,
75
+ store: StateStore,
41
76
  private readonly options: StubRailOptions = {},
42
77
  ) {
78
+ this.store = options.ledger ?? store;
43
79
  this.clock = options.clock ?? (() => new Date());
44
80
  this.uncertainPending = new Set(options.uncertainOnce ?? []);
45
81
  }
46
82
 
47
83
  private uncertainArmed = false;
48
84
  private readonly inFlightSeen = new Set<string>();
49
- /** Attempts that answered `uncertain` because the stub was armed: the "provider" did take them, and a later lookup finds them finished. */
50
- private readonly lateAttempts = new Set<string>();
51
85
  /** Attempts that already gave their one `uncertain`, so a re-presentation behaves normally. */
52
86
  private readonly uncertainAnswered = new Set<string>();
53
87
  /** How many times pay() reached the point of persisting a NEW attempt. */
@@ -59,12 +93,19 @@ export class StubRail implements PaymentRail {
59
93
  }
60
94
 
61
95
  async pay(payment: RailPayment): Promise<RailOutcome> {
96
+ // The same refusal the CodeSpar rail makes before its call, so a scenario exercises it.
97
+ const quoted = checkQuote(payment);
98
+ if (!quoted.ok) return { status: "failed", code: quoted.code, message: `${quoted.detail}; nothing was sent` };
99
+ const approval = checkSpendApproval(payment);
100
+ if (!approval.ok) return { status: "failed", code: approval.code, message: `${approval.detail}; nothing was sent` };
101
+ const { actor: _actor, ...request } = payment;
62
102
  const existing = this.store.stubRailGet(payment.attempt_id);
63
- if (existing) return existing.outcome as RailOutcome;
103
+ if (existing) return this.repeat(payment.attempt_id, existing.request as Omit<RailPayment, "actor">, existing.outcome as Recorded, request);
64
104
 
65
105
  const uncertainPayee = this.options.uncertainPayees?.includes(payment.payee) === true && !this.uncertainAnswered.has(payment.attempt_id);
66
106
  if (this.uncertainArmed || this.uncertainPending.has(payment.attempt_id) || uncertainPayee) {
67
- if (this.uncertainArmed || uncertainPayee) this.lateAttempts.add(payment.attempt_id);
107
+ // Armed and payee-scripted attempts were TAKEN by the provider, so they are on its books as in flight; `uncertainOnce` never reached it.
108
+ if (this.uncertainArmed || uncertainPayee) this.store.stubRailPut(payment.attempt_id, request, IN_FLIGHT, this.clock().toISOString());
68
109
  this.uncertainAnswered.add(payment.attempt_id);
69
110
  this.uncertainArmed = false;
70
111
  this.uncertainPending.delete(payment.attempt_id);
@@ -74,40 +115,52 @@ export class StubRail implements PaymentRail {
74
115
  const at = this.clock().toISOString();
75
116
  const outcome: RailOutcome = this.options.refusePayees?.includes(payment.payee)
76
117
  ? { status: "failed", code: "psp_dispatch_failed", message: `stub: provider refused payee ${payment.payee}` }
77
- : {
78
- status: "settled",
79
- transaction_id: `stubtx_${sha256Hex(payment.attempt_id).slice(0, 16)}`,
80
- receipt_id: `rcpt_stub_${sha256Hex(`receipt:${payment.attempt_id}`).slice(0, 16)}`,
81
- money_moved: false,
82
- sandbox: true,
83
- raw: { stub: true, attempt_id: payment.attempt_id, at },
84
- };
85
- const { actor: _actor, ...request } = payment;
118
+ : settledOutcome(payment.attempt_id, { stub: true, attempt_id: payment.attempt_id, at });
86
119
  this.payCount += 1;
87
120
  this.store.stubRailPut(payment.attempt_id, request, outcome, at);
88
121
  this.options.afterDispatch?.(payment.attempt_id);
89
122
  return outcome;
90
123
  }
91
124
 
125
+ /**
126
+ * A presentation of an attempt id the books already hold, answered the way
127
+ * the deployed API answers it (ent#1671, #1683), and never by paying:
128
+ * a different payment under the id is `attempt_id_conflict`, checked before
129
+ * the state; a settled one is its ORIGINAL outcome with `idempotent_replay`;
130
+ * one still in flight is `psp_attempt_in_flight`; a failed one is
131
+ * `psp_attempt_conflict`, spent for good.
132
+ */
133
+ private repeat(attemptId: string, recorded: Omit<RailPayment, "actor">, outcome: Recorded, asked: Omit<RailPayment, "actor">): RailOutcome {
134
+ const mismatched = mismatchedFields(recorded, asked);
135
+ if (mismatched.length > 0) {
136
+ return { status: "failed", code: "attempt_id_conflict", message: `stub: attempt ${attemptId} was already used for a different payment (differs in: ${mismatched.join(", ")}); nothing was held or sent`, held: "conflict" };
137
+ }
138
+ switch (outcome.status) {
139
+ case "in_flight":
140
+ return { status: "uncertain", code: "psp_attempt_in_flight", message: `stub: attempt ${attemptId} is claimed and its outcome is not yet recorded` };
141
+ case "settled":
142
+ return { ...outcome, replayed: true, raw: { ...(outcome.raw as Record<string, unknown>), idempotent_replay: true } };
143
+ case "failed":
144
+ return { status: "failed", code: "psp_attempt_conflict", message: `stub: attempt ${attemptId} already failed and moved no money; use a fresh attempt_id`, spent: true };
145
+ default:
146
+ return outcome;
147
+ }
148
+ }
149
+
92
150
  async lookup(attemptId: string, payment: RailPayment): Promise<RailLookup> {
93
- const recorded = this.store.stubRailGet(attemptId)?.outcome as RailOutcome | undefined;
94
- if (recorded) return recorded;
95
- if (this.options.inFlightThenSettled?.includes(attemptId) || this.lateAttempts.has(attemptId)) {
151
+ const { actor: _actor, ...request } = payment;
152
+ const existing = this.store.stubRailGet(attemptId);
153
+ const recorded = existing?.outcome as Recorded | undefined;
154
+ // Looking up IS presenting again on the real rail, so a recorded attempt answers exactly what a repeat of it answers.
155
+ if (existing && recorded && recorded.status !== "in_flight") return this.repeat(attemptId, existing.request as Omit<RailPayment, "actor">, recorded, request);
156
+ if (this.options.inFlightThenSettled?.includes(attemptId) || recorded?.status === "in_flight") {
96
157
  if (!this.inFlightSeen.has(attemptId)) {
97
158
  this.inFlightSeen.add(attemptId);
98
159
  return { status: "in_flight" };
99
160
  }
100
161
  // The provider finished on its own; the stub records the outcome as the rail would, without a new dispatch.
101
- const { actor: _actor, ...request } = payment;
102
- const outcome: RailOutcome = {
103
- status: "settled",
104
- transaction_id: `stubtx_${sha256Hex(attemptId).slice(0, 16)}`,
105
- receipt_id: `rcpt_stub_${sha256Hex(`receipt:${attemptId}`).slice(0, 16)}`,
106
- money_moved: false,
107
- sandbox: true,
108
- raw: { stub: true, attempt_id: attemptId, finished_in_background: true },
109
- };
110
- this.store.stubRailPut(attemptId, request, outcome, this.clock().toISOString());
162
+ const outcome = settledOutcome(attemptId, { stub: true, attempt_id: attemptId, finished_in_background: true });
163
+ this.store.stubRailReplace(attemptId, existing?.request ?? request, outcome, this.clock().toISOString());
111
164
  return outcome;
112
165
  }
113
166
  return undefined;
@@ -118,13 +171,17 @@ export class StubRail implements PaymentRail {
118
171
  if (!sealed) return undefined;
119
172
  const req = sealed.request as Omit<RailPayment, "actor">;
120
173
  const out = sealed.outcome as Extract<RailOutcome, { status: "settled" }>;
174
+ // Like the API: the payee on the receipt is the one the quote sealed, and none when the spend carried no quote.
121
175
  const body = {
122
176
  receipt_id: receiptId,
123
177
  state: "paid",
124
178
  mandate: { id: req.mandate_id },
125
- payment: { amount_minor: req.amount_minor, payee: req.payee, attempt_id: req.attempt_id, money_moved: false, sandbox: true, at: sealed.at },
179
+ // Like the API since ent#1670: the approval the spend carried is sealed and read back, batch_hash null when none was sent.
180
+ chain_version: req.approval ? 4 : 1,
181
+ approval: req.approval ? { items_hash: req.approval.items_hash, batch_hash: req.approval.batch_hash ?? null } : null,
182
+ payment: { amount_minor: req.amount_minor, payee: req.quote?.payee ?? null, attempt_id: req.attempt_id, money_moved: false, sandbox: true, at: sealed.at },
126
183
  };
127
- const chain = `sha256:${sha256Hex(JSON.stringify(body))}`;
184
+ const chain = `sha256:${sha256Hex(JSON.stringify(req.quote ? { ...body, quote: req.quote } : body))}`;
128
185
  return {
129
186
  ...body,
130
187
  chain,
@@ -139,3 +196,14 @@ export class StubRail implements PaymentRail {
139
196
  };
140
197
  }
141
198
  }
199
+
200
+ function settledOutcome(attemptId: string, raw: Record<string, unknown>): RailOutcome {
201
+ return {
202
+ status: "settled",
203
+ transaction_id: `stubtx_${sha256Hex(attemptId).slice(0, 16)}`,
204
+ receipt_id: `rcpt_stub_${sha256Hex(`receipt:${attemptId}`).slice(0, 16)}`,
205
+ money_moved: false,
206
+ sandbox: true,
207
+ raw,
208
+ };
209
+ }
@@ -3,10 +3,12 @@
3
3
  * authority comes before mandate-level authority: a tool that is not in
4
4
  * this file is refused before anything else looks at the call.
5
5
  *
6
- * Two kinds. `meta_tools` are CodeSpar meta-tool names, with the input the
7
- * kit accepts for them (a snapshot of the MCP shape, pinned by `mcp` in the
8
- * manifest; see OPEN_QUESTIONS on why the list is not fetched live).
9
- * `local_tools` are the agent's own read-only helpers.
6
+ * Two kinds. `meta_tools` borrow a CodeSpar meta-tool's name for the job
7
+ * they do, but the input shape is the kit's own and the kit's handler runs it:
8
+ * a payment or charge becomes a `drafted` execution, and the core sends the
9
+ * real call over REST. It is not a copy of the MCP's schema and does not match
10
+ * it (OPEN_QUESTIONS section 8 has the comparison). `local_tools` are the
11
+ * agent's own helpers: reads, and state the agent keeps for itself.
10
12
  */
11
13
  import { readFileSync } from "node:fs";
12
14
  import { z } from "zod";
@@ -17,8 +19,15 @@ const ToolDefinitionSchema = z
17
19
  name: z.string().regex(/^[a-z][a-z0-9_]*$/),
18
20
  description: z.string().min(1),
19
21
  input_schema: z.record(z.string(), z.unknown()),
20
- /** `payment` and `charge` tools create a `drafted` execution and nothing else; `read` tools never touch money. `charge` is the receivable side: the counterparty pays us. */
21
- effect: z.enum(["payment", "charge", "read"]),
22
+ /**
23
+ * `payment` and `charge` tools create a `drafted` execution and nothing
24
+ * else; `read` tools never touch money and change nothing. `charge` is the
25
+ * receivable side: the counterparty pays us. `state` changes durable local
26
+ * state (a cart in `state.db`) and moves no money — not a read, because
27
+ * calling it twice is not calling it once, and not a payment, because no
28
+ * execution comes out of it.
29
+ */
30
+ effect: z.enum(["payment", "charge", "read", "state"]),
22
31
  })
23
32
  .strict();
24
33
 
@@ -36,6 +45,8 @@ export const ToolsFileSchema = z
36
45
  }
37
46
  for (const [i, tool] of t.meta_tools.entries()) {
38
47
  if (!tool.name.startsWith("codespar_")) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["meta_tools", i, "name"], message: "meta-tools are named codespar_*" });
48
+ // A meta-tool borrows a CodeSpar name for a job CodeSpar does; the state the agent keeps for itself is not one.
49
+ if (tool.effect === "state") ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["meta_tools", i, "effect"], message: "a state tool is local: meta-tools move money or read" });
39
50
  }
40
51
  });
41
52
 
@@ -52,6 +52,45 @@ export interface ExecutionBatch {
52
52
  count: number;
53
53
  }
54
54
 
55
+ /**
56
+ * One line of what an execution's amount is MADE OF, as the kit resolved it:
57
+ * a cart line (`ref` a SKU, a quantity, the unit price the catalog gave it)
58
+ * or a discount line (a negative `amount`). Only `compositionHash` reads it;
59
+ * the core never stores the lines, only their hash and their count.
60
+ */
61
+ export interface CompositionLine {
62
+ ref: string;
63
+ quantity: number;
64
+ unit_amount: number;
65
+ amount: number;
66
+ currency: string;
67
+ }
68
+
69
+ /**
70
+ * What an execution's single amount was composed from, when it was composed
71
+ * from something: a sale is ONE charge, so ONE item whose amount is the
72
+ * order's total, and the lines of the cart behind that total are not items.
73
+ *
74
+ * That leaves the composition unattested by `items_hash`: two units at 100
75
+ * and one unit at 200 are the same item, the same amount, the same hash.
76
+ * `composition_hash` binds the lines — computed by the kit over the resolved
77
+ * lines in order, carried by the execution and by every artifact of it — so a
78
+ * cart recomposed at a constant total after approval is not the cart that was
79
+ * approved.
80
+ *
81
+ * Absent on an execution that is not composed from anything, and an artifact
82
+ * without it is the artifact of section 4.2 unchanged, byte for byte: the
83
+ * field is omitted rather than null, the same discipline as `batch`.
84
+ */
85
+ export interface ExecutionComposition {
86
+ /** The kit's reference for what was composed (`cart_id`). */
87
+ ref: string;
88
+ /** `compositionHash` over the resolved lines, in order. */
89
+ composition_hash: string;
90
+ /** How many lines that hash covers. */
91
+ line_count: number;
92
+ }
93
+
55
94
  /** The trigger of section 4.4 that sent a `mandate` execution to a human. */
56
95
  export type EscalationTrigger = "amount" | "new_beneficiary" | "outside_hours";
57
96
 
@@ -67,6 +106,8 @@ export type ExecutionReason =
67
106
  | "window_cap_exceeded"
68
107
  | "approval_expired"
69
108
  | "items_hash_mismatch"
109
+ /** The quote a spend would present does not name the approved amount and payee; refused before the call. */
110
+ | "quote_mismatch"
70
111
  | "mandate_changed"
71
112
  | "denied_by_approver"
72
113
  | "rail_failed"
@@ -80,7 +121,13 @@ export type ExecutionReason =
80
121
  /** A receivable was issued and the rail is waiting for the payer; the execution stays `executing` until `commerce.charge.*` closes it. */
81
122
  | "awaiting_settlement"
82
123
  | "charge_expired"
83
- | "charge_cancelled";
124
+ | "charge_cancelled"
125
+ /**
126
+ * An ISSUED receivable whose read now answers that its reference matches more than one charge. Terminal for the
127
+ * execution, but not "nothing moved": the charge exists and may still be paid. Reconcile it by the charge id; never
128
+ * issue another to the same payee until that is done, which is why the core refuses one (`policy`).
129
+ */
130
+ | "charge_reference_ambiguous";
84
131
 
85
132
  export interface MandateRef {
86
133
  id: string;
@@ -102,6 +149,8 @@ export interface ApprovalArtifact {
102
149
  items_hash: string;
103
150
  /** Present when this execution is one line of a batch: what binds the SET this line was approved inside. */
104
151
  batch?: ExecutionBatch;
152
+ /** Present when this execution's amount is composed of lines (a cart): what binds the composition that was approved. */
153
+ composition?: ExecutionComposition;
105
154
  /** Present when a section 4.4 trigger sent the execution to a human first. */
106
155
  escalation?: { trigger: EscalationTrigger; detail: string };
107
156
  actor: Actor;
@@ -117,6 +166,10 @@ export interface ItemOutcome {
117
166
  transaction_id?: string;
118
167
  /** The rail's code on a failed outcome (`charge_expired`, `charge_cancelled`, a provider code). */
119
168
  code?: string;
169
+ /** A failed outcome because the attempt id is held for another payment or another project (`RailOutcome.held`). */
170
+ held?: "conflict" | "unavailable";
171
+ /** A settled outcome the rail answered from its record of an earlier presentation (`RailOutcome.replayed`). */
172
+ replayed?: true;
120
173
  error?: string;
121
174
  /** What the payer is shown for an accepted receivable, as the rail handed it back. Presentation only; nothing here decides money. */
122
175
  instrument?: ChargeInstrument;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * OPEN_QUESTIONS §3: every spend carries the approval artifact's hashes, and
3
+ * the API seals them into the receipt's chain (v4, ent#1670). What is sent is
4
+ * the artifact's own `items_hash`, and for a batch line its `batch_hash` —
5
+ * the values the artifact's HMAC covers — and nothing the execution says
6
+ * later. A spend without them, or with a shape the API would refuse, never
7
+ * goes out; the bundle's receipt copy names the artifact by id so `verify`
8
+ * finds it by identity.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import type { ApiClient } from "@codespar/sdk";
13
+ import { describe, expect, it, vi } from "vitest";
14
+ import { CodeSparRail } from "../src/api/rail.js";
15
+ import { copyDisagreesWithRead, maskPayee } from "../src/bundle.js";
16
+ import { ReceiptSealMismatchError } from "../src/engine.js";
17
+ import { checkSpendApproval, spendApprovalOf, type PaymentRail, type RailPayment, type SealedSpendApproval } from "../src/rail.js";
18
+ import { ESCOLA, harness, MERCADO } from "./helpers.js";
19
+
20
+ const approver = { id: "usr_demo", channel: "terminal" };
21
+ const actor = { type: "agent" as const, agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" };
22
+ const ITEMS_HASH = `sha256:${"1".repeat(64)}`;
23
+
24
+ function payment(over: Partial<RailPayment> = {}): RailPayment {
25
+ return {
26
+ attempt_id: "att_seal_0",
27
+ mandate_id: "mdt_test_0001",
28
+ amount_minor: 185000,
29
+ currency: "BRL",
30
+ payee: ESCOLA,
31
+ purpose: "contas do mes",
32
+ agent_id: "bills-agent",
33
+ quote: { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, at: "2026-09-23T18:00:00.000Z" },
34
+ approval: { items_hash: ITEMS_HASH },
35
+ actor,
36
+ ...over,
37
+ };
38
+ }
39
+
40
+ function fakeApi(receiptBody?: unknown) {
41
+ const post = vi.fn(async (_path: string, _init: { body: Record<string, unknown> }) => ({ payment: { transactionId: "tx_1", moneyMoved: false }, receipt: { id: "rcpt_1" } }));
42
+ return { api: { post, get: vi.fn(async () => receiptBody) } as unknown as ApiClient, post };
43
+ }
44
+
45
+ /** The stub rail, but the receipt it hands back seals whatever approval the test says. */
46
+ function sealingApproval(sealed: SealedSpendApproval | null) {
47
+ return (inner: PaymentRail): PaymentRail => ({
48
+ name: inner.name,
49
+ pay: (p) => inner.pay(p),
50
+ lookup: (id, p, tx) => inner.lookup(id, p, tx),
51
+ receipt: async (id, a) => {
52
+ const r = await inner.receipt(id, a);
53
+ return r && { ...r, approval: sealed };
54
+ },
55
+ });
56
+ }
57
+
58
+ function recording() {
59
+ const h = harness({ mode: "human" });
60
+ const seen: RailPayment[] = [];
61
+ const pay = h.rail.pay.bind(h.rail);
62
+ h.rail.pay = async (p) => (seen.push(p), pay(p));
63
+ return { h, seen };
64
+ }
65
+
66
+ describe("every spend presents the artifact's hashes", () => {
67
+ it("a single execution: the artifact's items_hash on every line, no batch_hash, and the quote keeps the moment of approval", async () => {
68
+ const { h, seen } = recording();
69
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }, { payee: "mercado", amount: 32000 }] });
70
+ if (!d.ok) throw new Error("refused");
71
+ const approved = h.engine.approve(d.execution.id, approver);
72
+ const artifact = h.store.getApproval(approved.approval_id!)!;
73
+ expect((await h.engine.execute(approved.id)).state).toBe("settled");
74
+ expect(seen.map((p) => p.approval)).toEqual([{ items_hash: artifact.items_hash }, { items_hash: artifact.items_hash }]);
75
+ expect(seen.map((p) => p.quote?.at)).toEqual([artifact.approved_at, artifact.approved_at]);
76
+ // The form the API seals a timestamp in (RFC 3339, UTC, milliseconds), so the read gives back what was sent (ent#1670).
77
+ expect(artifact.approved_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
78
+ // What the "provider" holds for the attempt is what was sent.
79
+ expect((h.store.stubRailGet(seen[0]!.attempt_id)!.request as RailPayment).approval).toEqual({ items_hash: artifact.items_hash });
80
+ });
81
+
82
+ it("a batch line: the line's items_hash and the batch's batch_hash, and a quote with no time in it", async () => {
83
+ const { h, seen } = recording();
84
+ const batch = { ref: "folha-teste", batch_hash: `sha256:${"a".repeat(64)}`, index: 0, count: 2 };
85
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }], batch });
86
+ if (!d.ok) throw new Error("refused");
87
+ const approved = h.engine.approve(d.execution.id, approver);
88
+ const artifact = h.store.getApproval(approved.approval_id!)!;
89
+ await h.engine.execute(approved.id);
90
+ expect(seen[0]!.approval).toEqual({ items_hash: artifact.items_hash, batch_hash: batch.batch_hash });
91
+ expect(seen[0]!.quote).not.toHaveProperty("at");
92
+ });
93
+
94
+ it("spendApprovalOf carries exactly the artifact's two hashes", () => {
95
+ expect(spendApprovalOf({ items_hash: ITEMS_HASH })).toEqual({ items_hash: ITEMS_HASH });
96
+ expect(spendApprovalOf({ items_hash: ITEMS_HASH, batch: { ref: "r", batch_hash: `sha256:${"b".repeat(64)}`, index: 1, count: 3 } })).toEqual({ items_hash: ITEMS_HASH, batch_hash: `sha256:${"b".repeat(64)}` });
97
+ });
98
+ });
99
+
100
+ describe("the CodeSpar rail", () => {
101
+ it("sends approval on both spend routes, as the artifact spells it", async () => {
102
+ const withBatch = { items_hash: ITEMS_HASH, batch_hash: `sha256:${"b".repeat(64)}` };
103
+ const byEnvelope = fakeApi();
104
+ await new CodeSparRail(byEnvelope.api, { canonical: { any: "envelope" } as never, signature: "a".repeat(64) }).pay(payment({ approval: withBatch }));
105
+ expect(byEnvelope.post.mock.calls[0]![0]).toBe("/v1/consumer-payments/execute");
106
+ expect(byEnvelope.post.mock.calls[0]![1].body["approval"]).toEqual(withBatch);
107
+ const byId = fakeApi();
108
+ await new CodeSparRail(byId.api).pay(payment());
109
+ expect(byId.post.mock.calls[0]![0]).toBe("/v1/consumers/mandates/{id}/spend");
110
+ expect(byId.post.mock.calls[0]![1].body["approval"]).toEqual({ items_hash: ITEMS_HASH });
111
+ });
112
+
113
+ it("refuses a spend without it, or with a hash the API would refuse, without calling the API", async () => {
114
+ const { api, post } = fakeApi();
115
+ const rail = new CodeSparRail(api);
116
+ const { approval: _a, ...bare } = payment();
117
+ expect(await rail.pay(bare)).toMatchObject({ status: "failed", code: "approval_missing" });
118
+ expect(await rail.pay(payment({ approval: { items_hash: "sha256:ABC" } }))).toMatchObject({ status: "failed", code: "approval_malformed" });
119
+ expect(await rail.pay(payment({ approval: { items_hash: ITEMS_HASH, batch_hash: "f".repeat(63) } }))).toMatchObject({ status: "failed", code: "approval_malformed" });
120
+ expect(post).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it("checkSpendApproval accepts the two spellings the API accepts", () => {
124
+ expect(checkSpendApproval({ approval: { items_hash: ITEMS_HASH } }).ok).toBe(true);
125
+ expect(checkSpendApproval({ approval: { items_hash: "1".repeat(64) } }).ok).toBe(true);
126
+ expect(checkSpendApproval({ approval: { items_hash: `sha256:${"1".repeat(64)}`.toUpperCase() } }).ok).toBe(false);
127
+ });
128
+ });
129
+
130
+ describe("the receipt seals the approval the spend sent, or the run stops", () => {
131
+ it("the CodeSpar rail reads chain_version, the sealed approval and sig_sha256 through the SDK's types", async () => {
132
+ const read = {
133
+ receipt_id: "rcpt_1",
134
+ state: "paid",
135
+ chain_version: 4,
136
+ mandate: { id: "cm_1", nonce: "n", scope: "s", currency: "BRL", sig: "x", sig_sha256: "d".repeat(64) },
137
+ quote: { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, session_id: null, sig: "q", at: "2026-09-23T18:00:00.000Z" },
138
+ approval: { items_hash: ITEMS_HASH, batch_hash: null },
139
+ payment: { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_seal_0", money_moved: false, at: "2026-09-23T18:00:01.000Z" },
140
+ delivery: null,
141
+ chain: "c".repeat(64),
142
+ receipt_sig: "s",
143
+ exceptions: [],
144
+ };
145
+ const receipt = await new CodeSparRail(fakeApi(read).api).receipt("rcpt_1", actor);
146
+ expect(receipt).toMatchObject({ chain_version: 4, approval: { items_hash: ITEMS_HASH, batch_hash: null }, mandate: { id: "cm_1", sig_sha256: "d".repeat(64) } });
147
+ expect(JSON.stringify({ ...receipt, raw: undefined })).not.toContain('"sig":"x"');
148
+ const { approval: _a, ...older } = read;
149
+ expect((await new CodeSparRail(fakeApi({ ...older, chain_version: 1 }).api).receipt("rcpt_1", actor))?.approval).toBeNull();
150
+ });
151
+
152
+ for (const [name, sealed] of [
153
+ ["another list", { items_hash: `sha256:${"9".repeat(64)}`, batch_hash: null }],
154
+ ["no approval at all", null],
155
+ ] as const) {
156
+ it(`a receipt that seals ${name} is ReceiptSealMismatchError, after the outcome is saved`, async () => {
157
+ const h = harness({ mode: "human", wrapRail: sealingApproval(sealed) });
158
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
159
+ if (!d.ok) throw new Error("refused");
160
+ const approved = h.engine.approve(d.execution.id, approver);
161
+ const artifact = h.store.getApproval(approved.approval_id!)!;
162
+ await expect(h.engine.execute(approved.id)).rejects.toBeInstanceOf(ReceiptSealMismatchError);
163
+ expect(h.engine.get(approved.id)?.state).toBe("settled");
164
+ const mismatch = h.bundle.readEvents().find((e) => e["type"] === "receipt.seal_mismatch");
165
+ expect(mismatch?.["payload"]).toMatchObject({ sealed_approval: sealed, sent_approval: { items_hash: artifact.items_hash } });
166
+ });
167
+ }
168
+
169
+ it("the same hash in the other spelling of the prefix is the same approval", async () => {
170
+ const h = harness({ mode: "human" });
171
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
172
+ if (!d.ok) throw new Error("refused");
173
+ const approved = h.engine.approve(d.execution.id, approver);
174
+ const bare = h.store.getApproval(approved.approval_id!)!.items_hash.replace(/^sha256:/, "");
175
+ const spelled = harness({ mode: "human", dir: h.dir, wrapRail: sealingApproval({ items_hash: bare, batch_hash: null }) });
176
+ expect((await spelled.engine.execute(approved.id)).state).toBe("settled");
177
+ expect(spelled.bundle.readEvents().some((e) => e["type"] === "receipt.seal_mismatch")).toBe(false);
178
+ });
179
+ });
180
+
181
+ describe("the stub rail", () => {
182
+ it("makes the same refusal, so a scenario cannot pass without the hashes", async () => {
183
+ const h = harness();
184
+ const { approval: _a, ...bare } = payment();
185
+ expect(await h.rail.pay(bare)).toMatchObject({ status: "failed", code: "approval_missing" });
186
+ expect(h.rail.payCount).toBe(0);
187
+ });
188
+
189
+ it("answers a repeat under a different approval as attempt_id_conflict naming it, the way the API's fingerprint does (ent#1670)", async () => {
190
+ const h = harness();
191
+ expect(await h.rail.pay(payment())).toMatchObject({ status: "settled" });
192
+ const other = await h.rail.pay(payment({ approval: { items_hash: `sha256:${"9".repeat(64)}` } }));
193
+ expect(other).toMatchObject({ status: "failed", code: "attempt_id_conflict", held: "conflict" });
194
+ expect((other as { message: string }).message).toContain("approval");
195
+ expect(h.rail.payCount).toBe(1);
196
+ });
197
+ });
198
+
199
+ describe("the bundle's receipt copy", () => {
200
+ it("names the artifact the spend carried, by id, and stays masked", async () => {
201
+ const h = harness({ mode: "human" });
202
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
203
+ if (!d.ok) throw new Error("refused");
204
+ const approved = h.engine.approve(d.execution.id, approver);
205
+ const settled = await h.engine.execute(approved.id);
206
+ const copy = JSON.parse(readFileSync(join(h.bundle.dir, "receipts", `${settled.outcomes[0]!.receipt_id}.json`), "utf8")) as Record<string, unknown>;
207
+ expect(copy["approval_id"]).toBe(approved.approval_id);
208
+ expect(h.bundle.readApprovals().map((a) => a.approval_id)).toContain(copy["approval_id"]);
209
+ expect(JSON.stringify(copy)).not.toContain(ESCOLA);
210
+ });
211
+
212
+ it("is held against the read field by field, the payee in its masked form", () => {
213
+ const read = {
214
+ receipt_id: "rcpt_1",
215
+ chain: "c".repeat(64),
216
+ mandate: { id: "cm_1", nonce: "n" },
217
+ quote: { payee: ESCOLA },
218
+ payment: { rail: "pix", amount_minor: 185000, attempt_id: "att_0", money_moved: false, at: "2026-09-26T18:00:01.000Z" },
219
+ };
220
+ const copy = { receipt_id: "rcpt_1", chain: "c".repeat(64), mandate: { id: "cm_1" }, payment: { amount_minor: 185000, payee: maskPayee(ESCOLA), attempt_id: "att_0", money_moved: false, sandbox: true, at: "2026-09-26T18:00:01.000Z" } };
221
+ expect(copyDisagreesWithRead(copy, read)).toEqual([]);
222
+ expect(copyDisagreesWithRead({ ...copy, payment: { ...copy.payment, amount_minor: 1 } }, read)).toEqual(["payment.amount_minor"]);
223
+ expect(copyDisagreesWithRead({ ...copy, payment: { ...copy.payment, payee: maskPayee(MERCADO) } }, read)).toEqual(["payment.payee"]);
224
+ expect(copyDisagreesWithRead({ ...copy, chain: "d".repeat(64) }, read)).toEqual(["chain"]);
225
+ // A copy written since 0.16.10 carries what was sealed, and is held to it; one from before carries neither and is not faulted.
226
+ const sealedRead = { ...read, chain_version: 4, approval: { items_hash: ITEMS_HASH, batch_hash: null } };
227
+ const sealedCopy = { ...copy, chain_version: 4, approval: { batch_hash: null, items_hash: ITEMS_HASH } };
228
+ expect(copyDisagreesWithRead(sealedCopy, sealedRead)).toEqual([]);
229
+ expect(copyDisagreesWithRead({ ...sealedCopy, approval: { items_hash: `sha256:${"9".repeat(64)}`, batch_hash: null } }, sealedRead)).toEqual(["approval"]);
230
+ expect(copyDisagreesWithRead({ ...sealedCopy, chain_version: 3 }, sealedRead)).toEqual(["chain_version"]);
231
+ expect(copyDisagreesWithRead(copy, sealedRead)).toEqual([]);
232
+ });
233
+ });