@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.
- package/README.md +1 -0
- package/dist/surface.js +53 -1
- package/dist/surface.js.map +1 -1
- package/package.json +1 -1
- package/templates/bills-agent/README.md +1 -1
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
- package/templates/bills-agent/agents/bills-agent/README.md +6 -5
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
- package/templates/bills-agent/packages/agent-core/README.md +14 -1
- package/templates/bills-agent/packages/agent-core/package.json +1 -1
- package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
- package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/LICENSE +21 -0
- package/templates/checkout-agent/README.md +28 -0
- package/templates/checkout-agent/_gitignore +10 -0
- package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
- package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
- package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
- package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
- package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
- package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
- package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
- package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
- package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
- package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
- package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
- package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
- package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
- package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
- package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
- package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
- package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
- package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
- package/templates/checkout-agent/package.json +38 -0
- package/templates/checkout-agent/packages/agent-core/README.md +34 -0
- package/templates/checkout-agent/packages/agent-core/package.json +24 -0
- package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
- package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
- package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
- package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
- package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
- package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
- package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
- package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
- package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
- package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
- package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
- package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
- package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
- package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
- package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
- package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
- package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
- package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
- package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
- package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
- package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
- package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
- package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
- package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
- package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
- package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
- package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
- package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
- package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
- package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/checkout-agent/tsconfig.base.json +16 -0
- package/templates/checkout-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/README.md +6 -1
- package/templates/collections-agent/agents/collections-agent/README.md +12 -8
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
- package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
- package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
- package/templates/collections-agent/package.json +1 -0
- package/templates/collections-agent/packages/agent-core/README.md +14 -1
- package/templates/collections-agent/packages/agent-core/package.json +1 -1
- package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
- package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/hello-agent/README.md +1 -1
- package/templates/hello-agent/packages/agent-core/README.md +14 -1
- package/templates/hello-agent/packages/agent-core/package.json +1 -1
- package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
- package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/kits.lock.json +29 -6
- package/templates/supplier-payments-agent/README.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
- package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
- package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
|
@@ -47,16 +47,22 @@ export const ConversationScriptSchema = z
|
|
|
47
47
|
* different one, which is the secrecy rule as code rather than as prose.
|
|
48
48
|
*/
|
|
49
49
|
subject: z.string().min(1).optional(),
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The person's turns, in order: something they TYPED (`text`), or a quick
|
|
52
|
+
* reply they TAPPED (`reply`, the id of a button a declared template
|
|
53
|
+
* offers). Exactly one of the two.
|
|
54
|
+
*/
|
|
51
55
|
turns: z
|
|
52
56
|
.array(
|
|
53
57
|
z
|
|
54
58
|
.object({
|
|
55
|
-
text: z.string().min(1),
|
|
59
|
+
text: z.string().min(1).optional(),
|
|
60
|
+
reply: z.object({ id: z.string().min(1), title: z.string().min(1).optional() }).strict().optional(),
|
|
56
61
|
/** Seconds after the previous inbound turn, on the run's clock. Keeps a scripted run deterministic. */
|
|
57
62
|
after_seconds: z.number().int().nonnegative().default(0),
|
|
58
63
|
})
|
|
59
|
-
.strict()
|
|
64
|
+
.strict()
|
|
65
|
+
.refine((t) => (t.text === undefined) !== (t.reply === undefined), { message: "a turn is either text the person typed or a reply they tapped, exactly one" }),
|
|
60
66
|
)
|
|
61
67
|
.nonempty(),
|
|
62
68
|
})
|
|
@@ -86,6 +92,26 @@ export const TEMPLATE_REGISTRY_FILE = "templates.json";
|
|
|
86
92
|
* match is a `132000` from the Cloud API and there is no reason to learn that
|
|
87
93
|
* in production.
|
|
88
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* A quick-reply button a template offers, and what tapping it MEANS.
|
|
97
|
+
*
|
|
98
|
+
* `intent` is the turn the model receives when the person taps it — written
|
|
99
|
+
* here, by whoever wrote the template, and never by the model. A tap carries
|
|
100
|
+
* an id and a title; the title is copy and can change, the id cannot, and the
|
|
101
|
+
* intent is what the id stands for. An id this registry does not declare is
|
|
102
|
+
* not a turn at all.
|
|
103
|
+
*/
|
|
104
|
+
export const QuickReplySchema = z
|
|
105
|
+
.object({
|
|
106
|
+
id: z.string().regex(/^[a-z0-9_]{1,64}$/, "a reply id is lower-case letters, digits and underscores"),
|
|
107
|
+
/** What the button says. Meta caps a quick reply at 20 characters. */
|
|
108
|
+
title: z.string().min(1).max(20),
|
|
109
|
+
intent: z.string().min(1),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
|
|
113
|
+
export type QuickReply = z.infer<typeof QuickReplySchema>;
|
|
114
|
+
|
|
89
115
|
export const WhatsAppTemplateSchema = z
|
|
90
116
|
.object({
|
|
91
117
|
/** Meta's own naming rule for a template. */
|
|
@@ -94,6 +120,15 @@ export const WhatsAppTemplateSchema = z
|
|
|
94
120
|
language: z.string().regex(/^[a-z]{2}(_[A-Z]{2})?$/, 'expected a language code, e.g. "pt_BR"'),
|
|
95
121
|
description: z.string().min(1),
|
|
96
122
|
body: z.string().min(1),
|
|
123
|
+
/** Quick replies the template carries (Meta allows up to three on a template). */
|
|
124
|
+
buttons: z.array(QuickReplySchema).max(3).optional(),
|
|
125
|
+
/**
|
|
126
|
+
* The template a poll sends when the outcome it must tell has no copy of
|
|
127
|
+
* its own: it says there is news and asks the person to answer, and states
|
|
128
|
+
* nothing about the outcome — so it can never be the wrong news. Exactly
|
|
129
|
+
* one per registry, taking no variables (`npm run check`).
|
|
130
|
+
*/
|
|
131
|
+
fallback: z.boolean().optional(),
|
|
97
132
|
})
|
|
98
133
|
.strict();
|
|
99
134
|
|
|
@@ -120,12 +155,23 @@ export const TemplateRegistrySchema = z
|
|
|
120
155
|
.strict()
|
|
121
156
|
.superRefine((registry, ctx) => {
|
|
122
157
|
const seen = new Set<string>();
|
|
158
|
+
const replyIds = new Set<string>();
|
|
123
159
|
for (const [i, template] of registry.templates.entries()) {
|
|
124
160
|
if (seen.has(template.name)) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["templates", i, "name"], message: `${template.name} is declared twice` });
|
|
125
161
|
seen.add(template.name);
|
|
162
|
+
// A tap is resolved by its id alone, so one id means one intent across the whole registry.
|
|
163
|
+
for (const [j, button] of (template.buttons ?? []).entries()) {
|
|
164
|
+
if (replyIds.has(button.id)) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["templates", i, "buttons", j, "id"], message: `reply id ${button.id} is declared twice` });
|
|
165
|
+
replyIds.add(button.id);
|
|
166
|
+
}
|
|
126
167
|
}
|
|
127
168
|
});
|
|
128
169
|
|
|
170
|
+
/** Every quick reply the registry declares, by id: the only taps that can become a turn. */
|
|
171
|
+
export function declaredReplies(templates: readonly WhatsAppTemplate[]): Map<string, QuickReply> {
|
|
172
|
+
return new Map(templates.flatMap((t) => t.buttons ?? []).map((b) => [b.id, b]));
|
|
173
|
+
}
|
|
174
|
+
|
|
129
175
|
export type TemplateRegistry = z.infer<typeof TemplateRegistrySchema>;
|
|
130
176
|
|
|
131
177
|
export function parseTemplateRegistry(text: string): TemplateRegistry {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { parse as parseYaml } from "yaml";
|
|
11
|
-
import { ConversationScriptSchema, TemplateRegistrySchema, TEMPLATE_REGISTRY_FILE, templateArity } from "./channels.js";
|
|
11
|
+
import { ConversationScriptSchema, TemplateRegistrySchema, TEMPLATE_REGISTRY_FILE, declaredReplies, templateArity, type TemplateRegistry } from "./channels.js";
|
|
12
12
|
import { GuardrailsSchema } from "./guardrails.js";
|
|
13
13
|
import { loadManifest, ManifestSchema, type LoadedManifest } from "./manifest.js";
|
|
14
14
|
import { MandateSchema } from "./mandate.js";
|
|
@@ -121,12 +121,35 @@ export function checkAgent(agentDir: string): CheckReport {
|
|
|
121
121
|
if (!manifest.channels.includes("whatsapp") && scripts.length > 0) {
|
|
122
122
|
error("channels_undeclared", `channels/whatsapp/ ships ${scripts.length} conversation(s) but agent.yaml does not declare the whatsapp channel`);
|
|
123
123
|
}
|
|
124
|
+
const registryPath = join(whatsappDir, TEMPLATE_REGISTRY_FILE);
|
|
125
|
+
let registry: TemplateRegistry | undefined;
|
|
126
|
+
if (existsSync(registryPath)) {
|
|
127
|
+
const parsed = TemplateRegistrySchema.safeParse(JSON.parse(readFileSync(registryPath, "utf8")));
|
|
128
|
+
if (parsed.success) registry = parsed.data;
|
|
129
|
+
}
|
|
130
|
+
const replies = declaredReplies(registry?.templates ?? []);
|
|
124
131
|
for (const file of scripts) {
|
|
125
132
|
const parsed = ConversationScriptSchema.safeParse(JSON.parse(readFileSync(join(whatsappDir, file), "utf8")));
|
|
126
133
|
if (!parsed.success) error("channels_script_invalid", `channels/whatsapp/${file}: ${parsed.error.message}`);
|
|
127
134
|
else if (parsed.data.name !== file.replace(/\.json$/, "")) error("channels_script_invalid", `channels/whatsapp/${file} declares name ${parsed.data.name}`);
|
|
135
|
+
else {
|
|
136
|
+
// A scripted tap must be one a declared template offers: the channel drops any other, so the script would test nothing.
|
|
137
|
+
for (const turn of parsed.data.turns) {
|
|
138
|
+
if (turn.reply && !replies.has(turn.reply.id)) error("channels_script_invalid", `channels/whatsapp/${file} taps ${turn.reply.id}, which no template in ${TEMPLATE_REGISTRY_FILE} offers`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// An agent on WhatsApp closes outcomes days later, outside the window, where
|
|
143
|
+
// only a template goes. One it has no copy for still has to reach the
|
|
144
|
+
// person, so the registry declares a fallback that states no outcome.
|
|
145
|
+
if (manifest.channels.includes("whatsapp")) {
|
|
146
|
+
if (!existsSync(registryPath)) error("channels_templates_missing", `agent.yaml declares the whatsapp channel and channels/whatsapp/${TEMPLATE_REGISTRY_FILE} does not exist`);
|
|
147
|
+
else if (registry) {
|
|
148
|
+
const fallbacks = registry.templates.filter((t) => t.fallback);
|
|
149
|
+
if (fallbacks.length !== 1) error("channels_templates_fallback", `channels/whatsapp/${TEMPLATE_REGISTRY_FILE} declares ${fallbacks.length} fallback template(s); it must declare exactly one, for an outcome the agent has no template for`);
|
|
150
|
+
else if (templateArity(fallbacks[0]!.body) !== 0) error("channels_templates_fallback", `the fallback template ${fallbacks[0]!.name} takes variables; it is sent when nothing specific can be said, so it takes none`);
|
|
151
|
+
}
|
|
128
152
|
}
|
|
129
|
-
const registryPath = join(whatsappDir, TEMPLATE_REGISTRY_FILE);
|
|
130
153
|
if (existsSync(registryPath)) {
|
|
131
154
|
const parsed = TemplateRegistrySchema.safeParse(JSON.parse(readFileSync(registryPath, "utf8")));
|
|
132
155
|
if (!parsed.success) error("channels_templates_invalid", `channels/whatsapp/${TEMPLATE_REGISTRY_FILE}: ${parsed.error.message}`);
|
|
@@ -12,19 +12,21 @@
|
|
|
12
12
|
* meantime is caught at the last gate, which is the only one that matters.
|
|
13
13
|
*/
|
|
14
14
|
import { checkApprovalArtifact, createApprovalArtifact, type ApprovalSigner } from "./approval.js";
|
|
15
|
-
import type
|
|
15
|
+
import { maskPayee, type ProofBundle } from "./bundle.js";
|
|
16
16
|
import { evaluateEscalation, type Escalation } from "./escalate.js";
|
|
17
17
|
import { CHARGE_CANCELLED, CHARGE_EXPIRED, CHARGE_PAID, PAYMENT_FAILED, PAYMENT_SUCCEEDED, type PublishedEvent } from "./events.js";
|
|
18
18
|
import type { Guardrails } from "./guardrails.js";
|
|
19
|
-
import { itemsHash, sha256Hex } from "./hash.js";
|
|
19
|
+
import { batchAttemptId, itemsHash, sha256Hex } from "./hash.js";
|
|
20
20
|
import { newId } from "./ids.js";
|
|
21
21
|
import { mandateExpired, payeeAllowed, resolveBeneficiary, windowCap, windowStart, type Mandate } from "./mandate.js";
|
|
22
22
|
import type { Manifest } from "./manifest.js";
|
|
23
|
-
import
|
|
23
|
+
import { checkQuote, quoteFromApproval } from "./quote.js";
|
|
24
|
+
import { spendApprovalOf, type PaymentRail, type RailOutcome, type RailPayment, type SealedSpendApproval, type SpendApproval } from "./rail.js";
|
|
25
|
+
import { sameApprovalHash } from "./receipt-chain.js";
|
|
24
26
|
import type { MandateStatusReport, MandateStatusSource } from "./revocation.js";
|
|
25
27
|
import { isTerminal, transition, type Execution, type ExecutionState } from "./state-machine.js";
|
|
26
28
|
import type { StateStore } from "./state/store.js";
|
|
27
|
-
import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
|
|
29
|
+
import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionComposition, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
|
|
28
30
|
|
|
29
31
|
export interface ProposedItem {
|
|
30
32
|
/** An alias from the mandate's named payees, or a raw payee key. */
|
|
@@ -61,6 +63,13 @@ export interface Proposal {
|
|
|
61
63
|
* it. The core never invents one and never fills one in.
|
|
62
64
|
*/
|
|
63
65
|
batch?: ExecutionBatch;
|
|
66
|
+
/**
|
|
67
|
+
* What the proposal's amount is composed of, when it is composed of lines
|
|
68
|
+
* (a cart behind one order). The kit computes `compositionHash` over the
|
|
69
|
+
* resolved lines; the core stamps it on the execution and every artifact of
|
|
70
|
+
* it, and never computes, fills in or reads the lines.
|
|
71
|
+
*/
|
|
72
|
+
composition?: ExecutionComposition;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
export type DraftResult =
|
|
@@ -94,6 +103,14 @@ type Verdict =
|
|
|
94
103
|
|
|
95
104
|
type Gate = "draft" | "approve" | "execute";
|
|
96
105
|
|
|
106
|
+
/**
|
|
107
|
+
* How many spent generations of one batch line's attempt id a dispatch walks
|
|
108
|
+
* past before it reports the line failed. Each one is a payment the provider
|
|
109
|
+
* already refused for this exact line, so eight is a line that keeps failing,
|
|
110
|
+
* not a line still looking for its id.
|
|
111
|
+
*/
|
|
112
|
+
const MAX_ATTEMPT_GENERATION = 8;
|
|
113
|
+
|
|
97
114
|
/** Open executions reserve the window: a cap is a ceiling on what may be committed, not a balance. */
|
|
98
115
|
const WINDOW_STATES: ExecutionState[] = ["awaiting_approval", "approved", "executing", "settled"];
|
|
99
116
|
|
|
@@ -127,6 +144,7 @@ export class ExecutionEngine {
|
|
|
127
144
|
const items = proposal.items.map((p) => this.resolveItem(p));
|
|
128
145
|
const total = items.reduce((sum, i) => sum + i.amount, 0);
|
|
129
146
|
if (proposal.batch) assertBatchBinding(proposal.batch);
|
|
147
|
+
if (proposal.composition) assertCompositionBinding(proposal.composition);
|
|
130
148
|
const id = newId("exe");
|
|
131
149
|
const execution: Execution<"drafted"> = {
|
|
132
150
|
id,
|
|
@@ -140,6 +158,7 @@ export class ExecutionEngine {
|
|
|
140
158
|
...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
|
|
141
159
|
items_hash: itemsHash(items),
|
|
142
160
|
...(proposal.batch ? { batch: proposal.batch } : {}),
|
|
161
|
+
...(proposal.composition ? { composition: proposal.composition } : {}),
|
|
143
162
|
mandate: { id: this.deps.mandate.id, version: this.deps.mandate.version },
|
|
144
163
|
idempotency_key: `idk_${sha256Hex(`${this.deps.runId}:${id}`).slice(0, 32)}`,
|
|
145
164
|
blocking_reasons: [],
|
|
@@ -149,11 +168,58 @@ export class ExecutionEngine {
|
|
|
149
168
|
updated_at: now.toISOString(),
|
|
150
169
|
};
|
|
151
170
|
this.deps.store.saveExecution(execution);
|
|
152
|
-
this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null });
|
|
171
|
+
this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null, ...(execution.composition ? { composition: execution.composition } : {}) });
|
|
153
172
|
|
|
154
173
|
return { ok: true, execution: await this.evaluateDraft(execution) };
|
|
155
174
|
}
|
|
156
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The proposal behind an OPEN execution changed, and the execution follows
|
|
178
|
+
* it: a customer who changes the cart after the order was approved is
|
|
179
|
+
* changing the order, not opening a second one. The items, the total, the
|
|
180
|
+
* `items_hash` and the composition are replaced; the state, the approval
|
|
181
|
+
* and the idempotency key are NOT.
|
|
182
|
+
*
|
|
183
|
+
* That is the point. No transition happens here and nothing is judged: an
|
|
184
|
+
* `approved` execution keeps the artifact it was approved with, and the
|
|
185
|
+
* last gate (`execute`) compares the two and sends it back to
|
|
186
|
+
* `awaiting_approval` with `items_hash_mismatch` — the same edge a list
|
|
187
|
+
* tampered after approval takes, reached honestly. An `awaiting_approval`
|
|
188
|
+
* one is judged again by whoever decides it, on what it says now.
|
|
189
|
+
*
|
|
190
|
+
* The payees may not change, in number or in order: a restatement changes
|
|
191
|
+
* what is charged or paid, never to whom. Nothing is dispatched by an open
|
|
192
|
+
* execution, so no attempt can exist under the key it keeps.
|
|
193
|
+
*/
|
|
194
|
+
restate(executionId: string, proposal: Proposal): Execution {
|
|
195
|
+
const execution = this.mustGet(executionId);
|
|
196
|
+
if (execution.state !== "awaiting_approval" && execution.state !== "approved") throw new Error(`execution ${executionId} is ${execution.state}; only an open execution (awaiting_approval, approved) can be restated`);
|
|
197
|
+
if (proposal.batch) throw new Error("a batch line is not restated; the batch is presented again");
|
|
198
|
+
if (proposal.composition) assertCompositionBinding(proposal.composition);
|
|
199
|
+
const items = proposal.items.map((p) => this.resolveItem(p));
|
|
200
|
+
const before = execution.items.map((i) => i.payee);
|
|
201
|
+
if (items.length !== before.length || items.some((item, i) => item.payee !== before[i])) throw new Error(`execution ${executionId}: a restatement may not change the payees`);
|
|
202
|
+
const total = items.reduce((sum, i) => sum + i.amount, 0);
|
|
203
|
+
const { composition: _composition, model_claimed_total: _claimed, ...rest } = execution;
|
|
204
|
+
const restated: Execution = {
|
|
205
|
+
...rest,
|
|
206
|
+
items,
|
|
207
|
+
total,
|
|
208
|
+
...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
|
|
209
|
+
items_hash: itemsHash(items),
|
|
210
|
+
...(proposal.composition ? { composition: proposal.composition } : {}),
|
|
211
|
+
updated_at: this.clock().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
this.deps.store.saveExecution(restated);
|
|
214
|
+
this.record("execution.restated", execution.id, {
|
|
215
|
+
state: execution.state,
|
|
216
|
+
items_hash: { from: execution.items_hash, to: restated.items_hash },
|
|
217
|
+
total: { from: execution.total, to: total },
|
|
218
|
+
composition: { from: execution.composition ?? null, to: restated.composition ?? null },
|
|
219
|
+
});
|
|
220
|
+
return restated;
|
|
221
|
+
}
|
|
222
|
+
|
|
157
223
|
// ---- deterministic policy ---------------------------------------------
|
|
158
224
|
|
|
159
225
|
/**
|
|
@@ -179,6 +245,14 @@ export class ExecutionEngine {
|
|
|
179
245
|
return { kind: "deny", reason: "beneficiary_not_allowed", detail };
|
|
180
246
|
}
|
|
181
247
|
|
|
248
|
+
// A receivable closed `charge_reference_ambiguous` was issued and may still be paid. Another one to the same payee is how a debtor pays twice.
|
|
249
|
+
const unreconciled = this.deps.store
|
|
250
|
+
.listExecutions({ state: "failed", mandate_id: mandate.id })
|
|
251
|
+
.find((e) => e.id !== execution.id && e.reason === "charge_reference_ambiguous" && e.items.some((i) => execution.items.some((mine) => mine.payee === i.payee)));
|
|
252
|
+
if (unreconciled) {
|
|
253
|
+
return { kind: "deny", reason: "charge_reference_ambiguous", detail: `execution ${unreconciled.id} issued a receivable to this payee whose reference turned ambiguous; reconcile it by the charge id before issuing another` };
|
|
254
|
+
}
|
|
255
|
+
|
|
182
256
|
const over = execution.items.find((i) => i.amount > mandate.per_tx_cap_minor);
|
|
183
257
|
if (over) return { kind: "deny", reason: "per_tx_cap_exceeded", detail: `${over.beneficiary}: ${over.amount} is above the per-payment cap ${mandate.per_tx_cap_minor}` };
|
|
184
258
|
|
|
@@ -295,7 +369,7 @@ export class ExecutionEngine {
|
|
|
295
369
|
if (!artifact) throw new Error(`execution ${executionId} is approved without an approval artifact`);
|
|
296
370
|
|
|
297
371
|
// Everything below is one transaction: the last policy run, the artifact check, the outbox row and the state change land together or not at all.
|
|
298
|
-
const payments = this.paymentsFor(execution);
|
|
372
|
+
const payments = this.paymentsFor(execution, artifact);
|
|
299
373
|
const decided = this.deps.store.transaction((): Execution => {
|
|
300
374
|
// The approval was given under one mandate; a re-signed one (new version) is a new authorization, and the person decides again under it.
|
|
301
375
|
const current = { id: this.deps.mandate.id, version: this.deps.mandate.version };
|
|
@@ -310,6 +384,9 @@ export class ExecutionEngine {
|
|
|
310
384
|
}
|
|
311
385
|
return this.persist(transition(this.withoutApproval(approved), "awaiting_approval", { at, actor: this.agentActor, reason: "items_hash_mismatch", detail: `approval ${artifact.approval_id} does not match what would be executed (${check.problem})` }));
|
|
312
386
|
}
|
|
387
|
+
// The API only records a quote that disagrees with the spend, and pays anyway. The kit does not send one.
|
|
388
|
+
const unquoted = payments.map(checkQuote).find((q) => !q.ok);
|
|
389
|
+
if (unquoted && !unquoted.ok) return this.persist(transition(approved, "denied", { at, actor: this.agentActor, reason: "quote_mismatch", detail: `${unquoted.code}: ${unquoted.detail}; nothing was sent` }));
|
|
313
390
|
|
|
314
391
|
const verdict = this.policy(execution, now, "execute", artifact.approver.type === "person");
|
|
315
392
|
if (verdict.kind === "deny" || verdict.kind === "block") {
|
|
@@ -343,14 +420,33 @@ export class ExecutionEngine {
|
|
|
343
420
|
private async dispatch(execution: Execution<"executing">, payments: RailPayment[]): Promise<Execution> {
|
|
344
421
|
const outcomes: ItemOutcome[] = [...execution.outcomes];
|
|
345
422
|
const unknown: string[] = [];
|
|
423
|
+
const sealMismatches: string[] = [];
|
|
346
424
|
this.deps.store.updateOutbox(execution.idempotency_key, "sent", undefined, this.clock().toISOString());
|
|
347
425
|
|
|
348
|
-
|
|
426
|
+
const generations: Record<number, number> = { ...execution.attempt_generations };
|
|
427
|
+
for (const [index, presented] of payments.entries()) {
|
|
349
428
|
if (outcomes.some((o) => o.index === index)) continue;
|
|
429
|
+
let payment = presented;
|
|
350
430
|
// The dispatch line is the request. `idempotency_key` is what makes a retry the same payment, so the bundle names it next to the attempt.
|
|
351
431
|
this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
|
|
352
|
-
|
|
432
|
+
let outcome = await this.deps.rail.pay(payment);
|
|
353
433
|
this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
|
|
434
|
+
// A batch line whose derived id the rail holds as failed-with-no-money
|
|
435
|
+
// moves to the next generation of it, and only on that answer (see
|
|
436
|
+
// `batchAttemptId`). The generation is saved before the next id goes
|
|
437
|
+
// out, so a crash or an unknown answer is reconciled against the id
|
|
438
|
+
// that was actually sent.
|
|
439
|
+
while (execution.batch && outcome.status === "failed" && outcome.spent && (generations[index] ?? 0) < MAX_ATTEMPT_GENERATION) {
|
|
440
|
+
const generation = (generations[index] ?? 0) + 1;
|
|
441
|
+
const next = batchAttemptId(execution.mandate.id, execution.batch, index, generation);
|
|
442
|
+
this.record("rail.attempt_spent", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, next_attempt_id: next, generation });
|
|
443
|
+
generations[index] = generation;
|
|
444
|
+
this.deps.store.saveExecution({ ...execution, outcomes, attempt_generations: { ...generations }, updated_at: this.clock().toISOString() });
|
|
445
|
+
payment = { ...payment, attempt_id: next };
|
|
446
|
+
this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
|
|
447
|
+
outcome = await this.deps.rail.pay(payment);
|
|
448
|
+
this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
|
|
449
|
+
}
|
|
354
450
|
if (outcome.status === "uncertain") {
|
|
355
451
|
this.record("rail.uncertain", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, message: outcome.message });
|
|
356
452
|
// No outcome is recorded because there is none. The siblings are still
|
|
@@ -360,7 +456,7 @@ export class ExecutionEngine {
|
|
|
360
456
|
continue;
|
|
361
457
|
}
|
|
362
458
|
if (outcome.status === "failed") {
|
|
363
|
-
outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}
|
|
459
|
+
outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}`, ...(outcome.held ? { held: outcome.held } : {}) });
|
|
364
460
|
continue;
|
|
365
461
|
}
|
|
366
462
|
if (outcome.status === "accepted") {
|
|
@@ -369,10 +465,13 @@ export class ExecutionEngine {
|
|
|
369
465
|
this.recordInstrument(execution.id, payment.attempt_id, outcome.transaction_id, outcome.instrument);
|
|
370
466
|
continue;
|
|
371
467
|
}
|
|
372
|
-
outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}) });
|
|
373
|
-
await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
|
|
468
|
+
outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}), ...(outcome.replayed ? { replayed: true as const } : {}) });
|
|
469
|
+
const mismatch = await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
|
|
470
|
+
if (mismatch) sealMismatches.push(mismatch);
|
|
374
471
|
}
|
|
375
|
-
|
|
472
|
+
const closed = this.close({ ...execution, outcomes, ...(Object.keys(generations).length > 0 ? { attempt_generations: generations } : {}) }, payments.length, unknown.join("; ") || undefined);
|
|
473
|
+
if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
|
|
474
|
+
return closed;
|
|
376
475
|
}
|
|
377
476
|
|
|
378
477
|
/**
|
|
@@ -395,10 +494,13 @@ export class ExecutionEngine {
|
|
|
395
494
|
if (execution.outcomes.length < attempts) {
|
|
396
495
|
return this.leaveUnresolved(updated, unknownDetail ?? `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
|
|
397
496
|
}
|
|
398
|
-
|
|
497
|
+
// An ambiguous receivable outranks any other failure: it is the one outcome that must be reconciled rather than issued again.
|
|
498
|
+
const failed = execution.outcomes.find((o) => o.status === "failed" && o.code === "charge_reference_ambiguous") ?? execution.outcomes.find((o) => o.status === "failed");
|
|
399
499
|
if (failed) {
|
|
400
500
|
this.deps.store.updateOutbox(execution.idempotency_key, "failed", execution.outcomes, at);
|
|
401
|
-
|
|
501
|
+
// `org_paused` is the API refusing the spend itself: the kill switch was pressed after the gate read the status. Nothing moved.
|
|
502
|
+
const reason: ExecutionReason =
|
|
503
|
+
failed.code === "charge_expired" || failed.code === "charge_cancelled" || failed.code === "org_paused" || failed.code === "charge_reference_ambiguous" ? failed.code : "rail_failed";
|
|
402
504
|
return this.persist(transition(updated, "failed", { at, actor: this.agentActor, reason, detail: failed.error ?? "rail refused" }));
|
|
403
505
|
}
|
|
404
506
|
if (execution.outcomes.filter((o) => o.status === "settled").length === attempts) {
|
|
@@ -439,7 +541,8 @@ export class ExecutionEngine {
|
|
|
439
541
|
if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
440
542
|
}
|
|
441
543
|
|
|
442
|
-
|
|
544
|
+
/** Records a settled attempt and fetches its receipt; answers the seal mismatch, if the receipt disagrees with what was paid. */
|
|
545
|
+
private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<string | undefined> {
|
|
443
546
|
// The rail's answer is the settlement event the API publishes (section 4.3): `commerce.payment.succeeded` for a payout, `commerce.charge.paid` for a receivable. Keyed by attempt so a replay is a no-op.
|
|
444
547
|
const appended = this.deps.store.appendEvent({
|
|
445
548
|
run_id: this.deps.runId,
|
|
@@ -450,15 +553,38 @@ export class ExecutionEngine {
|
|
|
450
553
|
at: this.clock().toISOString(),
|
|
451
554
|
});
|
|
452
555
|
if (appended) this.deps.bundle.event({ ...appended, actor: this.agentActor, item_index: index });
|
|
453
|
-
|
|
556
|
+
return receiptId ? this.saveReceipt(execution.id, receiptId, payment) : undefined;
|
|
454
557
|
}
|
|
455
558
|
|
|
456
|
-
|
|
559
|
+
/**
|
|
560
|
+
* The bundle's copy is the receipt as it was sealed, payee included. A
|
|
561
|
+
* payment receipt whose sealed payee is not the payee this attempt paid —
|
|
562
|
+
* including one that sealed no payee — is recorded and answered as a
|
|
563
|
+
* mismatch, which the caller raises once the execution's outcome is saved:
|
|
564
|
+
* the money is where the rail says it is, and it is the EVIDENCE that
|
|
565
|
+
* disagrees. The same holds for the approval link (§3): a receipt that
|
|
566
|
+
* seals other hashes than the ones this attempt sent, or none, does not
|
|
567
|
+
* say this payment was made against the approved list. A rail that cannot
|
|
568
|
+
* report the link leaves it `undefined`, and nothing is compared. A paid
|
|
569
|
+
* charge carries no seal, so there is nothing to compare at all.
|
|
570
|
+
*/
|
|
571
|
+
private async saveReceipt(executionId: string, receiptId: string, payment: RailPayment): Promise<string | undefined> {
|
|
457
572
|
const receipt = await this.deps.rail.receipt(receiptId, this.agentActor);
|
|
458
|
-
if (receipt)
|
|
459
|
-
|
|
460
|
-
|
|
573
|
+
if (!receipt) return undefined;
|
|
574
|
+
const path = this.deps.bundle.receipt(receipt, { approval_id: this.deps.store.getExecution(executionId)?.approval_id });
|
|
575
|
+
this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
|
|
576
|
+
if (receipt.kind === "charge") return undefined;
|
|
577
|
+
const mismatches: string[] = [];
|
|
578
|
+
if (receipt.payment.payee !== payment.payee) {
|
|
579
|
+
const sealed = receipt.payment.payee === null ? null : maskPayee(receipt.payment.payee);
|
|
580
|
+
this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_payee: sealed, paid_payee: maskPayee(payment.payee) });
|
|
581
|
+
mismatches.push(`receipt ${receiptId} seals payee ${sealed ?? "none"}; attempt ${payment.attempt_id} paid ${maskPayee(payment.payee)}`);
|
|
461
582
|
}
|
|
583
|
+
if (receipt.approval !== undefined && payment.approval && !sealsApproval(receipt.approval, payment.approval)) {
|
|
584
|
+
this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_approval: receipt.approval, sent_approval: payment.approval });
|
|
585
|
+
mismatches.push(`receipt ${receiptId} seals approval ${receipt.approval ? receipt.approval.items_hash : "none"}; attempt ${payment.attempt_id} sent ${payment.approval.items_hash}`);
|
|
586
|
+
}
|
|
587
|
+
return mismatches.length > 0 ? mismatches.join("; ") : undefined;
|
|
462
588
|
}
|
|
463
589
|
|
|
464
590
|
|
|
@@ -470,12 +596,17 @@ export class ExecutionEngine {
|
|
|
470
596
|
async collectReceipts(executionId: string): Promise<string[]> {
|
|
471
597
|
const execution = this.mustGet(executionId);
|
|
472
598
|
const held = new Set(this.deps.bundle.listReceipts().map((f) => f.replace(/\.json$/, "")));
|
|
599
|
+
const payments = this.paymentsFor(execution, this.approvalOf(execution));
|
|
473
600
|
const fetched: string[] = [];
|
|
601
|
+
const sealMismatches: string[] = [];
|
|
474
602
|
for (const outcome of execution.outcomes) {
|
|
475
|
-
|
|
476
|
-
|
|
603
|
+
const payment = payments[outcome.index];
|
|
604
|
+
if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id) || !payment) continue;
|
|
605
|
+
const mismatch = await this.saveReceipt(execution.id, outcome.receipt_id, payment);
|
|
606
|
+
if (mismatch) sealMismatches.push(mismatch);
|
|
477
607
|
fetched.push(outcome.receipt_id);
|
|
478
608
|
}
|
|
609
|
+
if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
|
|
479
610
|
return fetched;
|
|
480
611
|
}
|
|
481
612
|
|
|
@@ -492,13 +623,14 @@ export class ExecutionEngine {
|
|
|
492
623
|
async reconcile(executionId: string): Promise<Execution> {
|
|
493
624
|
const execution = this.mustGet(executionId);
|
|
494
625
|
if (execution.state !== "executing") return execution;
|
|
495
|
-
const payments = this.paymentsFor(execution);
|
|
626
|
+
const payments = this.paymentsFor(execution, this.approvalOf(execution));
|
|
496
627
|
const outbox = this.deps.store.getOutbox(execution.idempotency_key);
|
|
497
628
|
if (outbox?.status === "pending") {
|
|
498
629
|
return this.leaveUnresolved(execution as Execution<"executing">, "outbox pending: nothing was ever sent; `resume` dispatches it, reconcile does not");
|
|
499
630
|
}
|
|
500
631
|
|
|
501
632
|
const outcomes: ItemOutcome[] = [...execution.outcomes];
|
|
633
|
+
const sealMismatches: string[] = [];
|
|
502
634
|
let unresolved: string | undefined;
|
|
503
635
|
for (const [index, payment] of payments.entries()) {
|
|
504
636
|
const prior = outcomes.find((o) => o.index === index);
|
|
@@ -536,15 +668,17 @@ export class ExecutionEngine {
|
|
|
536
668
|
continue;
|
|
537
669
|
}
|
|
538
670
|
if (seen.status === "failed") {
|
|
539
|
-
replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}
|
|
671
|
+
replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}`, ...(seen.held ? { held: seen.held } : {}) });
|
|
540
672
|
continue;
|
|
541
673
|
}
|
|
542
|
-
replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}) });
|
|
543
|
-
await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
|
|
674
|
+
replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}), ...(seen.replayed ? { replayed: true as const } : {}) });
|
|
675
|
+
const mismatch = await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
|
|
676
|
+
if (mismatch) sealMismatches.push(mismatch);
|
|
544
677
|
}
|
|
545
678
|
const updated: Execution<"executing"> = { ...(execution as Execution<"executing">), outcomes };
|
|
546
|
-
|
|
547
|
-
|
|
679
|
+
const reconciled = unresolved ? this.leaveUnresolved(updated, unresolved) : this.close(updated, payments.length);
|
|
680
|
+
if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
|
|
681
|
+
return reconciled;
|
|
548
682
|
}
|
|
549
683
|
|
|
550
684
|
/**
|
|
@@ -559,7 +693,7 @@ export class ExecutionEngine {
|
|
|
559
693
|
const outbox = this.deps.store.getOutbox(execution.idempotency_key);
|
|
560
694
|
if (outbox?.status !== "pending") return this.reconcile(executionId);
|
|
561
695
|
this.record("rail.resume", execution.id, { detail: "outbox pending: nothing was ever sent; dispatching once under the same attempt ids" });
|
|
562
|
-
return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution));
|
|
696
|
+
return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution, this.approvalOf(execution)));
|
|
563
697
|
}
|
|
564
698
|
|
|
565
699
|
/** Time-based exits: an open execution past its approval TTL closes as `expired`. */
|
|
@@ -770,21 +904,44 @@ export class ExecutionEngine {
|
|
|
770
904
|
};
|
|
771
905
|
}
|
|
772
906
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
907
|
+
/**
|
|
908
|
+
* The attempts of an execution. Each carries the quote of the SAME line of
|
|
909
|
+
* the approval artifact — what was approved, not what the execution says
|
|
910
|
+
* now — so a line that drifted after approval is a quote that disagrees,
|
|
911
|
+
* refused before the call. Each also carries the artifact's hashes, which
|
|
912
|
+
* the API seals into the receipt (OPEN_QUESTIONS §3): the artifact is fixed
|
|
913
|
+
* per execution, so a reconcile presents the same approval the first
|
|
914
|
+
* dispatch did, and a batch line's hashes are the same on every machine.
|
|
915
|
+
*/
|
|
916
|
+
private paymentsFor(execution: Execution, artifact: ApprovalArtifact): RailPayment[] {
|
|
917
|
+
return execution.items.map((item, index) => {
|
|
918
|
+
// A batch line is the same attempt, with the same quote, on every run of the same list, wherever it runs (§39c); anything else is its execution's own.
|
|
919
|
+
const quote = quoteFromApproval(artifact, index, this.deps.mandate.purpose, { stable: execution.batch !== undefined });
|
|
920
|
+
return {
|
|
921
|
+
attempt_id: execution.batch
|
|
922
|
+
? batchAttemptId(execution.mandate.id, execution.batch, index, execution.attempt_generations?.[index] ?? 0)
|
|
923
|
+
: `att_${execution.idempotency_key.slice(4)}_${index}`,
|
|
924
|
+
mandate_id: execution.mandate.id,
|
|
925
|
+
amount_minor: item.amount,
|
|
926
|
+
currency: item.currency,
|
|
927
|
+
payee: item.payee,
|
|
928
|
+
beneficiary: item.beneficiary,
|
|
929
|
+
purpose: this.deps.mandate.purpose,
|
|
930
|
+
agent_id: this.deps.mandate.agent_id,
|
|
931
|
+
consumer_id: this.deps.mandate.consumer_id,
|
|
932
|
+
...(item.description ? { description: item.description } : {}),
|
|
933
|
+
...(item.due_date ? { due_date: item.due_date } : {}),
|
|
934
|
+
...(quote ? { quote } : {}),
|
|
935
|
+
approval: spendApprovalOf(artifact),
|
|
936
|
+
actor: this.agentActor,
|
|
937
|
+
};
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private approvalOf(execution: Execution): ApprovalArtifact {
|
|
942
|
+
const artifact = execution.approval_id ? this.deps.store.getApproval(execution.approval_id) : undefined;
|
|
943
|
+
if (!artifact) throw new Error(`execution ${execution.id} is ${execution.state} without an approval artifact`);
|
|
944
|
+
return artifact;
|
|
788
945
|
}
|
|
789
946
|
|
|
790
947
|
private withoutApproval(execution: Execution<"approved">): Execution<"approved"> {
|
|
@@ -827,7 +984,7 @@ export class ExecutionEngine {
|
|
|
827
984
|
private storeApproval(artifact: ApprovalArtifact): void {
|
|
828
985
|
this.deps.store.saveApproval(artifact);
|
|
829
986
|
this.deps.bundle.approval(artifact);
|
|
830
|
-
this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, escalation: artifact.escalation ?? null });
|
|
987
|
+
this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, ...(artifact.composition ? { composition: artifact.composition } : {}), escalation: artifact.escalation ?? null });
|
|
831
988
|
}
|
|
832
989
|
|
|
833
990
|
private persist<S extends ExecutionState>(execution: Execution<S>): Execution<S> {
|
|
@@ -868,6 +1025,13 @@ function assertBatchBinding(batch: ExecutionBatch): void {
|
|
|
868
1025
|
}
|
|
869
1026
|
}
|
|
870
1027
|
|
|
1028
|
+
/** A composition binding is the kit's to compute and the core's to refuse when it cannot be true. */
|
|
1029
|
+
function assertCompositionBinding(composition: ExecutionComposition): void {
|
|
1030
|
+
if (!composition.ref.trim()) throw new Error("composition.ref must name what was composed");
|
|
1031
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(composition.composition_hash)) throw new Error("composition.composition_hash must be the compositionHash of the resolved lines");
|
|
1032
|
+
if (!Number.isInteger(composition.line_count) || composition.line_count < 1) throw new Error(`composition.line_count must be a positive integer, got ${composition.line_count}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
871
1035
|
/**
|
|
872
1036
|
* What the rail answered about one attempt, as the bundle records it: the
|
|
873
1037
|
* identifiers a reader follows the money by, and never `raw`, which carries
|
|
@@ -878,8 +1042,33 @@ function railAnswer(outcome: RailOutcome): Record<string, unknown> {
|
|
|
878
1042
|
case "accepted":
|
|
879
1043
|
return { transaction_id: outcome.transaction_id, sandbox: outcome.sandbox };
|
|
880
1044
|
case "settled":
|
|
881
|
-
return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox };
|
|
1045
|
+
return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox, ...(outcome.replayed ? { idempotent_replay: true } : {}) };
|
|
1046
|
+
case "failed":
|
|
1047
|
+
return { code: outcome.code, message: outcome.message, ...(outcome.spent ? { spent: true } : {}), ...(outcome.held ? { held: outcome.held } : {}) };
|
|
882
1048
|
default:
|
|
883
1049
|
return { code: outcome.code, message: outcome.message };
|
|
884
1050
|
}
|
|
885
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
/** The approval link a receipt sealed is the one a spend sent: the same hashes, in either spelling of the `sha256:` prefix. */
|
|
1054
|
+
function sealsApproval(sealed: SealedSpendApproval | null, sent: SpendApproval): boolean {
|
|
1055
|
+
return sealed !== null && sameApprovalHash(sealed.items_hash, sent.items_hash) && sameApprovalHash(sealed.batch_hash, sent.batch_hash ?? null);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* A payment settled and the receipt CodeSpar sealed for it names a different
|
|
1060
|
+
* payee, or none, or seals another approval than the one the spend sent.
|
|
1061
|
+
* Raised after the execution's outcome is saved, never instead of it: the
|
|
1062
|
+
* rail's answer is the record of where the money went, and this error is
|
|
1063
|
+
* about the evidence. Not a warning, because a receipt that does not name
|
|
1064
|
+
* what was paid proves nothing about that payment.
|
|
1065
|
+
*/
|
|
1066
|
+
export class ReceiptSealMismatchError extends Error {
|
|
1067
|
+
constructor(
|
|
1068
|
+
readonly executionId: string,
|
|
1069
|
+
readonly mismatches: string[],
|
|
1070
|
+
) {
|
|
1071
|
+
super(`execution ${executionId}: the sealed receipt does not seal what was paid — ${mismatches.join("; ")}`);
|
|
1072
|
+
this.name = "ReceiptSealMismatchError";
|
|
1073
|
+
}
|
|
1074
|
+
}
|