@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm start` interactive terminal
|
|
3
|
+
* `npm start -- --input "pague a escola de outubro"` one turn, no prompt
|
|
4
|
+
* `npm start -- --scenario happy-path` a scenario pack, both modes it declares
|
|
5
|
+
*
|
|
6
|
+
* `--json` puts machine data on stdout (valid JSON, nothing else) and the
|
|
7
|
+
* human messages on stderr.
|
|
8
|
+
*/
|
|
9
|
+
import { stderr, stdout } from "node:process";
|
|
10
|
+
import { relative, resolve } from "node:path";
|
|
11
|
+
import { NotATestKeyError, isTestKey } from "@codespar/agent-core";
|
|
12
|
+
import { closeTerminal, defaultAsk, handleExecution, interactive } from "../channels/terminal/index.js";
|
|
13
|
+
import { runEmbeddedConsent, loadLocalMandate } from "./modules/embedded-consent.js";
|
|
14
|
+
import { checkScenario, listScenarios, loadScenario, runScenario, scenariosDir } from "./scenarios.js";
|
|
15
|
+
import { AGENT_DIR, MANDATE_PATH, NoMandateError, readDotEnv, resolveRailKind, setup, type RailKind } from "./setup.js";
|
|
16
|
+
import { loadMandate, createCodeSparClient } from "@codespar/agent-core";
|
|
17
|
+
|
|
18
|
+
interface Args {
|
|
19
|
+
input?: string;
|
|
20
|
+
scenario?: string;
|
|
21
|
+
mode?: "human" | "mandate";
|
|
22
|
+
json: boolean;
|
|
23
|
+
decision?: "approve" | "deny";
|
|
24
|
+
provider?: "anthropic" | "replay";
|
|
25
|
+
transcript?: string;
|
|
26
|
+
rail?: RailKind;
|
|
27
|
+
user: string;
|
|
28
|
+
help: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseArgs(argv: string[]): Args {
|
|
32
|
+
const args: Args = { json: false, user: "usr_terminal", help: false };
|
|
33
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34
|
+
const a = argv[i]!;
|
|
35
|
+
const next = () => {
|
|
36
|
+
const v = argv[i + 1];
|
|
37
|
+
if (v === undefined) throw new Error(`${a} needs a value`);
|
|
38
|
+
i += 1;
|
|
39
|
+
return v;
|
|
40
|
+
};
|
|
41
|
+
if (a === "--input") args.input = next();
|
|
42
|
+
else if (a === "--scenario") args.scenario = next();
|
|
43
|
+
else if (a === "--mode") {
|
|
44
|
+
const v = next();
|
|
45
|
+
if (v !== "human" && v !== "mandate") throw new Error("--mode must be human or mandate");
|
|
46
|
+
args.mode = v;
|
|
47
|
+
} else if (a === "--json") args.json = true;
|
|
48
|
+
else if (a === "--approve") args.decision = "approve";
|
|
49
|
+
else if (a === "--deny") args.decision = "deny";
|
|
50
|
+
else if (a === "--provider") {
|
|
51
|
+
const v = next();
|
|
52
|
+
if (v !== "anthropic" && v !== "replay") throw new Error("--provider must be anthropic or replay");
|
|
53
|
+
args.provider = v;
|
|
54
|
+
} else if (a === "--transcript") args.transcript = next();
|
|
55
|
+
else if (a === "--rail") {
|
|
56
|
+
const v = next();
|
|
57
|
+
if (v !== "stub" && v !== "api") throw new Error("--rail must be stub or api");
|
|
58
|
+
args.rail = v;
|
|
59
|
+
} else if (a === "--user") args.user = next();
|
|
60
|
+
else if (a === "--help" || a === "-h") args.help = true;
|
|
61
|
+
else throw new Error(`unknown argument ${a}`);
|
|
62
|
+
}
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const USAGE = `bills-agent
|
|
67
|
+
npm start interactive terminal
|
|
68
|
+
npm start -- --input "pague a escola de outubro" one turn (add --approve/--deny to decide, --json for machine output)
|
|
69
|
+
npm start -- --scenario <name> run a scenario pack (${"see scenarios/"})
|
|
70
|
+
options: --mode human|mandate --provider anthropic|replay --transcript <file> --rail stub|api --user <id> --json`;
|
|
71
|
+
|
|
72
|
+
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
73
|
+
let args: Args;
|
|
74
|
+
try {
|
|
75
|
+
args = parseArgs(argv);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
stderr.write(`${err instanceof Error ? err.message : String(err)}\n${USAGE}\n`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
if (args.help) {
|
|
81
|
+
stderr.write(USAGE + "\n");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
readDotEnv();
|
|
85
|
+
const say = (line: string) => stderr.write(line + "\n");
|
|
86
|
+
|
|
87
|
+
if (args.scenario) return runScenarioCommand(args, say);
|
|
88
|
+
|
|
89
|
+
// Sandbox by construction: a live key dies here, before anything else runs.
|
|
90
|
+
const railKind = resolveRailKind(process.env, args.rail);
|
|
91
|
+
if (railKind === "api" && !isTestKey(process.env["CODESPAR_API_KEY"])) {
|
|
92
|
+
say(new NotATestKeyError().message);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Embedded consent: with a test key and no signed mandate, the mandate is born first.
|
|
97
|
+
if (railKind === "api" && !loadLocalMandate(MANDATE_PATH)) {
|
|
98
|
+
if (args.input) {
|
|
99
|
+
say(new NoMandateError().message);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const api = createCodeSparClient({ apiKey: process.env["CODESPAR_API_KEY"], baseUrl: process.env["CODESPAR_API_URL"], projectId: process.env["CODESPAR_PROJECT_ID"] });
|
|
103
|
+
const example = loadMandate(resolve(AGENT_DIR, "mandate.example.json"));
|
|
104
|
+
await runEmbeddedConsent({ api, example, mandatePath: MANDATE_PATH, say, confirm: async (q: string) => /^(s|sim|y|yes)$/i.test((await defaultAsk(q)).trim()) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// One-shot without a model: replay the recorded happy-path when the input is its first turn.
|
|
108
|
+
let transcript = args.transcript;
|
|
109
|
+
const provider = args.provider ?? (process.env["ANTHROPIC_API_KEY"] && process.env["ANTHROPIC_API_KEY"] !== "sk-ant-your_key_here" ? "anthropic" : "replay");
|
|
110
|
+
if (provider === "replay" && !transcript && args.input) {
|
|
111
|
+
const match = listScenarios().map(loadScenario).find((s) => s.turns[0].input === args.input);
|
|
112
|
+
if (match) transcript = resolve(scenariosDir(), match.transcript);
|
|
113
|
+
else {
|
|
114
|
+
say(`no ANTHROPIC_API_KEY and no recorded transcript for that input. Set the key, pass --transcript, or use one of: ${listScenarios().map((n) => `"${loadScenario(n).turns[0].input}"`).join(", ")}`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const s = setup({ mode: args.mode, rail: railKind, provider, transcript, say });
|
|
120
|
+
const approver = { id: args.user, channel: "terminal" };
|
|
121
|
+
try {
|
|
122
|
+
const runtime = s.makeRuntime();
|
|
123
|
+
if (!args.input) {
|
|
124
|
+
await interactive({ setup: s, approver, runtime, say });
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
const loop = s.makeLoop(runtime, (execution) => handleExecution(execution, { setup: s, approver, decision: args.decision ?? "none", say }));
|
|
128
|
+
const result = await loop.turn(args.input);
|
|
129
|
+
const executions = s.engine.list().filter((e) => e.run_id === s.runId);
|
|
130
|
+
const payload = {
|
|
131
|
+
run_id: s.runId,
|
|
132
|
+
agent: `${s.manifest.manifest.name}@${s.manifest.manifest.version}`,
|
|
133
|
+
mode: s.mode,
|
|
134
|
+
rail: s.railKind,
|
|
135
|
+
mandate_id: s.mandate.id,
|
|
136
|
+
actor: s.engine.agentActor,
|
|
137
|
+
reply: result.reply,
|
|
138
|
+
tool_calls: result.tool_calls,
|
|
139
|
+
executions: executions.map((e) => ({
|
|
140
|
+
id: e.id,
|
|
141
|
+
state: e.state,
|
|
142
|
+
reason: e.reason ?? null,
|
|
143
|
+
escalation: e.escalation ?? null,
|
|
144
|
+
total_minor: e.total,
|
|
145
|
+
items: e.items.map((i) => ({ beneficiary: i.beneficiary, amount_minor: i.amount })),
|
|
146
|
+
approval_id: e.approval_id ?? null,
|
|
147
|
+
receipt_ids: e.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id),
|
|
148
|
+
})),
|
|
149
|
+
receipts: s.bundle.listReceipts().map((f) => relative(process.cwd(), `${s.bundle.dir}/receipts/${f}`)),
|
|
150
|
+
bundle_dir: relative(process.cwd(), s.bundle.dir),
|
|
151
|
+
};
|
|
152
|
+
if (args.json) stdout.write(JSON.stringify(payload) + "\n");
|
|
153
|
+
else stdout.write(result.reply + "\n");
|
|
154
|
+
return executions.some((e) => e.state === "executing") ? 3 : 0;
|
|
155
|
+
} finally {
|
|
156
|
+
closeTerminal();
|
|
157
|
+
s.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function runScenarioCommand(args: Args, say: (l: string) => void): Promise<number> {
|
|
162
|
+
const scenario = loadScenario(args.scenario!);
|
|
163
|
+
const modes = args.mode ? [args.mode] : scenario.modes;
|
|
164
|
+
const results = [];
|
|
165
|
+
for (const mode of modes) {
|
|
166
|
+
say(`== scenario ${scenario.name} — approval: ${mode}`);
|
|
167
|
+
say(scenario.description);
|
|
168
|
+
const run = await runScenario(scenario, { mode, say: args.json ? () => undefined : say });
|
|
169
|
+
const check = checkScenario(scenario, run);
|
|
170
|
+
for (const reply of run.replies) say(`agente: ${reply}`);
|
|
171
|
+
say(check.ok ? `== ok — bundle em ${relative(process.cwd(), run.bundle_dir)}` : `== FAIL: ${check.failures.join("; ")}`);
|
|
172
|
+
results.push({ mode, ok: check.ok, failures: check.failures, run });
|
|
173
|
+
}
|
|
174
|
+
if (args.json) stdout.write(JSON.stringify({ scenario: scenario.name, results }) + "\n");
|
|
175
|
+
return results.every((r) => r.ok) ? 0 : 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
|
179
|
+
main()
|
|
180
|
+
.then((code) => process.exit(code))
|
|
181
|
+
.catch((err) => {
|
|
182
|
+
stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module `embedded-consent`: the mandate is born at a consent the CONSUMER
|
|
3
|
+
* authorizes. In the sandbox the kit runs the `partner` surface: it is the
|
|
4
|
+
* partner's backend, the titular is at the keyboard, and the submit carries
|
|
5
|
+
* an `in_person` attestation. That is what hands the kit the signed envelope
|
|
6
|
+
* `{ mandate, signature }`, which `POST /v1/consumer-payments/execute`
|
|
7
|
+
* presents on every spend. The `hosted` surface (a page the consumer opens)
|
|
8
|
+
* returns the envelope to a `callback_url` a terminal kit does not have;
|
|
9
|
+
* see docs/OPEN_QUESTIONS.md.
|
|
10
|
+
*
|
|
11
|
+
* With a test key and no `.codespar/mandate.json`, `npm start` runs this
|
|
12
|
+
* first. The stored file carries the signature: mode 0600, never in the
|
|
13
|
+
* bundle, never committed.
|
|
14
|
+
*/
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname } from "node:path";
|
|
17
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
18
|
+
import { MandateSchema, windowCap, type Mandate, describeApiError } from "@codespar/agent-core";
|
|
19
|
+
|
|
20
|
+
export interface ConsentOptions {
|
|
21
|
+
api: ApiClient;
|
|
22
|
+
example: Mandate;
|
|
23
|
+
mandatePath: string;
|
|
24
|
+
/** Where to write the lines the person reads. */
|
|
25
|
+
say: (line: string) => void;
|
|
26
|
+
/** Asks the titular to authorize. Returns true on yes. */
|
|
27
|
+
confirm: (question: string) => Promise<boolean>;
|
|
28
|
+
/** The consumer this mandate is for. Defaults to the example's. */
|
|
29
|
+
consumerId?: string;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const YEAR_SECONDS = 365 * 24 * 3600;
|
|
34
|
+
|
|
35
|
+
export function loadLocalMandate(path: string): Mandate | undefined {
|
|
36
|
+
if (!existsSync(path)) return undefined;
|
|
37
|
+
return MandateSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runEmbeddedConsent(options: ConsentOptions): Promise<Mandate> {
|
|
41
|
+
const { api, example, say } = options;
|
|
42
|
+
const now = options.now ?? (() => new Date());
|
|
43
|
+
const consumerId = options.consumerId ?? example.consumer_id;
|
|
44
|
+
|
|
45
|
+
const init = await api.post("/v1/consents/init", {
|
|
46
|
+
body: {
|
|
47
|
+
agent_id: example.agent_id,
|
|
48
|
+
intent: {
|
|
49
|
+
purpose: example.purpose,
|
|
50
|
+
cap_minor: example.cap_minor,
|
|
51
|
+
per_tx_cap_minor: example.per_tx_cap_minor,
|
|
52
|
+
currency: "BRL",
|
|
53
|
+
mandate_ttl_seconds: YEAR_SECONDS,
|
|
54
|
+
merchant_allowlist: example.beneficiaries.map((b) => b.payee),
|
|
55
|
+
merchant_pin_kind: "pix-key",
|
|
56
|
+
...(example.periodic_cap ? { periodic_cap: example.periodic_cap } : {}),
|
|
57
|
+
display_name: "bills-agent: contas do mes",
|
|
58
|
+
intent_note: `Pagar as contas do mes aos favorecidos nomeados: ${example.beneficiaries.map((b) => b.name).join(", ")}.`,
|
|
59
|
+
},
|
|
60
|
+
surface: "partner",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
say("");
|
|
65
|
+
say("O mandato ainda nao existe. Este e o consentimento que o titular assina (sandbox):");
|
|
66
|
+
say(` agente: ${example.agent_id} finalidade: ${example.purpose}`);
|
|
67
|
+
say(` teto por pagamento: ${example.per_tx_cap_minor} centavos teto do mes: ${windowCap(example)} centavos vitalicio: ${example.cap_minor} centavos`);
|
|
68
|
+
for (const b of example.beneficiaries) say(` favorecido: ${b.name} (${b.alias})`);
|
|
69
|
+
say(` validade: 1 ano (o token do consentimento vale ate ${init.expires_at})`);
|
|
70
|
+
const yes = await options.confirm(" Voce e o titular e autoriza este mandato? [s/N] ");
|
|
71
|
+
if (!yes) throw new Error("consent not authorized by the titular; nothing was created");
|
|
72
|
+
|
|
73
|
+
// The consumer's "browser" is this terminal: the submit is the same call the hosted page makes,
|
|
74
|
+
// plus the attestation the partner surface requires. Sandbox rails accept a placeholder token.
|
|
75
|
+
const submitted = (await api.request("post", "/v1/consents/{token}/submit" as never, {
|
|
76
|
+
path: { token: init.token },
|
|
77
|
+
body: {
|
|
78
|
+
consumer_id: consumerId,
|
|
79
|
+
rail: "pix-consent",
|
|
80
|
+
provider_token: `sandbox-pix-consent-${consumerId}`,
|
|
81
|
+
display_label: "bills-agent (sandbox)",
|
|
82
|
+
attestation: { method: "in_person", asserted_at: Math.floor(now().getTime() / 1000), reference: "terminal" },
|
|
83
|
+
},
|
|
84
|
+
} as never)) as { mandate_id: string; mandate: Record<string, unknown>; signature: string };
|
|
85
|
+
|
|
86
|
+
const signed = submitted.mandate;
|
|
87
|
+
const mandate = MandateSchema.parse({
|
|
88
|
+
id: submitted.mandate_id,
|
|
89
|
+
version: 1,
|
|
90
|
+
consumer_id: String(signed["consumer_id"]),
|
|
91
|
+
agent_id: String(signed["agent_id"]),
|
|
92
|
+
purpose: String(signed["purpose"]),
|
|
93
|
+
currency: String(signed["currency"]),
|
|
94
|
+
cap_minor: Number(signed["cap_minor"]),
|
|
95
|
+
per_tx_cap_minor: Number(signed["per_tx_cap_minor"]),
|
|
96
|
+
...(signed["periodic_cap"] ? { periodic_cap: signed["periodic_cap"] } : {}),
|
|
97
|
+
merchant_pin_kind: String(signed["merchant_pin_kind"]),
|
|
98
|
+
merchant_allowlist: signed["merchant_allowlist"],
|
|
99
|
+
beneficiaries: example.beneficiaries.filter((b) => (signed["merchant_allowlist"] as string[]).includes(b.payee)),
|
|
100
|
+
status: "active",
|
|
101
|
+
expires_at: new Date(Number(signed["expires_at"]) * 1000).toISOString(),
|
|
102
|
+
signature: submitted.signature,
|
|
103
|
+
canonical: signed,
|
|
104
|
+
source: "consent",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
mkdirSync(dirname(options.mandatePath), { recursive: true });
|
|
108
|
+
writeFileSync(options.mandatePath, JSON.stringify(mandate, null, 2) + "\n", { mode: 0o600 });
|
|
109
|
+
chmodSync(options.mandatePath, 0o600);
|
|
110
|
+
say(`Mandato assinado: ${mandate.id} (consumidor ${mandate.consumer_id}).`);
|
|
111
|
+
|
|
112
|
+
await fundSandbox(api, mandate, say);
|
|
113
|
+
return mandate;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Best effort: the sandbox account is credited with one window's cap. A `pix-consent` source has no Celcoin account to credit, and the sandbox spend does not need one. */
|
|
117
|
+
export async function fundSandbox(api: ApiClient, mandate: Mandate, say: (line: string) => void): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
const funded = await api.post("/v1/test/fund", { body: { consumer_id: mandate.consumer_id, amount_minor: windowCap(mandate) } });
|
|
120
|
+
say(`Sandbox creditado: ${funded.amount_minor} centavos em ${funded.account} (deposit ${funded.deposit_id}).`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const f = describeApiError(err);
|
|
123
|
+
say(`Sandbox nao creditado (${f.code}); o gasto de teste sob pix-consent nao depende disso.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module `pix-out`: the tool handlers the model can reach. `codespar_pay`
|
|
3
|
+
* hands the proposal to the engine and gets an execution in `drafted` (then
|
|
4
|
+
* whatever the core decided); it cannot pay. `codespar_ledger` and
|
|
5
|
+
* `list_bills` are reads.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolHandler } from "@codespar/agent-core";
|
|
8
|
+
import { formatBRL } from "../bills.js";
|
|
9
|
+
import { BILLS, MONTH } from "../bills.js";
|
|
10
|
+
|
|
11
|
+
interface PayInput {
|
|
12
|
+
action?: unknown;
|
|
13
|
+
items?: unknown;
|
|
14
|
+
total_minor?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const listBills: ToolHandler = async (_input, ctx) => {
|
|
18
|
+
const settled = ctx.engine.list({ state: "settled" });
|
|
19
|
+
const paidAliases = new Set(settled.flatMap((e) => e.items.map((i) => i.alias)).filter(Boolean));
|
|
20
|
+
return {
|
|
21
|
+
month: MONTH,
|
|
22
|
+
bills: BILLS.map((b) => ({ ...b, amount: formatBRL(b.amount_minor), paid_this_window: paidAliases.has(b.alias) })),
|
|
23
|
+
payees: ctx.engine.mandate.beneficiaries.map((b) => ({ alias: b.alias, name: b.name })),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const codesparPay: ToolHandler = async (raw, ctx) => {
|
|
28
|
+
const input = raw as PayInput;
|
|
29
|
+
if (input.action !== "pix") throw new Error(`codespar_pay: unsupported action ${String(input.action)}; this agent only does pix`);
|
|
30
|
+
if (!Array.isArray(input.items) || input.items.length === 0) throw new Error("codespar_pay: items must be a non-empty array");
|
|
31
|
+
const items = input.items.map((item, i) => {
|
|
32
|
+
const it = item as { payee?: unknown; amount_minor?: unknown; description?: unknown };
|
|
33
|
+
if (typeof it.payee !== "string" || !it.payee.trim()) throw new Error(`codespar_pay: items[${i}].payee must be a payee alias`);
|
|
34
|
+
if (typeof it.amount_minor !== "number" || !Number.isInteger(it.amount_minor) || it.amount_minor <= 0) throw new Error(`codespar_pay: items[${i}].amount_minor must be a positive integer`);
|
|
35
|
+
return { payee: it.payee, amount: it.amount_minor, ...(typeof it.description === "string" ? { description: it.description } : {}) };
|
|
36
|
+
});
|
|
37
|
+
const claimed = typeof input.total_minor === "number" ? { claimed_total: input.total_minor } : {};
|
|
38
|
+
|
|
39
|
+
const draft = await ctx.engine.draft({ items, ...claimed });
|
|
40
|
+
if (!draft.ok) {
|
|
41
|
+
return { status: "refused", reason: draft.reason, message: draft.message, paid: false };
|
|
42
|
+
}
|
|
43
|
+
const execution = await ctx.onExecution(draft.execution);
|
|
44
|
+
return {
|
|
45
|
+
status: execution.state,
|
|
46
|
+
execution_id: execution.id,
|
|
47
|
+
total_minor: execution.total,
|
|
48
|
+
total: formatBRL(execution.total),
|
|
49
|
+
items: execution.items.map((i) => ({ payee: i.beneficiary, amount: formatBRL(i.amount) })),
|
|
50
|
+
...(execution.reason ? { reason: execution.reason, detail: execution.detail } : {}),
|
|
51
|
+
...(execution.escalation ? { escalated_by: execution.escalation.trigger } : {}),
|
|
52
|
+
receipt_ids: execution.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id),
|
|
53
|
+
paid: execution.state === "settled",
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const codesparLedger: ToolHandler = async (raw, ctx) => {
|
|
58
|
+
const input = raw as { action?: unknown; receipt_id?: unknown };
|
|
59
|
+
if (input.action === "executions") {
|
|
60
|
+
return {
|
|
61
|
+
executions: ctx.engine.list().map((e) => ({ id: e.id, state: e.state, total: formatBRL(e.total), reason: e.reason ?? null, receipt_ids: e.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id) })),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (input.action === "receipt") {
|
|
65
|
+
if (typeof input.receipt_id !== "string") throw new Error("codespar_ledger: receipt_id is required for action=receipt");
|
|
66
|
+
const owner = ctx.engine.list().find((e) => e.outcomes.some((o) => o.receipt_id === input.receipt_id));
|
|
67
|
+
if (!owner) return { found: false, message: "no receipt with that id in this mandate's executions" };
|
|
68
|
+
const outcome = owner.outcomes.find((o) => o.receipt_id === input.receipt_id)!;
|
|
69
|
+
const item = owner.items[outcome.index];
|
|
70
|
+
return { found: true, receipt_id: outcome.receipt_id, state: owner.state, payee: item?.beneficiary, amount: item ? formatBRL(item.amount) : null, mandate_id: owner.mandate.id };
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`codespar_ledger: unsupported action ${String(input.action)}`);
|
|
73
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 12: scenario packs. A scenario is a JSON file with the person's
|
|
3
|
+
* turns, the decision the approver gives, the hooks that make the world
|
|
4
|
+
* change mid-run (revocation, an uncertain rail answer) and what the trail
|
|
5
|
+
* must look like at the end. The model's outputs come from a recorded
|
|
6
|
+
* transcript, so the same file feeds the CI, the live demo and the docs.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { basename, join, resolve } from "node:path";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { StubRail, type ApprovalMode, type Execution } from "@codespar/agent-core";
|
|
13
|
+
import { AGENT_DIR, RUNS_DIR, setup, type Setup } from "./setup.js";
|
|
14
|
+
import { handleExecution } from "../channels/terminal/index.js";
|
|
15
|
+
|
|
16
|
+
const TurnSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
input: z.string().min(1),
|
|
19
|
+
decision: z.enum(["approve", "deny", "none"]).default("approve"),
|
|
20
|
+
/** Runs after the execution is drafted and before the decision is taken. */
|
|
21
|
+
before_decision: z.enum(["revoke_mandate", "pause_all"]).optional(),
|
|
22
|
+
/** Runs after approval, before the rail is called; the core must catch it. */
|
|
23
|
+
before_execute: z.enum(["revoke_mandate", "pause_all"]).optional(),
|
|
24
|
+
/** The stub rail answers `uncertain` to the next attempt; the run ends with a reconcile. */
|
|
25
|
+
rail_uncertain: z.boolean().default(false),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
const ExpectSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
/** Final states of the executions, in order. */
|
|
32
|
+
states: z.array(z.string()).optional(),
|
|
33
|
+
/** History of each execution (the `to` of every transition). */
|
|
34
|
+
trails: z.array(z.array(z.string())).optional(),
|
|
35
|
+
reasons: z.array(z.string().nullable()).optional(),
|
|
36
|
+
triggers: z.array(z.string().nullable()).optional(),
|
|
37
|
+
receipts: z.number().int().nonnegative().optional(),
|
|
38
|
+
refused_before_draft: z.number().int().nonnegative().optional(),
|
|
39
|
+
/** Sum of the totals that reached `settled`. */
|
|
40
|
+
settled_total: z.number().int().nonnegative().optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
export const ScenarioSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
name: z.string().regex(/^[a-z0-9-]+$/),
|
|
47
|
+
description: z.string(),
|
|
48
|
+
modes: z.array(z.enum(["human", "mandate"])).nonempty(),
|
|
49
|
+
now: z.string().datetime().default("2026-09-23T18:00:00.000Z"),
|
|
50
|
+
transcript: z.string(),
|
|
51
|
+
turns: z.array(TurnSchema).nonempty(),
|
|
52
|
+
expect: z.record(z.enum(["human", "mandate", "both"]), ExpectSchema),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
|
|
56
|
+
export type Scenario = z.infer<typeof ScenarioSchema>;
|
|
57
|
+
|
|
58
|
+
export interface ScenarioRun {
|
|
59
|
+
scenario: string;
|
|
60
|
+
mode: ApprovalMode;
|
|
61
|
+
run_id: string;
|
|
62
|
+
bundle_dir: string;
|
|
63
|
+
replies: string[];
|
|
64
|
+
executions: Array<{ id: string; state: string; reason: string | null; trigger: string | null; total: number; trail: string[]; receipt_ids: string[] }>;
|
|
65
|
+
refused_before_draft: number;
|
|
66
|
+
receipts: number;
|
|
67
|
+
settled_total: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ScenarioCheck {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
mode: ApprovalMode;
|
|
73
|
+
failures: string[];
|
|
74
|
+
run: ScenarioRun;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function scenariosDir(): string {
|
|
78
|
+
return join(AGENT_DIR, "scenarios");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listScenarios(): string[] {
|
|
82
|
+
return readdirSync(scenariosDir())
|
|
83
|
+
.filter((f) => f.endsWith(".json"))
|
|
84
|
+
.map((f) => basename(f, ".json"))
|
|
85
|
+
.sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadScenario(name: string): Scenario {
|
|
89
|
+
const path = join(scenariosDir(), `${name}.json`);
|
|
90
|
+
if (!existsSync(path)) throw new Error(`unknown scenario ${name}; available: ${listScenarios().join(", ")}`);
|
|
91
|
+
return ScenarioSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RunScenarioOptions {
|
|
95
|
+
mode: ApprovalMode;
|
|
96
|
+
runsDir?: string;
|
|
97
|
+
stateDir?: string;
|
|
98
|
+
say?: (line: string) => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function runScenario(scenario: Scenario, options: RunScenarioOptions): Promise<ScenarioRun> {
|
|
102
|
+
const say = options.say ?? (() => undefined);
|
|
103
|
+
let clock = new Date(scenario.now);
|
|
104
|
+
const tick = () => {
|
|
105
|
+
clock = new Date(clock.getTime() + 1000);
|
|
106
|
+
return clock;
|
|
107
|
+
};
|
|
108
|
+
const stateDir = options.stateDir ?? mkdtempSync(join(tmpdir(), `bills-${scenario.name}-${options.mode}-`));
|
|
109
|
+
const s: Setup = setup({
|
|
110
|
+
mode: options.mode,
|
|
111
|
+
rail: "stub",
|
|
112
|
+
provider: "replay",
|
|
113
|
+
transcript: resolve(scenariosDir(), scenario.transcript),
|
|
114
|
+
runsDir: options.runsDir ?? RUNS_DIR,
|
|
115
|
+
stateDir,
|
|
116
|
+
now: tick,
|
|
117
|
+
say,
|
|
118
|
+
});
|
|
119
|
+
const stub = s.rail as StubRail;
|
|
120
|
+
const runtime = s.makeRuntime();
|
|
121
|
+
const replies: string[] = [];
|
|
122
|
+
const approver = { id: "usr_demo_titular", channel: "terminal" };
|
|
123
|
+
let refusedBeforeDraft = 0;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
for (const turn of scenario.turns) {
|
|
127
|
+
if (turn.rail_uncertain) stub.armUncertainOnce();
|
|
128
|
+
const loop = s.makeLoop(runtime, async (execution: Execution) => {
|
|
129
|
+
applyHook(s, turn.before_decision);
|
|
130
|
+
const decided = await handleExecution(execution, {
|
|
131
|
+
setup: s,
|
|
132
|
+
approver,
|
|
133
|
+
decision: turn.decision,
|
|
134
|
+
say,
|
|
135
|
+
ask: async () => (turn.decision === "approve" ? "s" : "n"),
|
|
136
|
+
});
|
|
137
|
+
if (turn.before_execute && decided.state === "approved") {
|
|
138
|
+
applyHook(s, turn.before_execute);
|
|
139
|
+
return s.engine.execute(decided.id);
|
|
140
|
+
}
|
|
141
|
+
return decided;
|
|
142
|
+
});
|
|
143
|
+
const result = await loop.turn(turn.input);
|
|
144
|
+
replies.push(result.reply);
|
|
145
|
+
refusedBeforeDraft += s.store.listEvents({ run_id: s.runId }).filter((e) => e.type === "execution.refused_before_draft").length - refusedBeforeDraft;
|
|
146
|
+
}
|
|
147
|
+
// Section 10: what is still executing is reconciled, never repeated. A rail that answers late needs more than one look.
|
|
148
|
+
for (let round = 0; round < 3 && s.engine.list({ state: "executing" }).length > 0; round += 1) {
|
|
149
|
+
for (const stuck of s.engine.list({ state: "executing" })) await s.engine.reconcile(stuck.id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const executions = s.engine.list().filter((e) => e.run_id === s.runId);
|
|
153
|
+
return {
|
|
154
|
+
scenario: scenario.name,
|
|
155
|
+
mode: options.mode,
|
|
156
|
+
run_id: s.runId,
|
|
157
|
+
bundle_dir: s.bundle.dir,
|
|
158
|
+
replies,
|
|
159
|
+
executions: executions.map((e) => ({
|
|
160
|
+
id: e.id,
|
|
161
|
+
state: e.state,
|
|
162
|
+
reason: e.reason ?? null,
|
|
163
|
+
trigger: e.escalation?.trigger ?? null,
|
|
164
|
+
total: e.total,
|
|
165
|
+
trail: e.history.map((h) => h.to),
|
|
166
|
+
receipt_ids: e.outcomes.filter((o) => o.receipt_id).map((o) => o.receipt_id as string),
|
|
167
|
+
})),
|
|
168
|
+
refused_before_draft: refusedBeforeDraft,
|
|
169
|
+
receipts: s.bundle.listReceipts().length,
|
|
170
|
+
settled_total: executions.filter((e) => e.state === "settled").reduce((sum, e) => sum + e.total, 0),
|
|
171
|
+
};
|
|
172
|
+
} finally {
|
|
173
|
+
s.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function applyHook(s: Setup, hook: "revoke_mandate" | "pause_all" | undefined): void {
|
|
178
|
+
if (hook === "revoke_mandate") s.gate.revoke(s.mandate.id, "revoked mid-run by the scenario");
|
|
179
|
+
if (hook === "pause_all") s.gate.pauseAll();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function checkScenario(scenario: Scenario, run: ScenarioRun): ScenarioCheck {
|
|
183
|
+
const failures: string[] = [];
|
|
184
|
+
const expectations = [scenario.expect["both"], scenario.expect[run.mode]].filter((e): e is z.infer<typeof ExpectSchema> => e !== undefined);
|
|
185
|
+
for (const expect of expectations) {
|
|
186
|
+
if (expect.states && JSON.stringify(run.executions.map((e) => e.state)) !== JSON.stringify(expect.states)) failures.push(`states ${JSON.stringify(run.executions.map((e) => e.state))} != ${JSON.stringify(expect.states)}`);
|
|
187
|
+
if (expect.trails && JSON.stringify(run.executions.map((e) => e.trail)) !== JSON.stringify(expect.trails)) failures.push(`trails ${JSON.stringify(run.executions.map((e) => e.trail))} != ${JSON.stringify(expect.trails)}`);
|
|
188
|
+
if (expect.reasons && JSON.stringify(run.executions.map((e) => e.reason)) !== JSON.stringify(expect.reasons)) failures.push(`reasons ${JSON.stringify(run.executions.map((e) => e.reason))} != ${JSON.stringify(expect.reasons)}`);
|
|
189
|
+
if (expect.triggers && JSON.stringify(run.executions.map((e) => e.trigger)) !== JSON.stringify(expect.triggers)) failures.push(`triggers ${JSON.stringify(run.executions.map((e) => e.trigger))} != ${JSON.stringify(expect.triggers)}`);
|
|
190
|
+
if (expect.receipts !== undefined && run.receipts !== expect.receipts) failures.push(`receipts ${run.receipts} != ${expect.receipts}`);
|
|
191
|
+
if (expect.refused_before_draft !== undefined && run.refused_before_draft !== expect.refused_before_draft) failures.push(`refused_before_draft ${run.refused_before_draft} != ${expect.refused_before_draft}`);
|
|
192
|
+
if (expect.settled_total !== undefined && run.settled_total !== expect.settled_total) failures.push(`settled_total ${run.settled_total} != ${expect.settled_total}`);
|
|
193
|
+
}
|
|
194
|
+
// Every run, every mode: nothing reaches executing without an approval artifact carrying its hash.
|
|
195
|
+
return { ok: failures.length === 0, mode: run.mode, failures, run };
|
|
196
|
+
}
|