@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STUB receivable rail. The deterministic stand-in for `POST /v1/charges`
|
|
3
|
+
* plus the sandbox payer, so the collections scenarios, the adversarial
|
|
4
|
+
* suite and the restart test run offline. Every issuance is persisted in
|
|
5
|
+
* state.db before the outcome is returned, so a process killed right after
|
|
6
|
+
* "the charge was issued" finds it again on `resume`.
|
|
7
|
+
*
|
|
8
|
+
* The fixture payer lives in the lookups: the first look after issuance
|
|
9
|
+
* finds the instrument registered (a cobranca com vencimento answers
|
|
10
|
+
* PROCESSING at create and PENDING once the clearing house has it), the
|
|
11
|
+
* second look finds what the payer did: paid (default), let it expire, or
|
|
12
|
+
* nothing yet. With a real key the scenario runner calls the sandbox pay
|
|
13
|
+
* route instead; this file never talks to the network.
|
|
14
|
+
*/
|
|
15
|
+
import { sha256Hex } from "../hash.js";
|
|
16
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
17
|
+
import type { StateStore } from "../state/store.js";
|
|
18
|
+
import type { Actor, ChargeInstrument } from "../types.js";
|
|
19
|
+
|
|
20
|
+
export type StubPayerBehaviour = "pays" | "expires" | "never";
|
|
21
|
+
|
|
22
|
+
export interface StubChargeRailOptions {
|
|
23
|
+
clock?: () => Date;
|
|
24
|
+
/** What the fixture payer does with a receivable, once it is payable. */
|
|
25
|
+
payer?: StubPayerBehaviour;
|
|
26
|
+
/** Debtor documents the issuer refuses (`failed` issuance), to script a partial failure. */
|
|
27
|
+
refusePayees?: string[];
|
|
28
|
+
/** Attempt ids whose create answers `uncertain` once (the issuer's answer was lost); the reservation still exists and a later look finds it. */
|
|
29
|
+
uncertainOnce?: string[];
|
|
30
|
+
/** Test hook: called after the issuance is persisted and before the outcome is returned. */
|
|
31
|
+
afterDispatch?: (attemptId: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PayerState {
|
|
35
|
+
looks: number;
|
|
36
|
+
fate: StubPayerBehaviour;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CURSOR = (attemptId: string) => `stub-charge-payer:${attemptId}`;
|
|
40
|
+
|
|
41
|
+
export class StubChargeRail implements PaymentRail {
|
|
42
|
+
readonly name = "stub-charge" as const;
|
|
43
|
+
private readonly clock: () => Date;
|
|
44
|
+
private fate: StubPayerBehaviour;
|
|
45
|
+
private readonly uncertainPending: Set<string>;
|
|
46
|
+
/** How many NEW receivables pay() issued. */
|
|
47
|
+
issueCount = 0;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly store: StateStore,
|
|
51
|
+
private readonly options: StubChargeRailOptions = {},
|
|
52
|
+
) {
|
|
53
|
+
this.clock = options.clock ?? (() => new Date());
|
|
54
|
+
this.fate = options.payer ?? "pays";
|
|
55
|
+
this.uncertainPending = new Set(options.uncertainOnce ?? []);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Changes what the fixture payer does with the receivables issued from now on. */
|
|
59
|
+
setPayer(behaviour: StubPayerBehaviour): void {
|
|
60
|
+
this.fate = behaviour;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Scripts the payer for ONE already-issued receivable (a scenario that pays or expires a specific parcel). */
|
|
64
|
+
decide(attemptId: string, behaviour: StubPayerBehaviour): void {
|
|
65
|
+
const state = this.payerState(attemptId);
|
|
66
|
+
this.savePayerState(attemptId, { looks: state?.looks ?? 0, fate: behaviour });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
70
|
+
const existing = this.store.stubRailGet(payment.attempt_id);
|
|
71
|
+
if (existing) return existing.outcome as RailOutcome;
|
|
72
|
+
|
|
73
|
+
if (this.uncertainPending.has(payment.attempt_id)) {
|
|
74
|
+
this.uncertainPending.delete(payment.attempt_id);
|
|
75
|
+
// The issuer took the create and the answer was lost: the reservation exists, so a later lookup finds it.
|
|
76
|
+
this.persistIssued(payment, "PROCESSING", false);
|
|
77
|
+
return { status: "uncertain", code: "issuance_unconfirmed", message: "stub: the issuer's answer to the create was lost; the key is reserved" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const at = this.clock().toISOString();
|
|
81
|
+
if (this.options.refusePayees?.includes(payment.payee)) {
|
|
82
|
+
const outcome: RailOutcome = { status: "failed", code: "issuer_refused", message: `stub: issuer refused a charge to ${payment.payee}` };
|
|
83
|
+
const { actor: _actor, ...request } = payment;
|
|
84
|
+
this.store.stubRailPut(payment.attempt_id, request, outcome, at);
|
|
85
|
+
this.options.afterDispatch?.(payment.attempt_id);
|
|
86
|
+
return outcome;
|
|
87
|
+
}
|
|
88
|
+
const outcome = this.persistIssued(payment, "PROCESSING", false);
|
|
89
|
+
this.options.afterDispatch?.(payment.attempt_id);
|
|
90
|
+
return outcome;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async lookup(attemptId: string, payment: RailPayment): Promise<RailLookup> {
|
|
94
|
+
const recorded = this.store.stubRailGet(attemptId);
|
|
95
|
+
if (!recorded) return undefined;
|
|
96
|
+
const outcome = recorded.outcome as RailOutcome;
|
|
97
|
+
if (outcome.status !== "accepted") return outcome;
|
|
98
|
+
|
|
99
|
+
const state = this.payerState(attemptId) ?? { looks: 0, fate: this.fate };
|
|
100
|
+
const looks = state.looks + 1;
|
|
101
|
+
this.savePayerState(attemptId, { ...state, looks });
|
|
102
|
+
|
|
103
|
+
// First look: the clearing house registered the instrument. It is payable from here.
|
|
104
|
+
if (looks === 1 || !outcome.instrument.payable) {
|
|
105
|
+
return this.update(attemptId, payment, "PENDING", true);
|
|
106
|
+
}
|
|
107
|
+
if (state.fate === "pays") {
|
|
108
|
+
const settled: RailOutcome = {
|
|
109
|
+
status: "settled",
|
|
110
|
+
transaction_id: outcome.transaction_id,
|
|
111
|
+
receipt_id: outcome.transaction_id,
|
|
112
|
+
money_moved: false,
|
|
113
|
+
sandbox: true,
|
|
114
|
+
raw: { stub: true, attempt_id: attemptId, simulated: true, settled_against: "sandbox_fixture", paid_at: this.clock().toISOString() },
|
|
115
|
+
};
|
|
116
|
+
this.replace(attemptId, payment, settled);
|
|
117
|
+
return settled;
|
|
118
|
+
}
|
|
119
|
+
if (state.fate === "expires") {
|
|
120
|
+
const failed: RailOutcome = { status: "failed", code: "charge_expired", message: `stub: the receivable ${outcome.transaction_id} reached its due date unpaid`, raw: { stub: true } };
|
|
121
|
+
this.replace(attemptId, payment, failed);
|
|
122
|
+
return failed;
|
|
123
|
+
}
|
|
124
|
+
return outcome;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
128
|
+
// The paid charge IS the record: found by its receipt id once the payer acted here, or by its charge id when the settlement came by event.
|
|
129
|
+
const sealed = this.store.stubRailFindByReceipt(receiptId) ?? this.store.stubRailFindByTransaction(receiptId);
|
|
130
|
+
if (!sealed) return undefined;
|
|
131
|
+
const req = sealed.request as Omit<RailPayment, "actor">;
|
|
132
|
+
const out = sealed.outcome as Extract<RailOutcome, { status: "settled" | "accepted" }>;
|
|
133
|
+
const raw = out.raw as { paid_at?: string };
|
|
134
|
+
return {
|
|
135
|
+
receipt_id: receiptId,
|
|
136
|
+
kind: "charge",
|
|
137
|
+
state: "paid",
|
|
138
|
+
mandate: { id: req.mandate_id },
|
|
139
|
+
payment: { amount_minor: req.amount_minor, payee: req.payee, attempt_id: req.attempt_id, money_moved: false, sandbox: true, at: raw.paid_at ?? sealed.at },
|
|
140
|
+
chain: null,
|
|
141
|
+
receipt_sig: null,
|
|
142
|
+
actor,
|
|
143
|
+
raw: { stub: true, charge_id: out.transaction_id, simulated: true, settled_against: "sandbox_fixture" },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private instrument(payment: RailPayment, status: string, payable: boolean, chargeId: string): ChargeInstrument {
|
|
148
|
+
return {
|
|
149
|
+
payable,
|
|
150
|
+
status,
|
|
151
|
+
pix_copy_paste: payable ? `00020126580014br.gov.bcb.pix0136stub-${chargeId}5204000053039865802BR5909CODESPAR6009SAO PAULO62070503***6304STUB` : null,
|
|
152
|
+
boleto_bank_line: payable ? `${sha256Hex(chargeId).replace(/[a-f]/g, "").padEnd(47, "0").slice(0, 47)}` : null,
|
|
153
|
+
boleto_bar_code: payable ? `${sha256Hex(`bar:${chargeId}`).replace(/[a-f]/g, "").padEnd(44, "0").slice(0, 44)}` : null,
|
|
154
|
+
due_date: payment.due_date ?? null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private persistIssued(payment: RailPayment, status: string, payable: boolean): Extract<RailOutcome, { status: "accepted" }> {
|
|
159
|
+
const chargeId = `chg_stub_${sha256Hex(payment.attempt_id).slice(0, 16)}`;
|
|
160
|
+
const outcome: Extract<RailOutcome, { status: "accepted" }> = {
|
|
161
|
+
status: "accepted",
|
|
162
|
+
transaction_id: chargeId,
|
|
163
|
+
instrument: this.instrument(payment, status, payable, chargeId),
|
|
164
|
+
sandbox: true,
|
|
165
|
+
raw: { stub: true, attempt_id: payment.attempt_id, issued_at: this.clock().toISOString() },
|
|
166
|
+
};
|
|
167
|
+
const { actor: _actor, ...request } = payment;
|
|
168
|
+
if (this.store.stubRailPut(payment.attempt_id, request, outcome, this.clock().toISOString())) this.issueCount += 1;
|
|
169
|
+
return outcome;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private update(attemptId: string, payment: RailPayment, status: string, payable: boolean): Extract<RailOutcome, { status: "accepted" }> {
|
|
173
|
+
const recorded = this.store.stubRailGet(attemptId)!;
|
|
174
|
+
const prior = recorded.outcome as Extract<RailOutcome, { status: "accepted" }>;
|
|
175
|
+
const outcome = { ...prior, instrument: this.instrument(payment, status, payable, prior.transaction_id) };
|
|
176
|
+
this.replace(attemptId, payment, outcome);
|
|
177
|
+
return outcome;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private replace(attemptId: string, payment: RailPayment, outcome: RailOutcome): void {
|
|
181
|
+
const { actor: _actor, ...request } = payment;
|
|
182
|
+
this.store.stubRailReplace(attemptId, request, outcome, this.clock().toISOString());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private payerState(attemptId: string): PayerState | undefined {
|
|
186
|
+
const raw = this.store.getCursor(CURSOR(attemptId));
|
|
187
|
+
return raw ? (JSON.parse(raw) as PayerState) : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private savePayerState(attemptId: string, state: PayerState): void {
|
|
191
|
+
this.store.setCursor(CURSOR(attemptId), JSON.stringify(state));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STUB, for runs without a key: the CI, the scenarios and `rerun`, where
|
|
3
|
+
* there is no network. Answers the section 4.7 status check from the local
|
|
4
|
+
* state.db, so a scenario can revoke or pause a mandate mid-run
|
|
5
|
+
* (`before_decision`, `before_execute` in the scenario packs) and the core
|
|
6
|
+
* reacts exactly as it does against the API. Behind the same
|
|
7
|
+
* `MandateStatusSource` interface as `api/mandate-status.ts`, which is what
|
|
8
|
+
* a run with a test key gets instead; this file is never consulted on that
|
|
9
|
+
* path.
|
|
10
|
+
*
|
|
11
|
+
* It also holds the organization kill switch (`pauseAll`), which the API
|
|
12
|
+
* does not expose yet, so `org_paused` can be exercised locally.
|
|
13
|
+
*/
|
|
14
|
+
import type { MandateStatusSource, MandateStatus, MandateStatusReport } from "../revocation.js";
|
|
15
|
+
import type { StateStore } from "../state/store.js";
|
|
16
|
+
|
|
17
|
+
export const STUB_ORG_ID = "org_local_stub";
|
|
18
|
+
|
|
19
|
+
export class LocalMandateStatusStub implements MandateStatusSource {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly store: StateStore,
|
|
22
|
+
private readonly clock: () => Date = () => new Date(),
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async check(mandateId: string): Promise<MandateStatusReport> {
|
|
26
|
+
const row = this.store.stubMandateStatus(mandateId);
|
|
27
|
+
return {
|
|
28
|
+
mandate_id: mandateId,
|
|
29
|
+
status: (row?.status as MandateStatus | undefined) ?? "active",
|
|
30
|
+
org_paused: this.store.stubOrgPaused(STUB_ORG_ID),
|
|
31
|
+
checked_at: this.clock().toISOString(),
|
|
32
|
+
source: "stub",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `codespar mandate revoke <id>`, locally. */
|
|
37
|
+
revoke(mandateId: string, reason = "revoked by operator"): void {
|
|
38
|
+
this.store.stubSetMandateStatus(mandateId, "revoked", reason, this.clock().toISOString());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pause(mandateId: string, reason = "paused by operator"): void {
|
|
42
|
+
this.store.stubSetMandateStatus(mandateId, "paused", reason, this.clock().toISOString());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resume(mandateId: string): void {
|
|
46
|
+
this.store.stubSetMandateStatus(mandateId, "active", null, this.clock().toISOString());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The organization kill switch, locally: the API has no `org pauseAll` yet. */
|
|
50
|
+
pauseAll(): void {
|
|
51
|
+
this.store.stubSetOrgPaused(STUB_ORG_ID, true, this.clock().toISOString());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resumeAll(): void {
|
|
55
|
+
this.store.stubSetOrgPaused(STUB_ORG_ID, false, this.clock().toISOString());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STUB rail. A deterministic stand-in for the CodeSpar sandbox so the
|
|
3
|
+
* scenarios, the adversarial suite and the restart test run offline. Every
|
|
4
|
+
* attempt is persisted in state.db before the outcome is returned, which is
|
|
5
|
+
* what lets a process killed right after "the money left" be reconciled on
|
|
6
|
+
* `resume` instead of paying again. Receipts here are HMAC-shaped only in
|
|
7
|
+
* form; nothing about them is verifiable by anyone.
|
|
8
|
+
*/
|
|
9
|
+
import { sha256Hex } from "../hash.js";
|
|
10
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
11
|
+
import type { StateStore } from "../state/store.js";
|
|
12
|
+
import type { Actor } from "../types.js";
|
|
13
|
+
|
|
14
|
+
export interface StubRailOptions {
|
|
15
|
+
clock?: () => Date;
|
|
16
|
+
/** Payees the stub refuses, to script a `failed` outcome. */
|
|
17
|
+
refusePayees?: string[];
|
|
18
|
+
/** Attempt ids the stub answers `uncertain` for, once. */
|
|
19
|
+
uncertainOnce?: string[];
|
|
20
|
+
/** Test hook: called after the attempt is persisted and before the outcome is returned. */
|
|
21
|
+
afterDispatch?: (attemptId: string) => void;
|
|
22
|
+
/** Attempt ids whose first lookup answers `in_flight` and whose second lookup finds the provider finished (settled), with no pay() in between. */
|
|
23
|
+
inFlightThenSettled?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class StubRail implements PaymentRail {
|
|
27
|
+
readonly name = "stub" as const;
|
|
28
|
+
private readonly clock: () => Date;
|
|
29
|
+
private readonly uncertainPending: Set<string>;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly store: StateStore,
|
|
33
|
+
private readonly options: StubRailOptions = {},
|
|
34
|
+
) {
|
|
35
|
+
this.clock = options.clock ?? (() => new Date());
|
|
36
|
+
this.uncertainPending = new Set(options.uncertainOnce ?? []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private uncertainArmed = false;
|
|
40
|
+
private readonly inFlightSeen = new Set<string>();
|
|
41
|
+
/** Attempts that answered `uncertain` because the stub was armed: the "provider" did take them, and a later lookup finds them finished. */
|
|
42
|
+
private readonly lateAttempts = new Set<string>();
|
|
43
|
+
/** How many times pay() reached the point of persisting a NEW attempt. */
|
|
44
|
+
payCount = 0;
|
|
45
|
+
|
|
46
|
+
/** The NEXT new attempt answers `uncertain` once, whatever its id; the provider still took it, so a later lookup sees it in flight and then settled. For scenarios that reconcile. */
|
|
47
|
+
armUncertainOnce(): void {
|
|
48
|
+
this.uncertainArmed = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
52
|
+
const existing = this.store.stubRailGet(payment.attempt_id);
|
|
53
|
+
if (existing) return existing.outcome as RailOutcome;
|
|
54
|
+
|
|
55
|
+
if (this.uncertainArmed || this.uncertainPending.has(payment.attempt_id)) {
|
|
56
|
+
if (this.uncertainArmed) this.lateAttempts.add(payment.attempt_id);
|
|
57
|
+
this.uncertainArmed = false;
|
|
58
|
+
this.uncertainPending.delete(payment.attempt_id);
|
|
59
|
+
return { status: "uncertain", code: "psp_dispatch_uncertain", message: "stub: outcome unknown on first presentation" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const at = this.clock().toISOString();
|
|
63
|
+
const outcome: RailOutcome = this.options.refusePayees?.includes(payment.payee)
|
|
64
|
+
? { status: "failed", code: "psp_dispatch_failed", message: `stub: provider refused payee ${payment.payee}` }
|
|
65
|
+
: {
|
|
66
|
+
status: "settled",
|
|
67
|
+
transaction_id: `stubtx_${sha256Hex(payment.attempt_id).slice(0, 16)}`,
|
|
68
|
+
receipt_id: `rcpt_stub_${sha256Hex(`receipt:${payment.attempt_id}`).slice(0, 16)}`,
|
|
69
|
+
money_moved: false,
|
|
70
|
+
sandbox: true,
|
|
71
|
+
raw: { stub: true, attempt_id: payment.attempt_id, at },
|
|
72
|
+
};
|
|
73
|
+
const { actor: _actor, ...request } = payment;
|
|
74
|
+
this.payCount += 1;
|
|
75
|
+
this.store.stubRailPut(payment.attempt_id, request, outcome, at);
|
|
76
|
+
this.options.afterDispatch?.(payment.attempt_id);
|
|
77
|
+
return outcome;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async lookup(attemptId: string, payment: RailPayment): Promise<RailLookup> {
|
|
81
|
+
const recorded = this.store.stubRailGet(attemptId)?.outcome as RailOutcome | undefined;
|
|
82
|
+
if (recorded) return recorded;
|
|
83
|
+
if (this.options.inFlightThenSettled?.includes(attemptId) || this.lateAttempts.has(attemptId)) {
|
|
84
|
+
if (!this.inFlightSeen.has(attemptId)) {
|
|
85
|
+
this.inFlightSeen.add(attemptId);
|
|
86
|
+
return { status: "in_flight" };
|
|
87
|
+
}
|
|
88
|
+
// The provider finished on its own; the stub records the outcome as the rail would, without a new dispatch.
|
|
89
|
+
const { actor: _actor, ...request } = payment;
|
|
90
|
+
const outcome: RailOutcome = {
|
|
91
|
+
status: "settled",
|
|
92
|
+
transaction_id: `stubtx_${sha256Hex(attemptId).slice(0, 16)}`,
|
|
93
|
+
receipt_id: `rcpt_stub_${sha256Hex(`receipt:${attemptId}`).slice(0, 16)}`,
|
|
94
|
+
money_moved: false,
|
|
95
|
+
sandbox: true,
|
|
96
|
+
raw: { stub: true, attempt_id: attemptId, finished_in_background: true },
|
|
97
|
+
};
|
|
98
|
+
this.store.stubRailPut(attemptId, request, outcome, this.clock().toISOString());
|
|
99
|
+
return outcome;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
105
|
+
const sealed = this.store.stubRailFindByReceipt(receiptId);
|
|
106
|
+
if (!sealed) return undefined;
|
|
107
|
+
const req = sealed.request as Omit<RailPayment, "actor">;
|
|
108
|
+
const out = sealed.outcome as Extract<RailOutcome, { status: "settled" }>;
|
|
109
|
+
const body = {
|
|
110
|
+
receipt_id: receiptId,
|
|
111
|
+
state: "paid",
|
|
112
|
+
mandate: { id: req.mandate_id },
|
|
113
|
+
payment: { amount_minor: req.amount_minor, payee: req.payee, attempt_id: req.attempt_id, money_moved: false, sandbox: true, at: sealed.at },
|
|
114
|
+
};
|
|
115
|
+
const chain = `sha256:${sha256Hex(JSON.stringify(body))}`;
|
|
116
|
+
return {
|
|
117
|
+
...body,
|
|
118
|
+
chain,
|
|
119
|
+
receipt_sig: `stub:${sha256Hex(`sig:${chain}`).slice(0, 32)}`,
|
|
120
|
+
actor,
|
|
121
|
+
raw: { stub: true, transaction_id: out.transaction_id },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tools.json`: the closed list of tools the model may call. Tool-level
|
|
3
|
+
* authority comes before mandate-level authority: a tool that is not in
|
|
4
|
+
* this file is refused before anything else looks at the call.
|
|
5
|
+
*
|
|
6
|
+
* Two kinds. `meta_tools` are CodeSpar meta-tool names, with the input the
|
|
7
|
+
* kit accepts for them (a snapshot of the MCP shape, pinned by `mcp` in the
|
|
8
|
+
* manifest; see OPEN_QUESTIONS on why the list is not fetched live).
|
|
9
|
+
* `local_tools` are the agent's own read-only helpers.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import type { ToolSpec } from "./providers/types.js";
|
|
14
|
+
|
|
15
|
+
const ToolDefinitionSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
name: z.string().regex(/^[a-z][a-z0-9_]*$/),
|
|
18
|
+
description: z.string().min(1),
|
|
19
|
+
input_schema: z.record(z.string(), z.unknown()),
|
|
20
|
+
/** `payment` and `charge` tools create a `drafted` execution and nothing else; `read` tools never touch money. `charge` is the receivable side: the counterparty pays us. */
|
|
21
|
+
effect: z.enum(["payment", "charge", "read"]),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
export const ToolsFileSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
meta_tools: z.array(ToolDefinitionSchema),
|
|
28
|
+
local_tools: z.array(ToolDefinitionSchema),
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
.superRefine((t, ctx) => {
|
|
32
|
+
const names = new Set<string>();
|
|
33
|
+
for (const tool of [...t.meta_tools, ...t.local_tools]) {
|
|
34
|
+
if (names.has(tool.name)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate tool ${tool.name}` });
|
|
35
|
+
names.add(tool.name);
|
|
36
|
+
}
|
|
37
|
+
for (const [i, tool] of t.meta_tools.entries()) {
|
|
38
|
+
if (!tool.name.startsWith("codespar_")) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["meta_tools", i, "name"], message: "meta-tools are named codespar_*" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type ToolsFile = z.infer<typeof ToolsFileSchema>;
|
|
43
|
+
export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
|
|
44
|
+
|
|
45
|
+
export function loadToolsFile(path: string): ToolsFile {
|
|
46
|
+
return ToolsFileSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toolSpecs(tools: ToolsFile): ToolSpec[] {
|
|
50
|
+
return [...tools.meta_tools, ...tools.local_tools].map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function allowedToolNames(tools: ToolsFile): Set<string> {
|
|
54
|
+
return new Set([...tools.meta_tools, ...tools.local_tools].map((t) => t.name));
|
|
55
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shapes of the core. Amounts are always integer minor units (BRL
|
|
3
|
+
* cents). Timestamps are ISO 8601 strings so they survive SQLite and JSON
|
|
4
|
+
* unchanged.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ApprovalMode = "human" | "mandate";
|
|
8
|
+
|
|
9
|
+
/** Section 4.5: who acted. Every API call and every receipt carries one. */
|
|
10
|
+
export type Actor =
|
|
11
|
+
| { type: "agent"; agent: string; on_behalf_of: string }
|
|
12
|
+
| { type: "human"; id: string; channel: string };
|
|
13
|
+
|
|
14
|
+
/** One line of an execution: a payee and an amount, resolved by the core. */
|
|
15
|
+
export interface ExecutionItem {
|
|
16
|
+
/** The alias the model used (`escola`), when it used one. */
|
|
17
|
+
alias?: string;
|
|
18
|
+
/** Display name from the mandate's named payees, when known. */
|
|
19
|
+
beneficiary: string;
|
|
20
|
+
/** The pinned payee key (a Pix key for `pix-key` mandates). */
|
|
21
|
+
payee: string;
|
|
22
|
+
amount: number;
|
|
23
|
+
currency: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
/** A receivable's due date (`YYYY-MM-DD`). Part of the `items_hash` when present: moving it after approval is a different charge. */
|
|
26
|
+
due_date?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The trigger of section 4.4 that sent a `mandate` execution to a human. */
|
|
30
|
+
export type EscalationTrigger = "amount" | "new_beneficiary" | "outside_hours";
|
|
31
|
+
|
|
32
|
+
/** Stable reasons a transition can carry. They are the readable-failure contract. */
|
|
33
|
+
export type ExecutionReason =
|
|
34
|
+
| "mandate_revoked"
|
|
35
|
+
| "mandate_paused"
|
|
36
|
+
| "mandate_expired"
|
|
37
|
+
| "mandate_status_unavailable"
|
|
38
|
+
| "org_paused"
|
|
39
|
+
| "beneficiary_not_allowed"
|
|
40
|
+
| "per_tx_cap_exceeded"
|
|
41
|
+
| "window_cap_exceeded"
|
|
42
|
+
| "approval_expired"
|
|
43
|
+
| "items_hash_mismatch"
|
|
44
|
+
| "mandate_changed"
|
|
45
|
+
| "denied_by_approver"
|
|
46
|
+
| "rail_failed"
|
|
47
|
+
| "rail_uncertain"
|
|
48
|
+
| "tool_not_allowed"
|
|
49
|
+
| "model_total_mismatch"
|
|
50
|
+
| "outside_hours"
|
|
51
|
+
| "escalated"
|
|
52
|
+
/** The agent's own envelope (guardrails) refused what the model proposed: a discount, an instalment count or a due date outside it. */
|
|
53
|
+
| "outside_envelope"
|
|
54
|
+
/** A receivable was issued and the rail is waiting for the payer; the execution stays `executing` until `commerce.charge.*` closes it. */
|
|
55
|
+
| "awaiting_settlement"
|
|
56
|
+
| "charge_expired"
|
|
57
|
+
| "charge_cancelled";
|
|
58
|
+
|
|
59
|
+
export interface MandateRef {
|
|
60
|
+
id: string;
|
|
61
|
+
version: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Section 4.2. */
|
|
65
|
+
export interface ApprovalArtifact {
|
|
66
|
+
approval_id: string;
|
|
67
|
+
execution_id: string;
|
|
68
|
+
mode: ApprovalMode;
|
|
69
|
+
approver:
|
|
70
|
+
| { type: "person"; id: string; channel: string }
|
|
71
|
+
| { type: "mandate"; id: string };
|
|
72
|
+
approved_at: string;
|
|
73
|
+
expires_at: string;
|
|
74
|
+
mandate: MandateRef;
|
|
75
|
+
items: ExecutionItem[];
|
|
76
|
+
items_hash: string;
|
|
77
|
+
/** Present when a section 4.4 trigger sent the execution to a human first. */
|
|
78
|
+
escalation?: { trigger: EscalationTrigger; detail: string };
|
|
79
|
+
actor: Actor;
|
|
80
|
+
signature: { alg: "HMAC-SHA256"; key_id: string; value: string };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ItemOutcome {
|
|
84
|
+
index: number;
|
|
85
|
+
attempt_id: string;
|
|
86
|
+
/** `accepted`: the rail took the attempt and the outcome comes later (a receivable waiting for its payer). Not terminal. */
|
|
87
|
+
status: "settled" | "failed" | "accepted";
|
|
88
|
+
receipt_id?: string;
|
|
89
|
+
transaction_id?: string;
|
|
90
|
+
/** The rail's code on a failed outcome (`charge_expired`, `charge_cancelled`, a provider code). */
|
|
91
|
+
code?: string;
|
|
92
|
+
error?: string;
|
|
93
|
+
/** What the payer is shown for an accepted receivable, as the rail handed it back. Presentation only; nothing here decides money. */
|
|
94
|
+
instrument?: ChargeInstrument;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The payable legs of a receivable. Null until the issuer registers the instrument (a cobranca com vencimento answers PROCESSING first). */
|
|
98
|
+
export interface ChargeInstrument {
|
|
99
|
+
payable: boolean;
|
|
100
|
+
pix_copy_paste: string | null;
|
|
101
|
+
boleto_bank_line: string | null;
|
|
102
|
+
boleto_bar_code: string | null;
|
|
103
|
+
due_date: string | null;
|
|
104
|
+
/** The issuer's normalized state as last read: PROCESSING, PENDING, CONFIRMED, EXPIRED, CANCELLED. */
|
|
105
|
+
status: string;
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { checkApprovalArtifact, createApprovalArtifact, hmacSigner } from "../src/approval.js";
|
|
3
|
+
import { itemsHash } from "../src/hash.js";
|
|
4
|
+
import type { Execution } from "../src/state-machine.js";
|
|
5
|
+
import type { Actor, ExecutionItem } from "../src/types.js";
|
|
6
|
+
|
|
7
|
+
const actor: Actor = { type: "human", id: "usr_1", channel: "terminal" };
|
|
8
|
+
const signer = hmacSigner("test", Buffer.alloc(32, 7));
|
|
9
|
+
const now = new Date("2026-09-23T14:03:11Z");
|
|
10
|
+
|
|
11
|
+
function execution(items: ExecutionItem[]): Execution<"awaiting_approval"> {
|
|
12
|
+
return {
|
|
13
|
+
id: "exe_1",
|
|
14
|
+
run_id: "run_1",
|
|
15
|
+
state: "awaiting_approval",
|
|
16
|
+
mode: "human",
|
|
17
|
+
actor: { type: "agent", agent: "bills-agent@0.1.0", on_behalf_of: "usr_1" },
|
|
18
|
+
items,
|
|
19
|
+
total: items.reduce((s, i) => s + i.amount, 0),
|
|
20
|
+
currency: "BRL",
|
|
21
|
+
items_hash: itemsHash(items),
|
|
22
|
+
mandate: { id: "mdt_1", version: 3 },
|
|
23
|
+
idempotency_key: "idk_1",
|
|
24
|
+
blocking_reasons: [],
|
|
25
|
+
outcomes: [],
|
|
26
|
+
history: [],
|
|
27
|
+
created_at: now.toISOString(),
|
|
28
|
+
updated_at: now.toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const escola: ExecutionItem = { alias: "escola", beneficiary: "Escola", payee: "escola@exemplo.com.br", amount: 185000, currency: "BRL" };
|
|
33
|
+
|
|
34
|
+
describe("section 4.2: approval artifact", () => {
|
|
35
|
+
it("has the schema of the spec and an HMAC signature that verifies", () => {
|
|
36
|
+
const exe = execution([escola]);
|
|
37
|
+
const artifact = createApprovalArtifact(signer, { execution: exe, approver: { type: "person", id: "usr_1", channel: "terminal" }, actor, now });
|
|
38
|
+
expect(artifact.approval_id).toMatch(/^apr_/);
|
|
39
|
+
expect(artifact.execution_id).toBe("exe_1");
|
|
40
|
+
expect(artifact.mode).toBe("human");
|
|
41
|
+
expect(artifact.mandate).toEqual({ id: "mdt_1", version: 3 });
|
|
42
|
+
expect(artifact.items_hash).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
43
|
+
expect(artifact.signature.alg).toBe("HMAC-SHA256");
|
|
44
|
+
expect(artifact.actor).toEqual(actor);
|
|
45
|
+
expect(new Date(artifact.expires_at).getTime() - new Date(artifact.approved_at).getTime()).toBe(15 * 60 * 1000);
|
|
46
|
+
expect(checkApprovalArtifact(signer, artifact, exe, now)).toEqual({ ok: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("items_hash ignores presentation fields and changes when the money fields change", () => {
|
|
50
|
+
const base = itemsHash([escola]);
|
|
51
|
+
expect(itemsHash([{ ...escola, description: "outubro", beneficiary: "Escola X" }])).toBe(base);
|
|
52
|
+
expect(itemsHash([{ ...escola, amount: 185001 }])).not.toBe(base);
|
|
53
|
+
expect(itemsHash([{ ...escola, payee: "outra@chave" }])).not.toBe(base);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("refuses when the list changed after approval (items_hash mismatch)", () => {
|
|
57
|
+
const exe = execution([escola]);
|
|
58
|
+
const artifact = createApprovalArtifact(signer, { execution: exe, approver: { type: "person", id: "usr_1", channel: "terminal" }, actor, now });
|
|
59
|
+
const tampered = { ...exe, items: [{ ...escola, amount: 500000 }] };
|
|
60
|
+
expect(checkApprovalArtifact(signer, artifact, tampered, now)).toEqual({ ok: false, problem: "items_hash_mismatch" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("refuses a tampered artifact, an expired one and a foreign key", () => {
|
|
64
|
+
const exe = execution([escola]);
|
|
65
|
+
const artifact = createApprovalArtifact(signer, { execution: exe, approver: { type: "person", id: "usr_1", channel: "terminal" }, actor, now });
|
|
66
|
+
expect(checkApprovalArtifact(signer, { ...artifact, items_hash: itemsHash([{ ...escola, amount: 1 }]) }, exe, now)).toEqual({ ok: false, problem: "signature_invalid" });
|
|
67
|
+
expect(checkApprovalArtifact(signer, artifact, exe, new Date(now.getTime() + 16 * 60 * 1000))).toEqual({ ok: false, problem: "expired" });
|
|
68
|
+
expect(checkApprovalArtifact(hmacSigner("other", Buffer.alloc(32, 9)), artifact, exe, now)).toEqual({ ok: false, problem: "signature_invalid" });
|
|
69
|
+
expect(checkApprovalArtifact(signer, artifact, { ...exe, mandate: { id: "mdt_1", version: 4 } }, now)).toEqual({ ok: false, problem: "wrong_mandate" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("records the escalation trigger when there was one", () => {
|
|
73
|
+
const exe = execution([escola]);
|
|
74
|
+
const artifact = createApprovalArtifact(signer, {
|
|
75
|
+
execution: exe,
|
|
76
|
+
approver: { type: "person", id: "usr_1", channel: "terminal" },
|
|
77
|
+
actor,
|
|
78
|
+
now,
|
|
79
|
+
escalation: { trigger: "amount", detail: "above 150000" },
|
|
80
|
+
});
|
|
81
|
+
expect(artifact.escalation).toEqual({ trigger: "amount", detail: "above 150000" });
|
|
82
|
+
});
|
|
83
|
+
});
|