@codespar/cli 0.14.0 → 0.16.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 +44 -17
- package/dist/commands/audit-replay.js +267 -0
- package/dist/commands/audit-replay.js.map +1 -0
- package/dist/commands/init.js +28 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.js +2 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mandate-verify.js +75 -27
- package/dist/commands/mandate-verify.js.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/did.js +262 -41
- package/dist/did.js.map +1 -1
- package/dist/index.js +33 -7
- package/dist/index.js.map +1 -1
- package/dist/surface.js +4 -1
- package/dist/surface.js.map +1 -1
- package/package.json +1 -1
- package/templates/bills-agent/README.md +8 -5
- package/templates/bills-agent/agents/bills-agent/.env.example +3 -3
- 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 +62 -22
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +1 -1
- package/templates/bills-agent/agents/bills-agent/agent.yaml +2 -2
- package/templates/bills-agent/agents/bills-agent/package.json +14 -11
- package/templates/bills-agent/agents/bills-agent/src/kit.ts +193 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +8 -7
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +19 -2
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +72 -24
- package/templates/bills-agent/agents/bills-agent/test/fixtures/excursao-300.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/test/fixtures/material-400.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/test/inspect.test.ts +124 -0
- package/templates/bills-agent/agents/bills-agent/test/provider.test.ts +25 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +10 -9
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +0 -1
- package/templates/bills-agent/package.json +5 -2
- package/templates/bills-agent/packages/agent-core/README.md +2 -0
- package/templates/bills-agent/packages/agent-core/package.json +1 -1
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +27 -6
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +16 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +20 -7
- package/templates/bills-agent/packages/agent-core/src/approval.ts +16 -2
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +50 -4
- package/templates/bills-agent/packages/agent-core/src/channels.ts +140 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +90 -8
- package/templates/bills-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +119 -18
- package/templates/bills-agent/packages/agent-core/src/hash.ts +13 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +3 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +1 -1
- package/templates/bills-agent/packages/agent-core/src/rail.ts +16 -2
- package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +401 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +3 -1
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +2 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +19 -2
- package/templates/bills-agent/packages/agent-core/src/types.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +36 -2
- package/templates/bills-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/bills-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +73 -1
- package/templates/bills-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/bills-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +1 -1
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +2 -2
- package/templates/bills-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +51 -0
- package/templates/bills-agent/packages/agent-runtime/README.md +68 -0
- package/templates/bills-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/bills-agent/packages/agent-runtime/package.json +23 -0
- package/templates/bills-agent/{agents/bills-agent → packages/agent-runtime}/src/adversarial.ts +36 -44
- package/templates/bills-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +124 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +358 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +206 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +225 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +143 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +113 -0
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +113 -0
- package/templates/bills-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +317 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +144 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +43 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +131 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +245 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +129 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +39 -0
- package/templates/bills-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/bills-agent/packages/agent-runtime/src/index.ts +12 -0
- package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +797 -0
- package/templates/bills-agent/packages/agent-runtime/src/kit.ts +206 -0
- package/templates/{collections-agent/agents/collections-agent → bills-agent/packages/agent-runtime}/src/poll.ts +22 -11
- package/templates/{collections-agent/agents/collections-agent → bills-agent/packages/agent-runtime}/src/scenarios.ts +50 -34
- package/templates/bills-agent/{agents/bills-agent → packages/agent-runtime}/src/setup.ts +88 -66
- package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +154 -0
- package/templates/{collections-agent/agents/collections-agent/channels/webhook/index.ts → bills-agent/packages/agent-runtime/src/webhook.ts} +1 -1
- package/templates/bills-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +127 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +232 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +204 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +135 -0
- package/templates/bills-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/bills-agent/vitest.config.ts +1 -1
- package/templates/collections-agent/README.md +8 -5
- package/templates/collections-agent/agents/collections-agent/.env.example +14 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +6 -4
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +6 -4
- package/templates/collections-agent/agents/collections-agent/README.md +184 -16
- package/templates/collections-agent/agents/collections-agent/agent.yaml +3 -3
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +111 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042.json +11 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1103.json +11 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +23 -0
- package/templates/collections-agent/agents/collections-agent/package.json +12 -10
- package/templates/collections-agent/agents/collections-agent/runbook.md +52 -1
- package/templates/collections-agent/agents/collections-agent/src/kit.ts +265 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +9 -8
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +43 -1
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +56 -18
- package/templates/collections-agent/agents/collections-agent/test/fixtures/agreed-1042-awaiting-payer.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +11 -10
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +4 -7
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +271 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +213 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +0 -1
- package/templates/collections-agent/package.json +3 -1
- package/templates/collections-agent/packages/agent-core/README.md +2 -0
- package/templates/collections-agent/packages/agent-core/package.json +1 -1
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +27 -6
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +16 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +20 -7
- package/templates/collections-agent/packages/agent-core/src/approval.ts +16 -2
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +50 -4
- package/templates/collections-agent/packages/agent-core/src/channels.ts +140 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +90 -8
- package/templates/collections-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +119 -18
- package/templates/collections-agent/packages/agent-core/src/hash.ts +13 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +3 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +1 -1
- package/templates/collections-agent/packages/agent-core/src/rail.ts +16 -2
- package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +401 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +3 -1
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +2 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +19 -2
- package/templates/collections-agent/packages/agent-core/src/types.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +36 -2
- package/templates/collections-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/collections-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +73 -1
- package/templates/collections-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/collections-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +1 -1
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +2 -2
- package/templates/collections-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +51 -0
- package/templates/collections-agent/packages/agent-runtime/README.md +68 -0
- package/templates/collections-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/collections-agent/packages/agent-runtime/package.json +23 -0
- package/templates/collections-agent/{agents/collections-agent → packages/agent-runtime}/src/adversarial.ts +29 -50
- package/templates/collections-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +124 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +358 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +206 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +225 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +143 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +113 -0
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +113 -0
- package/templates/collections-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +317 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +144 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +43 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +131 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +245 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +129 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +39 -0
- package/templates/collections-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/collections-agent/packages/agent-runtime/src/index.ts +12 -0
- package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +797 -0
- package/templates/collections-agent/packages/agent-runtime/src/kit.ts +206 -0
- package/templates/collections-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/{bills-agent/agents/bills-agent → collections-agent/packages/agent-runtime}/src/scenarios.ts +98 -26
- package/templates/collections-agent/packages/agent-runtime/src/setup.ts +229 -0
- package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +154 -0
- package/templates/collections-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/collections-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +127 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +232 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +204 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +135 -0
- package/templates/collections-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/collections-agent/vitest.config.ts +1 -1
- package/templates/hello-agent/LICENSE +21 -0
- package/templates/hello-agent/README.md +23 -0
- package/templates/hello-agent/_gitignore +10 -0
- package/templates/hello-agent/agents/hello-agent/.env.example +5 -0
- package/templates/hello-agent/agents/hello-agent/AGENTS.md +15 -0
- package/templates/hello-agent/agents/hello-agent/CLAUDE.md +15 -0
- package/templates/hello-agent/agents/hello-agent/README.md +33 -0
- package/templates/hello-agent/agents/hello-agent/SYSTEM_PROMPT.md +19 -0
- package/templates/hello-agent/agents/hello-agent/agent.yaml +19 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/beneficiary-swap.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/exfiltration.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/exfiltration.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/false-authority.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/fractioning.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/fractioning.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/model-total.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/prompt-injection.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/evals/adversarial/webhook-replay.json +6 -0
- package/templates/hello-agent/agents/hello-agent/evals/eval.yaml +7 -0
- package/templates/hello-agent/agents/hello-agent/guardrails.json +8 -0
- package/templates/hello-agent/agents/hello-agent/mandate.example.json +17 -0
- package/templates/hello-agent/agents/hello-agent/package.json +19 -0
- package/templates/hello-agent/agents/hello-agent/runbook.md +10 -0
- package/templates/hello-agent/agents/hello-agent/scenarios/happy-path.json +6 -0
- package/templates/hello-agent/agents/hello-agent/scenarios/happy-path.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/scenarios/prompt-injection.json +6 -0
- package/templates/hello-agent/agents/hello-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/hello-agent/agents/hello-agent/src/bills.ts +23 -0
- package/templates/hello-agent/agents/hello-agent/src/kit.ts +33 -0
- package/templates/hello-agent/agents/hello-agent/tools.json +11 -0
- package/templates/hello-agent/agents/hello-agent/tsconfig.json +9 -0
- package/templates/hello-agent/package.json +30 -0
- package/templates/hello-agent/packages/agent-core/README.md +21 -0
- package/templates/hello-agent/packages/agent-core/package.json +24 -0
- package/templates/hello-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +175 -0
- package/templates/hello-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/hello-agent/packages/agent-core/src/api/rail.ts +117 -0
- package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +58 -0
- package/templates/hello-agent/packages/agent-core/src/approval.ts +123 -0
- package/templates/hello-agent/packages/agent-core/src/bundle.ts +159 -0
- package/templates/hello-agent/packages/agent-core/src/channels.ts +140 -0
- package/templates/hello-agent/packages/agent-core/src/check.ts +253 -0
- package/templates/hello-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/hello-agent/packages/agent-core/src/engine.ts +885 -0
- package/templates/hello-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/hello-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/hello-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/hello-agent/packages/agent-core/src/hash.ts +62 -0
- package/templates/hello-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/hello-agent/packages/agent-core/src/index.ts +32 -0
- package/templates/hello-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/hello-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/hello-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/hello-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/hello-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/hello-agent/packages/agent-core/src/rail.ts +101 -0
- package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +401 -0
- package/templates/hello-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/hello-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/hello-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/hello-agent/packages/agent-core/src/state-machine.ts +148 -0
- package/templates/hello-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
- package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +141 -0
- package/templates/hello-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/hello-agent/packages/agent-core/src/types.ts +134 -0
- package/templates/hello-agent/packages/agent-core/test/approval.test.ts +117 -0
- package/templates/hello-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/hello-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/hello-agent/packages/agent-core/test/engine.test.ts +596 -0
- package/templates/hello-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/hello-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/hello-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/hello-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/hello-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +316 -0
- package/templates/hello-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/hello-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/hello-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/hello-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/hello-agent/packages/agent-runtime/README.md +68 -0
- package/templates/hello-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/hello-agent/packages/agent-runtime/package.json +23 -0
- package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +169 -0
- package/templates/hello-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +124 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +358 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +206 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +225 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +143 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +113 -0
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +113 -0
- package/templates/hello-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +317 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +144 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +43 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +131 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +245 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +129 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +39 -0
- package/templates/hello-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/hello-agent/packages/agent-runtime/src/index.ts +12 -0
- package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +797 -0
- package/templates/hello-agent/packages/agent-runtime/src/kit.ts +206 -0
- package/templates/hello-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +268 -0
- package/templates/hello-agent/packages/agent-runtime/src/setup.ts +229 -0
- package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +154 -0
- package/templates/hello-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/hello-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +127 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +232 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +204 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +135 -0
- package/templates/hello-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/hello-agent/tsconfig.base.json +16 -0
- package/templates/hello-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +60 -12
- package/templates/supplier-payments-agent/LICENSE +21 -0
- package/templates/supplier-payments-agent/README.md +23 -0
- package/templates/supplier-payments-agent/_gitignore +10 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/.env.example +7 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +21 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +21 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +118 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/SYSTEM_PROMPT.md +29 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/agent.yaml +26 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/batch-tampering.json +12 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/batch-tampering.transcript.jsonl +3 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/model-total.json +10 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/evals/eval.yaml +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/guardrails.json +15 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/mandate.example.json +70 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/package.json +26 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/runbook.md +19 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/happy-path.json +19 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/partial-batch-failure.json +21 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/partial-batch-failure.transcript.jsonl +3 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/prompt-injection.json +12 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +176 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +345 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/pix-out.ts +118 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/payables.ts +76 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/adversarial.test.ts +53 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +384 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/check.test.ts +109 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/scenarios.test.ts +81 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/tools.json +53 -0
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/tsconfig.json +9 -0
- package/templates/supplier-payments-agent/package.json +36 -0
- package/templates/supplier-payments-agent/packages/agent-core/README.md +21 -0
- package/templates/supplier-payments-agent/packages/agent-core/package.json +24 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +175 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +117 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +58 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +123 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +159 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +140 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +253 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +885 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +62 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +32 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +101 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +401 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +148 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +141 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +134 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/approval.test.ts +117 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/engine.test.ts +596 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +316 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/supplier-payments-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/README.md +68 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/package.json +23 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +169 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +124 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +358 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +206 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +225 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +143 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +113 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +113 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +317 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +144 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +43 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +131 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +245 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +129 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +39 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +12 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +797 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +206 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +268 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +229 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +154 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +127 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +232 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +204 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +135 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/supplier-payments-agent/tsconfig.base.json +16 -0
- package/templates/supplier-payments-agent/vitest.config.ts +12 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +0 -108
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +0 -9
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +0 -13
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +0 -26
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +0 -45
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +0 -7
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +0 -37
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +0 -38
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +0 -65
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +0 -30
- package/templates/bills-agent/agents/bills-agent/src/main.ts +0 -185
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +0 -178
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +0 -10
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +0 -13
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +0 -48
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +0 -7
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +0 -39
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +0 -35
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +0 -38
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +0 -55
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +0 -30
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +0 -36
- package/templates/collections-agent/agents/collections-agent/src/main.ts +0 -193
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +0 -250
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.6: the model proposes, the code executes. The model's only way
|
|
3
|
+
* in is `draft()`, which creates an execution in `drafted` and nothing else.
|
|
4
|
+
* Every check that decides whether money moves lives here, in
|
|
5
|
+
* deterministic code: mandate status, allowlist, per-transaction and window
|
|
6
|
+
* caps, `escalate_above`, the approval artifact and its `items_hash`.
|
|
7
|
+
*
|
|
8
|
+
* The policy runs THREE times, not once: at draft, when a human approves,
|
|
9
|
+
* and immediately before `executing`, inside the same transaction that
|
|
10
|
+
* writes the outbox row. A list that changed after approval, a window that
|
|
11
|
+
* filled up between approval and execution, or a mandate revoked in the
|
|
12
|
+
* meantime is caught at the last gate, which is the only one that matters.
|
|
13
|
+
*/
|
|
14
|
+
import { checkApprovalArtifact, createApprovalArtifact, type ApprovalSigner } from "./approval.js";
|
|
15
|
+
import type { ProofBundle } from "./bundle.js";
|
|
16
|
+
import { evaluateEscalation, type Escalation } from "./escalate.js";
|
|
17
|
+
import { CHARGE_CANCELLED, CHARGE_EXPIRED, CHARGE_PAID, PAYMENT_FAILED, PAYMENT_SUCCEEDED, type PublishedEvent } from "./events.js";
|
|
18
|
+
import type { Guardrails } from "./guardrails.js";
|
|
19
|
+
import { itemsHash, sha256Hex } from "./hash.js";
|
|
20
|
+
import { newId } from "./ids.js";
|
|
21
|
+
import { mandateExpired, payeeAllowed, resolveBeneficiary, windowCap, windowStart, type Mandate } from "./mandate.js";
|
|
22
|
+
import type { Manifest } from "./manifest.js";
|
|
23
|
+
import type { PaymentRail, RailOutcome, RailPayment } from "./rail.js";
|
|
24
|
+
import type { MandateStatusReport, MandateStatusSource } from "./revocation.js";
|
|
25
|
+
import { isTerminal, transition, type Execution, type ExecutionState } from "./state-machine.js";
|
|
26
|
+
import type { StateStore } from "./state/store.js";
|
|
27
|
+
import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
|
|
28
|
+
|
|
29
|
+
export interface ProposedItem {
|
|
30
|
+
/** An alias from the mandate's named payees, or a raw payee key. */
|
|
31
|
+
payee: string;
|
|
32
|
+
amount: number;
|
|
33
|
+
description?: string;
|
|
34
|
+
/** A receivable's due date, `YYYY-MM-DD`. */
|
|
35
|
+
due_date?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PolicyExtensionContext {
|
|
39
|
+
gate: "draft" | "approve" | "execute";
|
|
40
|
+
now: Date;
|
|
41
|
+
mode: ApprovalMode;
|
|
42
|
+
mandate: Mandate;
|
|
43
|
+
guardrails: Guardrails;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The kit's own deterministic check, run by the core at every gate after
|
|
48
|
+
* the mandate's (allowlist, caps, window). It can only refuse; it cannot
|
|
49
|
+
* widen anything. A collections agent uses it for its negotiation envelope.
|
|
50
|
+
*/
|
|
51
|
+
export type PolicyExtension = (execution: Execution, ctx: PolicyExtensionContext) => { reason: ExecutionReason; detail: string } | undefined;
|
|
52
|
+
|
|
53
|
+
export interface Proposal {
|
|
54
|
+
items: ProposedItem[];
|
|
55
|
+
/** What the model said the total was. Recorded; never used to pay. */
|
|
56
|
+
claimed_total?: number;
|
|
57
|
+
/**
|
|
58
|
+
* The batch this proposal is one line of, when it is one. The kit computes
|
|
59
|
+
* it once over the whole ordered list before drafting any of it; the core
|
|
60
|
+
* stamps it on the execution and every artifact of that execution carries
|
|
61
|
+
* it. The core never invents one and never fills one in.
|
|
62
|
+
*/
|
|
63
|
+
batch?: ExecutionBatch;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type DraftResult =
|
|
67
|
+
| { ok: true; execution: Execution }
|
|
68
|
+
| { ok: false; refused_before_draft: true; reason: ExecutionReason; message: string };
|
|
69
|
+
|
|
70
|
+
export interface EngineDeps {
|
|
71
|
+
store: StateStore;
|
|
72
|
+
rail: PaymentRail;
|
|
73
|
+
status: MandateStatusSource;
|
|
74
|
+
signer: ApprovalSigner;
|
|
75
|
+
manifest: Manifest;
|
|
76
|
+
guardrails: Guardrails;
|
|
77
|
+
mandate: Mandate;
|
|
78
|
+
bundle: ProofBundle;
|
|
79
|
+
mode: ApprovalMode;
|
|
80
|
+
runId: string;
|
|
81
|
+
/** The person the agent acts for. */
|
|
82
|
+
onBehalfOf: string;
|
|
83
|
+
clock?: () => Date;
|
|
84
|
+
/** The kit's own envelope check, run at every gate after the mandate's. */
|
|
85
|
+
policyExtension?: PolicyExtension | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What the deterministic policy says about an execution at one of its three gates. */
|
|
89
|
+
type Verdict =
|
|
90
|
+
| { kind: "ok" }
|
|
91
|
+
| { kind: "deny"; reason: ExecutionReason; detail: string }
|
|
92
|
+
| { kind: "block"; reason: ExecutionReason; detail: string }
|
|
93
|
+
| { kind: "escalate"; escalation: Escalation };
|
|
94
|
+
|
|
95
|
+
type Gate = "draft" | "approve" | "execute";
|
|
96
|
+
|
|
97
|
+
/** Open executions reserve the window: a cap is a ceiling on what may be committed, not a balance. */
|
|
98
|
+
const WINDOW_STATES: ExecutionState[] = ["awaiting_approval", "approved", "executing", "settled"];
|
|
99
|
+
|
|
100
|
+
export class ExecutionEngine {
|
|
101
|
+
readonly clock: () => Date;
|
|
102
|
+
readonly agentActor: Actor;
|
|
103
|
+
|
|
104
|
+
constructor(private readonly deps: EngineDeps) {
|
|
105
|
+
this.clock = deps.clock ?? (() => new Date());
|
|
106
|
+
this.agentActor = { type: "agent", agent: `${deps.manifest.name}@${deps.manifest.version}`, on_behalf_of: deps.onBehalfOf };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get mode(): ApprovalMode {
|
|
110
|
+
return this.deps.mode;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get mandate(): Mandate {
|
|
114
|
+
return this.deps.mandate;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- the model's only entry point -------------------------------------
|
|
118
|
+
|
|
119
|
+
async draft(proposal: Proposal): Promise<DraftResult> {
|
|
120
|
+
const now = this.clock();
|
|
121
|
+
const gate = await this.mandateGate();
|
|
122
|
+
if (gate) {
|
|
123
|
+
this.record("execution.refused_before_draft", null, { reason: gate.reason, detail: gate.detail });
|
|
124
|
+
return { ok: false, refused_before_draft: true, reason: gate.reason, message: gate.detail };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const items = proposal.items.map((p) => this.resolveItem(p));
|
|
128
|
+
const total = items.reduce((sum, i) => sum + i.amount, 0);
|
|
129
|
+
if (proposal.batch) assertBatchBinding(proposal.batch);
|
|
130
|
+
const id = newId("exe");
|
|
131
|
+
const execution: Execution<"drafted"> = {
|
|
132
|
+
id,
|
|
133
|
+
run_id: this.deps.runId,
|
|
134
|
+
state: "drafted",
|
|
135
|
+
mode: this.deps.mode,
|
|
136
|
+
actor: this.agentActor,
|
|
137
|
+
items,
|
|
138
|
+
total,
|
|
139
|
+
currency: this.deps.mandate.currency,
|
|
140
|
+
...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
|
|
141
|
+
items_hash: itemsHash(items),
|
|
142
|
+
...(proposal.batch ? { batch: proposal.batch } : {}),
|
|
143
|
+
mandate: { id: this.deps.mandate.id, version: this.deps.mandate.version },
|
|
144
|
+
idempotency_key: `idk_${sha256Hex(`${this.deps.runId}:${id}`).slice(0, 32)}`,
|
|
145
|
+
blocking_reasons: [],
|
|
146
|
+
outcomes: [],
|
|
147
|
+
history: [],
|
|
148
|
+
created_at: now.toISOString(),
|
|
149
|
+
updated_at: now.toISOString(),
|
|
150
|
+
};
|
|
151
|
+
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 });
|
|
153
|
+
|
|
154
|
+
return { ok: true, execution: await this.evaluateDraft(execution) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- deterministic policy ---------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The one policy, run at every gate. `humanApproved` is true when a person
|
|
161
|
+
* already decided this execution: their decision satisfies `escalate_above`,
|
|
162
|
+
* and only that. Everything else — allowlist, caps, window — is the
|
|
163
|
+
* mandate's and no approval widens it.
|
|
164
|
+
*/
|
|
165
|
+
private policy(execution: Execution, now: Date, gate: Gate, humanApproved: boolean): Verdict {
|
|
166
|
+
const { mandate, guardrails, manifest } = this.deps;
|
|
167
|
+
|
|
168
|
+
if (gate === "draft" && execution.model_claimed_total !== undefined && execution.model_claimed_total !== execution.total) {
|
|
169
|
+
const detail = `the model stated ${execution.model_claimed_total}; the core computed ${execution.total} and that is the only number that counts`;
|
|
170
|
+
if (guardrails.model_total_mismatch === "refuse") return { kind: "deny", reason: "model_total_mismatch", detail };
|
|
171
|
+
this.record("execution.model_total_ignored", execution.id, { detail });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const blocked = execution.items.filter((i) => !payeeAllowed(mandate, i.payee));
|
|
175
|
+
if (blocked.length > 0) {
|
|
176
|
+
const detail = `payee not named in the signed allowlist: ${blocked.map((i) => i.beneficiary).join(", ")}`;
|
|
177
|
+
// At draft in `human` the person is told; at any later gate, or in `mandate`, it is a refusal. escalate_above never widens the allowlist.
|
|
178
|
+
if (gate === "draft" && this.deps.mode === "human") return { kind: "block", reason: "beneficiary_not_allowed", detail };
|
|
179
|
+
return { kind: "deny", reason: "beneficiary_not_allowed", detail };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const over = execution.items.find((i) => i.amount > mandate.per_tx_cap_minor);
|
|
183
|
+
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
|
+
|
|
185
|
+
const committed = this.committedInWindow(now, execution.id);
|
|
186
|
+
if (committed + execution.total > windowCap(mandate)) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "deny",
|
|
189
|
+
reason: "window_cap_exceeded",
|
|
190
|
+
detail: `${committed} already committed this ${mandate.periodic_cap?.window ?? "month"} (settled, in flight and awaiting a decision) plus ${execution.total} exceeds the cap ${windowCap(mandate)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The kit's envelope, at every gate: a person's yes does not widen it either.
|
|
195
|
+
const outside = this.deps.policyExtension?.(execution, { gate, now, mode: this.deps.mode, mandate, guardrails });
|
|
196
|
+
if (outside) return { kind: "deny", reason: outside.reason, detail: outside.detail };
|
|
197
|
+
|
|
198
|
+
if (this.deps.mode === "mandate" && !humanApproved) {
|
|
199
|
+
const escalation = evaluateEscalation(manifest.escalate_above, guardrails, execution.items, {
|
|
200
|
+
now,
|
|
201
|
+
timezone: guardrails.timezone,
|
|
202
|
+
knownPayees: this.knownPayees(),
|
|
203
|
+
recentByPayee: this.recentByPayee(now, execution.id),
|
|
204
|
+
});
|
|
205
|
+
if (escalation) {
|
|
206
|
+
if (escalation.trigger === "outside_hours" && guardrails.outside_hours_action === "refuse") return { kind: "deny", reason: "outside_hours", detail: escalation.detail };
|
|
207
|
+
return { kind: "escalate", escalation };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { kind: "ok" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async evaluateDraft(execution: Execution<"drafted">): Promise<Execution> {
|
|
215
|
+
const now = this.clock();
|
|
216
|
+
const at = now.toISOString();
|
|
217
|
+
const gate = await this.mandateGate();
|
|
218
|
+
if (gate) return this.persist(transition(execution, gate.to, { at, actor: this.agentActor, reason: gate.reason, detail: gate.detail }));
|
|
219
|
+
|
|
220
|
+
const verdict = this.policy(execution, now, "draft", false);
|
|
221
|
+
switch (verdict.kind) {
|
|
222
|
+
case "deny":
|
|
223
|
+
return this.persist(transition(execution, "denied", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
|
|
224
|
+
case "block":
|
|
225
|
+
return this.persist(transition({ ...execution, blocking_reasons: [verdict.reason] }, "awaiting_approval", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
|
|
226
|
+
case "escalate":
|
|
227
|
+
return this.persist(transition({ ...execution, escalation: verdict.escalation }, "awaiting_approval", { at, actor: this.agentActor, reason: "escalated", detail: `${verdict.escalation.trigger}: ${verdict.escalation.detail}` }));
|
|
228
|
+
case "ok":
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (this.deps.mode === "human") return this.persist(transition(execution, "awaiting_approval", { at, actor: this.agentActor }));
|
|
232
|
+
|
|
233
|
+
// The allowance covers it: the approver is the mandate, and the artifact says so.
|
|
234
|
+
const artifact = createApprovalArtifact(this.deps.signer, {
|
|
235
|
+
execution,
|
|
236
|
+
approver: { type: "mandate", id: this.deps.mandate.id },
|
|
237
|
+
actor: this.agentActor,
|
|
238
|
+
now,
|
|
239
|
+
ttlMs: this.deps.guardrails.approval_ttl_minutes * 60_000,
|
|
240
|
+
});
|
|
241
|
+
this.storeApproval(artifact);
|
|
242
|
+
return this.persist(transition({ ...execution, approval_id: artifact.approval_id }, "approved", { at, actor: this.agentActor, detail: "within the signed allowance" }));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- the human's entry points -----------------------------------------
|
|
246
|
+
|
|
247
|
+
approve(executionId: string, approver: { id: string; channel: string }): Execution {
|
|
248
|
+
const execution = this.mustGet(executionId);
|
|
249
|
+
if (execution.state !== "awaiting_approval") throw new Error(`execution ${executionId} is ${execution.state}, not awaiting_approval`);
|
|
250
|
+
const now = this.clock();
|
|
251
|
+
const at = now.toISOString();
|
|
252
|
+
const humanActor: Actor = { type: "human", id: approver.id, channel: approver.channel };
|
|
253
|
+
const awaiting = execution as Execution<"awaiting_approval">;
|
|
254
|
+
if (execution.blocking_reasons.length > 0) {
|
|
255
|
+
const reason = execution.blocking_reasons[0] as ExecutionReason;
|
|
256
|
+
return this.persist(transition(awaiting, "denied", { at, actor: humanActor, reason, detail: `a human said yes, but ${reason} cannot be approved: the mandate does not authorize it` }));
|
|
257
|
+
}
|
|
258
|
+
// A yes does not skip the policy: the list may have changed since it was presented (4.2), or the window may have filled.
|
|
259
|
+
const verdict = this.policy(execution, now, "approve", true);
|
|
260
|
+
if (verdict.kind === "deny") return this.persist(transition(awaiting, "denied", { at, actor: humanActor, reason: verdict.reason, detail: `a human said yes, but ${verdict.detail}` }));
|
|
261
|
+
|
|
262
|
+
const artifact = createApprovalArtifact(this.deps.signer, {
|
|
263
|
+
execution,
|
|
264
|
+
approver: { type: "person", id: approver.id, channel: approver.channel },
|
|
265
|
+
actor: humanActor,
|
|
266
|
+
now,
|
|
267
|
+
ttlMs: this.deps.guardrails.approval_ttl_minutes * 60_000,
|
|
268
|
+
...(execution.escalation ? { escalation: execution.escalation } : {}),
|
|
269
|
+
});
|
|
270
|
+
this.storeApproval(artifact);
|
|
271
|
+
return this.persist(transition({ ...awaiting, approval_id: artifact.approval_id }, "approved", { at, actor: humanActor }));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
deny(executionId: string, approver: { id: string; channel: string }, detail = "denied by the approver"): Execution {
|
|
275
|
+
const execution = this.mustGet(executionId);
|
|
276
|
+
if (execution.state !== "awaiting_approval") throw new Error(`execution ${executionId} is ${execution.state}, not awaiting_approval`);
|
|
277
|
+
const humanActor: Actor = { type: "human", id: approver.id, channel: approver.channel };
|
|
278
|
+
return this.persist(transition(execution as Execution<"awaiting_approval">, "denied", { at: this.clock().toISOString(), actor: humanActor, reason: "denied_by_approver", detail }));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- the only path to `executing` -------------------------------------
|
|
282
|
+
|
|
283
|
+
async execute(executionId: string): Promise<Execution> {
|
|
284
|
+
const execution = this.mustGet(executionId);
|
|
285
|
+
if (execution.state !== "approved") throw new Error(`execution ${executionId} is ${execution.state}, not approved`);
|
|
286
|
+
const now = this.clock();
|
|
287
|
+
const at = now.toISOString();
|
|
288
|
+
const approved = execution as Execution<"approved">;
|
|
289
|
+
|
|
290
|
+
// 4.7: the artifact may predate a revocation. Ask the source, every time.
|
|
291
|
+
const gate = await this.mandateGate();
|
|
292
|
+
if (gate) return this.persist(transition(approved, gate.to, { at, actor: this.agentActor, reason: gate.reason, detail: gate.detail }));
|
|
293
|
+
|
|
294
|
+
const artifact = execution.approval_id ? this.deps.store.getApproval(execution.approval_id) : undefined;
|
|
295
|
+
if (!artifact) throw new Error(`execution ${executionId} is approved without an approval artifact`);
|
|
296
|
+
|
|
297
|
+
// 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);
|
|
299
|
+
const decided = this.deps.store.transaction((): Execution => {
|
|
300
|
+
// 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
|
+
const current = { id: this.deps.mandate.id, version: this.deps.mandate.version };
|
|
302
|
+
if (execution.mandate.id !== current.id || execution.mandate.version !== current.version) {
|
|
303
|
+
return this.persist(transition({ ...this.withoutApproval(approved), mandate: current }, "awaiting_approval", { at, actor: this.agentActor, reason: "mandate_changed", detail: `approved under mandate ${execution.mandate.id} v${execution.mandate.version}; the mandate is now ${current.id} v${current.version}` }));
|
|
304
|
+
}
|
|
305
|
+
// 4.2: recompute the hash of what is about to be executed and compare.
|
|
306
|
+
const check = checkApprovalArtifact(this.deps.signer, artifact, execution, now);
|
|
307
|
+
if (!check.ok) {
|
|
308
|
+
if (check.problem === "expired") {
|
|
309
|
+
return this.persist(transition(approved, "expired", { at, actor: this.agentActor, reason: "approval_expired", detail: `approval ${artifact.approval_id} expired at ${artifact.expires_at}` }));
|
|
310
|
+
}
|
|
311
|
+
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
|
+
}
|
|
313
|
+
|
|
314
|
+
const verdict = this.policy(execution, now, "execute", artifact.approver.type === "person");
|
|
315
|
+
if (verdict.kind === "deny" || verdict.kind === "block") {
|
|
316
|
+
return this.persist(transition(approved, "denied", { at, actor: this.agentActor, reason: verdict.reason, detail: verdict.detail }));
|
|
317
|
+
}
|
|
318
|
+
if (verdict.kind === "escalate") {
|
|
319
|
+
return this.persist(transition({ ...this.withoutApproval(approved), escalation: verdict.escalation }, "awaiting_approval", { at, actor: this.agentActor, reason: "escalated", detail: `${verdict.escalation.trigger}: ${verdict.escalation.detail}` }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.deps.store.putOutbox({
|
|
323
|
+
idempotency_key: execution.idempotency_key,
|
|
324
|
+
execution_id: execution.id,
|
|
325
|
+
kind: "rail.pay",
|
|
326
|
+
payload: { attempts: payments.map((p) => p.attempt_id), items: execution.items },
|
|
327
|
+
status: "pending",
|
|
328
|
+
response: undefined,
|
|
329
|
+
created_at: at,
|
|
330
|
+
});
|
|
331
|
+
return this.persist(transition(approved, "executing", { at, actor: this.agentActor, detail: `idempotency_key ${execution.idempotency_key}` }));
|
|
332
|
+
});
|
|
333
|
+
if (decided.state !== "executing") return decided;
|
|
334
|
+
|
|
335
|
+
return this.dispatch(decided as Execution<"executing">, payments);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Sends the attempts of an `executing` execution to the rail. The outbox row
|
|
340
|
+
* flips to `sent` BEFORE the first call, so a crash after that point is
|
|
341
|
+
* reconciled and never re-sent.
|
|
342
|
+
*/
|
|
343
|
+
private async dispatch(execution: Execution<"executing">, payments: RailPayment[]): Promise<Execution> {
|
|
344
|
+
const outcomes: ItemOutcome[] = [...execution.outcomes];
|
|
345
|
+
const unknown: string[] = [];
|
|
346
|
+
this.deps.store.updateOutbox(execution.idempotency_key, "sent", undefined, this.clock().toISOString());
|
|
347
|
+
|
|
348
|
+
for (const [index, payment] of payments.entries()) {
|
|
349
|
+
if (outcomes.some((o) => o.index === index)) continue;
|
|
350
|
+
// 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
|
+
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
|
+
const outcome = await this.deps.rail.pay(payment);
|
|
353
|
+
this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
|
|
354
|
+
if (outcome.status === "uncertain") {
|
|
355
|
+
this.record("rail.uncertain", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, message: outcome.message });
|
|
356
|
+
// No outcome is recorded because there is none. The siblings are still
|
|
357
|
+
// sent: an unknown answer about THIS payee says nothing about the next
|
|
358
|
+
// one, and the execution stays open until reconciliation settles it.
|
|
359
|
+
unknown.push(`attempt ${payment.attempt_id}: ${outcome.code} — outcome unknown, kept for reconciliation`);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (outcome.status === "failed") {
|
|
363
|
+
outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}` });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (outcome.status === "accepted") {
|
|
367
|
+
// A receivable was issued; the payer decides the rest. Nothing settles here.
|
|
368
|
+
outcomes.push({ index, attempt_id: payment.attempt_id, status: "accepted", transaction_id: outcome.transaction_id, instrument: outcome.instrument });
|
|
369
|
+
this.recordInstrument(execution.id, payment.attempt_id, outcome.transaction_id, outcome.instrument);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
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);
|
|
374
|
+
}
|
|
375
|
+
return this.close({ ...execution, outcomes }, payments.length, unknown.join("; ") || undefined);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Closes an `executing` execution from its recorded outcomes: stays while a
|
|
380
|
+
* receivable is accepted and unpaid, stays while any attempt has no outcome
|
|
381
|
+
* at all, failed if any attempt failed, settled when every attempt settled.
|
|
382
|
+
*
|
|
383
|
+
* The order matters. An execution does not close while one of its attempts
|
|
384
|
+
* is unknown, even when another one failed: "some of this moved and one of
|
|
385
|
+
* them we cannot see" is not a closed outcome, and closing it as `failed`
|
|
386
|
+
* would report a batch as finished while a payment may still be in flight.
|
|
387
|
+
*/
|
|
388
|
+
private close(execution: Execution<"executing">, attempts: number, unknownDetail?: string): Execution {
|
|
389
|
+
const at = this.clock().toISOString();
|
|
390
|
+
const updated: Execution<"executing"> = { ...execution, updated_at: at };
|
|
391
|
+
const accepted = execution.outcomes.filter((o) => o.status === "accepted");
|
|
392
|
+
if (accepted.length > 0) {
|
|
393
|
+
return this.leaveAwaiting(updated, `${accepted.length} receivable(s) issued and unpaid: ${accepted.map((o) => o.transaction_id ?? o.attempt_id).join(", ")}`);
|
|
394
|
+
}
|
|
395
|
+
if (execution.outcomes.length < attempts) {
|
|
396
|
+
return this.leaveUnresolved(updated, unknownDetail ?? `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
|
|
397
|
+
}
|
|
398
|
+
const failed = execution.outcomes.find((o) => o.status === "failed");
|
|
399
|
+
if (failed) {
|
|
400
|
+
this.deps.store.updateOutbox(execution.idempotency_key, "failed", execution.outcomes, at);
|
|
401
|
+
const reason: ExecutionReason = failed.code === "charge_expired" || failed.code === "charge_cancelled" ? failed.code : "rail_failed";
|
|
402
|
+
return this.persist(transition(updated, "failed", { at, actor: this.agentActor, reason, detail: failed.error ?? "rail refused" }));
|
|
403
|
+
}
|
|
404
|
+
if (execution.outcomes.filter((o) => o.status === "settled").length === attempts) {
|
|
405
|
+
this.deps.store.updateOutbox(execution.idempotency_key, "done", execution.outcomes, at);
|
|
406
|
+
return this.persist(transition(updated, "settled", { at, actor: this.agentActor }));
|
|
407
|
+
}
|
|
408
|
+
return this.leaveUnresolved(updated, `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private leaveUnresolved(execution: Execution<"executing">, detail: string): Execution {
|
|
412
|
+
const kept: Execution<"executing"> = { ...execution, reason: "rail_uncertain", detail, updated_at: this.clock().toISOString() };
|
|
413
|
+
this.deps.store.saveExecution(kept);
|
|
414
|
+
this.record("execution.uncertain", execution.id, { detail });
|
|
415
|
+
return kept;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** A receivable is out and its payer has not acted. Not uncertain: the rail knows the attempt; the outcome is simply not yet decided. */
|
|
419
|
+
private leaveAwaiting(execution: Execution<"executing">, detail: string): Execution {
|
|
420
|
+
const kept: Execution<"executing"> = { ...execution, reason: "awaiting_settlement", detail, updated_at: this.clock().toISOString() };
|
|
421
|
+
this.deps.store.saveExecution(kept);
|
|
422
|
+
// Deduplicated on what was observed, so a poll every few seconds does not fill the log with the same line.
|
|
423
|
+
const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: execution.id, event_id: `awaiting:${execution.id}:${sha256Hex(detail).slice(0, 16)}`, type: "execution.awaiting_settlement", payload: { detail }, at: kept.updated_at });
|
|
424
|
+
if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
425
|
+
return kept;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The instrument the payer is shown, logged once per observed state (registration flips `payable`; the log says when). */
|
|
429
|
+
private recordInstrument(executionId: string, attemptId: string, transactionId: string, instrument: ItemOutcome["instrument"]): void {
|
|
430
|
+
if (!instrument) return;
|
|
431
|
+
const stored = this.deps.store.appendEvent({
|
|
432
|
+
run_id: this.deps.runId,
|
|
433
|
+
execution_id: executionId,
|
|
434
|
+
event_id: `instrument:${attemptId}:${instrument.status}:${instrument.payable ? "payable" : "not-payable"}`,
|
|
435
|
+
type: "charge.instrument",
|
|
436
|
+
payload: { attempt_id: attemptId, charge_id: transactionId, status: instrument.status, payable: instrument.payable, has_pix: instrument.pix_copy_paste !== null, has_boleto: instrument.boleto_bank_line !== null, due_date: instrument.due_date },
|
|
437
|
+
at: this.clock().toISOString(),
|
|
438
|
+
});
|
|
439
|
+
if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<void> {
|
|
443
|
+
// 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
|
+
const appended = this.deps.store.appendEvent({
|
|
445
|
+
run_id: this.deps.runId,
|
|
446
|
+
execution_id: execution.id,
|
|
447
|
+
event_id: `succeeded:${payment.attempt_id}`,
|
|
448
|
+
type: eventType,
|
|
449
|
+
payload: { attempt_id: payment.attempt_id, amount: payment.amount_minor, payee: payment.payee, receipt_id: receiptId, actor: this.agentActor },
|
|
450
|
+
at: this.clock().toISOString(),
|
|
451
|
+
});
|
|
452
|
+
if (appended) this.deps.bundle.event({ ...appended, actor: this.agentActor, item_index: index });
|
|
453
|
+
if (receiptId) await this.saveReceipt(execution.id, receiptId);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private async saveReceipt(executionId: string, receiptId: string): Promise<void> {
|
|
457
|
+
const receipt = await this.deps.rail.receipt(receiptId, this.agentActor);
|
|
458
|
+
if (receipt) {
|
|
459
|
+
const path = this.deps.bundle.receipt(receipt);
|
|
460
|
+
this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Fetches into the bundle the receipts of settled attempts the bundle does
|
|
467
|
+
* not hold yet (a settlement that arrived by event names a receipt id and
|
|
468
|
+
* nothing else). Read-only on the rail; never dispatches.
|
|
469
|
+
*/
|
|
470
|
+
async collectReceipts(executionId: string): Promise<string[]> {
|
|
471
|
+
const execution = this.mustGet(executionId);
|
|
472
|
+
const held = new Set(this.deps.bundle.listReceipts().map((f) => f.replace(/\.json$/, "")));
|
|
473
|
+
const fetched: string[] = [];
|
|
474
|
+
for (const outcome of execution.outcomes) {
|
|
475
|
+
if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id)) continue;
|
|
476
|
+
await this.saveReceipt(execution.id, outcome.receipt_id);
|
|
477
|
+
fetched.push(outcome.receipt_id);
|
|
478
|
+
}
|
|
479
|
+
return fetched;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ---- section 10: resume and reconcile ---------------------------------
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* An execution left in `executing` is reconciled, NEVER dispatched. Each
|
|
486
|
+
* attempt without an outcome is looked up on the rail; a recorded settled
|
|
487
|
+
* or failed answer is taken, and anything else (in flight, uncertain,
|
|
488
|
+
* unknown) leaves the execution in `executing` with an `execution.uncertain`
|
|
489
|
+
* event, for a human. "Unknown" is not "never sent": on the real rail a
|
|
490
|
+
* receipt is indexed seconds after settlement (~43 s measured on Celcoin).
|
|
491
|
+
*/
|
|
492
|
+
async reconcile(executionId: string): Promise<Execution> {
|
|
493
|
+
const execution = this.mustGet(executionId);
|
|
494
|
+
if (execution.state !== "executing") return execution;
|
|
495
|
+
const payments = this.paymentsFor(execution);
|
|
496
|
+
const outbox = this.deps.store.getOutbox(execution.idempotency_key);
|
|
497
|
+
if (outbox?.status === "pending") {
|
|
498
|
+
return this.leaveUnresolved(execution as Execution<"executing">, "outbox pending: nothing was ever sent; `resume` dispatches it, reconcile does not");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const outcomes: ItemOutcome[] = [...execution.outcomes];
|
|
502
|
+
let unresolved: string | undefined;
|
|
503
|
+
for (const [index, payment] of payments.entries()) {
|
|
504
|
+
const prior = outcomes.find((o) => o.index === index);
|
|
505
|
+
// Settled and failed are final. An accepted receivable is looked at again: its payer may have acted.
|
|
506
|
+
if (prior && prior.status !== "accepted") continue;
|
|
507
|
+
const seen = await this.deps.rail.lookup(payment.attempt_id, payment, prior?.transaction_id);
|
|
508
|
+
const found = seen ? seen.status : "absent";
|
|
509
|
+
const stored = this.deps.store.appendEvent({
|
|
510
|
+
run_id: this.deps.runId,
|
|
511
|
+
execution_id: execution.id,
|
|
512
|
+
// A poll that sees the same thing twice logs it once; a change in what it sees is a new line.
|
|
513
|
+
...(prior ? { event_id: `reconcile:${payment.attempt_id}:${found}:${seen && seen.status === "accepted" ? `${seen.instrument.status}:${seen.instrument.payable}` : ""}` } : {}),
|
|
514
|
+
type: "rail.reconcile",
|
|
515
|
+
payload: { attempt_id: payment.attempt_id, found },
|
|
516
|
+
at: this.clock().toISOString(),
|
|
517
|
+
});
|
|
518
|
+
if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
519
|
+
if (!seen || seen.status === "uncertain" || seen.status === "in_flight") {
|
|
520
|
+
if (prior) continue; // the receivable is known to exist; the rail just did not answer this time
|
|
521
|
+
// One attempt the rail cannot answer for does not stop us learning
|
|
522
|
+
// about the others. A lookup moves no money, so there is nothing to
|
|
523
|
+
// be careful about here, and stopping would mean a later attempt's
|
|
524
|
+
// settlement is never recorded at all.
|
|
525
|
+
unresolved ??= `attempt ${payment.attempt_id}: ${seen ? seen.status : "unknown to the rail"}; a human decides, nothing is re-sent`;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const replace = (outcome: ItemOutcome) => {
|
|
529
|
+
const at = outcomes.findIndex((o) => o.index === index);
|
|
530
|
+
if (at >= 0) outcomes[at] = outcome;
|
|
531
|
+
else outcomes.push(outcome);
|
|
532
|
+
};
|
|
533
|
+
if (seen.status === "accepted") {
|
|
534
|
+
replace({ index, attempt_id: payment.attempt_id, status: "accepted", transaction_id: seen.transaction_id, instrument: seen.instrument });
|
|
535
|
+
this.recordInstrument(execution.id, payment.attempt_id, seen.transaction_id, seen.instrument);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (seen.status === "failed") {
|
|
539
|
+
replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}` });
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
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);
|
|
544
|
+
}
|
|
545
|
+
const updated: Execution<"executing"> = { ...(execution as Execution<"executing">), outcomes };
|
|
546
|
+
if (unresolved) return this.leaveUnresolved(updated, unresolved);
|
|
547
|
+
return this.close(updated, payments.length);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* `resume` only: dispatches the attempts of an `executing` execution whose
|
|
552
|
+
* outbox row is still `pending`. `pending` flips to `sent` before the first
|
|
553
|
+
* rail call, so a `pending` row proves nothing ever left; anything `sent`
|
|
554
|
+
* goes through `reconcile()` instead and is never re-sent.
|
|
555
|
+
*/
|
|
556
|
+
async resumePending(executionId: string): Promise<Execution> {
|
|
557
|
+
const execution = this.mustGet(executionId);
|
|
558
|
+
if (execution.state !== "executing") return execution;
|
|
559
|
+
const outbox = this.deps.store.getOutbox(execution.idempotency_key);
|
|
560
|
+
if (outbox?.status !== "pending") return this.reconcile(executionId);
|
|
561
|
+
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));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Time-based exits: an open execution past its approval TTL closes as `expired`. */
|
|
566
|
+
expireStale(): Execution[] {
|
|
567
|
+
const now = this.clock();
|
|
568
|
+
const ttl = this.deps.guardrails.approval_ttl_minutes * 60_000;
|
|
569
|
+
const out: Execution[] = [];
|
|
570
|
+
for (const execution of this.deps.store.listExecutions({ state: ["awaiting_approval", "approved"] })) {
|
|
571
|
+
const opened = new Date(execution.updated_at).getTime();
|
|
572
|
+
if (now.getTime() - opened <= ttl) continue;
|
|
573
|
+
out.push(this.persist(transition(execution as Execution<"awaiting_approval">, "expired", { at: now.toISOString(), actor: this.agentActor, reason: "approval_expired", detail: `no decision within ${this.deps.guardrails.approval_ttl_minutes} minutes` })));
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* An event arriving from outside (a trigger, a webhook): deduplicated by
|
|
580
|
+
* id, applied to the ONE attempt it names, and the execution closes only
|
|
581
|
+
* when every attempt has its outcome. `paid` before `created` is fine;
|
|
582
|
+
* `paid` twice settles once.
|
|
583
|
+
*/
|
|
584
|
+
ingestExternalEvent(event: { event_id: string; type: string; attempt_id?: string; transaction_id?: string; at?: string }): { applied: boolean; reason: string } {
|
|
585
|
+
const located = this.locateAttempt(event);
|
|
586
|
+
if (!located) return { applied: false, reason: event.attempt_id || event.transaction_id ? "unknown attempt" : "event names no attempt_id and no transaction_id" };
|
|
587
|
+
const { row, attemptId } = located;
|
|
588
|
+
const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: row.execution_id, event_id: event.event_id, type: event.type, payload: { ...event, attempt_id: attemptId }, at: event.at ?? this.clock().toISOString() });
|
|
589
|
+
if (!stored) return { applied: false, reason: "duplicate event id" };
|
|
590
|
+
this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
591
|
+
const execution = this.mustGet(row.execution_id);
|
|
592
|
+
if (isTerminal(execution.state)) return { applied: false, reason: `execution already ${execution.state}` };
|
|
593
|
+
if (execution.state !== "executing") return { applied: false, reason: `execution is ${execution.state}; the rail event cannot move it` };
|
|
594
|
+
const attempts = (row.payload as { attempts: string[] }).attempts;
|
|
595
|
+
const index = attempts.indexOf(attemptId);
|
|
596
|
+
const prior = execution.outcomes.find((o) => o.index === index);
|
|
597
|
+
// Settled and failed are final. An accepted receivable is exactly what a `commerce.charge.*` event decides.
|
|
598
|
+
if (prior && prior.status !== "accepted") return { applied: false, reason: "attempt already has an outcome" };
|
|
599
|
+
|
|
600
|
+
const carry = prior ? { ...(prior.transaction_id ? { transaction_id: prior.transaction_id } : {}) } : {};
|
|
601
|
+
let outcome: ItemOutcome;
|
|
602
|
+
switch (event.type) {
|
|
603
|
+
case PAYMENT_SUCCEEDED:
|
|
604
|
+
outcome = { index, attempt_id: attemptId, status: "settled", ...carry };
|
|
605
|
+
break;
|
|
606
|
+
case CHARGE_PAID:
|
|
607
|
+
// The paid receivable is its own record on the API; `collectReceipts()` fetches it into the bundle.
|
|
608
|
+
outcome = { index, attempt_id: attemptId, status: "settled", ...carry, ...(prior?.transaction_id ? { receipt_id: prior.transaction_id } : {}) };
|
|
609
|
+
break;
|
|
610
|
+
case PAYMENT_FAILED:
|
|
611
|
+
outcome = { index, attempt_id: attemptId, status: "failed", code: "rail_failed", error: `event ${event.event_id}` };
|
|
612
|
+
break;
|
|
613
|
+
case CHARGE_EXPIRED:
|
|
614
|
+
outcome = { index, attempt_id: attemptId, status: "failed", code: "charge_expired", error: `charge_expired: event ${event.event_id}`, ...carry };
|
|
615
|
+
break;
|
|
616
|
+
case CHARGE_CANCELLED:
|
|
617
|
+
outcome = { index, attempt_id: attemptId, status: "failed", code: "charge_cancelled", error: `charge_cancelled: event ${event.event_id}`, ...carry };
|
|
618
|
+
break;
|
|
619
|
+
default:
|
|
620
|
+
return { applied: false, reason: `event type ${event.type} moves nothing` };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const outcomes = execution.outcomes.filter((o) => o.index !== index).concat(outcome).sort((a, b) => a.index - b.index);
|
|
624
|
+
const closed = this.close({ ...(execution as Execution<"executing">), outcomes }, attempts.length);
|
|
625
|
+
return { applied: true, reason: closed.state === "executing" ? `attempt recorded; ${closed.detail}` : closed.state };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** An event names our attempt id, or the rail's transaction id we recorded when the attempt was accepted. */
|
|
629
|
+
private locateAttempt(event: { attempt_id?: string; transaction_id?: string }): { row: ReturnType<StateStore["listOutbox"]>[number]; attemptId: string } | undefined {
|
|
630
|
+
const rows = this.deps.store.listOutbox();
|
|
631
|
+
if (event.attempt_id) {
|
|
632
|
+
const attemptId = event.attempt_id;
|
|
633
|
+
const row = rows.find((o) => ((o.payload as { attempts?: string[] }).attempts ?? []).includes(attemptId));
|
|
634
|
+
return row ? { row, attemptId } : undefined;
|
|
635
|
+
}
|
|
636
|
+
if (event.transaction_id) {
|
|
637
|
+
for (const row of rows) {
|
|
638
|
+
const execution = this.deps.store.getExecution(row.execution_id);
|
|
639
|
+
const hit = execution?.outcomes.find((o) => o.transaction_id === event.transaction_id);
|
|
640
|
+
if (hit) return { row, attemptId: hit.attempt_id };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ---- the channel's small writes -----------------------------------------
|
|
647
|
+
|
|
648
|
+
/** A line in the event log written by the channel (a message to the person, a sandbox payer call). Never a transition. */
|
|
649
|
+
note(type: string, executionId: string | null, payload: Record<string, unknown>): void {
|
|
650
|
+
this.record(type, executionId, payload);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Once-only presentation: true the first time a given attempt's instrument
|
|
655
|
+
* is marked shown, false afterwards, across restarts (it is a cursor in
|
|
656
|
+
* state.db, not memory). The debtor sees each QR once.
|
|
657
|
+
*/
|
|
658
|
+
markShown(executionId: string, attemptId: string): boolean {
|
|
659
|
+
const key = `shown:${executionId}:${attemptId}`;
|
|
660
|
+
if (this.deps.store.getCursor(key)) return false;
|
|
661
|
+
this.deps.store.setCursor(key, this.clock().toISOString());
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Once-only message per execution outcome, same mechanism. */
|
|
666
|
+
markTold(executionId: string, outcome: string): boolean {
|
|
667
|
+
const key = `told:${executionId}:${outcome}`;
|
|
668
|
+
if (this.deps.store.getCursor(key)) return false;
|
|
669
|
+
this.deps.store.setCursor(key, this.clock().toISOString());
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* A durable claim for work whose unit is bigger than one execution: a batch
|
|
675
|
+
* line, a scheduled instruction. `claimed` reads which execution covers a
|
|
676
|
+
* key, `claim` records it. Same cursor table as `markShown`, which is this
|
|
677
|
+
* with a boolean answer.
|
|
678
|
+
*
|
|
679
|
+
* It exists because a tool handler's only durable surface is this engine,
|
|
680
|
+
* and a batch that must not pay the same payee twice across runs has to
|
|
681
|
+
* remember which execution already covers each line. What a held claim
|
|
682
|
+
* MEANS is the kit's to decide: the engine records the pairing and reads
|
|
683
|
+
* nothing into it, because "settled, so skip" and "denied, so retry" are
|
|
684
|
+
* the batch's rules and not the state machine's.
|
|
685
|
+
*/
|
|
686
|
+
claimed(key: string): string | undefined {
|
|
687
|
+
return this.deps.store.getCursor(`claim:${key}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
claim(key: string, executionId: string): void {
|
|
691
|
+
this.deps.store.setCursor(`claim:${key}`, executionId);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ---- reads --------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
get(executionId: string): Execution | undefined {
|
|
697
|
+
return this.deps.store.getExecution(executionId);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
list(filter: { state?: ExecutionState | ExecutionState[] } = {}): Execution[] {
|
|
701
|
+
return this.deps.store.listExecutions({ ...filter, mandate_id: this.deps.mandate.id });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* The items `draft()` would build from these proposals, resolved against
|
|
706
|
+
* the mandate and WITHOUT the validation that refuses one. It exists for a
|
|
707
|
+
* caller that must hash a whole list before it drafts any of it — a batch
|
|
708
|
+
* computing its `batch_hash` — and it does not throw on purpose: a line the
|
|
709
|
+
* core will refuse is still a line of the presented list, and dropping it
|
|
710
|
+
* from the hash would make a batch holding a broken line hash the same as
|
|
711
|
+
* the shorter batch without it, which is the hole `batch_hash` closes.
|
|
712
|
+
*
|
|
713
|
+
* It resolves nothing the draft will not resolve identically, which is what
|
|
714
|
+
* makes the hash computed here the hash the drafts below carry.
|
|
715
|
+
*/
|
|
716
|
+
preview(items: readonly ProposedItem[]): ExecutionItem[] {
|
|
717
|
+
return items.map((p) => this.previewItem(p));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ---- helpers ------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Section 4.7, fail-closed: `executing` is reached on `active` and on
|
|
724
|
+
* nothing else. A source that cannot answer (a failed read, a status this
|
|
725
|
+
* code does not know, an exception) is `mandate_status_unavailable`, never
|
|
726
|
+
* "assume active". An expired mandate closes the execution as `expired`;
|
|
727
|
+
* the other refusals close it as `denied`.
|
|
728
|
+
*/
|
|
729
|
+
private async mandateGate(): Promise<{ reason: ExecutionReason; detail: string; to: "denied" | "expired" } | undefined> {
|
|
730
|
+
const { mandate } = this.deps;
|
|
731
|
+
let report: MandateStatusReport;
|
|
732
|
+
try {
|
|
733
|
+
report = await this.deps.status.check(mandate.id);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
return { to: "denied", reason: "mandate_status_unavailable", detail: `the status of mandate ${mandate.id} could not be read (${err instanceof Error ? err.message : String(err)}); nothing executes until it can` };
|
|
736
|
+
}
|
|
737
|
+
if (report.org_paused) return { to: "denied", reason: "org_paused", detail: "the organization paused every mandate (kill switch)" };
|
|
738
|
+
switch (report.status) {
|
|
739
|
+
case "revoked":
|
|
740
|
+
return { to: "denied", reason: "mandate_revoked", detail: `mandate ${mandate.id} was revoked (source: ${report.source})` };
|
|
741
|
+
case "paused":
|
|
742
|
+
return { to: "denied", reason: "mandate_paused", detail: `mandate ${mandate.id} is paused (source: ${report.source})` };
|
|
743
|
+
case "expired":
|
|
744
|
+
return { to: "expired", reason: "mandate_expired", detail: `mandate ${mandate.id} expired at ${mandate.expires_at} (source: ${report.source})` };
|
|
745
|
+
case "active":
|
|
746
|
+
if (mandateExpired(mandate, this.clock())) return { to: "expired", reason: "mandate_expired", detail: `mandate ${mandate.id} expired at ${mandate.expires_at}` };
|
|
747
|
+
return undefined;
|
|
748
|
+
default:
|
|
749
|
+
return { to: "denied", reason: "mandate_status_unavailable", detail: `the status of mandate ${mandate.id} could not be read (${report.detail ?? "no detail"}); nothing executes until it can` };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private resolveItem(proposed: ProposedItem): ExecutionItem {
|
|
754
|
+
const amount = Math.trunc(proposed.amount);
|
|
755
|
+
if (!Number.isFinite(amount) || amount <= 0) throw new Error(`amount must be a positive integer in minor units, got ${proposed.amount}`);
|
|
756
|
+
if (proposed.due_date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(proposed.due_date)) throw new Error(`due_date must be YYYY-MM-DD, got ${proposed.due_date}`);
|
|
757
|
+
return this.previewItem(proposed);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private previewItem(proposed: ProposedItem): ExecutionItem {
|
|
761
|
+
const known = resolveBeneficiary(this.deps.mandate, proposed.payee);
|
|
762
|
+
return {
|
|
763
|
+
...(known ? { alias: known.alias } : {}),
|
|
764
|
+
beneficiary: known?.name ?? proposed.payee,
|
|
765
|
+
payee: known?.payee ?? proposed.payee,
|
|
766
|
+
amount: Math.trunc(proposed.amount),
|
|
767
|
+
currency: this.deps.mandate.currency,
|
|
768
|
+
...(proposed.description ? { description: proposed.description } : {}),
|
|
769
|
+
...(proposed.due_date ? { due_date: proposed.due_date } : {}),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private paymentsFor(execution: Execution): RailPayment[] {
|
|
774
|
+
return execution.items.map((item, index) => ({
|
|
775
|
+
attempt_id: `att_${execution.idempotency_key.slice(4)}_${index}`,
|
|
776
|
+
mandate_id: execution.mandate.id,
|
|
777
|
+
amount_minor: item.amount,
|
|
778
|
+
currency: item.currency,
|
|
779
|
+
payee: item.payee,
|
|
780
|
+
beneficiary: item.beneficiary,
|
|
781
|
+
purpose: this.deps.mandate.purpose,
|
|
782
|
+
agent_id: this.deps.mandate.agent_id,
|
|
783
|
+
consumer_id: this.deps.mandate.consumer_id,
|
|
784
|
+
...(item.description ? { description: item.description } : {}),
|
|
785
|
+
...(item.due_date ? { due_date: item.due_date } : {}),
|
|
786
|
+
actor: this.agentActor,
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private withoutApproval(execution: Execution<"approved">): Execution<"approved"> {
|
|
791
|
+
const { approval_id: _stale, ...rest } = execution;
|
|
792
|
+
return rest as Execution<"approved">;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Settled, in flight or awaiting a decision under this mandate inside the cap window, excluding the execution being judged. */
|
|
796
|
+
private committedInWindow(now: Date, excludeId: string): number {
|
|
797
|
+
const start = windowStart(this.deps.mandate, now).getTime();
|
|
798
|
+
return this.list({ state: WINDOW_STATES })
|
|
799
|
+
.filter((e) => e.id !== excludeId && new Date(e.created_at).getTime() >= start)
|
|
800
|
+
.reduce((sum, e) => sum + e.total, 0);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private knownPayees(): Set<string> {
|
|
804
|
+
const out = new Set<string>();
|
|
805
|
+
for (const e of this.list({ state: "settled" })) for (const i of e.items) out.add(i.payee);
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* What the agent ran ALONE per payee inside the velocity window: mandate-mode
|
|
811
|
+
* executions the allowance approved without a human. A payment a human
|
|
812
|
+
* approved is not fractioning, so it does not count against the threshold.
|
|
813
|
+
*/
|
|
814
|
+
private recentByPayee(now: Date, excludeId: string): Map<string, number> {
|
|
815
|
+
const out = new Map<string, number>();
|
|
816
|
+
const hours = this.deps.guardrails.velocity?.window_hours;
|
|
817
|
+
if (!hours) return out;
|
|
818
|
+
const since = now.getTime() - hours * 3_600_000;
|
|
819
|
+
for (const e of this.list({ state: ["approved", "executing", "settled"] })) {
|
|
820
|
+
if (e.id === excludeId || e.mode !== "mandate" || e.escalation !== undefined) continue;
|
|
821
|
+
if (new Date(e.created_at).getTime() < since) continue;
|
|
822
|
+
for (const i of e.items) out.set(i.payee, (out.get(i.payee) ?? 0) + i.amount);
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private storeApproval(artifact: ApprovalArtifact): void {
|
|
828
|
+
this.deps.store.saveApproval(artifact);
|
|
829
|
+
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 });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private persist<S extends ExecutionState>(execution: Execution<S>): Execution<S> {
|
|
834
|
+
this.deps.store.saveExecution(execution);
|
|
835
|
+
const last = execution.history[execution.history.length - 1];
|
|
836
|
+
if (last) {
|
|
837
|
+
const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: execution.id, type: "execution.transition", payload: last, at: last.at });
|
|
838
|
+
if (stored) this.deps.bundle.event({ ...stored, actor: last.actor });
|
|
839
|
+
}
|
|
840
|
+
return execution;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private record(type: string, executionId: string | null, payload: Record<string, unknown>): void {
|
|
844
|
+
const stored = this.deps.store.appendEvent({ run_id: this.deps.runId, execution_id: executionId, type, payload, at: this.clock().toISOString() });
|
|
845
|
+
if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private mustGet(executionId: string): Execution {
|
|
849
|
+
const execution = this.deps.store.getExecution(executionId);
|
|
850
|
+
if (!execution) throw new Error(`unknown execution ${executionId}`);
|
|
851
|
+
return execution;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* A batch binding is the kit's to compute and the core's to refuse when it
|
|
857
|
+
* cannot be true. `index` outside `count`, or a `count` of zero, would let a
|
|
858
|
+
* bundle say "line 5 of 3" and make the "N of M" an artifact reader sees
|
|
859
|
+
* meaningless, so it is rejected where the execution is minted rather than
|
|
860
|
+
* carried into the signature.
|
|
861
|
+
*/
|
|
862
|
+
function assertBatchBinding(batch: ExecutionBatch): void {
|
|
863
|
+
if (!batch.ref.trim()) throw new Error("batch.ref must name the batch");
|
|
864
|
+
if (!batch.batch_hash.trim()) throw new Error("batch.batch_hash must be the hash of the presented lines");
|
|
865
|
+
if (!Number.isInteger(batch.count) || batch.count < 1) throw new Error(`batch.count must be a positive integer, got ${batch.count}`);
|
|
866
|
+
if (!Number.isInteger(batch.index) || batch.index < 0 || batch.index >= batch.count) {
|
|
867
|
+
throw new Error(`batch.index must be a position inside the batch (0..${batch.count - 1}), got ${batch.index}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* What the rail answered about one attempt, as the bundle records it: the
|
|
873
|
+
* identifiers a reader follows the money by, and never `raw`, which carries
|
|
874
|
+
* whatever the provider chose to echo back.
|
|
875
|
+
*/
|
|
876
|
+
function railAnswer(outcome: RailOutcome): Record<string, unknown> {
|
|
877
|
+
switch (outcome.status) {
|
|
878
|
+
case "accepted":
|
|
879
|
+
return { transaction_id: outcome.transaction_id, sandbox: outcome.sandbox };
|
|
880
|
+
case "settled":
|
|
881
|
+
return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox };
|
|
882
|
+
default:
|
|
883
|
+
return { code: outcome.code, message: outcome.message };
|
|
884
|
+
}
|
|
885
|
+
}
|