@codespar/cli 0.12.1 → 0.14.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 +54 -1
- package/dist/api.js +1 -3
- package/dist/api.js.map +1 -1
- package/dist/commands/agent.js +260 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mandate-revoke.js +30 -0
- package/dist/commands/mandate-revoke.js.map +1 -0
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +36 -3
- package/dist/index.js.map +1 -1
- package/dist/surface.js +1 -1
- package/dist/surface.js.map +1 -1
- package/package.json +3 -1
- package/templates/bills-agent/LICENSE +21 -0
- package/templates/bills-agent/README.md +21 -0
- package/templates/bills-agent/_gitignore +10 -0
- package/templates/bills-agent/agents/bills-agent/.env.example +7 -0
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +16 -0
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +16 -0
- package/templates/bills-agent/agents/bills-agent/README.md +89 -0
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +23 -0
- package/templates/bills-agent/agents/bills-agent/agent.yaml +26 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +108 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/eval.yaml +9 -0
- package/templates/bills-agent/agents/bills-agent/guardrails.json +15 -0
- package/templates/bills-agent/agents/bills-agent/mandate.example.json +46 -0
- package/templates/bills-agent/agents/bills-agent/package.json +24 -0
- package/templates/bills-agent/agents/bills-agent/runbook.md +19 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/src/adversarial.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/src/bills.ts +28 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +9 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +13 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +26 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +45 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +7 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +37 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +38 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +65 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +30 -0
- package/templates/bills-agent/agents/bills-agent/src/main.ts +185 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +125 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/pix-out.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/src/scenarios.ts +196 -0
- package/templates/bills-agent/agents/bills-agent/src/setup.ts +207 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +40 -0
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +85 -0
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/tools.json +51 -0
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +11 -0
- package/templates/bills-agent/package.json +34 -0
- package/templates/bills-agent/packages/agent-core/README.md +19 -0
- package/templates/bills-agent/packages/agent-core/package.json +24 -0
- package/templates/bills-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/bills-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/bills-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/bills-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/bills-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/bills-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/bills-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/bills-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/bills-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/bills-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/bills-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/bills-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/bills-agent/tsconfig.base.json +16 -0
- package/templates/bills-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/LICENSE +21 -0
- package/templates/collections-agent/README.md +20 -0
- package/templates/collections-agent/_gitignore +10 -0
- package/templates/collections-agent/agents/collections-agent/.env.example +4 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +19 -0
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +19 -0
- package/templates/collections-agent/agents/collections-agent/README.md +94 -0
- package/templates/collections-agent/agents/collections-agent/SYSTEM_PROMPT.md +31 -0
- package/templates/collections-agent/agents/collections-agent/agent.yaml +28 -0
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +178 -0
- package/templates/collections-agent/agents/collections-agent/channels/webhook/index.ts +131 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap-mandate.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.json +19 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.json +13 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/eval.yaml +11 -0
- package/templates/collections-agent/agents/collections-agent/guardrails.json +20 -0
- package/templates/collections-agent/agents/collections-agent/mandate.example.json +40 -0
- package/templates/collections-agent/agents/collections-agent/package.json +29 -0
- package/templates/collections-agent/agents/collections-agent/runbook.md +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.json +36 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.transcript.jsonl +5 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.json +60 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.transcript.jsonl +6 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/src/adversarial.ts +190 -0
- package/templates/collections-agent/agents/collections-agent/src/agreements.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +10 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +13 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +48 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +7 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +39 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +35 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +38 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +30 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +36 -0
- package/templates/collections-agent/agents/collections-agent/src/envelope.ts +103 -0
- package/templates/collections-agent/agents/collections-agent/src/main.ts +193 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +105 -0
- package/templates/collections-agent/agents/collections-agent/src/poll.ts +94 -0
- package/templates/collections-agent/agents/collections-agent/src/scenarios.ts +252 -0
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +250 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +51 -0
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +172 -0
- package/templates/collections-agent/agents/collections-agent/test/envelope.test.ts +53 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1103.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +95 -0
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +100 -0
- package/templates/collections-agent/agents/collections-agent/tools.json +40 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +11 -0
- package/templates/collections-agent/package.json +35 -0
- package/templates/collections-agent/packages/agent-core/README.md +19 -0
- package/templates/collections-agent/packages/agent-core/package.json +24 -0
- package/templates/collections-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/collections-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/collections-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/collections-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/collections-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/collections-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/collections-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/collections-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/collections-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/collections-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/collections-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/collections-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/collections-agent/tsconfig.base.json +16 -0
- package/templates/collections-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +41 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The consumer delegates the month's bills to an agent that pays under a signed mandate: per-payment cap, monthly cap, named payees, expiry. Every payment returns a receipt.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"packages/agent-core",
|
|
10
|
+
"agents/bills-agent"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "npm start --workspace=agents/bills-agent --",
|
|
14
|
+
"check": "npm run check --workspaces --if-present --",
|
|
15
|
+
"test": "vitest run --pool=forks --maxWorkers=1",
|
|
16
|
+
"typecheck": "tsc --noEmit -p packages/agent-core/tsconfig.json && tsc --noEmit -p agents/bills-agent/tsconfig.json",
|
|
17
|
+
"approve": "npm run approve --workspace=agents/bills-agent --",
|
|
18
|
+
"consent": "npm run consent --workspace=agents/bills-agent --",
|
|
19
|
+
"deny": "npm run deny --workspace=agents/bills-agent --",
|
|
20
|
+
"eval": "npm run eval --workspace=agents/bills-agent --",
|
|
21
|
+
"reconcile": "npm run reconcile --workspace=agents/bills-agent --",
|
|
22
|
+
"rerun": "npm run rerun --workspace=agents/bills-agent --",
|
|
23
|
+
"resume": "npm run resume --workspace=agents/bills-agent --"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "22.20.4",
|
|
27
|
+
"tsx": "4.20.5",
|
|
28
|
+
"typescript": "5.9.2",
|
|
29
|
+
"vitest": "3.2.4"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.13"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @codespar/agent-core
|
|
2
|
+
|
|
3
|
+
The primitives every CodeSpar starter-kit agent inherits (spec v5.1.1, section 4): the execution state machine, the approval artifact, the `agent.yaml` manifest, `escalate_above`, `actor`, local state in SQLite, the proof bundle, the rail contract and the two providers.
|
|
4
|
+
|
|
5
|
+
## The state machine, and what the types do and do not prove
|
|
6
|
+
|
|
7
|
+
`transition(execution, to, input)` accepts only a `to` that the table in `state-machine.ts` lists for `execution.state`. On a **narrowed** value (`Execution<"drafted">`, `Execution<"executing">`, ...) a transition outside the table is a compile error, and `test/transitions.type-test.ts` keeps that true with `@ts-expect-error` lines that `tsc` refuses to leave unused. An **un-narrowed** `Execution` (the union, which is what a row read back from SQLite is) compiles for any target; the engine narrows with casts at its call sites. So the guard that always holds is the runtime one: `transition()` throws `IllegalTransitionError` on any pair the table does not list, whatever the static type said. The type test proves the table is closed; the runtime check enforces it.
|
|
8
|
+
|
|
9
|
+
## The one policy, three gates
|
|
10
|
+
|
|
11
|
+
`ExecutionEngine.policy()` runs at draft, at a human's approval and again immediately before `executing`, inside the transaction that writes the outbox row. Mandate status, allowlist, per-transaction cap, the window cap (counting settled, in flight AND awaiting executions as reservations), `escalate_above` and the artifact's `items_hash` are all re-checked at the last gate. A person's approval satisfies `escalate_above` and nothing else.
|
|
12
|
+
|
|
13
|
+
## Reconcile never re-dispatches
|
|
14
|
+
|
|
15
|
+
An execution left in `executing` is looked up on the rail, attempt by attempt. A recorded settled or failed outcome closes it; `in_flight`, `uncertain` or unknown leaves it in `executing` with `reason: rail_uncertain` for a human. The single exception is an outbox row still `pending` (the state changed, no call was ever made): those attempts go out once, under the same ids.
|
|
16
|
+
|
|
17
|
+
## Stubs
|
|
18
|
+
|
|
19
|
+
`stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, and a read that does not answer is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codespar/agent-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The primitives every CodeSpar starter-kit agent inherits: the execution state machine, the approval artifact, the agent.yaml manifest, escalate_above, actor, local state and providers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./providers/anthropic": "./src/providers/anthropic.ts",
|
|
12
|
+
"./providers/replay": "./src/providers/replay.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
16
|
+
"test": "vitest run --pool=forks --maxWorkers=1 --root ../.. packages/agent-core"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@anthropic-ai/sdk": "0.128.0",
|
|
20
|
+
"@codespar/sdk": "0.16.4",
|
|
21
|
+
"yaml": "2.9.1",
|
|
22
|
+
"zod": "3.25.76"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool-use loop. The provider says what the model wants; this file
|
|
3
|
+
* dispatches only the tools `tools.json` names, records every step in the
|
|
4
|
+
* transcript, and hands payment tools to the engine, which is the only
|
|
5
|
+
* thing that can move an execution past `drafted`.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProofBundle } from "./bundle.js";
|
|
8
|
+
import type { ExecutionEngine } from "./engine.js";
|
|
9
|
+
import type { AgentRuntime, ConversationMessage, ToolCall, ToolSpec } from "./providers/types.js";
|
|
10
|
+
import { isReply } from "./providers/types.js";
|
|
11
|
+
import type { Execution } from "./state-machine.js";
|
|
12
|
+
import type { ToolsFile } from "./tools.js";
|
|
13
|
+
import { allowedToolNames, toolSpecs } from "./tools.js";
|
|
14
|
+
|
|
15
|
+
export interface ToolContext {
|
|
16
|
+
engine: ExecutionEngine;
|
|
17
|
+
/** Fires when a payment tool created an execution; the channel decides what to do with it (ask, or run). */
|
|
18
|
+
onExecution: (execution: Execution) => Promise<Execution>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ToolHandler = (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
22
|
+
|
|
23
|
+
export interface AgentLoopOptions {
|
|
24
|
+
runtime: AgentRuntime;
|
|
25
|
+
tools: ToolsFile;
|
|
26
|
+
handlers: Record<string, ToolHandler>;
|
|
27
|
+
system: string;
|
|
28
|
+
bundle: ProofBundle;
|
|
29
|
+
engine: ExecutionEngine;
|
|
30
|
+
onExecution: ToolContext["onExecution"];
|
|
31
|
+
/** A hard stop on tool round-trips per turn, so a looping model cannot run forever. */
|
|
32
|
+
maxSteps?: number;
|
|
33
|
+
clock?: () => Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TurnResult {
|
|
37
|
+
reply: string;
|
|
38
|
+
tool_calls: Array<{ name: string; refused: boolean }>;
|
|
39
|
+
executions: Execution[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AgentLoop {
|
|
43
|
+
private readonly messages: ConversationMessage[] = [];
|
|
44
|
+
private readonly allowed: Set<string>;
|
|
45
|
+
private readonly specs: ToolSpec[];
|
|
46
|
+
private readonly clock: () => Date;
|
|
47
|
+
|
|
48
|
+
constructor(private readonly options: AgentLoopOptions) {
|
|
49
|
+
this.allowed = allowedToolNames(options.tools);
|
|
50
|
+
this.specs = toolSpecs(options.tools);
|
|
51
|
+
this.clock = options.clock ?? (() => new Date());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get history(): readonly ConversationMessage[] {
|
|
55
|
+
return this.messages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async turn(userText: string): Promise<TurnResult> {
|
|
59
|
+
const { runtime, bundle } = this.options;
|
|
60
|
+
const maxSteps = this.options.maxSteps ?? 8;
|
|
61
|
+
const calls: TurnResult["tool_calls"] = [];
|
|
62
|
+
const executions: Execution[] = [];
|
|
63
|
+
|
|
64
|
+
this.messages.push({ role: "user", content: userText });
|
|
65
|
+
bundle.transcript({ at: this.now(), kind: "user", text: userText });
|
|
66
|
+
|
|
67
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
68
|
+
const out = await runtime.step({ system: this.options.system, messages: this.messages }, this.specs);
|
|
69
|
+
if (isReply(out)) {
|
|
70
|
+
this.messages.push({ role: "assistant", content: out.text });
|
|
71
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", reply: out.text });
|
|
72
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text: out.text });
|
|
73
|
+
return { reply: out.text, tool_calls: calls, executions };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.messages.push({ role: "assistant", content: "", tool_calls: out });
|
|
77
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", tool_calls: out });
|
|
78
|
+
|
|
79
|
+
for (const call of out) {
|
|
80
|
+
const result = await this.dispatch(call, executions);
|
|
81
|
+
calls.push({ name: call.name, refused: result.refused });
|
|
82
|
+
this.messages.push({ role: "tool", tool_call_id: call.id, name: call.name, content: JSON.stringify(result.content), ...(result.refused ? { is_error: true } : {}) });
|
|
83
|
+
bundle.transcript({ at: this.now(), kind: "tool_result", tool_call_id: call.id, name: call.name, refused: result.refused, content: result.content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const text = "I stopped: too many tool steps in one turn. Nothing was paid beyond what the trail shows.";
|
|
88
|
+
this.messages.push({ role: "assistant", content: text });
|
|
89
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text, truncated: true });
|
|
90
|
+
return { reply: text, tool_calls: calls, executions };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async dispatch(call: ToolCall, executions: Execution[]): Promise<{ refused: boolean; content: unknown }> {
|
|
94
|
+
const { bundle, handlers, engine, onExecution } = this.options;
|
|
95
|
+
bundle.transcript({ at: this.now(), kind: "tool_call", tool_call_id: call.id, name: call.name, input: call.input });
|
|
96
|
+
|
|
97
|
+
if (!this.allowed.has(call.name)) {
|
|
98
|
+
// Section 15, closed tools: a tool outside tools.json is refused, and the refusal is in the trail.
|
|
99
|
+
bundle.event({ at: this.now(), type: "tool.refused", tool: call.name, reason: "tool_not_allowed", actor: engine.agentActor });
|
|
100
|
+
return { refused: true, content: { error: "tool_not_allowed", message: `${call.name} is not in this agent's tools.json` } };
|
|
101
|
+
}
|
|
102
|
+
const handler = handlers[call.name];
|
|
103
|
+
if (!handler) return { refused: true, content: { error: "tool_not_implemented", message: `${call.name} has no handler` } };
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await handler(call.input, {
|
|
107
|
+
engine,
|
|
108
|
+
onExecution: async (execution) => {
|
|
109
|
+
const settledOrNot = await onExecution(execution);
|
|
110
|
+
executions.push(settledOrNot);
|
|
111
|
+
return settledOrNot;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { refused: false, content };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
bundle.event({ at: this.now(), type: "tool.error", tool: call.name, message, actor: engine.agentActor });
|
|
118
|
+
return { refused: true, content: { error: "tool_failed", message } };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private now(): string {
|
|
123
|
+
return this.clock().toISOString();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeSpar receivable rail: the merchant COLLECTS. `pay()` issues a
|
|
3
|
+
* cobranca com vencimento through `POST /v1/charges` (the REST form of the
|
|
4
|
+
* `codespar_charge` meta-tool, action=create): `method: "boleto"` plus a
|
|
5
|
+
* `due_date` is what makes it ONE receivable the payer settles either by
|
|
6
|
+
* boleto or by Pix; `idempotency_key` is REQUIRED on that create and is our
|
|
7
|
+
* attempt id, so a retry returns the same charge instead of a second
|
|
8
|
+
* receivable the same debtor could pay twice. An immediate Pix
|
|
9
|
+
* (`method: "pix"`) is deliberately not used: the API does not replay its
|
|
10
|
+
* create on a key and does not read it back, so it cannot close a loop.
|
|
11
|
+
*
|
|
12
|
+
* `lookup()` is `GET /v1/charges/{id}`, which accepts our idempotency key as
|
|
13
|
+
* the id; the kit polls it to close the cycle when no webhook can reach a
|
|
14
|
+
* terminal. Branching is on `local_status` and the normalized `status`, never
|
|
15
|
+
* on prose. Amounts cross the wire in MAJOR units (`amount: 1080.5`), which
|
|
16
|
+
* is what `coerceMetaChargeArgs` reads; the answer's `amount_minor` is
|
|
17
|
+
* compared with what was asked and a mismatch is a failure, not a charge.
|
|
18
|
+
*
|
|
19
|
+
* There is no receipt object for a receivable; the paid charge as the API
|
|
20
|
+
* reports it is what the bundle keeps, marked `kind: "charge"` and unsealed.
|
|
21
|
+
*/
|
|
22
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
23
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
24
|
+
import type { Actor, ChargeInstrument } from "../types.js";
|
|
25
|
+
import { describeApiError, isUncertain } from "./client.js";
|
|
26
|
+
|
|
27
|
+
/** The `/v1/charges` answer, as the SDK types it. */
|
|
28
|
+
export interface ChargeView {
|
|
29
|
+
id: string | null;
|
|
30
|
+
status: string;
|
|
31
|
+
local_status: string;
|
|
32
|
+
status_conflict: boolean;
|
|
33
|
+
method: string;
|
|
34
|
+
currency: string;
|
|
35
|
+
amount: number;
|
|
36
|
+
amount_minor: number;
|
|
37
|
+
due_date: string | null;
|
|
38
|
+
payable: boolean;
|
|
39
|
+
boleto_bar_code: string | null;
|
|
40
|
+
boleto_bank_line: string | null;
|
|
41
|
+
pix_copy_paste: string | null;
|
|
42
|
+
credit_correlation_armed: boolean;
|
|
43
|
+
payment_in_flight: boolean;
|
|
44
|
+
settlement: "confirmed" | "pending" | "unconfirmable" | null;
|
|
45
|
+
issuance_unconfirmed: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function instrumentOf(view: ChargeView): ChargeInstrument {
|
|
49
|
+
return {
|
|
50
|
+
payable: view.payable,
|
|
51
|
+
status: view.status,
|
|
52
|
+
pix_copy_paste: view.pix_copy_paste,
|
|
53
|
+
boleto_bank_line: view.boleto_bank_line,
|
|
54
|
+
boleto_bar_code: view.boleto_bar_code,
|
|
55
|
+
due_date: view.due_date,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a charge view means for the attempt. The row's own status wins over the issuer's normalized one where they name a terminal state. */
|
|
60
|
+
export function outcomeOf(view: ChargeView): RailOutcome | { status: "in_flight" } {
|
|
61
|
+
if (view.issuance_unconfirmed || !view.id) return { status: "in_flight" };
|
|
62
|
+
const id = view.id;
|
|
63
|
+
if (view.local_status === "settled" || view.status === "CONFIRMED" || view.settlement === "confirmed") {
|
|
64
|
+
return { status: "settled", transaction_id: id, receipt_id: id, money_moved: false, sandbox: true, raw: view };
|
|
65
|
+
}
|
|
66
|
+
if (view.local_status === "expired" || view.status === "EXPIRED") {
|
|
67
|
+
return { status: "failed", code: "charge_expired", message: `charge ${id} reached its due date unpaid`, raw: view };
|
|
68
|
+
}
|
|
69
|
+
if (view.status === "CANCELLED" || view.local_status === "cancelled") {
|
|
70
|
+
return { status: "failed", code: "charge_cancelled", message: `charge ${id} was withdrawn`, raw: view };
|
|
71
|
+
}
|
|
72
|
+
return { status: "accepted", transaction_id: id, instrument: instrumentOf(view), sandbox: true, raw: view };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class CodeSparChargeRail implements PaymentRail {
|
|
76
|
+
readonly name = "codespar-charge" as const;
|
|
77
|
+
|
|
78
|
+
constructor(private readonly api: ApiClient) {}
|
|
79
|
+
|
|
80
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
81
|
+
if (!payment.due_date) return { status: "failed", code: "due_date_required", message: "a receivable needs a due_date; the immediate Pix charge cannot be read back or replayed" };
|
|
82
|
+
let view: ChargeView;
|
|
83
|
+
try {
|
|
84
|
+
view = (await this.api.post("/v1/charges", {
|
|
85
|
+
body: {
|
|
86
|
+
amount: payment.amount_minor / 100,
|
|
87
|
+
currency: payment.currency,
|
|
88
|
+
method: "boleto",
|
|
89
|
+
description: payment.description ?? payment.purpose,
|
|
90
|
+
buyer: { name: payment.beneficiary ?? payment.payee, document: payment.payee },
|
|
91
|
+
due_date: payment.due_date,
|
|
92
|
+
idempotency_key: payment.attempt_id,
|
|
93
|
+
},
|
|
94
|
+
})) as ChargeView;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const failure = describeApiError(err);
|
|
97
|
+
if (isUncertain(failure) || failure.code === "issuance_unconfirmed") return { status: "uncertain", code: failure.code, message: failure.message };
|
|
98
|
+
return { status: "failed", code: failure.code, message: failure.message };
|
|
99
|
+
}
|
|
100
|
+
if (view.issuance_unconfirmed || !view.id) return { status: "uncertain", code: "issuance_unconfirmed", message: "the issuer's answer to the create was lost; the key is reserved and a later read resolves it" };
|
|
101
|
+
if (view.amount_minor !== payment.amount_minor) {
|
|
102
|
+
// The API understood another amount than the one approved. The receivable exists; withdraw it rather than leave a debt nobody approved.
|
|
103
|
+
const withdrawn = await this.withdraw(view.id);
|
|
104
|
+
return { status: "failed", code: "amount_mismatch", message: `the API issued ${view.amount_minor} for a request of ${payment.amount_minor} (charge ${view.id}); ${withdrawn ? "withdrawn" : "withdrawal refused, an operator must cancel it"}` };
|
|
105
|
+
}
|
|
106
|
+
const outcome = outcomeOf(view);
|
|
107
|
+
// `in_flight` is excluded above (`issuance_unconfirmed` / no id); the create answers a state, never "still running".
|
|
108
|
+
return outcome.status === "in_flight" ? { status: "uncertain", code: "issuance_unconfirmed", message: "the create answered no readable state" } : outcome;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async lookup(attemptId: string, _payment: RailPayment): Promise<RailLookup> {
|
|
112
|
+
try {
|
|
113
|
+
const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId: attemptId } })) as ChargeView;
|
|
114
|
+
return outcomeOf(view);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const failure = describeApiError(err);
|
|
117
|
+
if (failure.status === 404) return undefined;
|
|
118
|
+
if (failure.status === 409) return { status: "in_flight" };
|
|
119
|
+
return { status: "uncertain", code: failure.code, message: failure.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async receipt(chargeId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
124
|
+
try {
|
|
125
|
+
const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId } })) as ChargeView;
|
|
126
|
+
const raw = view as ChargeView & { settled_at?: string };
|
|
127
|
+
return {
|
|
128
|
+
receipt_id: chargeId,
|
|
129
|
+
kind: "charge",
|
|
130
|
+
state: view.local_status === "settled" ? "paid" : view.local_status,
|
|
131
|
+
mandate: { id: "n/a" },
|
|
132
|
+
// The read names no buyer, so the payee is not on this record; the execution's item names the debtor.
|
|
133
|
+
payment: { amount_minor: view.amount_minor, payee: null, attempt_id: chargeId, money_moved: false, sandbox: true, at: raw.settled_at ?? new Date().toISOString() },
|
|
134
|
+
chain: null,
|
|
135
|
+
receipt_sig: null,
|
|
136
|
+
actor,
|
|
137
|
+
raw: view,
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const failure = describeApiError(err);
|
|
141
|
+
if (failure.status === 404) return undefined;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async withdraw(chargeId: string): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
await this.api.post("/v1/charges/{chargeId}/cancel", { path: { chargeId } });
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place the kit talks to the CodeSpar API. The key is checked for
|
|
3
|
+
* the `csk_test_` prefix before the client is built, so no live key ever
|
|
4
|
+
* reaches the network from this repository.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiClient, CodesparApiError, TimeoutError } from "@codespar/sdk";
|
|
7
|
+
import { assertTestKey } from "../secrets.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_BASE_URL = "https://api.codespar.dev";
|
|
10
|
+
|
|
11
|
+
export interface CodeSparClientOptions {
|
|
12
|
+
apiKey: string | undefined;
|
|
13
|
+
baseUrl?: string | undefined;
|
|
14
|
+
projectId?: string | undefined;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createCodeSparClient(options: CodeSparClientOptions): ApiClient {
|
|
19
|
+
const apiKey = assertTestKey(options.apiKey);
|
|
20
|
+
return new ApiClient({
|
|
21
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
22
|
+
apiKey,
|
|
23
|
+
...(options.projectId ? { projectId: options.projectId } : {}),
|
|
24
|
+
timeout: options.timeoutMs ?? 30_000,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ApiFailure {
|
|
29
|
+
status: number;
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
body?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Normalise the SDK's two error classes into one readable shape without leaking the body into logs. */
|
|
36
|
+
export function describeApiError(err: unknown): ApiFailure {
|
|
37
|
+
if (err instanceof TimeoutError) return { status: 0, code: "timeout", message: `request timed out after ${err.timeoutMs}ms` };
|
|
38
|
+
if (err instanceof CodesparApiError) {
|
|
39
|
+
const body = err.body as { error?: { code?: string; message?: string } } | undefined;
|
|
40
|
+
return {
|
|
41
|
+
status: err.status,
|
|
42
|
+
code: err.code ?? body?.error?.code ?? (err.status === 0 ? "network" : `http_${err.status}`),
|
|
43
|
+
message: body?.error?.message ?? err.message,
|
|
44
|
+
body: err.body,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { status: 0, code: "unknown", message: err instanceof Error ? err.message : String(err) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 5xx, timeouts and `psp_dispatch_uncertain` mean "the money may have moved". */
|
|
51
|
+
export function isUncertain(failure: ApiFailure): boolean {
|
|
52
|
+
return failure.status === 0 || failure.status >= 500 || failure.code === "psp_dispatch_uncertain" || failure.code === "psp_attempt_uncertain";
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandate status from the API: `GET /v1/mandates/{id}`, the one registered
|
|
3
|
+
* spelling of the consumer-allowance point read (the SDK's OpenAPI says the
|
|
4
|
+
* path has no alias; `/v1/consumers/mandates/{id}` answers 404 on staging,
|
|
5
|
+
* measured 2026-09-23). Consulted before `executing` (section 4.7), through
|
|
6
|
+
* the same `csk_test_` client every other call of the kit uses.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed: this source never throws and never assumes. A transport
|
|
9
|
+
* failure, a timeout, a 404, a 5xx, an unreadable body or a status outside
|
|
10
|
+
* `active | paused | revoked | expired` is reported as `unknown`, and the
|
|
11
|
+
* engine executes nothing on `unknown`.
|
|
12
|
+
*
|
|
13
|
+
* The organization kill switch (`org pauseAll`) has no API surface, so
|
|
14
|
+
* `org_paused` is `false` here: the API cannot say it, and this source does
|
|
15
|
+
* not invent it.
|
|
16
|
+
*/
|
|
17
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import type { MandateStatusReport, MandateStatusSource } from "../revocation.js";
|
|
20
|
+
import { describeApiError } from "./client.js";
|
|
21
|
+
|
|
22
|
+
/** The two fields of the 14-field projection the gate reads; the rest is passed through untouched. */
|
|
23
|
+
const MandateReadSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
status: z.enum(["active", "paused", "revoked", "expired"]),
|
|
26
|
+
expires_at: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.passthrough();
|
|
29
|
+
|
|
30
|
+
export class ApiMandateStatusSource implements MandateStatusSource {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly api: ApiClient,
|
|
33
|
+
private readonly clock: () => Date = () => new Date(),
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async check(mandateId: string): Promise<MandateStatusReport> {
|
|
37
|
+
const base = { mandate_id: mandateId, org_paused: false, checked_at: this.clock().toISOString(), source: "api" as const };
|
|
38
|
+
let body: unknown;
|
|
39
|
+
try {
|
|
40
|
+
body = await this.api.get("/v1/mandates/{id}", { path: { id: mandateId } });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const failure = describeApiError(err);
|
|
43
|
+
const detail =
|
|
44
|
+
failure.status === 404
|
|
45
|
+
? `the API has no mandate ${mandateId} for this key (${failure.code})`
|
|
46
|
+
: `GET /v1/mandates/${mandateId} did not answer (${failure.code}): ${failure.message}`;
|
|
47
|
+
return { ...base, status: "unknown", detail };
|
|
48
|
+
}
|
|
49
|
+
const parsed = MandateReadSchema.safeParse(body);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
const status = body && typeof body === "object" && "status" in body ? String((body as { status: unknown }).status) : "missing";
|
|
52
|
+
return { ...base, status: "unknown", detail: `GET /v1/mandates/${mandateId} answered a status this kit does not read (${status})` };
|
|
53
|
+
}
|
|
54
|
+
const expiresAt = new Date(parsed.data.expires_at).getTime();
|
|
55
|
+
const expired = Number.isFinite(expiresAt) && expiresAt <= this.clock().getTime();
|
|
56
|
+
return { ...base, status: expired && parsed.data.status === "active" ? "expired" : parsed.data.status };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeSpar sandbox rail. With the signed envelope the consent returned
|
|
3
|
+
* (`mandate.canonical` + `mandate.signature`) it spends through
|
|
4
|
+
* `POST /v1/consumer-payments/execute`; without it, by mandate id through
|
|
5
|
+
* `POST /v1/consumers/mandates/{id}/spend`. Both are idempotent on
|
|
6
|
+
* `attempt_id`. The by-id route answered `bad_signature` on staging for a
|
|
7
|
+
* mandate carrying `periodic_cap` (OPEN_QUESTIONS §14), which is why the
|
|
8
|
+
* envelope is preferred when it exists. Receipts come back from
|
|
9
|
+
* `GET /v1/consumers/receipts/{id}`.
|
|
10
|
+
*
|
|
11
|
+
* The API has no `actor` field on the wire today (OPEN_QUESTIONS §2). The
|
|
12
|
+
* spend carries `agent_id`, which the mandate binds; the full actor is
|
|
13
|
+
* stamped on the local receipt copy and on every event of the bundle.
|
|
14
|
+
*/
|
|
15
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
16
|
+
import type { Mandate } from "../mandate.js";
|
|
17
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
18
|
+
import type { Actor } from "../types.js";
|
|
19
|
+
import { describeApiError, isUncertain } from "./client.js";
|
|
20
|
+
|
|
21
|
+
export class CodeSparRail implements PaymentRail {
|
|
22
|
+
readonly name = "codespar" as const;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly api: ApiClient,
|
|
26
|
+
private readonly envelope?: Pick<Mandate, "canonical" | "signature"> | undefined,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
30
|
+
try {
|
|
31
|
+
const outcome =
|
|
32
|
+
this.envelope?.canonical && this.envelope.signature
|
|
33
|
+
? await this.api.post("/v1/consumer-payments/execute", {
|
|
34
|
+
body: {
|
|
35
|
+
mandate: this.envelope.canonical,
|
|
36
|
+
signature: this.envelope.signature,
|
|
37
|
+
amount_minor: payment.amount_minor,
|
|
38
|
+
purpose: payment.purpose,
|
|
39
|
+
agent_id: payment.agent_id,
|
|
40
|
+
payee: payment.payee,
|
|
41
|
+
attempt_id: payment.attempt_id,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
: await this.api.post("/v1/consumers/mandates/{id}/spend", {
|
|
45
|
+
path: { id: payment.mandate_id },
|
|
46
|
+
body: { amount_minor: payment.amount_minor, payee: payment.payee, agent_id: payment.agent_id, attempt_id: payment.attempt_id },
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
status: "settled",
|
|
50
|
+
transaction_id: outcome.payment.transactionId,
|
|
51
|
+
receipt_id: outcome.receipt?.id ?? null,
|
|
52
|
+
money_moved: outcome.payment.moneyMoved,
|
|
53
|
+
sandbox: !outcome.payment.moneyMoved,
|
|
54
|
+
raw: outcome,
|
|
55
|
+
};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const failure = describeApiError(err);
|
|
58
|
+
if (isUncertain(failure)) return { status: "uncertain", code: failure.code, message: failure.message };
|
|
59
|
+
return { status: "failed", code: failure.code, message: failure.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* There is no read route for an attempt. The documented way is to present
|
|
65
|
+
* the same `attempt_id` again: the lifecycle is idempotent on it and
|
|
66
|
+
* answers the state it reached. `psp_attempt_in_flight` means the first
|
|
67
|
+
* presentation is still running.
|
|
68
|
+
*/
|
|
69
|
+
async lookup(_attemptId: string, payment: RailPayment): Promise<RailLookup> {
|
|
70
|
+
const outcome = await this.pay(payment);
|
|
71
|
+
if (outcome.status === "failed" && outcome.code === "psp_attempt_in_flight") return { status: "in_flight" };
|
|
72
|
+
return outcome;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
76
|
+
try {
|
|
77
|
+
const r = await this.api.get("/v1/consumers/receipts/{id}", { path: { id: receiptId } });
|
|
78
|
+
return {
|
|
79
|
+
receipt_id: r.receipt_id,
|
|
80
|
+
state: r.state,
|
|
81
|
+
mandate: { id: r.mandate.id },
|
|
82
|
+
payment: {
|
|
83
|
+
amount_minor: r.payment.amount_minor,
|
|
84
|
+
payee: r.quote?.payee ?? null,
|
|
85
|
+
attempt_id: r.payment.attempt_id,
|
|
86
|
+
money_moved: r.payment.money_moved,
|
|
87
|
+
sandbox: r.payment.sandbox === true,
|
|
88
|
+
at: r.payment.at,
|
|
89
|
+
},
|
|
90
|
+
chain: r.chain,
|
|
91
|
+
receipt_sig: r.receipt_sig,
|
|
92
|
+
actor,
|
|
93
|
+
raw: r,
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const failure = describeApiError(err);
|
|
97
|
+
if (failure.status === 404) return undefined;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox PAYER: `POST /v1/test/charges/{chargeId}/pay` (alias
|
|
3
|
+
* `POST /v1/charges/{chargeId}/sandbox/pay`), test environment only. It
|
|
4
|
+
* plays the debtor: the charge goes through the same settlement path a
|
|
5
|
+
* provider `charge-in` webhook takes, `commerce.charge.paid` fans out to the
|
|
6
|
+
* project's triggers, and every record it leaves carries `simulated: true`
|
|
7
|
+
* plus `settled_against: "sandbox_fixture"`. No money moves anywhere. A
|
|
8
|
+
* live-environment key is refused with `sandbox_pay_not_permitted` before
|
|
9
|
+
* anything is read. The route is not in the SDK's typed operations yet
|
|
10
|
+
* (merged 2026-09-23), so it is called by path.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
13
|
+
import { describeApiError, type ApiFailure } from "./client.js";
|
|
14
|
+
|
|
15
|
+
export interface SandboxPaidState {
|
|
16
|
+
charge_id: string;
|
|
17
|
+
status: "paid";
|
|
18
|
+
local_status: string;
|
|
19
|
+
currency: string;
|
|
20
|
+
quoted_minor: number;
|
|
21
|
+
paid_minor: number;
|
|
22
|
+
payment: "full" | "partial" | "over";
|
|
23
|
+
paid_via: string;
|
|
24
|
+
wallet_id: string;
|
|
25
|
+
ledger_entry_id: string | null;
|
|
26
|
+
event: { id: string; type: string } | null;
|
|
27
|
+
simulated: boolean;
|
|
28
|
+
settled_against: string | null;
|
|
29
|
+
money_moved: false;
|
|
30
|
+
idempotent_replay: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SandboxPayResult = { ok: true; state: SandboxPaidState } | { ok: false; failure: ApiFailure };
|
|
34
|
+
|
|
35
|
+
export async function paySandboxCharge(api: ApiClient, chargeRef: string, amountMinor?: number): Promise<SandboxPayResult> {
|
|
36
|
+
try {
|
|
37
|
+
const state = (await api.request("post", "/v1/test/charges/{chargeId}/pay" as never, {
|
|
38
|
+
path: { chargeId: chargeRef },
|
|
39
|
+
body: amountMinor !== undefined ? { amount_minor: amountMinor } : {},
|
|
40
|
+
} as never)) as SandboxPaidState;
|
|
41
|
+
return { ok: true, state };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { ok: false, failure: describeApiError(err) };
|
|
44
|
+
}
|
|
45
|
+
}
|