@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
@@ -0,0 +1,204 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ // Deterministic ids, so an artifact is the same bytes on every run and its hash can be pinned.
4
+ vi.mock("../src/ids.js", async (original) => {
5
+ const real = await original<typeof import("../src/ids.js")>();
6
+ let n = 0;
7
+ return { ...real, newId: (prefix: string) => `${prefix}_${String((n += 1)).padStart(16, "0")}` };
8
+ });
9
+
10
+ import { checkApprovalArtifact, createApprovalArtifact, hmacSigner } from "../src/approval.js";
11
+ import { canonicalJson, compositionHash, itemsHash, sha256Hex } from "../src/hash.js";
12
+ import type { Execution } from "../src/state-machine.js";
13
+ import { ToolsFileSchema } from "../src/tools.js";
14
+ import type { CompositionLine, ExecutionComposition, ExecutionItem } from "../src/types.js";
15
+ import { harness } from "./helpers.js";
16
+
17
+ const signer = hmacSigner("local-dev-stub", Buffer.alloc(32, 1));
18
+ const now = new Date("2026-09-23T18:00:00.000Z");
19
+ const person = { type: "person" as const, id: "usr_terminal", channel: "terminal" };
20
+ const human = { type: "human" as const, id: "usr_terminal", channel: "terminal" };
21
+ const approver = { id: "usr_terminal", channel: "terminal" };
22
+
23
+ function execution(items: ExecutionItem[], over: Partial<Execution> = {}): Execution {
24
+ return {
25
+ id: "exe_1",
26
+ run_id: "run_1",
27
+ state: "awaiting_approval",
28
+ mode: "human",
29
+ actor: { type: "agent", agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" },
30
+ items,
31
+ total: items.reduce((s, i) => s + i.amount, 0),
32
+ currency: "BRL",
33
+ items_hash: itemsHash(items),
34
+ mandate: { id: "mdt_test_0001", version: 1 },
35
+ idempotency_key: "idk_1",
36
+ blocking_reasons: [],
37
+ outcomes: [],
38
+ history: [],
39
+ created_at: now.toISOString(),
40
+ updated_at: now.toISOString(),
41
+ ...over,
42
+ } as Execution;
43
+ }
44
+
45
+ const escola: ExecutionItem = { alias: "escola", beneficiary: "Escola Aurora", payee: "escola@exemplo.com.br", amount: 185000, currency: "BRL", description: "outubro" };
46
+
47
+ const line = (ref: string, quantity: number, unit_amount: number, amount = quantity * unit_amount): CompositionLine => ({ ref, quantity, unit_amount, amount, currency: "BRL" });
48
+ const twoLessons = [line("aula-avulsa", 2, 10000)];
49
+ const oneConsulting = [line("consultoria-1h", 1, 20000)];
50
+ const composition = (ref: string, lines: CompositionLine[]): ExecutionComposition => ({ ref, composition_hash: compositionHash(lines), line_count: lines.length });
51
+
52
+ describe("dependency 9.7: an artifact without a composition is the artifact it was before, byte for byte", () => {
53
+ it("hashes to what the same artifact hashed to on main before `composition` existed", () => {
54
+ // The bills-agent's shape: one payee, no batch, no composition. The two
55
+ // constants were computed on origin/main at 768c1a2, before this change,
56
+ // with the same ids, key, clock and execution. If `composition` were
57
+ // written as null — or any key added — both would move.
58
+ const artifact = createApprovalArtifact(signer, { execution: execution([escola]), approver: person, actor: human, now });
59
+ expect("composition" in artifact).toBe(false);
60
+ expect(sha256Hex(canonicalJson(artifact))).toBe("3c7da9ead8078c58e6727dc6645ff1cde3f7e69d78502ea17350887ba201f030");
61
+ expect(artifact.signature.value).toBe("5fd40687454f7a67a84cb6bb375eb91b7d9ba3d20242d1fe95c7845c9dc31a0a");
62
+ });
63
+ });
64
+
65
+ describe("dependency 9.7: the composition is carried, signed and compared", () => {
66
+ it("an artifact carries the composition of the execution it approves, and the signature covers it", () => {
67
+ const order: ExecutionItem = { beneficiary: "Marina Costa", payee: "11144477735", amount: 20000, currency: "BRL", due_date: "2026-09-23" };
68
+ const exe = execution([order], { composition: composition("cart-1", twoLessons) });
69
+ const artifact = createApprovalArtifact(signer, { execution: exe, approver: person, actor: human, now });
70
+ expect(artifact.composition).toEqual(exe.composition);
71
+ expect(checkApprovalArtifact(signer, artifact, exe, now)).toEqual({ ok: true });
72
+ for (const changed of [composition("cart-1", oneConsulting), { ...exe.composition!, ref: "cart-2" }, { ...exe.composition!, line_count: 2 }]) {
73
+ expect(checkApprovalArtifact(signer, { ...artifact, composition: changed }, exe, now)).toEqual({ ok: false, problem: "signature_invalid" });
74
+ expect(checkApprovalArtifact(signer, artifact, { ...exe, composition: changed }, now)).toEqual({ ok: false, problem: "composition_mismatch" });
75
+ }
76
+ });
77
+
78
+ it("the case items_hash cannot see: the same total, another cart", () => {
79
+ const order: ExecutionItem = { beneficiary: "Marina Costa", payee: "11144477735", amount: 20000, currency: "BRL", due_date: "2026-09-23" };
80
+ const approved = execution([order], { composition: composition("cart-1", twoLessons) });
81
+ const artifact = createApprovalArtifact(signer, { execution: approved, approver: person, actor: human, now });
82
+ const recomposed = { ...approved, composition: composition("cart-1", oneConsulting) };
83
+ expect(recomposed.items_hash).toBe(artifact.items_hash);
84
+ expect(checkApprovalArtifact(signer, artifact, recomposed, now)).toEqual({ ok: false, problem: "composition_mismatch" });
85
+ });
86
+
87
+ it("gaining or losing a composition after approval is a different execution", () => {
88
+ const order: ExecutionItem = { beneficiary: "Marina Costa", payee: "11144477735", amount: 20000, currency: "BRL" };
89
+ const bare = execution([order]);
90
+ const bareArtifact = createApprovalArtifact(signer, { execution: bare, approver: person, actor: human, now });
91
+ expect(checkApprovalArtifact(signer, bareArtifact, { ...bare, composition: composition("cart-1", twoLessons) }, now)).toEqual({ ok: false, problem: "composition_mismatch" });
92
+ const composed = execution([order], { composition: composition("cart-1", twoLessons) });
93
+ const composedArtifact = createApprovalArtifact(signer, { execution: composed, approver: person, actor: human, now });
94
+ const { composition: _dropped, ...stripped } = composed;
95
+ expect(checkApprovalArtifact(signer, composedArtifact, stripped as Execution, now)).toEqual({ ok: false, problem: "composition_mismatch" });
96
+ });
97
+
98
+ it("the hash sees quantity, unit price and order, which items_hash's fields cannot", () => {
99
+ // Ten at a 10% discount and nine at list price: the same line amount, not the same sale.
100
+ expect(compositionHash([line("aula-avulsa", 10, 10000, 90000)])).not.toBe(compositionHash([line("aula-avulsa", 9, 10000, 90000)]));
101
+ expect(compositionHash([line("aula-avulsa", 10, 9000, 90000)])).not.toBe(compositionHash([line("aula-avulsa", 10, 10000, 90000)]));
102
+ const a = line("aula-avulsa", 1, 10000);
103
+ const b = line("avaliacao-inicial", 1, 8990);
104
+ expect(compositionHash([a, b])).not.toBe(compositionHash([b, a]));
105
+ expect(compositionHash([a, b])).toMatch(/^sha256:[0-9a-f]{64}$/);
106
+ // A discount line is a line: a coupon swapped at a constant total is another composition.
107
+ expect(compositionHash([a, line("coupon:BEMVINDO10", 1, -1000)])).not.toBe(compositionHash([a, line("discount:order", 1, -1000)]));
108
+ });
109
+ });
110
+
111
+ describe("dependency 9.7 in the engine: a cart recomposed after approval goes back to a person", () => {
112
+ const draftOrder = async (h: ReturnType<typeof harness>, lines: CompositionLine[]) => {
113
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 20000 }], composition: composition("cart-1", lines) });
114
+ if (!d.ok) throw new Error("refused");
115
+ return d.execution;
116
+ };
117
+
118
+ it("draft stamps the composition on the execution and on every artifact of it", async () => {
119
+ const h = harness({ mode: "human" });
120
+ const drafted = await draftOrder(h, twoLessons);
121
+ expect(drafted.composition).toEqual(composition("cart-1", twoLessons));
122
+ const approved = h.engine.approve(drafted.id, approver);
123
+ expect(h.store.getApproval(approved.approval_id!)!.composition).toEqual(composition("cart-1", twoLessons));
124
+ });
125
+
126
+ it("restated at a constant total: items_hash unchanged, the last gate refuses, a second yes mints a second artifact", async () => {
127
+ const h = harness({ mode: "human" });
128
+ const approved = h.engine.approve((await draftOrder(h, twoLessons)).id, approver);
129
+ const first = approved.approval_id!;
130
+ const restated = h.engine.restate(approved.id, { items: [{ payee: "escola", amount: 20000 }], composition: composition("cart-1", oneConsulting) });
131
+ expect(restated.state).toBe("approved");
132
+ expect(restated.items_hash).toBe(approved.items_hash);
133
+ expect(restated.approval_id).toBe(first);
134
+
135
+ const back = await h.engine.execute(approved.id);
136
+ expect(back.state).toBe("awaiting_approval");
137
+ expect(back.reason).toBe("items_hash_mismatch");
138
+ expect(back.detail).toContain("composition_mismatch");
139
+ expect(back.approval_id).toBeUndefined();
140
+ expect(h.store.listOutbox()).toHaveLength(0);
141
+
142
+ const again = h.engine.approve(back.id, approver);
143
+ expect(again.approval_id).not.toBe(first);
144
+ expect(h.store.getApproval(again.approval_id!)!.composition).toEqual(composition("cart-1", oneConsulting));
145
+ const ran = await h.engine.execute(again.id);
146
+ expect(ran.state).toBe("settled");
147
+ expect(ran.history.map((t) => t.to)).toEqual(["awaiting_approval", "approved", "awaiting_approval", "approved", "executing", "settled"]);
148
+ });
149
+
150
+ it("restated with a new total: the new total is the one judged and run", async () => {
151
+ const h = harness({ mode: "human" });
152
+ const approved = h.engine.approve((await draftOrder(h, twoLessons)).id, approver);
153
+ const fiveLessons = [line("aula-avulsa", 5, 10000)];
154
+ h.engine.restate(approved.id, { items: [{ payee: "escola", amount: 50000 }], composition: composition("cart-1", fiveLessons) });
155
+ const back = await h.engine.execute(approved.id);
156
+ expect(back.state).toBe("awaiting_approval");
157
+ expect(back.total).toBe(50000);
158
+ const ran = await h.engine.execute(h.engine.approve(back.id, approver).id);
159
+ expect(ran.state).toBe("settled");
160
+ expect(h.engine.list({ state: "settled" }).map((e) => e.total)).toEqual([50000]);
161
+ });
162
+
163
+ it("restate changes what is charged, never to whom, and never an execution that is no longer open", async () => {
164
+ const h = harness({ mode: "human" });
165
+ const drafted = await draftOrder(h, twoLessons);
166
+ expect(() => h.engine.restate(drafted.id, { items: [{ payee: "mercado", amount: 20000 }] })).toThrow(/payees/);
167
+ expect(() => h.engine.restate(drafted.id, { items: [{ payee: "escola", amount: 10000 }, { payee: "escola", amount: 10000 }] })).toThrow(/payees/);
168
+ expect(() => h.engine.restate(drafted.id, { items: [{ payee: "escola", amount: 20000 }], batch: { ref: "b", batch_hash: "sha256:x", index: 0, count: 1 } })).toThrow(/batch/);
169
+ const denied = h.engine.deny(drafted.id, approver);
170
+ expect(() => h.engine.restate(denied.id, { items: [{ payee: "escola", amount: 20000 }] })).toThrow(/only an open execution/);
171
+ });
172
+
173
+ it("a composition that cannot be a hash of lines is refused where the execution is minted", async () => {
174
+ const h = harness({ mode: "human" });
175
+ await expect(h.engine.draft({ items: [{ payee: "escola", amount: 20000 }], composition: { ref: "cart-1", composition_hash: "sha256:nope", line_count: 1 } })).rejects.toThrow(/composition_hash/);
176
+ await expect(h.engine.draft({ items: [{ payee: "escola", amount: 20000 }], composition: { ...composition("cart-1", twoLessons), line_count: 0 } })).rejects.toThrow(/line_count/);
177
+ await expect(h.engine.draft({ items: [{ payee: "escola", amount: 20000 }], composition: { ...composition("cart-1", twoLessons), ref: " " } })).rejects.toThrow(/ref/);
178
+ });
179
+
180
+ it("an execution without a composition carries no composition key, in state or in the bundle", async () => {
181
+ const h = harness({ mode: "human" });
182
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
183
+ if (!d.ok) throw new Error("refused");
184
+ const approved = h.engine.approve(d.execution.id, approver);
185
+ expect("composition" in approved).toBe(false);
186
+ expect("composition" in h.store.getApproval(approved.approval_id!)!).toBe(false);
187
+ for (const e of h.store.listEvents()) expect(JSON.stringify(e.payload)).not.toContain("composition");
188
+ });
189
+ });
190
+
191
+ describe("dependency 9.8: `state` is an effect, and only a local tool has it", () => {
192
+ const tool = (effect: string) => ({ name: "cart_update", effect, description: "replaces the cart", input_schema: { type: "object" } });
193
+ it("a local tool may change state", () => {
194
+ expect(ToolsFileSchema.safeParse({ meta_tools: [], local_tools: [tool("state")] }).success).toBe(true);
195
+ });
196
+ it("a meta-tool may not: CodeSpar's names are for money and reads", () => {
197
+ const parsed = ToolsFileSchema.safeParse({ meta_tools: [{ ...tool("state"), name: "codespar_cart" }], local_tools: [] });
198
+ expect(parsed.success).toBe(false);
199
+ expect(parsed.error?.message).toContain("a state tool is local");
200
+ });
201
+ it("an effect outside the enum is still refused", () => {
202
+ expect(ToolsFileSchema.safeParse({ meta_tools: [], local_tools: [tool("write")] }).success).toBe(false);
203
+ });
204
+ });
@@ -0,0 +1 @@
1
+ {"issuer":"did:web:id.codespar.dev","key_namespace":"production","algorithm":"Ed25519","signing_string":"codespar-receipt:v1:<receipt_id>:<chain>","chain_recipe":{"canonicalization":"RFC 8785 (JCS)","digest":"SHA-256 over the UTF-8 bytes of the canonical JSON, lowercase hex","input":"{ \"v\": <chain_version>, \"links\": [ <each link below that applies, in this order> ] }","version":{"field":"chain_version","rule":"4 when `approval` is present; else 3 when `payment.amount_authorized` is present; else 2 when `payment.amount_atomic` is not null; else 1"},"optional_marker":"?","links":[{"link":"mandate","from":"mandate","when":"always","fields_by_version":{"1":["id","nonce","scope","currency","sig"],"2":["id","nonce","scope","currency","sig"],"3":["id","nonce","scope","currency","sig"],"4":["id","nonce","scope","currency","sig_sha256"]}},{"link":"quote","from":"quote","when":"quote is not null","fields":["seller","resource","price_minor","payee","session_id","sig","at"]},{"link":"approval","from":"approval","when":"chain_version is 4","fields":["items_hash","batch_hash"]},{"link":"payment","from":"payment","when":"always","fields":["rail","provider","tx_id","amount_minor","amount_atomic?","amount_authorized?","amount_charged?","amount_refunded?","metering?","sandbox?","attempt_id","money_moved","at"]},{"link":"delivery","from":"delivery","when":"delivery is not null","fields":["result","proof","kind","nfe_chave","at"]}],"notes":["`mandate.sig_sha256` is SHA-256 over the UTF-8 bytes of `mandate.sig`, lowercase hex. A v4 chain seals it in place of `sig`, which must not be shared; v1-v3 seal `sig` itself, so only the tenant, whose read carries `sig`, can recompute those. Anyone can still verify their signature over `chain`.","Recomputing needs `quote.payee` unmasked. Whoever shares a receipt decides whether to hand it over.","Timestamps are RFC 3339 UTC with milliseconds, exactly as read."]},"keys":[{"kid":"did:web:id.codespar.dev#1","kty":"OKP","crv":"Ed25519","x":"fz1svKf1AFlnIEZSseNqH5zCXkKqPmtkjx9mMNBz8pU","alg":"EdDSA","use":"sig","status":"retired","created_at":"2026-07-05T13:29:23.276Z","retired_at":"2026-09-25T02:43:45.971Z"},{"kid":"did:web:id.codespar.dev#production-2","kty":"OKP","crv":"Ed25519","x":"WKVTphnIma44d5T7bugYnLvI6h79LhaSdNDrkOFxmRs","alg":"EdDSA","use":"sig","status":"active","created_at":"2026-09-25T02:43:45.971Z","retired_at":null}]}
@@ -9,6 +9,7 @@ import { MandateSchema, type Mandate } from "../src/mandate.js";
9
9
  import { ManifestSchema, type Manifest } from "../src/manifest.js";
