@codespar/cli 0.13.0 → 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 +51 -1
- package/dist/commands/agent.js +13 -7
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payment rail the core dispatches to once an execution is `executing`.
|
|
3
|
+
* Two implementations: the CodeSpar sandbox through the API (needs a
|
|
4
|
+
* `csk_test_` key) and a local stub persisted in state.db for CI and
|
|
5
|
+
* scenarios. Both are idempotent on `attempt_id`: presenting the same
|
|
6
|
+
* attempt again answers the earlier outcome instead of paying twice.
|
|
7
|
+
*/
|
|
8
|
+
import type { Actor, ChargeInstrument } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export interface RailPayment {
|
|
11
|
+
attempt_id: string;
|
|
12
|
+
mandate_id: string;
|
|
13
|
+
amount_minor: number;
|
|
14
|
+
currency: string;
|
|
15
|
+
payee: string;
|
|
16
|
+
purpose: string;
|
|
17
|
+
agent_id: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Display name of the counterparty (a receivable's debtor). The rail that issues a charge needs it; a payout rail ignores it. */
|
|
20
|
+
beneficiary?: string;
|
|
21
|
+
/** Due date of a receivable, `YYYY-MM-DD`. */
|
|
22
|
+
due_date?: string;
|
|
23
|
+
/** Section 4.5: carried on every call. The API has no wire field for it yet; see OPEN_QUESTIONS. */
|
|
24
|
+
actor: Actor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RailOutcome =
|
|
28
|
+
| {
|
|
29
|
+
/**
|
|
30
|
+
* The rail took the attempt and the outcome comes later: a receivable
|
|
31
|
+
* was issued and its payer has not acted yet. The execution stays
|
|
32
|
+
* `executing`; `lookup()` or a `commerce.charge.*` event closes it.
|
|
33
|
+
*/
|
|
34
|
+
status: "accepted";
|
|
35
|
+
transaction_id: string;
|
|
36
|
+
instrument: ChargeInstrument;
|
|
37
|
+
sandbox: boolean;
|
|
38
|
+
raw: unknown;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
status: "settled";
|
|
42
|
+
transaction_id: string;
|
|
43
|
+
receipt_id: string | null;
|
|
44
|
+
money_moved: boolean;
|
|
45
|
+
sandbox: boolean;
|
|
46
|
+
raw: unknown;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
/** The rail answered and refused. Nothing moved. */
|
|
50
|
+
status: "failed";
|
|
51
|
+
code: string;
|
|
52
|
+
message: string;
|
|
53
|
+
raw?: unknown;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
/** The outcome is unknown (timeout, 5xx, `psp_dispatch_uncertain`). Never retried blind: reconciled. */
|
|
57
|
+
status: "uncertain";
|
|
58
|
+
code: string;
|
|
59
|
+
message: string;
|
|
60
|
+
raw?: unknown;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface RailReceipt {
|
|
64
|
+
receipt_id: string;
|
|
65
|
+
/** `payment` (the API's sealed receipt) or `charge` (the paid receivable as the API reports it; no seal exists for it today). */
|
|
66
|
+
kind?: "payment" | "charge";
|
|
67
|
+
state: string;
|
|
68
|
+
mandate: { id: string };
|
|
69
|
+
payment: { amount_minor: number; payee: string | null; attempt_id: string; money_moved: boolean; sandbox: boolean; at: string };
|
|
70
|
+
/** Null when the API seals nothing for this kind of record. */
|
|
71
|
+
chain: string | null;
|
|
72
|
+
receipt_sig: string | null;
|
|
73
|
+
/** Section 4.5: the kit stamps the actor onto the local copy of every receipt. */
|
|
74
|
+
actor: Actor;
|
|
75
|
+
raw: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What a reconcile learns: a recorded outcome, "still running", or nothing. Never a new payment. */
|
|
79
|
+
export type RailLookup = RailOutcome | { status: "in_flight" } | undefined;
|
|
80
|
+
|
|
81
|
+
export interface PaymentRail {
|
|
82
|
+
readonly name: "stub" | "codespar" | "stub-charge" | "codespar-charge";
|
|
83
|
+
pay(payment: RailPayment): Promise<RailOutcome>;
|
|
84
|
+
/** Answers the outcome of an attempt already presented, `in_flight` while the rail is still on it, or `undefined` when the rail never saw it. */
|
|
85
|
+
lookup(attemptId: string, payment: RailPayment): Promise<RailLookup>;
|
|
86
|
+
receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.7: the core asks a status source before `executing`. The
|
|
3
|
+
* approval artifact alone is not enough, because it may predate a
|
|
4
|
+
* revocation. The interface is the contract. Two implementations ship:
|
|
5
|
+
* `api/mandate-status.ts` reads `GET /v1/mandates/{id}` and is what a run
|
|
6
|
+
* with a test key gets; `stubs/mandate-status.ts` answers from the local
|
|
7
|
+
* state.db and is what a run without a key (the CI, the scenarios, `rerun`)
|
|
8
|
+
* gets, so revocation can be driven with no network.
|
|
9
|
+
*
|
|
10
|
+
* `unknown` is a status too: the source could not say. The engine treats it
|
|
11
|
+
* as "do not execute" (`mandate_status_unavailable`), never as `active`.
|
|
12
|
+
*/
|
|
13
|
+
export type MandateStatus = "active" | "paused" | "revoked" | "expired";
|
|
14
|
+
|
|
15
|
+
export interface MandateStatusReport {
|
|
16
|
+
mandate_id: string;
|
|
17
|
+
status: MandateStatus | "unknown";
|
|
18
|
+
/** True when the organization pressed the kill switch (`org pauseAll`). The API has no such read yet; only the stub can say it. */
|
|
19
|
+
org_paused: boolean;
|
|
20
|
+
checked_at: string;
|
|
21
|
+
/** Which source answered, so the bundle says whether the check was real. */
|
|
22
|
+
source: "stub" | "api";
|
|
23
|
+
/** Why the status is `unknown`, for the trail. Never carries a response body. */
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MandateStatusSource {
|
|
28
|
+
/** Must not throw for a read that failed: answer `unknown` and say why. A source that throws is still fail-closed at the gate. */
|
|
29
|
+
check(mandateId: string): Promise<MandateStatusReport>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox by construction: a key outside the `csk_test_` pattern fails
|
|
3
|
+
* before any network call. Nothing in this repository ever holds a live key.
|
|
4
|
+
*/
|
|
5
|
+
export const TEST_KEY_PREFIX = "csk_test_";
|
|
6
|
+
|
|
7
|
+
export class NotATestKeyError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(
|
|
10
|
+
`CODESPAR_API_KEY must be a test key (prefix "${TEST_KEY_PREFIX}"). ` +
|
|
11
|
+
"This kit runs in the sandbox only; a live key is refused before any call is made.",
|
|
12
|
+
);
|
|
13
|
+
this.name = "NotATestKeyError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isTestKey(key: string | undefined): key is string {
|
|
18
|
+
return typeof key === "string" && key.startsWith(TEST_KEY_PREFIX) && key.length > TEST_KEY_PREFIX.length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function assertTestKey(key: string | undefined): string {
|
|
22
|
+
if (!isTestKey(key)) throw new NotATestKeyError();
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** For logs and bundles: the prefix and the last four characters, nothing else. */
|
|
27
|
+
export function redactKey(key: string): string {
|
|
28
|
+
if (key.length <= 12) return "csk_****";
|
|
29
|
+
return `${key.slice(0, 9)}…${key.slice(-4)}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 10: local state in `.codespar/state.db` (SQLite, via `node:sqlite`,
|
|
3
|
+
* so there is no native build step). Executions and their states, the
|
|
4
|
+
* append-only event log, the event cursor, the outbox with its
|
|
5
|
+
* `idempotency_key`, the approval artifacts, and two stub tables the
|
|
6
|
+
* sandbox stubs persist into so a killed process finds them again.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { DatabaseSync } from "node:sqlite";
|
|
11
|
+
import type { Execution, ExecutionState } from "../state-machine.js";
|
|
12
|
+
import type { ApprovalArtifact } from "../types.js";
|
|
13
|
+
|
|
14
|
+
export interface EventRow {
|
|
15
|
+
seq: number;
|
|
16
|
+
run_id: string;
|
|
17
|
+
execution_id: string | null;
|
|
18
|
+
type: string;
|
|
19
|
+
payload: unknown;
|
|
20
|
+
at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OutboxStatus = "pending" | "sent" | "done" | "failed";
|
|
24
|
+
|
|
25
|
+
export interface OutboxRow {
|
|
26
|
+
idempotency_key: string;
|
|
27
|
+
execution_id: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
payload: unknown;
|
|
30
|
+
status: OutboxStatus;
|
|
31
|
+
response: unknown;
|
|
32
|
+
created_at: string;
|
|
33
|
+
updated_at: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SCHEMA = `
|
|
37
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
run_id TEXT NOT NULL,
|
|
40
|
+
state TEXT NOT NULL,
|
|
41
|
+
mandate_id TEXT NOT NULL,
|
|
42
|
+
json TEXT NOT NULL,
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS executions_run ON executions(run_id);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS executions_state ON executions(state);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
50
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
run_id TEXT NOT NULL,
|
|
52
|
+
execution_id TEXT,
|
|
53
|
+
event_id TEXT UNIQUE,
|
|
54
|
+
type TEXT NOT NULL,
|
|
55
|
+
payload TEXT NOT NULL,
|
|
56
|
+
at TEXT NOT NULL
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS cursors (
|
|
60
|
+
name TEXT PRIMARY KEY,
|
|
61
|
+
value TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
65
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
66
|
+
execution_id TEXT NOT NULL,
|
|
67
|
+
kind TEXT NOT NULL,
|
|
68
|
+
payload TEXT NOT NULL,
|
|
69
|
+
status TEXT NOT NULL,
|
|
70
|
+
response TEXT,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
updated_at TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
76
|
+
approval_id TEXT PRIMARY KEY,
|
|
77
|
+
execution_id TEXT NOT NULL,
|
|
78
|
+
json TEXT NOT NULL,
|
|
79
|
+
created_at TEXT NOT NULL
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS stub_rail_attempts (
|
|
83
|
+
attempt_id TEXT PRIMARY KEY,
|
|
84
|
+
request TEXT NOT NULL,
|
|
85
|
+
outcome TEXT NOT NULL,
|
|
86
|
+
at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS stub_mandate_status (
|
|
90
|
+
mandate_id TEXT PRIMARY KEY,
|
|
91
|
+
status TEXT NOT NULL,
|
|
92
|
+
reason TEXT,
|
|
93
|
+
updated_at TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS stub_org (
|
|
97
|
+
org_id TEXT PRIMARY KEY,
|
|
98
|
+
paused INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
updated_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
export class StateStore {
|
|
104
|
+
private readonly db: DatabaseSync;
|
|
105
|
+
|
|
106
|
+
constructor(readonly path: string) {
|
|
107
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
108
|
+
this.db = new DatabaseSync(path);
|
|
109
|
+
this.db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON;");
|
|
110
|
+
this.db.exec(SCHEMA);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
close(): void {
|
|
114
|
+
this.db.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
transaction<T>(fn: () => T): T {
|
|
118
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
119
|
+
try {
|
|
120
|
+
const out = fn();
|
|
121
|
+
this.db.exec("COMMIT");
|
|
122
|
+
return out;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this.db.exec("ROLLBACK");
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// executions
|
|
130
|
+
|
|
131
|
+
saveExecution(execution: Execution): void {
|
|
132
|
+
this.db
|
|
133
|
+
.prepare(
|
|
134
|
+
`INSERT INTO executions (id, run_id, state, mandate_id, json, created_at, updated_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
136
|
+
ON CONFLICT(id) DO UPDATE SET state = excluded.state, json = excluded.json, updated_at = excluded.updated_at`,
|
|
137
|
+
)
|
|
138
|
+
.run(execution.id, execution.run_id, execution.state, execution.mandate.id, JSON.stringify(execution), execution.created_at, execution.updated_at);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getExecution(id: string): Execution | undefined {
|
|
142
|
+
const row = this.db.prepare("SELECT json FROM executions WHERE id = ?").get(id) as { json: string } | undefined;
|
|
143
|
+
return row ? (JSON.parse(row.json) as Execution) : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
listExecutions(filter: { run_id?: string; state?: ExecutionState | ExecutionState[]; mandate_id?: string } = {}): Execution[] {
|
|
147
|
+
const where: string[] = [];
|
|
148
|
+
const args: string[] = [];
|
|
149
|
+
if (filter.run_id) {
|
|
150
|
+
where.push("run_id = ?");
|
|
151
|
+
args.push(filter.run_id);
|
|
152
|
+
}
|
|
153
|
+
if (filter.mandate_id) {
|
|
154
|
+
where.push("mandate_id = ?");
|
|
155
|
+
args.push(filter.mandate_id);
|
|
156
|
+
}
|
|
157
|
+
if (filter.state) {
|
|
158
|
+
const states = Array.isArray(filter.state) ? filter.state : [filter.state];
|
|
159
|
+
where.push(`state IN (${states.map(() => "?").join(",")})`);
|
|
160
|
+
args.push(...states);
|
|
161
|
+
}
|
|
162
|
+
const sql = `SELECT json FROM executions ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY created_at, id`;
|
|
163
|
+
return (this.db.prepare(sql).all(...args) as { json: string }[]).map((r) => JSON.parse(r.json) as Execution);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// events
|
|
167
|
+
|
|
168
|
+
appendEvent(event: { run_id: string; execution_id?: string | null; event_id?: string; type: string; payload: unknown; at: string }): EventRow | undefined {
|
|
169
|
+
if (event.event_id) {
|
|
170
|
+
const dup = this.db.prepare("SELECT seq FROM events WHERE event_id = ?").get(event.event_id);
|
|
171
|
+
if (dup) return undefined;
|
|
172
|
+
}
|
|
173
|
+
const result = this.db
|
|
174
|
+
.prepare("INSERT INTO events (run_id, execution_id, event_id, type, payload, at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
175
|
+
.run(event.run_id, event.execution_id ?? null, event.event_id ?? null, event.type, JSON.stringify(event.payload ?? null), event.at);
|
|
176
|
+
return { seq: Number(result.lastInsertRowid), run_id: event.run_id, execution_id: event.execution_id ?? null, type: event.type, payload: event.payload, at: event.at };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
listEvents(filter: { run_id?: string; execution_id?: string; after_seq?: number } = {}): EventRow[] {
|
|
180
|
+
const where: string[] = [];
|
|
181
|
+
const args: (string | number)[] = [];
|
|
182
|
+
if (filter.run_id) {
|
|
183
|
+
where.push("run_id = ?");
|
|
184
|
+
args.push(filter.run_id);
|
|
185
|
+
}
|
|
186
|
+
if (filter.execution_id) {
|
|
187
|
+
where.push("execution_id = ?");
|
|
188
|
+
args.push(filter.execution_id);
|
|
189
|
+
}
|
|
190
|
+
if (filter.after_seq !== undefined) {
|
|
191
|
+
where.push("seq > ?");
|
|
192
|
+
args.push(filter.after_seq);
|
|
193
|
+
}
|
|
194
|
+
const sql = `SELECT seq, run_id, execution_id, type, payload, at FROM events ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY seq`;
|
|
195
|
+
return (this.db.prepare(sql).all(...args) as Array<Omit<EventRow, "payload"> & { payload: string }>).map((r) => ({ ...r, payload: JSON.parse(r.payload) }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getCursor(name: string): string | undefined {
|
|
199
|
+
const row = this.db.prepare("SELECT value FROM cursors WHERE name = ?").get(name) as { value: string } | undefined;
|
|
200
|
+
return row?.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setCursor(name: string, value: string): void {
|
|
204
|
+
this.db.prepare("INSERT INTO cursors (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value").run(name, value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// outbox
|
|
208
|
+
|
|
209
|
+
putOutbox(row: Omit<OutboxRow, "updated_at">): void {
|
|
210
|
+
this.db
|
|
211
|
+
.prepare(
|
|
212
|
+
`INSERT INTO outbox (idempotency_key, execution_id, kind, payload, status, response, created_at, updated_at)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
214
|
+
ON CONFLICT(idempotency_key) DO NOTHING`,
|
|
215
|
+
)
|
|
216
|
+
.run(row.idempotency_key, row.execution_id, row.kind, JSON.stringify(row.payload), row.status, row.response === undefined ? null : JSON.stringify(row.response), row.created_at, row.created_at);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
updateOutbox(idempotencyKey: string, status: OutboxStatus, response: unknown, at: string): void {
|
|
220
|
+
this.db.prepare("UPDATE outbox SET status = ?, response = ?, updated_at = ? WHERE idempotency_key = ?").run(status, response === undefined ? null : JSON.stringify(response), at, idempotencyKey);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getOutbox(idempotencyKey: string): OutboxRow | undefined {
|
|
224
|
+
const row = this.db.prepare("SELECT * FROM outbox WHERE idempotency_key = ?").get(idempotencyKey) as (Omit<OutboxRow, "payload" | "response"> & { payload: string; response: string | null }) | undefined;
|
|
225
|
+
return row ? { ...row, payload: JSON.parse(row.payload), response: row.response ? JSON.parse(row.response) : undefined } : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
listOutbox(filter: { execution_id?: string; status?: OutboxStatus[] } = {}): OutboxRow[] {
|
|
229
|
+
const where: string[] = [];
|
|
230
|
+
const args: string[] = [];
|
|
231
|
+
if (filter.execution_id) {
|
|
232
|
+
where.push("execution_id = ?");
|
|
233
|
+
args.push(filter.execution_id);
|
|
234
|
+
}
|
|
235
|
+
if (filter.status) {
|
|
236
|
+
where.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
237
|
+
args.push(...filter.status);
|
|
238
|
+
}
|
|
239
|
+
const sql = `SELECT * FROM outbox ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY created_at, idempotency_key`;
|
|
240
|
+
return (this.db.prepare(sql).all(...args) as Array<Omit<OutboxRow, "payload" | "response"> & { payload: string; response: string | null }>).map((r) => ({
|
|
241
|
+
...r,
|
|
242
|
+
payload: JSON.parse(r.payload),
|
|
243
|
+
response: r.response ? JSON.parse(r.response) : undefined,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// approvals
|
|
248
|
+
|
|
249
|
+
saveApproval(artifact: ApprovalArtifact): void {
|
|
250
|
+
this.db.prepare("INSERT OR REPLACE INTO approvals (approval_id, execution_id, json, created_at) VALUES (?, ?, ?, ?)").run(artifact.approval_id, artifact.execution_id, JSON.stringify(artifact), artifact.approved_at);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getApproval(approvalId: string): ApprovalArtifact | undefined {
|
|
254
|
+
const row = this.db.prepare("SELECT json FROM approvals WHERE approval_id = ?").get(approvalId) as { json: string } | undefined;
|
|
255
|
+
return row ? (JSON.parse(row.json) as ApprovalArtifact) : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// stub rail (sandbox stand-in, persisted so a restart still sees the attempt)
|
|
259
|
+
|
|
260
|
+
stubRailGet(attemptId: string): { request: unknown; outcome: unknown; at: string } | undefined {
|
|
261
|
+
const row = this.db.prepare("SELECT request, outcome, at FROM stub_rail_attempts WHERE attempt_id = ?").get(attemptId) as { request: string; outcome: string; at: string } | undefined;
|
|
262
|
+
return row ? { request: JSON.parse(row.request), outcome: JSON.parse(row.outcome), at: row.at } : undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
stubRailFindByReceipt(receiptId: string): { request: unknown; outcome: unknown; at: string } | undefined {
|
|
266
|
+
const row = this.db.prepare("SELECT request, outcome, at FROM stub_rail_attempts WHERE json_extract(outcome, '$.receipt_id') = ?").get(receiptId) as { request: string; outcome: string; at: string } | undefined;
|
|
267
|
+
return row ? { request: JSON.parse(row.request), outcome: JSON.parse(row.outcome), at: row.at } : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
stubRailFindByTransaction(transactionId: string): { request: unknown; outcome: unknown; at: string } | undefined {
|
|
271
|
+
const row = this.db.prepare("SELECT request, outcome, at FROM stub_rail_attempts WHERE json_extract(outcome, '$.transaction_id') = ?").get(transactionId) as { request: string; outcome: string; at: string } | undefined;
|
|
272
|
+
return row ? { request: JSON.parse(row.request), outcome: JSON.parse(row.outcome), at: row.at } : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
stubRailPut(attemptId: string, request: unknown, outcome: unknown, at: string): boolean {
|
|
276
|
+
const result = this.db.prepare("INSERT OR IGNORE INTO stub_rail_attempts (attempt_id, request, outcome, at) VALUES (?, ?, ?, ?)").run(attemptId, JSON.stringify(request), JSON.stringify(outcome), at);
|
|
277
|
+
return result.changes === 1;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The stub receivable rail: an issued charge changes state as its payer acts, under the same attempt id. */
|
|
281
|
+
stubRailReplace(attemptId: string, request: unknown, outcome: unknown, at: string): void {
|
|
282
|
+
this.db
|
|
283
|
+
.prepare("INSERT INTO stub_rail_attempts (attempt_id, request, outcome, at) VALUES (?, ?, ?, ?) ON CONFLICT(attempt_id) DO UPDATE SET outcome = excluded.outcome, at = excluded.at")
|
|
284
|
+
.run(attemptId, JSON.stringify(request), JSON.stringify(outcome), at);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// stub mandate status / org pause (the local stand-in of stubs/mandate-status.ts, for runs without a key)
|
|
288
|
+
|
|
289
|
+
stubMandateStatus(mandateId: string): { status: string; reason: string | null } | undefined {
|
|
290
|
+
return this.db.prepare("SELECT status, reason FROM stub_mandate_status WHERE mandate_id = ?").get(mandateId) as { status: string; reason: string | null } | undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
stubSetMandateStatus(mandateId: string, status: string, reason: string | null, at: string): void {
|
|
294
|
+
this.db
|
|
295
|
+
.prepare("INSERT INTO stub_mandate_status (mandate_id, status, reason, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(mandate_id) DO UPDATE SET status = excluded.status, reason = excluded.reason, updated_at = excluded.updated_at")
|
|
296
|
+
.run(mandateId, status, reason, at);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
stubOrgPaused(orgId: string): boolean {
|
|
300
|
+
const row = this.db.prepare("SELECT paused FROM stub_org WHERE org_id = ?").get(orgId) as { paused: number } | undefined;
|
|
301
|
+
return row?.paused === 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
stubSetOrgPaused(orgId: string, paused: boolean, at: string): void {
|
|
305
|
+
this.db.prepare("INSERT INTO stub_org (org_id, paused, updated_at) VALUES (?, ?, ?) ON CONFLICT(org_id) DO UPDATE SET paused = excluded.paused, updated_at = excluded.updated_at").run(orgId, paused ? 1 : 0, at);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.1: the execution as a closed state machine.
|
|
3
|
+
*
|
|
4
|
+
* The transition table is a type. `transition()` only accepts a target that
|
|
5
|
+
* the table lists for the current state, so a transition outside the table
|
|
6
|
+
* does not compile; the runtime check below refuses the same pair when the
|
|
7
|
+
* types were bypassed (a cast, a row read back from SQLite).
|
|
8
|
+
*/
|
|
9
|
+
import type { Actor, ApprovalMode, EscalationTrigger, ExecutionItem, ExecutionReason, ItemOutcome, MandateRef } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export type ExecutionState =
|
|
12
|
+
| "drafted"
|
|
13
|
+
| "awaiting_approval"
|
|
14
|
+
| "approved"
|
|
15
|
+
| "executing"
|
|
16
|
+
| "settled"
|
|
17
|
+
| "failed"
|
|
18
|
+
| "denied"
|
|
19
|
+
| "expired";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The table of section 4.1, plus the two exits section 4.7 adds (revocation
|
|
23
|
+
* from `drafted` / `approved`) and the return section 4.2 adds (`approved`
|
|
24
|
+
* back to `awaiting_approval` when the list changed after approval).
|
|
25
|
+
*/
|
|
26
|
+
export type TransitionTable = {
|
|
27
|
+
drafted: "awaiting_approval" | "approved" | "denied" | "expired";
|
|
28
|
+
awaiting_approval: "approved" | "denied" | "expired";
|
|
29
|
+
approved: "executing" | "awaiting_approval" | "denied" | "expired";
|
|
30
|
+
executing: "settled" | "failed";
|
|
31
|
+
settled: never;
|
|
32
|
+
failed: never;
|
|
33
|
+
denied: never;
|
|
34
|
+
expired: never;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NextState<S extends ExecutionState> = TransitionTable[S];
|
|
38
|
+
|
|
39
|
+
export const TRANSITIONS: { readonly [S in ExecutionState]: readonly TransitionTable[S][] } = {
|
|
40
|
+
drafted: ["awaiting_approval", "approved", "denied", "expired"],
|
|
41
|
+
awaiting_approval: ["approved", "denied", "expired"],
|
|
42
|
+
approved: ["executing", "awaiting_approval", "denied", "expired"],
|
|
43
|
+
executing: ["settled", "failed"],
|
|
44
|
+
settled: [],
|
|
45
|
+
failed: [],
|
|
46
|
+
denied: [],
|
|
47
|
+
expired: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const TERMINAL_STATES: readonly ExecutionState[] = ["settled", "failed", "denied", "expired"];
|
|
51
|
+
|
|
52
|
+
export function isTerminal(state: ExecutionState): boolean {
|
|
53
|
+
return TERMINAL_STATES.includes(state);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TransitionRecord {
|
|
57
|
+
from: ExecutionState;
|
|
58
|
+
to: ExecutionState;
|
|
59
|
+
at: string;
|
|
60
|
+
reason?: ExecutionReason;
|
|
61
|
+
detail?: string;
|
|
62
|
+
actor: Actor;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Execution<S extends ExecutionState = ExecutionState> {
|
|
66
|
+
id: string;
|
|
67
|
+
run_id: string;
|
|
68
|
+
state: S;
|
|
69
|
+
mode: ApprovalMode;
|
|
70
|
+
/** Who created the execution: always the agent acting for someone. */
|
|
71
|
+
actor: Actor;
|
|
72
|
+
items: ExecutionItem[];
|
|
73
|
+
/** Computed by the core, never by the model. */
|
|
74
|
+
total: number;
|
|
75
|
+
currency: string;
|
|
76
|
+
/** What the model claimed the total was, when it claimed one. Recorded, never used. */
|
|
77
|
+
model_claimed_total?: number;
|
|
78
|
+
items_hash: string;
|
|
79
|
+
mandate: MandateRef;
|
|
80
|
+
/** Written once, before the rail is called. Retrying reuses it. */
|
|
81
|
+
idempotency_key: string;
|
|
82
|
+
approval_id?: string;
|
|
83
|
+
escalation?: { trigger: EscalationTrigger; detail: string };
|
|
84
|
+
/** Reasons the core will refuse to approve, even if a human says yes. */
|
|
85
|
+
blocking_reasons: ExecutionReason[];
|
|
86
|
+
reason?: ExecutionReason;
|
|
87
|
+
detail?: string;
|
|
88
|
+
outcomes: ItemOutcome[];
|
|
89
|
+
history: TransitionRecord[];
|
|
90
|
+
created_at: string;
|
|
91
|
+
updated_at: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class IllegalTransitionError extends Error {
|
|
95
|
+
constructor(
|
|
96
|
+
readonly executionId: string,
|
|
97
|
+
readonly from: ExecutionState,
|
|
98
|
+
readonly to: ExecutionState,
|
|
99
|
+
) {
|
|
100
|
+
super(`illegal transition ${from} -> ${to} on ${executionId}`);
|
|
101
|
+
this.name = "IllegalTransitionError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function canTransition(from: ExecutionState, to: ExecutionState): boolean {
|
|
106
|
+
return (TRANSITIONS[from] as readonly ExecutionState[]).includes(to);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface TransitionInput {
|
|
110
|
+
at: string;
|
|
111
|
+
actor: Actor;
|
|
112
|
+
reason?: ExecutionReason;
|
|
113
|
+
detail?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Move an execution to a state the table allows. Returns a new object with
|
|
118
|
+
* the transition appended to `history`; the caller persists it.
|
|
119
|
+
*/
|
|
120
|
+
export function transition<S extends ExecutionState, T extends NextState<S>>(
|
|
121
|
+
execution: Execution<S>,
|
|
122
|
+
to: T,
|
|
123
|
+
input: TransitionInput,
|
|
124
|
+
): Execution<T> {
|
|
125
|
+
if (!canTransition(execution.state, to)) {
|
|
126
|
+
throw new IllegalTransitionError(execution.id, execution.state, to);
|
|
127
|
+
}
|
|
128
|
+
const record: TransitionRecord = {
|
|
129
|
+
from: execution.state,
|
|
130
|
+
to,
|
|
131
|
+
at: input.at,
|
|
132
|
+
actor: input.actor,
|
|
133
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
134
|
+
...(input.detail ? { detail: input.detail } : {}),
|
|
135
|
+
};
|
|
136
|
+
// `reason`/`detail` describe the LATEST transition; a step without one clears what the previous step left.
|
|
137
|
+
const { reason: _reason, detail: _detail, ...rest } = execution;
|
|
138
|
+
return {
|
|
139
|
+
...rest,
|
|
140
|
+
state: to,
|
|
141
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
142
|
+
...(input.detail ? { detail: input.detail } : {}),
|
|
143
|
+
history: [...execution.history, record],
|
|
144
|
+
updated_at: input.at,
|
|
145
|
+
} as Execution<T>;
|
|
146
|
+
}
|