@codespar/cli 0.16.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.
- package/README.md +1 -0
- package/dist/commands/mandate-verify.js +98 -19
- package/dist/commands/mandate-verify.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mandate-codec.js +31 -20
- package/dist/mandate-codec.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
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import type { ExecutionItem } from "./types.js";
|
|
2
|
+
import type { CompositionLine, ExecutionBatch, ExecutionItem } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/** JSON with keys sorted at every level, so the same value always hashes the same. */
|
|
5
5
|
export function canonicalJson(value: unknown): string {
|
|
@@ -52,6 +52,62 @@ export function batchHash(lines: readonly ExecutionItem[]): string {
|
|
|
52
52
|
return itemsHash(lines);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The hash of what an execution's amount is composed of (a cart's resolved
|
|
57
|
+
* lines, in order). The same canonicalisation as `itemsHash` — sorted-key
|
|
58
|
+
* JSON, SHA-256, the `sha256:` prefix — over the fields that decide a
|
|
59
|
+
* composition, and deliberately not `itemsHash` itself: its fields are the
|
|
60
|
+
* ones that decide where money goes (payee, amount, currency, due date), and
|
|
61
|
+
* none of them sees a quantity or a unit price. Ten units at a 10% discount
|
|
62
|
+
* and nine at list price are the same line amount; they are not the same
|
|
63
|
+
* sale. Order is part of it, like a batch.
|
|
64
|
+
*/
|
|
65
|
+
export function compositionHash(lines: readonly CompositionLine[]): string {
|
|
66
|
+
const canonical = lines.map((line) => ({
|
|
67
|
+
ref: line.ref,
|
|
68
|
+
quantity: line.quantity,
|
|
69
|
+
unit_amount: line.unit_amount,
|
|
70
|
+
amount: line.amount,
|
|
71
|
+
currency: line.currency,
|
|
72
|
+
}));
|
|
73
|
+
return `sha256:${sha256Hex(canonicalJson(canonical))}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The `attempt_id` of an item of a batch line, derived from WHAT is being
|
|
78
|
+
* paid rather than from the execution that happens to pay it:
|
|
79
|
+
*
|
|
80
|
+
* ska_ + hex(sha256(mandate_id | batch_hash | line index | item index))
|
|
81
|
+
*
|
|
82
|
+
* An execution's own attempt ids come from its `idempotency_key`, which comes
|
|
83
|
+
* from a random execution id, so a second run of the same batch on a machine
|
|
84
|
+
* that has lost `.codespar/state.db` (or never had it) would present fresh ids
|
|
85
|
+
* and the API would take each line as a new payment (OPEN_QUESTIONS §39c).
|
|
86
|
+
* Derived this way, the same line of the same list under the same mandate is
|
|
87
|
+
* the same attempt on any machine, and the API answers a repeat of it from its
|
|
88
|
+
* record of that attempt (ent#1671): the original body when it settled, a 409
|
|
89
|
+
* while it is in flight or pinned, never a second dispatch.
|
|
90
|
+
*
|
|
91
|
+
* Every input decides the payment: the mandate is whose money, the hash is the
|
|
92
|
+
* exact ordered list (payee, amount, currency of every line), the index is
|
|
93
|
+
* which line of it. A list that changes in any line is a different hash and so
|
|
94
|
+
* different ids. `ska_` names the namespace (starter-kit attempt) and 68
|
|
95
|
+
* characters sit inside the API's 128.
|
|
96
|
+
*
|
|
97
|
+
* `generation` exists because the API never runs an attempt id twice: once an
|
|
98
|
+
* attempt FAILED and moved no money, a repeat of it answers
|
|
99
|
+
* `psp_attempt_conflict` for good. A line whose payment the provider refused
|
|
100
|
+
* must still be payable by a later run of the same list, so it moves to the
|
|
101
|
+
* next generation, and only on that answer: the server saying the id is
|
|
102
|
+
* spent is the one fact every machine reads the same way, so every machine
|
|
103
|
+
* walks the same sequence and none of them finds a "free" id past one that
|
|
104
|
+
* paid. Generation 0 hashes exactly as above.
|
|
105
|
+
*/
|
|
106
|
+
export function batchAttemptId(mandateId: string, batch: Pick<ExecutionBatch, "batch_hash" | "index">, item: number, generation = 0): string {
|
|
107
|
+
const base = `${mandateId}|${batch.batch_hash}|${batch.index}|${item}`;
|
|
108
|
+
return `ska_${sha256Hex(generation === 0 ? base : `${base}|${generation}`)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
55
111
|
export function hmacSha256Hex(key: Buffer, payload: string): string {
|
|
56
112
|
return createHmac("sha256", key).update(payload).digest("hex");
|
|
57
113
|
}
|
|
@@ -12,7 +12,9 @@ export * from "./clock.js";
|
|
|
12
12
|
export * from "./secrets.js";
|
|
13
13
|
export * from "./revocation.js";
|
|
14
14
|
export * from "./rail.js";
|
|
15
|
+
export * from "./quote.js";
|
|
15
16
|
export * from "./receipt-verification.js";
|
|
17
|
+
export * from "./receipt-chain.js";
|
|
16
18
|
export * from "./tools.js";
|
|
17
19
|
export * from "./bundle.js";
|
|
18
20
|
export * from "./engine.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The offer a spend presents to the API (`SpendQuote`): seller, resource,
|
|
3
|
+
* price and payee. When a spend carries one, the API binds it into the
|
|
4
|
+
* receipt's chain between the mandate and the payment, so the sealed receipt
|
|
5
|
+
* names the payee; without one the chain carries no payee at all, and
|
|
6
|
+
* `GET /v1/consumers/receipts/{id}` answers `quote: null` (OPEN_QUESTIONS §18).
|
|
7
|
+
*
|
|
8
|
+
* The quote is built from the approval artifact's items, the list its
|
|
9
|
+
* `items_hash` covers, and from nothing the model says after approval. The
|
|
10
|
+
* API compares the quote to what settled in OBSERVE mode only: a divergence is
|
|
11
|
+
* recorded on the receipt and the money still moves. So the kit refuses a
|
|
12
|
+
* divergence itself, before the call.
|
|
13
|
+
*/
|
|
14
|
+
import type { ApprovalArtifact } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export interface SpendQuote {
|
|
17
|
+
seller: string;
|
|
18
|
+
resource: string;
|
|
19
|
+
price_minor: number;
|
|
20
|
+
payee: string;
|
|
21
|
+
/**
|
|
22
|
+
* When the offer was approved: the artifact's `approved_at`. Absent on a
|
|
23
|
+
* batch line, whose quote must be the same on every run of the list: the
|
|
24
|
+
* API compares the quote's digest on a repeat of an `attempt_id`, so an
|
|
25
|
+
* approval time in it would make the same line a different payment on
|
|
26
|
+
* every machine (OPEN_QUESTIONS §39c). The time is still in the artifact.
|
|
27
|
+
*/
|
|
28
|
+
at?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The quote for line `index` of what was approved, or undefined when the
|
|
33
|
+
* artifact has no such line. `stable` leaves the approval time out, for a
|
|
34
|
+
* batch line (see `SpendQuote.at`).
|
|
35
|
+
*/
|
|
36
|
+
export function quoteFromApproval(artifact: ApprovalArtifact, index: number, purpose: string, options: { stable?: boolean } = {}): SpendQuote | undefined {
|
|
37
|
+
const item = artifact.items[index];
|
|
38
|
+
if (!item) return undefined;
|
|
39
|
+
const quote = { seller: item.beneficiary, resource: item.description ?? purpose, price_minor: item.amount, payee: item.payee };
|
|
40
|
+
return options.stable ? quote : { ...quote, at: artifact.approved_at };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The quote a spend presents, or why it may not go out: none, or one that does not name exactly the amount and the payee it pays. */
|
|
44
|
+
export function checkQuote(payment: { amount_minor: number; payee: string; quote?: SpendQuote | undefined }): { ok: true; quote: SpendQuote } | { ok: false; code: "quote_missing" | "quote_mismatch"; detail: string } {
|
|
45
|
+
const { quote } = payment;
|
|
46
|
+
if (!quote) return { ok: false, code: "quote_missing", detail: "a spend carries the quote of what was approved, or it does not go out" };
|
|
47
|
+
if (quote.price_minor !== payment.amount_minor) return { ok: false, code: "quote_mismatch", detail: `the approved price ${quote.price_minor} is not the amount ${payment.amount_minor} about to be spent` };
|
|
48
|
+
if (quote.payee !== payment.payee) return { ok: false, code: "quote_mismatch", detail: "the approved payee is not the payee about to be paid" };
|
|
49
|
+
return { ok: true, quote };
|
|
50
|
+
}
|