@codespar/cli 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/templates/bills-agent/README.md +1 -1
  4. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  5. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  7. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  8. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  9. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  10. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  11. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  12. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  13. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  14. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  15. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  16. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  17. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  18. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  19. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  20. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  21. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  22. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  23. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  24. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  25. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  26. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  27. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  28. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  29. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  30. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  31. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  32. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  33. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  34. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  35. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  36. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  37. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  38. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  39. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  40. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  41. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  42. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  43. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  44. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  45. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  46. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  52. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  53. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  59. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  60. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  61. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  62. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  63. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  64. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  65. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  66. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  67. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  69. package/templates/checkout-agent/LICENSE +21 -0
  70. package/templates/checkout-agent/README.md +28 -0
  71. package/templates/checkout-agent/_gitignore +10 -0
  72. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  73. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  74. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  75. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  76. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  77. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  78. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  79. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  83. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  84. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  108. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  109. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  110. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  111. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  112. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  113. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  141. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  146. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  147. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  156. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  157. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  158. package/templates/checkout-agent/package.json +38 -0
  159. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  160. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  161. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  162. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  163. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  167. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  168. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  169. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  170. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  171. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  172. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  173. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  174. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  175. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  176. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  177. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  178. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  179. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  180. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  181. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  182. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  184. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  185. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  186. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  187. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  188. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  189. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  190. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  191. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  192. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  193. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  195. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  196. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  197. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  198. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  199. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  200. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  201. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  202. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  203. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  204. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  205. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  206. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  208. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  209. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  210. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  211. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  212. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  213. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  216. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  217. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  218. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  219. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  220. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  221. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  222. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  223. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  224. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  260. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  261. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  267. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  268. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  269. package/templates/checkout-agent/tsconfig.base.json +16 -0
  270. package/templates/checkout-agent/vitest.config.ts +12 -0
  271. package/templates/collections-agent/README.md +6 -1
  272. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  273. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  274. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  276. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  277. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  278. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  279. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  280. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  281. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  283. package/templates/collections-agent/package.json +1 -0
  284. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  285. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  286. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  287. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  288. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  289. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  290. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  291. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  292. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  293. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  294. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  295. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  296. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  297. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  298. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  299. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  300. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  301. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  302. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  303. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  304. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  305. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  306. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  307. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  308. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  309. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  310. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  311. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  312. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  313. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  314. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  315. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  316. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  317. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  318. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  319. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  320. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  321. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  322. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  328. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  329. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  335. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  336. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  337. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  338. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  339. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  340. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  341. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  342. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  343. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  345. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  346. package/templates/hello-agent/README.md +1 -1
  347. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  348. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  349. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  350. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  351. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  352. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  353. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  354. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  355. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  356. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  357. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  358. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  359. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  360. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  361. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  362. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  363. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  364. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  365. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  366. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  367. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  368. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  369. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  370. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  371. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  372. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  373. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  374. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  375. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  376. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  377. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  378. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  379. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  380. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  381. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  382. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  383. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  384. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  385. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  391. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  392. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  398. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  399. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  400. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  401. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  402. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  403. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  404. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  405. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  406. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  408. package/templates/kits.lock.json +29 -6
  409. package/templates/supplier-payments-agent/README.md +1 -1
  410. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  411. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  416. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  417. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  418. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  420. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  424. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  425. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  426. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  427. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  428. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  429. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  430. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  431. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  432. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  433. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  435. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  437. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  438. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  439. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  440. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  441. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  442. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  443. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  446. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  447. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  451. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  452. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  453. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  473. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  474. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  477. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  478. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  479. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -8,7 +8,7 @@
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { stdin, stderr, stdout } from "node:process";
10
10
  import { relative } from "node:path";
11
- import type { AgentRuntime, ChargeInstrument, Execution } from "@codespar/agent-core";
11
+ import type { AgentRuntime, BatchGesture, BatchPresentation, ChargeInstrument, Execution } from "@codespar/agent-core";
12
12
  import { pollUntilClosed, type PollResult } from "./poll.js";
13
13
  import type { Setup } from "./setup.js";
14
14
 