10
10
  import { StateStore } from "../src/state/store.js";
11
11
  import { LocalMandateStatusStub } from "../src/stubs/mandate-status.js";
12
+ import type { PaymentRail } from "../src/rail.js";
12
13
  import type { MandateStatusSource } from "../src/revocation.js";
13
14
  import { StubRail, type StubRailOptions } from "../src/stubs/rail.js";
14
15
  import type { ApprovalMode } from "../src/types.js";
@@ -91,6 +92,8 @@ export interface HarnessOptions {
91
92
  mandate?: Partial<Mandate>;
92
93
  manifest?: Partial<Manifest>;
93
94
  rail?: StubRailOptions;
95
+ /** What the engine dispatches to instead of the stub itself, e.g. the stub with a different receipt. `rail` on the harness stays the stub. */
96
+ wrapRail?: (stub: StubRail) => PaymentRail;
94
97
  dir?: string;
95
98
  runId?: string;
96
99
  /** The section 4.7 status source; the local stub unless a test wires the API one. */
@@ -109,7 +112,7 @@ export function harness(options: HarnessOptions = {}): Harness {
109
112
  const mode = options.mode ?? "human";
110
113
  const deps: EngineDeps = {
111
114
  store,
112
- rail,
115
+ rail: options.wrapRail ? options.wrapRail(rail) : rail,
113
116
  status: options.status ?? gate,
114
117
  signer: hmacSigner("test", Buffer.alloc(32, 1)),
115
118
  manifest: testManifest(options.manifest),
@@ -2,12 +2,16 @@
2
2
  * Section 4.7 against the real client: `ApiMandateStatusSource` reads
3
3
  * `GET /v1/mandates/{id}` on a mocked HTTP server, and the engine is driven
4
4
  * through it. Every answer but `active` must keep `rail.pay` from ever
5
- * being called, and a read that does not answer must refuse too.
5
+ * being called, and a read that does not answer must refuse too. The
6
+ * organization kill switch (ent#1648) rides on the same read as `org_paused`,
7
+ * and the spend route refuses 403 `org_paused` on its own when the switch is
8
+ * pressed after the read.
6
9
  */
7
10
  import { createServer, type Server } from "node:http";
8
11
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
9
12
  import { createCodeSparClient } from "../src/api/client.js";
10
13
  import { ApiMandateStatusSource } from "../src/api/mandate-status.js";
14
+ import { CodeSparRail } from "../src/api/rail.js";
11
15
  import type { MandateStatusSource } from "../src/revocation.js";
12
16
  import { harness, type Harness } from "./helpers.js";
13
17
 
@@ -15,7 +19,7 @@ const MANDATE_ID = "mdt_test_0001";
15
19
  const NOW = new Date("2026-09-23T18:00:00Z");
16
20
  const approver = { id: "usr_demo_titular", channel: "terminal" };
17
21
 
18
- /** The 14-field projection `GET /v1/mandates/{id}` answers (measured on staging, 2026-09-23). */
22
+ /** The 14-field projection `GET /v1/mandates/{id}` answers (measured on staging, 2026-09-23), plus the two kill-switch fields of ent#1648. */
19
23
  function mandateBody(over: Record<string, unknown> = {}): Record<string, unknown> {
20
24
  return {
21
25
  id: MANDATE_ID,
@@ -32,6 +36,8 @@ function mandateBody(over: Record<string, unknown> = {}): Record<string, unknown
32
36
  status: "active",
33
37
  expires_at: "2027-09-23T00:00:00.000Z",
34
38
  created_at: "2026-09-23T00:00:00.000Z",
39
+ org_paused: false,
40
+ org_paused_at: null,
35
41
  ...over,
36
42
  };
37
43
  }
@@ -41,14 +47,17 @@ type Answer = { status: number; body?: unknown; hang?: boolean };
41
47
  let server: Server;
42
48
  let baseUrl: string;
43
49
  let next: Answer = { status: 200, body: mandateBody() };
50
+ /** What a POST (the spend) answers, when a test sends one; the mandate read keeps answering `next`. */
51
+ let spendNext: Answer | undefined;
44
52
  const requests: Array<{ method: string; url: string; bearerIsTestKey: boolean }> = [];
45
53
 
46
54
  beforeAll(async () => {
47
55
  server = createServer((req, res) => {
48
56
  requests.push({ method: req.method ?? "", url: req.url ?? "", bearerIsTestKey: (req.headers.authorization ?? "").startsWith("Bearer csk_test_") });
49
- if (next.hang) return; // never answers: the client's timeout is the only way out
50
- res.writeHead(next.status, { "content-type": "application/json" });
51
- res.end(typeof next.body === "string" ? next.body : JSON.stringify(next.body));
57
+ const answer = req.method === "POST" && spendNext ? spendNext : next;
58
+ if (answer.hang) return; // never answers: the client's timeout is the only way out
59
+ res.writeHead(answer.status, { "content-type": "application/json" });
60
+ res.end(typeof answer.body === "string" ? answer.body : JSON.stringify(answer.body));
52
61
  });
53
62
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
54
63
  baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
@@ -61,12 +70,17 @@ afterAll(async () => {
61
70
 
62
71
  beforeEach(() => {
63
72
  next = { status: 200, body: mandateBody() };
73
+ spendNext = undefined;
64
74
  requests.length = 0;
65
75
  });
66
76
 
67
- function apiSource(timeoutMs = 5_000): ApiMandateStatusSource {
77
+ function apiClient(timeoutMs = 5_000) {
68
78
  // The placeholder passes the `csk_test_` guard and is the one test-key-shaped string the secret scan allows.
69
- return new ApiMandateStatusSource(createCodeSparClient({ apiKey: "csk_test_your_key_here", baseUrl, timeoutMs }), () => NOW);
79
+ return createCodeSparClient({ apiKey: "csk_test_your_key_here", baseUrl, timeoutMs });
80
+ }
81
+
82
+ function apiSource(timeoutMs = 5_000): ApiMandateStatusSource {
83
+ return new ApiMandateStatusSource(apiClient(timeoutMs), () => NOW);
70
84
  }
71
85
 
72
86
  function apiHarness(status: MandateStatusSource = apiSource()): Harness {
@@ -147,7 +161,69 @@ describe("section 4.7 through the engine, on the API source: executing only on a
147
161
  expect(h.rail.payCount).toBe(0);
148
162
  });
149
163
 
150
- it("the API source never reports org_paused: the API has no kill-switch read, and the source does not invent one", async () => {
151
- expect((await apiSource().check(MANDATE_ID)).org_paused).toBe(false);
164
+ });
165
+
166
+ describe("the organization kill switch (ent#1648): org_paused on the read, 403 org_paused on the spend", () => {
167
+ const PAUSED = { org_paused: true, org_paused_at: "2026-09-23T17:30:00.000Z" };
168
+
169
+ it("the source reports org_paused as the API says it, and keeps the mandate's own status next to it", async () => {
170
+ next = { status: 200, body: mandateBody(PAUSED) };
171
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "active", org_paused: true, source: "api" });
172
+ });
173
+
174
+ it("org_paused missing, or not a boolean, is unknown: never read as a running organization", async () => {
175
+ const { org_paused: _dropped, ...withoutFlag } = mandateBody();
176
+ next = { status: 200, body: withoutFlag };
177
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "unknown", org_paused: false, detail: expect.stringContaining("org_paused") });
178
+ next = { status: 200, body: mandateBody({ org_paused: "false" }) };
179
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "unknown", detail: expect.stringContaining("org_paused") });
180
+ next = { status: 200, body: mandateBody({ org_paused: null }) };
181
+ expect((await apiSource().check(MANDATE_ID)).status).toBe("unknown");
182
+ });
183
+
184
+ it("paused before execute: the gate denies (org_paused) whatever status says, no outbox row, rail.pay never called", async () => {
185
+ const h = apiHarness();
186
+ const id = await approvedThen(h, { status: 200, body: mandateBody(PAUSED) });
187
+ const out = await h.engine.execute(id);
188
+ expect(out).toMatchObject({ state: "denied", reason: "org_paused" });
189
+ expect(h.store.listOutbox()).toHaveLength(0);
190
+ expect(h.rail.payCount).toBe(0);
191
+ expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, refused_before_draft: true, reason: "org_paused" });
192
+ });
193
+
194
+ it("a read without org_paused after approval: denied (mandate_status_unavailable), rail.pay never called", async () => {
195
+ const h = apiHarness();
196
+ const { org_paused: _dropped, ...withoutFlag } = mandateBody();
197
+ const id = await approvedThen(h, { status: 200, body: withoutFlag });
198
+ const out = await h.engine.execute(id);
199
+ expect(out).toMatchObject({ state: "denied", reason: "mandate_status_unavailable" });
200
+ expect(out.detail).toContain("org_paused");
201
+ expect(h.rail.payCount).toBe(0);
152
202
  });
203
+
204
+ // Paused between the gate and the spend: the read said running, the API refuses the spend itself before any hold.
205
+ // The execution is already `executing`, and section 4.7 closes that state as `settled` or `failed` only: no new edge.
206
+ const spendRefusals: Array<[string, unknown]> = [
207
+ ["the documented envelope", { error: { code: "org_paused", message: "the organization paused all agent spend (kill switch)" }, request_id: null }],
208
+ ["the flat body the guardrail sends", { error: "org_paused", message: "the organization paused all agent spend (kill switch)" }],
209
+ ];
210
+ for (const [shape, body] of spendRefusals) {
211
+ it(`paused between the gate and the spend, 403 org_paused (${shape}): failed (org_paused), and nothing but the read and the one spend reached the API`, async () => {
212
+ const h = harness({ mode: "mandate", now: NOW, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, status: apiSource(), wrapRail: () => new CodeSparRail(apiClient()) });
213
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
214
+ if (!d.ok) throw new Error(`refused before draft: ${d.reason}`);
215
+ spendNext = { status: 403, body };
216
+ const out = await h.engine.execute(d.execution.id);
217
+ expect(out.state).toBe("failed");
218
+ expect(out.reason).toBe("org_paused");
219
+ expect(out.history.at(-1)).toMatchObject({ from: "executing", to: "failed", reason: "org_paused" });
220
+ expect(out.outcomes).toEqual([expect.objectContaining({ status: "failed", code: "org_paused" })]);
221
+ expect(out.outcomes.some((o) => "receipt_id" in o || "transaction_id" in o)).toBe(false);
222
+ expect(h.store.listOutbox({ execution_id: out.id })).toEqual([expect.objectContaining({ status: "failed" })]);
223
+ // No receipt read, no ledger or fund call, no second spend: the mandate read(s) and exactly one POST.
224
+ const spend = `/v1/consumers/mandates/${out.mandate.id}/spend`;
225
+ expect(requests.filter((r) => r.method === "POST").map((r) => r.url)).toEqual([spend]);
226
+ expect(requests.filter((r) => !(r.method === "GET" && r.url === `/v1/mandates/${out.mandate.id}`) && r.url !== spend)).toEqual([]);
227
+ });
228
+ }
153
229
  });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * OPEN_QUESTIONS §18: the sealed receipt names the payee only when the spend
3
+ * carries a `SpendQuote`. Every spend carries one, built from the approval
4
+ * artifact's line; a spend without one, or with one that disagrees with the
5
+ * money about to move, never reaches the API; and a receipt whose sealed
6
+ * payee is not the payee that was paid is an error, not a line in a log.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import type { ApiClient } from "@codespar/sdk";
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { hmacSigner } from "../src/approval.js";
13
+ import { CodeSparRail } from "../src/api/rail.js";
14
+ import { maskPayee } from "../src/bundle.js";
15
+ import { ReceiptSealMismatchError } from "../src/engine.js";
16
+ import { canonicalJson } from "../src/hash.js";
17
+ import { checkQuote } from "../src/quote.js";
18
+ import type { PaymentRail, RailPayment } from "../src/rail.js";
19
+ import { ESCOLA, harness, MERCADO } from "./helpers.js";
20
+
21
+ const approver = { id: "usr_demo", channel: "terminal" };
22
+ const actor = { type: "agent" as const, agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" };
23
+
24
+ function payment(over: Partial<RailPayment> = {}): RailPayment {
25
+ return {
26
+ attempt_id: "att_quote_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: `sha256:${"1".repeat(64)}` },
35
+ actor,
36
+ ...over,
37
+ };
38
+ }
39
+
40
+ /** An ApiClient that records what it was asked and answers a settled spend. */
41
+ function fakeApi(receiptBody?: unknown) {
42
+ const post = vi.fn(async (_path: string, _init: { body: Record<string, unknown> }) => ({ payment: { transactionId: "tx_1", moneyMoved: false }, receipt: { id: "rcpt_1" } }));
43
+ const get = vi.fn(async () => receiptBody);
44
+ return { api: { post, get } as unknown as ApiClient, post, get };
45
+ }
46
+
47
+ /** The stub rail, but the receipt it hands back seals whatever payee the test says. */
48
+ function sealing(inner: PaymentRail, sealedPayee: string | null): PaymentRail {
49
+ return {
50
+ name: inner.name,
51
+ pay: (p) => inner.pay(p),
52
+ lookup: (id, p, tx) => inner.lookup(id, p, tx),
53
+ receipt: async (id, a) => {
54
+ const r = await inner.receipt(id, a);
55
+ return r && { ...r, payment: { ...r.payment, payee: sealedPayee } };
56
+ },
57
+ };
58
+ }
59
+
60
+ describe("§18: every spend presents the quote of what was approved", () => {
61
+ it("the quote is the approved line: seller, resource, price, payee and the moment of approval", async () => {
62
+ const h = harness({ mode: "human" });
63
+ const seen: RailPayment[] = [];
64
+ const inner = h.rail;
65
+ const pay = inner.pay.bind(inner);
66
+ inner.pay = async (p) => (seen.push(p), pay(p));
67
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000, description: "outubro" }, { payee: "mercado", amount: 32000 }] });
68
+ if (!d.ok) throw new Error("refused");
69
+ const approved = h.engine.approve(d.execution.id, approver);
70
+ const artifact = h.store.getApproval(approved.approval_id!)!;
71
+ expect((await h.engine.execute(approved.id)).state).toBe("settled");
72
+
73
+ expect(seen.map((p) => p.quote)).toEqual([
74
+ { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, at: artifact.approved_at },
75
+ // No description on the line: the resource is the mandate's purpose, never something the model adds later.
76
+ { seller: "Mercado do Bairro", resource: "contas do mes", price_minor: 32000, payee: MERCADO, at: artifact.approved_at },
77
+ ]);
78
+ });
79
+
80
+ it("the local receipt copy carries the payee the receipt sealed, masked", async () => {
81
+ const h = harness({ mode: "human" });
82
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
83
+ if (!d.ok) throw new Error("refused");
84
+ const settled = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
85
+ const copy = JSON.parse(readFileSync(join(h.bundle.dir, "receipts", `${settled.outcomes[0]!.receipt_id}.json`), "utf8"));
86
+ expect(copy.payment.payee).toBe(maskPayee(ESCOLA));
87
+ expect(JSON.stringify(copy)).not.toContain(ESCOLA);
88
+ });
89
+
90
+ it("an artifact whose line disagrees with the execution is refused before the call, even when its hash and signature hold", async () => {
91
+ const h = harness({ mode: "human" });
92
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
93
+ if (!d.ok) throw new Error("refused");
94
+ const approved = h.engine.approve(d.execution.id, approver);
95
+ // A writer bug, not an attacker: the artifact's items no longer say what its items_hash says. Re-signed with the harness key.
96
+ const { signature: _old, ...unsigned } = h.store.getApproval(approved.approval_id!)!;
97
+ const drifted = { ...unsigned, items: unsigned.items.map((i) => ({ ...i, amount: 999 })) };
98
+ h.store.saveApproval({ ...drifted, signature: { alg: "HMAC-SHA256", key_id: "test", value: hmacSigner("test", Buffer.alloc(32, 1)).sign(canonicalJson(drifted)) } });
99
+
100
+ const denied = await h.engine.execute(approved.id);
101
+ expect(denied.state).toBe("denied");
102
+ expect(denied.reason).toBe("quote_mismatch");
103
+ expect(h.rail.payCount).toBe(0);
104
+ });
105
+ });
106
+
107
+ describe("§18: a spend that would not seal what was approved never goes out", () => {
108
+ it("checkQuote: missing, wrong price, wrong payee", () => {
109
+ expect(checkQuote(payment())).toMatchObject({ ok: true });
110
+ const { quote: _q, ...bare } = payment();
111
+ expect(checkQuote(bare)).toMatchObject({ ok: false, code: "quote_missing" });
112
+ expect(checkQuote(payment({ amount_minor: 185001 }))).toMatchObject({ ok: false, code: "quote_mismatch" });
113
+ expect(checkQuote(payment({ payee: MERCADO }))).toMatchObject({ ok: false, code: "quote_mismatch" });
114
+ });
115
+
116
+ it("the CodeSpar rail refuses a spend without a quote, and one that disagrees, without calling the API", async () => {
117
+ const { api, post } = fakeApi();
118
+ const rail = new CodeSparRail(api, { canonical: { any: "envelope" } as never, signature: "a".repeat(64) });
119
+ const { quote: _q, ...bare } = payment();
120
+ expect(await rail.pay(bare)).toMatchObject({ status: "failed", code: "quote_missing" });
121
+ expect(await rail.pay(payment({ amount_minor: 1 }))).toMatchObject({ status: "failed", code: "quote_mismatch" });
122
+ expect(await rail.pay(payment({ payee: MERCADO }))).toMatchObject({ status: "failed", code: "quote_mismatch" });
123
+ expect(post).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("the CodeSpar rail sends the quote on both spend routes", async () => {
127
+ const quote = payment().quote;
128
+ const byEnvelope = fakeApi();
129
+ await new CodeSparRail(byEnvelope.api, { canonical: { any: "envelope" } as never, signature: "a".repeat(64) }).pay(payment());
130
+ expect(byEnvelope.post.mock.calls[0]![0]).toBe("/v1/consumer-payments/execute");
131
+ expect(byEnvelope.post.mock.calls[0]![1].body["quote"]).toEqual(quote);
132
+ const byId = fakeApi();
133
+ await new CodeSparRail(byId.api).pay(payment());
134
+ expect(byId.post.mock.calls[0]![0]).toBe("/v1/consumers/mandates/{id}/spend");
135
+ expect(byId.post.mock.calls[0]![1].body["quote"]).toEqual(quote);
136
+ });
137
+
138
+ it("the stub rail makes the same refusal, so a scenario cannot pass without a quote", async () => {
139
+ const h = harness();
140
+ const { quote: _q, ...bare } = payment();
141
+ expect(await h.rail.pay(bare)).toMatchObject({ status: "failed", code: "quote_missing" });
142
+ expect(h.rail.payCount).toBe(0);
143
+ });
144
+ });
145
+
146
+ describe("§18: the receipt's payee is the sealed one, and a mismatch is an error", () => {
147
+ const sealed = (quote: unknown) => ({
148
+ receipt_id: "rcpt_1",
149
+ state: "paid",
150
+ mandate: { id: "cm_1", nonce: "n", scope: "s", currency: "BRL", sig: "x" },
151
+ quote,
152
+ payment: { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_quote_0", money_moved: false, at: "2026-09-23T18:00:01.000Z" },
153
+ delivery: null,
154
+ chain: "c".repeat(64),
155
+ receipt_sig: "s",
156
+ exceptions: [],
157
+ });
158
+
159
+ it("the CodeSpar rail reads the payee from the sealed quote, and null when the receipt sealed none", async () => {
160
+ const withQuote = await new CodeSparRail(fakeApi(sealed({ seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, session_id: null, sig: "q", at: null })).api).receipt("rcpt_1", actor);
161
+ expect(withQuote?.payment.payee).toBe(ESCOLA);
162
+ const without = await new CodeSparRail(fakeApi(sealed(null)).api).receipt("rcpt_1", actor);
163
+ expect(without?.payment.payee).toBeNull();
164
+ });
165
+
166
+ it("a mixed-case key sealed as it was sent raises nothing; the same key sealed lower-cased is a mismatch", async () => {
167
+ const MIXED = "Financeiro@Escola-Aurora.example.com.br";
168
+ const mandate = { merchant_allowlist: [MIXED] as [string], beneficiaries: [{ alias: "escola", name: "Escola Aurora", payee: MIXED }] as [{ alias: string; name: string; payee: string }] };
169
+ const verbatim = harness({ mode: "human", mandate });
170
+ const d1 = await verbatim.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
171
+ if (!d1.ok) throw new Error("refused");
172
+ expect((await verbatim.engine.execute(verbatim.engine.approve(d1.execution.id, approver).id)).state).toBe("settled");
173
+ expect(verbatim.bundle.readEvents().some((e) => e["type"] === "receipt.seal_mismatch")).toBe(false);
174
+
175
+ // The API stores and returns quote.payee verbatim (OPEN_QUESTIONS §18); if that ever changes, this is what the kit says.
176
+ const lowered = harness({ mode: "human", mandate, wrapRail: (stub) => sealing(stub, MIXED.toLowerCase()) });
177
+ const d2 = await lowered.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
178
+ if (!d2.ok) throw new Error("refused");
179
+ await expect(lowered.engine.execute(lowered.engine.approve(d2.execution.id, approver).id)).rejects.toBeInstanceOf(ReceiptSealMismatchError);
180
+ });
181
+
182
+ for (const [label, sealedPayee] of [["a different payee", MERCADO], ["no payee at all (a spend the API sealed without a quote)", null]] as const) {
183
+ it(`a receipt sealing ${label} throws after the outcome is saved, and the bundle says why without the key in the clear`, async () => {
184
+ const h = harness({ mode: "human", wrapRail: (stub) => sealing(stub, sealedPayee) });
185
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
186
+ if (!d.ok) throw new Error("refused");
187
+ const approved = h.engine.approve(d.execution.id, approver);
188
+
189
+ await expect(h.engine.execute(approved.id)).rejects.toBeInstanceOf(ReceiptSealMismatchError);
190
+ // The money is recorded where the rail put it; the error is about the evidence.
191
+ expect(h.engine.get(approved.id)?.state).toBe("settled");
192
+ const mismatch = h.bundle.readEvents().find((e) => e["type"] === "receipt.seal_mismatch");
193
+ expect(mismatch?.["payload"]).toMatchObject({ sealed_payee: sealedPayee === null ? null : maskPayee(sealedPayee), paid_payee: maskPayee(ESCOLA) });
194
+ expect(JSON.stringify(mismatch)).not.toContain(ESCOLA);
195
+ if (sealedPayee) expect(JSON.stringify(mismatch)).not.toContain(sealedPayee);
196
+ });
197
+ }
198
+ });