@@ -23,8 +23,12 @@ export interface TerminalOptions {
23
23
  simulatePayer?: boolean | undefined;
24
24
  say?: ((line: string) => void) | undefined;
25
25
  ask?: ((question: string) => Promise<string>) | undefined;
26
- /** Where the lines for the counterparty go (the conversation); `say` is the operator's console. */
27
- tell?: ((line: string) => void) | undefined;
26
+ /**
27
+ * Where the lines for the counterparty go (the conversation); `say` is the
28
+ * operator's console. `about` is the execution whose OUTCOME the line tells,
29
+ * so a channel can tie a failed delivery of it back to that outcome.
30
+ */
31
+ tell?: ((line: string, about?: Execution) => void) | undefined;
28
32
  /**
29
33
  * How a payable receivable is put in front of the counterparty. Defaults to
30
34
  * the kit's own, which writes lines to a console. A channel overrides it,
@@ -32,8 +36,18 @@ export interface TerminalOptions {
32
36
  * message, and neither of those is a line of text.
33
37
  */
34
38
  presentInstrument?: ((execution: Execution, instalment: number, chargeId: string, instrument: ChargeInstrument, tell: (line: string) => void) => void | Promise<void>) | undefined;
39
+ /**
40
+ * The gestures taken on whole batches in this session, by batch ref. When a
41
+ * line of a batch arrives and its ref is here, the line is decided by the
42
+ * gesture instead of by a question; see `presentBatch`.
43
+ */
44
+ gestures?: BatchGestures | undefined;
35
45
  }
36
46
 
47
+ /** One gesture per batch ref: the list it was taken on, and the positions vetoed. */
48
+ export type BatchGestures = Map<string, BatchGestureRecord>;
49
+ type BatchGestureRecord = { batch_hash: string; count: number; vetoed: Set<number> };
50
+
37
51
  export function describeExecution(execution: Execution, setup: Setup): string[] {
38
52
  return setup.kit.describeExecution(execution, setup);
39
53
  }
@@ -43,6 +57,16 @@ export function announceOutcome(execution: Execution, setup: Setup, tell: (line:
43
57
  return setup.kit.announceOutcome?.(execution, setup, tell) ?? false;
44
58
  }
45
59
 
60
+ /** The kit's follow-up to an outcome. Never throws: what follows a paid order cannot un-pay it. */
61
+ export async function followUp(execution: Execution, setup: Setup, say: (line: string) => void): Promise<void> {
62
+ if (!setup.kit.followUp) return;
63
+ try {
64
+ await setup.kit.followUp(execution, setup, say);
65
+ } catch (err) {
66
+ say(` follow-up of ${execution.id} failed and changed nothing about it: ${err instanceof Error ? err.message : String(err)}`);
67
+ }
68
+ }
69
+
46
70
  function receiptLines(execution: Execution, setup: Setup, say: (line: string) => void): void {
47
71
  for (const outcome of execution.outcomes) {
48
72
  if (outcome.receipt_id) say(` ${setup.kit.labels.receiptWord}: ${relative(process.cwd(), `${setup.bundle.dir}/receipts/${outcome.receipt_id}.json`)}`);
@@ -61,23 +85,29 @@ export async function handleExecution(execution: Execution, options: TerminalOpt
61
85
 
62
86
  let current = execution;
63
87
  if (current.state === "awaiting_approval") {
64
- let decision = options.decision;
65
- if (!decision) {
66
- if (current.blocking_reasons.length > 0) say(" (o unico desfecho possivel e negar; aperte Enter)");
67
- const ask = options.ask ?? defaultAsk;
68
- const answer = (await ask(labels.approveQuestion)).trim().toLowerCase();
69
- decision = answer === "s" || answer === "sim" || answer === "y" || answer === "yes" ? "approve" : "deny";
70
- }
71
- if (decision === "approve") current = engine.approve(current.id, approver);
72
- else if (decision === "deny") current = engine.deny(current.id, approver);
73
- else {
74
- say(" deixado em awaiting_approval (rode de novo com --approve ou --deny)");
75
- return current;
88
+ const gesture = options.decision === undefined && current.batch ? options.gestures?.get(current.batch.ref) : undefined;
89
+ if (gesture) {
90
+ current = decideByGesture(current, gesture, setup, approver);
91
+ } else {
92
+ let decision = options.decision;
93
+ if (!decision) {
94
+ if (current.blocking_reasons.length > 0) say(" (o unico desfecho possivel e negar; aperte Enter)");
95
+ const ask = options.ask ?? defaultAsk;
96
+ const answer = (await ask(labels.approveQuestion)).trim().toLowerCase();
97
+ decision = answer === "s" || answer === "sim" || answer === "y" || answer === "yes" ? "approve" : "deny";
98
+ }
99
+ if (decision === "approve") current = engine.approve(current.id, approver);
100
+ else if (decision === "deny") current = engine.deny(current.id, approver);
101
+ else {
102
+ say(" deixado em awaiting_approval (rode de novo com --approve ou --deny)");
103
+ return current;
104
+ }
76
105
  }
77
106
  say(` -> ${current.state}${current.reason ? ` (${current.reason})` : ""}`);
78
107
  }
79
108
 
80
- if (current.state === "approved") {
109
+ // An agent that issues on request (`executeOnApproval: false`) leaves the approved execution for its own tool to issue.
110
+ if (current.state === "approved" && setup.kit.executeOnApproval !== false) {
81
111
  current = await engine.execute(current.id);
82
112
  say(` -> ${current.state}${current.reason ? ` (${current.reason})` : ""}`);
83
113
  if (setup.settlement === "immediate") {
@@ -95,11 +125,87 @@ export async function handleExecution(execution: Execution, options: TerminalOpt
95
125
  } else if (current.state === "executing") {
96
126
  say(labels.uncertainDispatch);
97
127
  }
98
- announceOutcome(current, setup, tell);
128
+ const told = current;
129
+ announceOutcome(told, setup, (line) => tell(line, told));
130
+ await followUp(current, setup, say);
99
131
  }
100
132
  return current;
101
133
  }
102
134
 
135
+ /**
136
+ * A line of a batch the person already decided on as a list. The gesture
137
+ * approves or vetoes a POSITION in ONE list: a line that names another list
138
+ * under the same ref is not covered by it, and "todas" must not become a yes
139
+ * to a list nobody was shown. `approve` still runs the policy and still mints
140
+ * this line's own artifact, which carries the `batch_hash` the gesture was
141
+ * taken on — that is what makes the gesture attested rather than asserted.
142
+ */
143
+ function decideByGesture(execution: Execution, gesture: BatchGestureRecord, setup: Setup, approver: TerminalOptions["approver"]): Execution {
144
+ const engine = setup.engine;
145
+ const { batch_hash, index, count } = execution.batch!;
146
+ if (batch_hash !== gesture.batch_hash || count !== gesture.count) {
147
+ return engine.deny(execution.id, approver, `the list changed after the gesture: it was taken on ${gesture.count} line(s) hashing to ${gesture.batch_hash}, and this line belongs to ${count} hashing to ${batch_hash}`);
148
+ }
149
+ if (gesture.vetoed.has(index)) return engine.deny(execution.id, approver, `vetoed in the batch gesture (line ${index + 1} of ${count})`);
150
+ return engine.approve(execution.id, approver);
151
+ }
152
+
153
+ /**
154
+ * What the person answered to a whole list, or `undefined` when the answer
155
+ * cannot be read as one. Lines are numbered from 1, as they were shown.
156
+ * "todas" / "all", "todas exceto 3,7" / "all except 3,7", and "nenhuma" /
157
+ * "none" (also an empty answer: the default is no, as it is per line). A
158
+ * number outside the list is unreadable rather than ignored, because a typo
159
+ * that silently vetoed nothing would pay the line the person meant to stop.
160
+ */
161
+ export function parseBatchGesture(answer: string, count: number): { vetoed: number[] } | undefined {
162
+ const text = answer.trim().toLowerCase();
163
+ const all = Array.from({ length: count }, (_, i) => i);
164
+ if (text === "" || text === "nenhuma" || text === "none" || text === "n" || text === "nao") return { vetoed: all };
165
+ if (text === "todas" || text === "all") return { vetoed: [] };
166
+ const except = /^(?:todas|all)\s+(?:exceto|except)\s+([\d\s,]+)$/.exec(text);
167
+ if (!except) return undefined;
168
+ const numbers = except[1]!.split(/[\s,]+/).filter(Boolean).map(Number);
169
+ if (numbers.length === 0 || numbers.some((n) => !Number.isInteger(n) || n < 1 || n > count)) return undefined;
170
+ return { vetoed: [...new Set(numbers.map((n) => n - 1))].sort((a, b) => a - b) };
171
+ }
172
+
173
+ /**
174
+ * Section 3, for a list: the person sees every line, the total and the hash
175
+ * prefix, and answers once. What they answer is recorded in the bundle as a
176
+ * `batch.gesture` event and kept for the lines that follow; each line is then
177
+ * decided by `decideByGesture` when it arrives, and each one it approves mints
178
+ * its own artifact as before.
179
+ */
180
+ export async function presentBatch(batch: BatchPresentation, options: TerminalOptions & { gestures: BatchGestures }): Promise<BatchGesture> {
181
+ const { setup, approver } = options;
182
+ const say = options.say ?? ((l: string) => stderr.write(l + "\n"));
183
+ const ask = options.ask ?? defaultAsk;
184
+ say(` lote ${batch.ref} — ${batch.label}: ${batch.count} linha(s), total ${batch.total}, batch_hash ${batch.batch_hash.slice(0, 19)}…`);
185
+ for (const line of batch.lines) {
186
+ const note =
187
+ line.status === "already_settled"
188
+ ? " (ja paga; nao roda de novo)"
189
+ : line.status === "in_progress"
190
+ ? " (em andamento; nao roda de novo)"
191
+ : line.status === "attempt_id_conflict"
192
+ ? " (tentativa presa a outro pagamento; nao roda de novo)"
193
+ : "";
194
+ say(` ${line.index + 1}. ${line.beneficiary}: ${line.amount}${note}`);
195
+ }
196
+ let parsed: { vetoed: number[] } | undefined;
197
+ while (!parsed) {
198
+ parsed = parseBatchGesture(await ask(" Aprovar a lista? [todas / todas exceto 3,7 / nenhuma] "), batch.count);
199
+ if (!parsed) say(` nao entendi; responda todas, todas exceto <numeros de 1 a ${batch.count}> ou nenhuma`);
200
+ }
201
+ const vetoed = new Set(parsed.vetoed);
202
+ const gesture: BatchGesture = { batch_hash: batch.batch_hash, approved: batch.lines.map((l) => l.index).filter((i) => !vetoed.has(i)), vetoed: parsed.vetoed };
203
+ options.gestures.set(batch.ref, { batch_hash: batch.batch_hash, count: batch.count, vetoed });
204
+ setup.engine.note("batch.gesture", null, { batch_ref: batch.ref, batch_hash: batch.batch_hash, count: batch.count, total_minor: batch.total_minor, approved: gesture.approved, vetoed: gesture.vetoed, approver: { type: "human", id: approver.id, channel: approver.channel } });
205
+ say(` -> lista ${gesture.vetoed.length === 0 ? "aprovada inteira" : gesture.approved.length === 0 ? "negada inteira" : `aprovada exceto ${gesture.vetoed.map((i) => i + 1).join(", ")}`}`);
206
+ return gesture;
207
+ }
208
+
103
209
  export async function waitForPayer(executionId: string, options: TerminalOptions): Promise<PollResult> {
104
210
  const { setup } = options;
105
211
  const say = options.say ?? ((l: string) => stderr.write(l + "\n"));
@@ -133,7 +239,10 @@ export async function interactive(options: TerminalOptions & { runtime: AgentRun
133
239
  const { setup } = options;
134
240
  const say = options.say ?? ((l: string) => stderr.write(l + "\n"));
135
241
  const labels = setup.kit.labels;
136
- const loop = setup.makeLoop(options.runtime, (execution) => handleExecution(execution, options));
242
+ // Interactive only: a person at the keyboard can decide a list in one go. The one-shot and the scenario runner pass one decision to every line and never reach this.
243
+ const gestures: BatchGestures = new Map();
244
+ const withGestures = { ...options, gestures };
245
+ const loop = setup.makeLoop(options.runtime, (execution) => handleExecution(execution, withGestures), (batch) => presentBatch(batch, withGestures));
137
246
  say(`${setup.manifest.manifest.name} ${setup.manifest.manifest.version} — approval: ${setup.mode} — trilho: ${setup.railKind} — ${labels.mandateWord} ${setup.mandate.id}`);
138
247
  say(`run ${setup.runId} — bundle em ${relative(process.cwd(), setup.bundle.dir)}`);
139
248
  say(labels.intro);
@@ -7,12 +7,15 @@
7
7
  * verifier is someone who has the receipt file and nothing else. The key set
8
8
  * is a local copy, so no test opens a socket.
9
9
  */
10
- import { spawnSync } from "node:child_process";
11
- import { generateKeyPairSync, sign as signDetached } from "node:crypto";
12
- import { mkdtempSync, writeFileSync } from "node:fs";
10
+ import { spawn, spawnSync } from "node:child_process";
11
+ import { createHash, generateKeyPairSync, sign as signDetached } from "node:crypto";
12
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { createServer, type Server } from "node:http";
14
+ import type { AddressInfo } from "node:net";
13
15
  import { tmpdir } from "node:os";
14
16
  import { join, resolve } from "node:path";
15
- import { describe, expect, it } from "vitest";
17
+ import { canonicalize, itemsHash, maskPayee, type ExecutionItem } from "@codespar/agent-core";
18
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
16
19
 
17
20
  const BIN = resolve(import.meta.dirname, "../bin.mjs");
18
21
  const KID = "did:web:id.codespar.dev#1";
@@ -55,13 +58,15 @@ function run(args: string[], cwd: string) {
55
58
  }
56
59
 
57
60
  describe("codespar-agent verify", () => {
58
- it("verifies a sealed receipt from a directory with no agent and no key", () => {
61
+ it("checks a run's copy from a directory with no agent and no key, and says it proves the signature only (exit 7)", () => {
59
62
  const { dir, receiptPath, keysPath } = fixture();
60
63
  const out = run([receiptPath, "--keys", keysPath], dir);
61
- expect(out.code).toBe(0);
62
- expect(out.stdout).toContain("VERIFIED");
64
+ expect(out.code).toBe(7);
65
+ expect(out.stdout).toContain("SIGNATURE_ONLY");
63
66
  expect(out.stdout).toContain(RECEIPT_ID);
64
- expect(out.stderr).toContain("was sealed by CodeSpar");
67
+ expect(out.stdout).toContain("read_required");
68
+ expect(out.stderr).toContain("carries CodeSpar's signature");
69
+ expect(out.stderr).toContain("--from-api");
65
70
  });
66
71
 
67
72
  it("puts valid JSON and nothing else on stdout with --json; the sentence goes to stderr", () => {
@@ -70,7 +75,7 @@ describe("codespar-agent verify", () => {
70
75
  const lines = out.stdout.split("\n").filter(Boolean);
71
76
  expect(lines).toHaveLength(1);
72
77
  const report = JSON.parse(lines[0]!) as { verdict: string; kid: string; signing_string: string; message: string };
73
- expect(report.verdict).toBe("verified");
78
+ expect(report.verdict).toBe("signature_only");
74
79
  expect(report.kid).toBe(KID);
75
80
  expect(report.signing_string).toBe(`codespar-receipt:v1:${RECEIPT_ID}:${CHAIN}`);
76
81
  expect(out.stderr).toContain(report.message);
@@ -125,3 +130,200 @@ describe("codespar-agent verify", () => {
125
130
  expect(run(["--help"], dir).code).toBe(0);
126
131
  });
127
132
  });
133
+
134
+ /*
135
+ * The body and the approval (OPEN_QUESTIONS §3, §47). A v4 receipt read, its
136
+ * chain written out link by link and signed with a key generated here, under
137
+ * the recipe production published on 2026-09-26.
138
+ */
139
+ const PUBLISHED = JSON.parse(readFileSync(resolve(import.meta.dirname, "../../agent-core/test/fixtures/production-receipt-keys.json"), "utf8")) as Record<string, unknown>;
140
+ const V4_KID = "did:web:id.codespar.dev#production-2";
141
+ const PAYEE = "escola@exemplo.com.br";
142
+ const ITEMS: ExecutionItem[] = [{ alias: "escola", beneficiary: "Escola Aurora", payee: PAYEE, amount: 185000, currency: "BRL", description: "outubro" }];
143
+ const sha256 = (v: string) => createHash("sha256").update(v, "utf8").digest("hex");
144
+
145
+ function v4Fixture(sealedItemsHash = itemsHash(ITEMS)) {
146
+ const dir = mkdtempSync(join(tmpdir(), "verify-cli-v4-"));
147
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
148
+ const x = Buffer.from(publicKey.export({ format: "der", type: "spki" }).subarray(-32)).toString("base64url");
149
+ const mandate = { id: "cm_test_1", nonce: "nonce-1", scope: "contas do mes", currency: "BRL", sig_sha256: sha256("s".repeat(64)) };
150
+ const quote = { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: PAYEE, session_id: null, sig: "q".repeat(64), at: "2026-09-26T18:00:00.000Z" };
151
+ const approval = { items_hash: sealedItemsHash, batch_hash: null };
152
+ const payment = { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_1_0", money_moved: false, at: "2026-09-26T18:00:01.000Z" };
153
+ const chain = sha256(
154
+ canonicalize({
155
+ v: 4,
156
+ links: [
157
+ mandate,
158
+ quote,
159
+ approval,
160
+ { rail: payment.rail, provider: payment.provider, tx_id: payment.tx_id, amount_minor: payment.amount_minor, sandbox: true, attempt_id: payment.attempt_id, money_moved: false, at: payment.at },
161
+ ],
162
+ }),
163
+ );
164
+ const signature = signDetached(null, Buffer.from(`codespar-receipt:v1:${RECEIPT_ID}:${chain}`, "utf8"), privateKey).toString("base64url");
165
+ const read = { receipt_id: RECEIPT_ID, state: "paid", chain_version: 4, mandate, quote, approval, payment, delivery: null, chain, receipt_sig: "hmac-not-read-here", receipt_sig_ed25519: signature, receipt_sig_kid: V4_KID, exceptions: [] };
166
+ const keys = { ...PUBLISHED, keys: [{ kid: V4_KID, kty: "OKP", crv: "Ed25519", x, alg: "EdDSA", use: "sig", status: "active" }] };
167
+ const artifact = { approval_id: "apr_1", execution_id: "exe_1", items: ITEMS, items_hash: itemsHash(ITEMS), approved_at: "2026-09-26T18:00:00.000Z" };
168
+
169
+ // A run's bundle, as the engine writes it: the masked copy naming its artifact, and approval.json beside receipts/.
170
+ const run = join(dir, "runs", "run_1");
171
+ mkdirSync(join(run, "receipts"), { recursive: true });
172
+ const copy = {
173
+ receipt_id: RECEIPT_ID,
174
+ state: "paid",
175
+ mandate: { id: mandate.id },
176
+ payment: { amount_minor: 185000, payee: maskPayee(PAYEE), attempt_id: payment.attempt_id, money_moved: false, sandbox: true, at: payment.at },
177
+ chain,
178
+ receipt_sig: "hmac-not-read-here",
179
+ receipt_sig_ed25519: signature,
180
+ receipt_sig_kid: V4_KID,
181
+ actor: { type: "agent", agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" },
182
+ approval_id: artifact.approval_id,
183
+ };
184
+ const paths = {
185
+ read: join(dir, "receipt-read.json"),
186
+ keys: join(dir, "codespar-receipt-keys.json"),
187
+ artifact: join(dir, "approval.json"),
188
+ copy: join(run, "receipts", `${RECEIPT_ID}.json`),
189
+ runApprovals: join(run, "approval.json"),
190
+ };
191
+ writeFileSync(paths.read, JSON.stringify(read, null, 2) + "\n");
192
+ writeFileSync(paths.keys, JSON.stringify(keys, null, 2) + "\n");
193
+ writeFileSync(paths.artifact, JSON.stringify([artifact], null, 2) + "\n");
194
+ writeFileSync(paths.copy, JSON.stringify(copy, null, 2) + "\n");
195
+ writeFileSync(paths.runApprovals, JSON.stringify([artifact], null, 2) + "\n");
196
+ return { dir, read, keys, copy, artifact, paths };
197
+ }
198
+
199
+ describe("codespar-agent verify, the body and the approval of a v4 receipt read", () => {
200
+ it("exits 0 when the chain recomputes from the read and the sealed approval is the artifact's", () => {
201
+ const f = v4Fixture();
202
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
203
+ expect(out.code).toBe(0);
204
+ expect(out.stdout).toContain("VERIFIED");
205
+ expect(out.stdout).toContain("body recomputed (chain v4)");
206
+ expect(out.stdout).toContain("approval matched");
207
+ expect(out.stderr).toContain("paid against the list artifact apr_1 approved");
208
+ });
209
+
210
+ it("exits 9, approval_mismatch, when the receipt sealed another list than the artifact's", () => {
211
+ const f = v4Fixture(`sha256:${"9".repeat(64)}`);
212
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--json"], f.dir);
213
+ expect(out.code).toBe(9);
214
+ const report = JSON.parse(out.stdout) as { verdict: string; reason: string; approval_check: { status: string } };
215
+ expect(report).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs", approval_check: { status: "mismatch" } });
216
+ });
217
+
218
+ it("exits 8, chain_mismatch, when the payee beside a genuine signature was changed", () => {
219
+ const f = v4Fixture();
220
+ writeFileSync(f.paths.read, JSON.stringify({ ...f.read, quote: { ...f.read.quote, payee: "outra@exemplo.com.br" } }));
221
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
222
+ expect(out.code).toBe(8);
223
+ expect(out.stdout).toContain("CHAIN_MISMATCH");
224
+ });
225
+
226
+ it("exits 7 for a v2 read, and says the chain is not recomputable without the mandate signature", () => {
227
+ const f = v4Fixture();
228
+ writeFileSync(f.paths.read, JSON.stringify({ ...f.read, chain_version: 2 }));
229
+ const out = run([f.paths.read, "--keys", f.paths.keys], f.dir);
230
+ expect(out.code).toBe(7);
231
+ expect(out.stderr).toContain("not recomputable without the mandate signature");
232
+ });
233
+
234
+ it("refuses an --approval file with more than one artifact unless --approval-id names one", () => {
235
+ const f = v4Fixture();
236
+ writeFileSync(f.paths.artifact, JSON.stringify([f.artifact, { ...f.artifact, approval_id: "apr_2" }]));
237
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir).code).toBe(2);
238
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--approval-id", "apr_1"], f.dir).code).toBe(0);
239
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval-id", "apr_1"], f.dir).code).toBe(2);
240
+ });
241
+ });
242
+
243
+ /*
244
+ * `--from-api`: a run's masked copy, bound through the API's read. A local
245
+ * server plays the deployment — the receipt route and its well-known key set —
246
+ * so the read and the key set come from ONE origin, as the command requires.
247
+ */
248
+ describe("codespar-agent verify --from-api", () => {
249
+ let server: Server;
250
+ let baseUrl: string;
251
+ let served: { read: unknown; keys: unknown } = { read: {}, keys: {} };
252
+ const asked: string[] = [];
253
+
254
+ beforeAll(async () => {
255
+ server = createServer((req, res) => {
256
+ asked.push(`${req.method} ${req.url} ${req.headers["authorization"] ? "auth" : "anon"}`);
257
+ const body = req.url === "/.well-known/codespar-receipt-keys.json" ? served.keys : req.url === `/v1/consumers/receipts/${RECEIPT_ID}` ? served.read : undefined;
258
+ res.writeHead(body === undefined ? 404 : 200, { "content-type": "application/json" });
259
+ res.end(JSON.stringify(body ?? { error: { code: "not_found", message: "no" } }));
260
+ });
261
+ await new Promise<void>((ok) => server.listen(0, "127.0.0.1", ok));
262
+ baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
263
+ });
264
+
265
+ afterAll(async () => {
266
+ server.closeAllConnections();
267
+ await new Promise<void>((ok) => server.close(() => ok()));
268
+ });
269
+
270
+ /** The server answers while the command runs, so the process is awaited, never spawned synchronously. */
271
+ function runAsync(args: string[], cwd: string): Promise<{ code: number | null; stdout: string; stderr: string }> {
272
+ return new Promise((done) => {
273
+ const child = spawn(process.execPath, [BIN, "verify", ...args], {
274
+ cwd,
275
+ // The placeholder passes the csk_test_ guard and is the one test-key-shaped string the secret scan allows.
276
+ env: { ...process.env, CODESPAR_API_KEY: "csk_test_your_key_here", CODESPAR_API_URL: baseUrl, ANTHROPIC_API_KEY: "" },
277
+ });
278
+ let stdout = "";
279
+ let stderr = "";
280
+ child.stdout.on("data", (d: Buffer) => (stdout += d.toString()));
281
+ child.stderr.on("data", (d: Buffer) => (stderr += d.toString()));
282
+ child.on("close", (code) => done({ code, stdout, stderr }));
283
+ });
284
+ }
285
+
286
+ it("binds the copy's body through the read and checks the run's own artifact: exit 0, and nothing unmasked is written", async () => {
287
+ const f = v4Fixture();
288
+ served = { read: f.read, keys: f.keys };
289
+ asked.length = 0;
290
+ const out = await runAsync([f.paths.copy, "--from-api"], f.dir);
291
+ expect(out.code).toBe(0);
292
+ expect(out.stdout).toContain("approval matched");
293
+ expect(asked).toEqual([`GET /.well-known/codespar-receipt-keys.json anon`, `GET /v1/consumers/receipts/${RECEIPT_ID} auth`]);
294
+ expect(readFileSync(f.paths.copy, "utf8")).not.toContain(PAYEE);
295
+ });
296
+
297
+ it("exits 9 when the run's artifact is not the list the receipt sealed", async () => {
298
+ const f = v4Fixture();
299
+ served = { read: f.read, keys: f.keys };
300
+ writeFileSync(f.paths.runApprovals, JSON.stringify([{ ...f.artifact, items: [{ ...ITEMS[0]!, amount: 1 }], items_hash: itemsHash([{ ...ITEMS[0]!, amount: 1 }]) }]));
301
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
302
+ expect(out.code).toBe(9);
303
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs" });
304
+ });
305
+
306
+ it("exits 8 when the copy was edited after it was written: the signature does not cover its body, the read does", async () => {
307
+ const f = v4Fixture();
308
+ served = { read: f.read, keys: f.keys };
309
+ writeFileSync(f.paths.copy, JSON.stringify({ ...f.copy, payment: { ...f.copy.payment, amount_minor: 1 } }));
310
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
311
+ expect(out.code).toBe(8);
312
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "chain_mismatch", reason: "copy_differs_from_read" });
313
+ });
314
+
315
+ it("answers signature_only, never a failure, when the read cannot be fetched", async () => {
316
+ const f = v4Fixture();
317
+ served = { read: undefined as unknown, keys: f.keys };
318
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
319
+ expect(out.code).toBe(7);
320
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "signature_only", reason: "read_unavailable" });
321
+ });
322
+
323
+ it("refuses a key set from another deployment than the read", async () => {
324
+ const f = v4Fixture();
325
+ const out = await runAsync([f.paths.copy, "--from-api", "--url", "https://api.codespar.dev/.well-known/codespar-receipt-keys.json"], f.dir);
326
+ expect(out.code).toBe(2);
327
+ expect(out.stderr).toContain("same deployment");
328
+ });
329
+ });
@@ -20,6 +20,7 @@ import {
20
20
  isMetaBaseUrl,
21
21
  loadCloudApiConfig,
22
22
  parseInbound,
23
+ providerRefusal,
23
24
  verifyChallenge,
24
25
  verifyWebhookSignature,
25
26
  WhatsAppCloudApi,
@@ -230,3 +231,47 @@ describe("deliver, with the network replaced by an assertion", () => {
230
231
  expect(sent.refused?.rule).toBe("provider_refused");
231
232
  });
232
233
  });
234
+
235
+ describe("what Meta's error body says, read rather than collapsed into a status", () => {
236
+ const body = (error: unknown) => JSON.stringify({ error });
237
+
238
+ it("reads 131047 as the window rule it is, so a caller can answer it with a template", () => {
239
+ const refusal = providerRefusal(400, body({ code: 131047, message: "(#131047) Message failed to send", error_data: { details: "the 24h customer service window closed" } }));
240
+ expect(refusal.rule).toBe("session_window_closed");
241
+ expect(refusal.detail).toContain("400/131047");
242
+ expect(refusal.detail).toContain("the 24h customer service window closed");
243
+ });
244
+
245
+ it("keeps any other code a provider refusal, with the code named", () => {
246
+ expect(providerRefusal(400, body({ code: 131026, message: "Message undeliverable" }))).toEqual({ rule: "provider_refused", detail: "400/131026 from the Cloud API" });
247
+ });
248
+
249
+ it("does not mistake a body without Meta's envelope for anything but the status", () => {
250
+ expect(providerRefusal(502, "<html>bad gateway</html>")).toEqual({ rule: "provider_refused", detail: "502 from the Cloud API" });
251
+ });
252
+ });
253
+
254
+ describe("a redelivered message is the same message", () => {
255
+ it("hands a message on once however many times it arrives, and says it dropped the rest", async () => {
256
+ const lines: string[] = [];
257
+ const port = 40000 + Math.floor(Math.random() * 20000);
258
+ const backend = new WhatsAppCloudApi({ config: { ...CONFIG, webhookPort: port }, conversation: { contact: "+5511987654321" }, say: (l) => lines.push(l) });
259
+ await backend.open();
260
+ try {
261
+ const raw = JSON.stringify({
262
+ entry: [{ changes: [{ value: { messages: [{ id: "wamid.ONCE", from: "5511987654321", timestamp: "1758643200", type: "text", text: { body: "fechado" } }] } }] }],
263
+ });
264
+ for (let i = 0; i < 2; i++) {
265
+ const response = await fetch(`http://127.0.0.1:${port}/`, { method: "POST", headers: { "x-hub-signature-256": sign(raw) }, body: raw });
266
+ // Still 200: Meta retries anything else, and a duplicate is not an error.
267
+ expect(response.status).toBe(200);
268
+ }
269
+ expect((await backend.next())?.id).toBe("wamid.ONCE");
270
+ const second = await Promise.race([backend.next(), new Promise((resolve) => setTimeout(() => resolve("nothing"), 200))]);
271
+ expect(second).toBe("nothing");
272
+ expect(lines).toContain("[whatsapp] duplicate delivery of wamid.ONCE dropped");
273
+ } finally {
274
+ await backend.close();
275
+ }
276
+ });
277
+ });
@@ -104,6 +104,38 @@ describe("a conversation a later process comes back to", () => {
104
104
  });
105
105
  });
106
106
 
107
+ describe("when the provider's clock disagrees with ours", () => {
108
+ /** A provider whose window is shut whatever ours says: free text is refused the way Meta refuses it, templates go. */
109
+ class ShutProvider extends Recorder {
110
+ override async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
111
+ if (body.kind === "template") return super.deliver(to, body);
112
+ return { id: "", state: "failed", refused: { rule: "session_window_closed", detail: "the provider refused the free-form message (400/131047)" } };
113
+ }
114
+ }
115
+
116
+ it("believes the provider: after a 131047 the window reads shut, and a template still goes", async () => {
117
+ const { channel: c, backend } = channel(TUESDAY, Math.floor(TUESDAY.getTime() / 1000) - 3600, new ShutProvider());
118
+ expect(c.sessionOpen).toBe(true);
119
+ const sent = await c.say("Recebemos, acordo quitado.");
120
+ expect(sent.refused?.rule).toBe("session_window_closed");
121
+ expect(c.sessionOpen).toBe(false);
122
+ expect(c.sessionRemainingSeconds).toBe(0);
123
+ // Refused by our own rule from here, so a second free-form message never reaches the provider.
124
+ expect((await c.say("de novo")).refused?.detail).toContain("more than 24h");
125
+ const template = await c.send({ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042"] });
126
+ expect(template.refused).toBeUndefined();
127
+ expect(backend.delivered).toHaveLength(1);
128
+ });
129
+
130
+ it("reopens when the person writes again, because that is what opens a window at Meta too", () => {
131
+ const window = new SessionWindow([], { lastInboundAt: Math.floor(TUESDAY.getTime() / 1000) - 3600 });
132
+ window.observeProviderShut();
133
+ expect(window.open(TUESDAY)).toBe(false);
134
+ window.observeInbound(Math.floor(TUESDAY.getTime() / 1000));
135
+ expect(window.open(TUESDAY)).toBe(true);
136
+ });
137
+ });
138
+
107
139
  describe("the local registry refuses what Meta would refuse", () => {
108
140
  it("a name the agent never declared", async () => {
109
141
  const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));