@codespar/cli 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/dist/api.js +1 -3
- package/dist/api.js.map +1 -1
- package/dist/commands/agent.js +260 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mandate-revoke.js +30 -0
- package/dist/commands/mandate-revoke.js.map +1 -0
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +36 -3
- package/dist/index.js.map +1 -1
- package/dist/surface.js +1 -1
- package/dist/surface.js.map +1 -1
- package/package.json +3 -1
- package/templates/bills-agent/LICENSE +21 -0
- package/templates/bills-agent/README.md +21 -0
- package/templates/bills-agent/_gitignore +10 -0
- package/templates/bills-agent/agents/bills-agent/.env.example +7 -0
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +16 -0
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +16 -0
- package/templates/bills-agent/agents/bills-agent/README.md +89 -0
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +23 -0
- package/templates/bills-agent/agents/bills-agent/agent.yaml +26 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +108 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/eval.yaml +9 -0
- package/templates/bills-agent/agents/bills-agent/guardrails.json +15 -0
- package/templates/bills-agent/agents/bills-agent/mandate.example.json +46 -0
- package/templates/bills-agent/agents/bills-agent/package.json +24 -0
- package/templates/bills-agent/agents/bills-agent/runbook.md +19 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/src/adversarial.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/src/bills.ts +28 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +9 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +13 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +26 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +45 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +7 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +37 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +38 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +65 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +30 -0
- package/templates/bills-agent/agents/bills-agent/src/main.ts +185 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +125 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/pix-out.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/src/scenarios.ts +196 -0
- package/templates/bills-agent/agents/bills-agent/src/setup.ts +207 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +40 -0
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +85 -0
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/tools.json +51 -0
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +11 -0
- package/templates/bills-agent/package.json +34 -0
- package/templates/bills-agent/packages/agent-core/README.md +19 -0
- package/templates/bills-agent/packages/agent-core/package.json +24 -0
- package/templates/bills-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/bills-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/bills-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/bills-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/bills-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/bills-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/bills-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/bills-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/bills-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/bills-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/bills-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/bills-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/bills-agent/tsconfig.base.json +16 -0
- package/templates/bills-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/LICENSE +21 -0
- package/templates/collections-agent/README.md +20 -0
- package/templates/collections-agent/_gitignore +10 -0
- package/templates/collections-agent/agents/collections-agent/.env.example +4 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +19 -0
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +19 -0
- package/templates/collections-agent/agents/collections-agent/README.md +94 -0
- package/templates/collections-agent/agents/collections-agent/SYSTEM_PROMPT.md +31 -0
- package/templates/collections-agent/agents/collections-agent/agent.yaml +28 -0
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +178 -0
- package/templates/collections-agent/agents/collections-agent/channels/webhook/index.ts +131 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap-mandate.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.json +19 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.json +13 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/eval.yaml +11 -0
- package/templates/collections-agent/agents/collections-agent/guardrails.json +20 -0
- package/templates/collections-agent/agents/collections-agent/mandate.example.json +40 -0
- package/templates/collections-agent/agents/collections-agent/package.json +29 -0
- package/templates/collections-agent/agents/collections-agent/runbook.md +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.json +36 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.transcript.jsonl +5 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.json +60 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.transcript.jsonl +6 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/src/adversarial.ts +190 -0
- package/templates/collections-agent/agents/collections-agent/src/agreements.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +10 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +13 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +48 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +7 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +39 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +35 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +38 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +30 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +36 -0
- package/templates/collections-agent/agents/collections-agent/src/envelope.ts +103 -0
- package/templates/collections-agent/agents/collections-agent/src/main.ts +193 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +105 -0
- package/templates/collections-agent/agents/collections-agent/src/poll.ts +94 -0
- package/templates/collections-agent/agents/collections-agent/src/scenarios.ts +252 -0
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +250 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +51 -0
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +172 -0
- package/templates/collections-agent/agents/collections-agent/test/envelope.test.ts +53 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1103.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +95 -0
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +100 -0
- package/templates/collections-agent/agents/collections-agent/tools.json +40 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +11 -0
- package/templates/collections-agent/package.json +35 -0
- package/templates/collections-agent/packages/agent-core/README.md +19 -0
- package/templates/collections-agent/packages/agent-core/package.json +24 -0
- package/templates/collections-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/collections-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/collections-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/collections-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/collections-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/collections-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/collections-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/collections-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/collections-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/collections-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/collections-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/collections-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/collections-agent/tsconfig.base.json +16 -0
- package/templates/collections-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +41 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires the core for this agent: manifest, guardrails, mandate, state,
|
|
3
|
+
* rail, status source, signer, bundle, provider. Everything the commands
|
|
4
|
+
* and the terminal channel share.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
LocalMandateStatusStub,
|
|
11
|
+
AgentLoop,
|
|
12
|
+
ApiMandateStatusSource,
|
|
13
|
+
CodeSparRail,
|
|
14
|
+
ExecutionEngine,
|
|
15
|
+
ProofBundle,
|
|
16
|
+
ReplayRuntime,
|
|
17
|
+
StateStore,
|
|
18
|
+
StubRail,
|
|
19
|
+
createCodeSparClient,
|
|
20
|
+
isTestKey,
|
|
21
|
+
loadGuardrails,
|
|
22
|
+
loadManifest,
|
|
23
|
+
loadMandate,
|
|
24
|
+
loadOrCreateLocalApprovalKey,
|
|
25
|
+
loadToolsFile,
|
|
26
|
+
newRunId,
|
|
27
|
+
NotATestKeyError,
|
|
28
|
+
type AgentRuntime,
|
|
29
|
+
type ApprovalMode,
|
|
30
|
+
type Execution,
|
|
31
|
+
type LoadedManifest,
|
|
32
|
+
type Mandate,
|
|
33
|
+
type MandateStatusSource,
|
|
34
|
+
type PaymentRail,
|
|
35
|
+
type StubRailOptions,
|
|
36
|
+
type ToolHandler,
|
|
37
|
+
} from "@codespar/agent-core";
|
|
38
|
+
import { AnthropicRuntime } from "@codespar/agent-core/providers/anthropic";
|
|
39
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
40
|
+
import { codesparLedger, codesparPay, listBills } from "./modules/pix-out.js";
|
|
41
|
+
import { loadLocalMandate } from "./modules/embedded-consent.js";
|
|
42
|
+
|
|
43
|
+
export const AGENT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
|
+
export const STATE_DIR = join(AGENT_DIR, ".codespar");
|
|
45
|
+
export const RUNS_DIR = join(AGENT_DIR, "runs");
|
|
46
|
+
export const MANDATE_PATH = join(STATE_DIR, "mandate.json");
|
|
47
|
+
|
|
48
|
+
/** The runs folder in use: the agent's own, or the scratch one the restart test points at. */
|
|
49
|
+
export function runsDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
50
|
+
return env["BILLS_RUNS_DIR"] ?? RUNS_DIR;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type RailKind = "stub" | "api";
|
|
54
|
+
|
|
55
|
+
export interface SetupOptions {
|
|
56
|
+
mode?: ApprovalMode | undefined;
|
|
57
|
+
rail?: RailKind | undefined;
|
|
58
|
+
provider?: "anthropic" | "replay" | undefined;
|
|
59
|
+
transcript?: string | undefined;
|
|
60
|
+
runId?: string | undefined;
|
|
61
|
+
runsDir?: string | undefined;
|
|
62
|
+
stateDir?: string | undefined;
|
|
63
|
+
now?: (() => Date) | undefined;
|
|
64
|
+
stubRail?: StubRailOptions | undefined;
|
|
65
|
+
/** Use this mandate instead of the local or example one (scenarios). */
|
|
66
|
+
mandate?: Mandate | undefined;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
say?: ((line: string) => void) | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Setup {
|
|
72
|
+
manifest: LoadedManifest;
|
|
73
|
+
mode: ApprovalMode;
|
|
74
|
+
mandate: Mandate;
|
|
75
|
+
store: StateStore;
|
|
76
|
+
gate: LocalMandateStatusStub;
|
|
77
|
+
rail: PaymentRail;
|
|
78
|
+
railKind: RailKind;
|
|
79
|
+
api: ApiClient | undefined;
|
|
80
|
+
status: MandateStatusSource;
|
|
81
|
+
bundle: ProofBundle;
|
|
82
|
+
runId: string;
|
|
83
|
+
engine: ExecutionEngine;
|
|
84
|
+
system: string;
|
|
85
|
+
handlers: Record<string, ToolHandler>;
|
|
86
|
+
tools: ReturnType<typeof loadToolsFile>;
|
|
87
|
+
makeLoop(runtime: AgentRuntime, onExecution: (execution: Execution) => Promise<Execution>): AgentLoop;
|
|
88
|
+
makeRuntime(): AgentRuntime;
|
|
89
|
+
close(): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function readDotEnv(agentDir = AGENT_DIR): void {
|
|
93
|
+
const path = join(agentDir, ".env");
|
|
94
|
+
if (!existsSync(path)) return;
|
|
95
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
96
|
+
const m = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
|
97
|
+
if (!m || !m[1]) continue;
|
|
98
|
+
const value = (m[2] ?? "").replace(/^["']|["']$/g, "");
|
|
99
|
+
if (process.env[m[1]] === undefined) process.env[m[1]] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveRailKind(env: NodeJS.ProcessEnv, requested: RailKind | undefined): RailKind {
|
|
104
|
+
if (requested) return requested;
|
|
105
|
+
const key = env["CODESPAR_API_KEY"];
|
|
106
|
+
if (!key || key === "csk_test_your_key_here") return "stub";
|
|
107
|
+
return "api";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function setup(options: SetupOptions = {}): Setup {
|
|
111
|
+
const env = options.env ?? process.env;
|
|
112
|
+
const say = options.say ?? ((line: string) => process.stderr.write(line + "\n"));
|
|
113
|
+
const manifest = loadManifest(join(AGENT_DIR, "agent.yaml"));
|
|
114
|
+
const guardrails = loadGuardrails(manifest.resolvePath(manifest.manifest.guardrails));
|
|
115
|
+
const tools = loadToolsFile(manifest.resolvePath(manifest.manifest.tools));
|
|
116
|
+
const system = readFileSync(join(AGENT_DIR, "SYSTEM_PROMPT.md"), "utf8");
|
|
117
|
+
const mode: ApprovalMode = options.mode ?? manifest.manifest.default_approval;
|
|
118
|
+
if (!manifest.manifest.approval.includes(mode)) throw new Error(`agent.yaml does not support approval: ${mode}`);
|
|
119
|
+
|
|
120
|
+
// BILLS_STATE_DIR / BILLS_RUNS_DIR exist for the restart test, which drives a child process over a scratch state.
|
|
121
|
+
const stateDir = options.stateDir ?? env["BILLS_STATE_DIR"] ?? STATE_DIR;
|
|
122
|
+
const runs = options.runsDir ?? runsDir(env);
|
|
123
|
+
const store = new StateStore(join(stateDir, "state.db"));
|
|
124
|
+
const gate = new LocalMandateStatusStub(store, options.now);
|
|
125
|
+
const signer = loadOrCreateLocalApprovalKey(stateDir);
|
|
126
|
+
|
|
127
|
+
const railKind = resolveRailKind(env, options.rail);
|
|
128
|
+
let api: ApiClient | undefined;
|
|
129
|
+
let rail: PaymentRail;
|
|
130
|
+
let status: MandateStatusSource = gate;
|
|
131
|
+
let mandate: Mandate;
|
|
132
|
+
if (railKind === "api") {
|
|
133
|
+
if (!isTestKey(env["CODESPAR_API_KEY"])) throw new NotATestKeyError();
|
|
134
|
+
api = createCodeSparClient({ apiKey: env["CODESPAR_API_KEY"], baseUrl: env["CODESPAR_API_URL"], projectId: env["CODESPAR_PROJECT_ID"] });
|
|
135
|
+
const local = options.mandate ?? loadLocalMandate(MANDATE_PATH);
|
|
136
|
+
if (!local) throw new NoMandateError();
|
|
137
|
+
mandate = local;
|
|
138
|
+
rail = new CodeSparRail(api, { canonical: mandate.canonical, signature: mandate.signature });
|
|
139
|
+
// Section 4.7 against the real status: the local stub answers only runs without a key.
|
|
140
|
+
status = new ApiMandateStatusSource(api, options.now);
|
|
141
|
+
} else {
|
|
142
|
+
// BILLS_KILL_AFTER_DISPATCH=1 simulates a crash right after the rail accepted the attempt and before the outcome was recorded.
|
|
143
|
+
const killAfterDispatch = env["BILLS_KILL_AFTER_DISPATCH"] === "1" ? { afterDispatch: () => process.exit(137) } : {};
|
|
144
|
+
// BILLS_STUB_REFUSE=<payee,payee>: the stub rail refuses these payees, to drive a partial failure from a test process.
|
|
145
|
+
const refuse = env["BILLS_STUB_REFUSE"] ? { refusePayees: env["BILLS_STUB_REFUSE"].split(",").map((p) => p.trim()).filter(Boolean) } : {};
|
|
146
|
+
rail = new StubRail(store, { ...(options.now ? { clock: options.now } : {}), ...killAfterDispatch, ...refuse, ...(options.stubRail ?? {}) });
|
|
147
|
+
mandate = options.mandate ?? loadMandate(manifest.resolvePath(manifest.manifest.mandate_schema));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const runId = options.runId ?? newRunId(mode);
|
|
151
|
+
const bundle = new ProofBundle(runs, runId);
|
|
152
|
+
bundle.mandateSnapshot(mandate);
|
|
153
|
+
bundle.meta({ run_id: runId, agent: `${manifest.manifest.name}@${manifest.manifest.version}`, mode, rail: railKind, mandate_id: mandate.id, started_at: (options.now ?? (() => new Date()))().toISOString() });
|
|
154
|
+
|
|
155
|
+
const engine = new ExecutionEngine({
|
|
156
|
+
store,
|
|
157
|
+
rail,
|
|
158
|
+
status,
|
|
159
|
+
signer,
|
|
160
|
+
manifest: manifest.manifest,
|
|
161
|
+
guardrails: { ...guardrails, approval: mode },
|
|
162
|
+
mandate,
|
|
163
|
+
bundle,
|
|
164
|
+
mode,
|
|
165
|
+
runId,
|
|
166
|
+
onBehalfOf: mandate.consumer_id,
|
|
167
|
+
...(options.now ? { clock: options.now } : {}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const handlers: Record<string, ToolHandler> = { codespar_pay: codesparPay, codespar_ledger: codesparLedger, list_bills: listBills };
|
|
171
|
+
|
|
172
|
+
const makeRuntime = (): AgentRuntime => {
|
|
173
|
+
const provider = options.provider ?? (env["ANTHROPIC_API_KEY"] && env["ANTHROPIC_API_KEY"] !== "sk-ant-your_key_here" ? "anthropic" : "replay");
|
|
174
|
+
if (provider === "anthropic") return new AnthropicRuntime({ apiKey: env["ANTHROPIC_API_KEY"] });
|
|
175
|
+
if (!options.transcript) throw new Error("the replay provider needs a transcript (--transcript <file> or --scenario <name>)");
|
|
176
|
+
say(`[replay] no ANTHROPIC_API_KEY: replaying ${options.transcript}`);
|
|
177
|
+
return ReplayRuntime.fromFile(options.transcript);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
manifest,
|
|
182
|
+
mode,
|
|
183
|
+
mandate,
|
|
184
|
+
store,
|
|
185
|
+
gate,
|
|
186
|
+
rail,
|
|
187
|
+
railKind,
|
|
188
|
+
api,
|
|
189
|
+
status,
|
|
190
|
+
bundle,
|
|
191
|
+
runId,
|
|
192
|
+
engine,
|
|
193
|
+
system,
|
|
194
|
+
handlers,
|
|
195
|
+
tools,
|
|
196
|
+
makeRuntime,
|
|
197
|
+
makeLoop: (runtime, onExecution) => new AgentLoop({ runtime, tools, handlers, system, bundle, engine, onExecution, ...(options.now ? { clock: options.now } : {}) }),
|
|
198
|
+
close: () => store.close(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export class NoMandateError extends Error {
|
|
203
|
+
constructor() {
|
|
204
|
+
super("no signed mandate yet: run `npm run consent` (or `npm start`, which starts the consent when a test key is present)");
|
|
205
|
+
this.name = "NoMandateError";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { listAdversarialCases, loadAdversarialCase, runAdversarialCase } from "../src/adversarial.js";
|
|
6
|
+
|
|
7
|
+
const runsDir = mkdtempSync(join(tmpdir(), "bills-adv-runs-"));
|
|
8
|
+
|
|
9
|
+
describe("section 9: adversarial suite (blocks merge)", () => {
|
|
10
|
+
const required = ["prompt-injection", "beneficiary-swap", "false-authority", "fractioning", "exfiltration", "model-total", "webhook-replay"];
|
|
11
|
+
|
|
12
|
+
it("ships every attack of the table", () => {
|
|
13
|
+
for (const name of required) expect(listAdversarialCases()).toContain(name);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
for (const name of listAdversarialCases()) {
|
|
17
|
+
it(`${name}: the state machine, not the wording, decides`, async () => {
|
|
18
|
+
const result = await runAdversarialCase(loadAdversarialCase(name), { runsDir });
|
|
19
|
+
expect(result.failures).toEqual([]);
|
|
20
|
+
expect(result.ok).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it("prompt injection: a model that complies still cannot pay", async () => {
|
|
25
|
+
const r = await runAdversarialCase(loadAdversarialCase("prompt-injection"), { runsDir });
|
|
26
|
+
expect(r.tools_called).toContain("codespar_pay");
|
|
27
|
+
expect(r.states).toEqual(["denied"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("exfiltration: tools outside tools.json are refused before any handler runs", async () => {
|
|
31
|
+
const r = await runAdversarialCase(loadAdversarialCase("exfiltration"), { runsDir });
|
|
32
|
+
expect(r.tools_refused).toEqual(["codespar_wallet", "codespar_manage_connections"]);
|
|
33
|
+
expect(r.tools_called).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("fractioning: at most the threshold runs alone; the rest waits for a human", async () => {
|
|
37
|
+
const r = await runAdversarialCase(loadAdversarialCase("fractioning"), { runsDir });
|
|
38
|
+
expect(r.states).toEqual(["settled", "settled", "settled", "awaiting_approval", "awaiting_approval"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cpSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { checkAgent } from "@codespar/agent-core";
|
|
6
|
+
|
|
7
|
+
const AGENT_DIR = resolve(import.meta.dirname, "..");
|
|
8
|
+
|
|
9
|
+
function copyAgent(): string {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "bills-check-"));
|
|
11
|
+
for (const f of ["agent.yaml", "SYSTEM_PROMPT.md", "tools.json", "guardrails.json", "mandate.example.json", "AGENTS.md", "CLAUDE.md", "README.md", "runbook.md", ".env.example"]) cpSync(join(AGENT_DIR, f), join(dir, f));
|
|
12
|
+
cpSync(join(AGENT_DIR, "scenarios"), join(dir, "scenarios"), { recursive: true });
|
|
13
|
+
cpSync(join(AGENT_DIR, "evals"), join(dir, "evals"), { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const codes = (dir: string) => checkAgent(dir).findings.filter((f) => f.level === "error").map((f) => f.code);
|
|
18
|
+
|
|
19
|
+
describe("npm run check: the manifest is the index", () => {
|
|
20
|
+
it("passes on the shipped agent", () => {
|
|
21
|
+
expect(codes(AGENT_DIR)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("fails when guardrails contradict the manifest", () => {
|
|
25
|
+
const dir = copyAgent();
|
|
26
|
+
const g = JSON.parse(readFileSync(join(dir, "guardrails.json"), "utf8")) as { approval: string; escalate_above: { amount: number } };
|
|
27
|
+
g.approval = "mandate";
|
|
28
|
+
writeFileSync(join(dir, "guardrails.json"), JSON.stringify(g));
|
|
29
|
+
expect(codes(dir)).toContain("guardrails_contradict_manifest");
|
|
30
|
+
g.approval = "human";
|
|
31
|
+
g.escalate_above.amount = 999;
|
|
32
|
+
writeFileSync(join(dir, "guardrails.json"), JSON.stringify(g));
|
|
33
|
+
expect(codes(dir)).toContain("guardrails_contradict_manifest");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("fails when the prompt names a tool tools.json does not have, or when tools.json drops the payment tool", () => {
|
|
37
|
+
const dir = copyAgent();
|
|
38
|
+
writeFileSync(join(dir, "SYSTEM_PROMPT.md"), readFileSync(join(dir, "SYSTEM_PROMPT.md"), "utf8") + "\nYou may also call codespar_wallet.\n");
|
|
39
|
+
expect(codes(dir)).toContain("prompt_contradicts_tools");
|
|
40
|
+
const t = JSON.parse(readFileSync(join(dir, "tools.json"), "utf8")) as { meta_tools: Array<{ effect: string }> };
|
|
41
|
+
t.meta_tools = t.meta_tools.filter((x) => x.effect !== "payment");
|
|
42
|
+
writeFileSync(join(dir, "tools.json"), JSON.stringify(t));
|
|
43
|
+
expect(codes(dir)).toContain("tools_contradict_manifest");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("fails when AGENTS.md and CLAUDE.md diverge", () => {
|
|
47
|
+
const dir = copyAgent();
|
|
48
|
+
writeFileSync(join(dir, "CLAUDE.md"), readFileSync(join(dir, "CLAUDE.md"), "utf8") + "\nextra line\n");
|
|
49
|
+
expect(codes(dir)).toContain("agents_md_diverges");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("fails when mcp, cli or schema are missing", () => {
|
|
53
|
+
for (const field of ["mcp", "cli", "schema"]) {
|
|
54
|
+
const dir = copyAgent();
|
|
55
|
+
writeFileSync(join(dir, "agent.yaml"), readFileSync(join(dir, "agent.yaml"), "utf8").split("\n").filter((l) => !l.startsWith(`${field}:`)).join("\n"));
|
|
56
|
+
expect(codes(dir)).toContain("manifest_field_missing");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("fails when a README claims third-party verifiability or .env.example grows a third key", () => {
|
|
61
|
+
const dir = copyAgent();
|
|
62
|
+
writeFileSync(join(dir, "README.md"), readFileSync(join(dir, "README.md"), "utf8") + "\nO recibo é verificável por terceiro.\n");
|
|
63
|
+
expect(codes(dir)).toContain("doc_overclaims");
|
|
64
|
+
writeFileSync(join(dir, ".env.example"), readFileSync(join(dir, ".env.example"), "utf8") + "APPROVAL_KEY=x\n");
|
|
65
|
+
expect(codes(dir)).toContain("env_example_extra");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("fails when agent.yaml declares an event the API does not publish; the charge family the collections-agent declares passes", () => {
|
|
69
|
+
const dir = copyAgent();
|
|
70
|
+
const manifest = join(dir, "agent.yaml");
|
|
71
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8").replace("commerce.payment.succeeded", "commerce.payment.settled"));
|
|
72
|
+
const report = checkAgent(dir);
|
|
73
|
+
expect(report.findings.filter((f) => f.code === "events_unknown").map((f) => f.message)).toEqual([expect.stringContaining("commerce.payment.settled")]);
|
|
74
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8").replace("[commerce.payment.settled, commerce.payment.failed]", "[commerce.charge.paid, commerce.charge.expired]"));
|
|
75
|
+
expect(codes(dir)).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("fails when eval.yaml does not extend the manifest or redeclares a field", () => {
|
|
79
|
+
const dir = copyAgent();
|
|
80
|
+
writeFileSync(join(dir, "evals", "eval.yaml"), "extends: ../agent.yaml\nname: other\n");
|
|
81
|
+
expect(codes(dir)).toContain("eval_redeclares_manifest");
|
|
82
|
+
writeFileSync(join(dir, "evals", "eval.yaml"), "cases: []\n");
|
|
83
|
+
expect(codes(dir)).toContain("eval_extends");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contracts that need a real process: `--json` on stdout and nothing
|
|
3
|
+
* else, the restart in `executing` followed by `resume`, and `rerun`.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
const AGENT_DIR = resolve(import.meta.dirname, "..");
|
|
13
|
+
const NODE = process.execPath;
|
|
14
|
+
|
|
15
|
+
function run(script: string, args: string[], env: Record<string, string>) {
|
|
16
|
+
const result = spawnSync(NODE, ["--disable-warning=ExperimentalWarning", "--import", "tsx", script, ...args], {
|
|
17
|
+
cwd: AGENT_DIR,
|
|
18
|
+
env: { ...process.env, ANTHROPIC_API_KEY: "", CODESPAR_API_KEY: "", ...env },
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
timeout: 60_000,
|
|
21
|
+
});
|
|
22
|
+
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("npm start -- --input ... --json", () => {
|
|
26
|
+
it("prints valid JSON on stdout and nothing else; people go to stderr", () => {
|
|
27
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-json-"));
|
|
28
|
+
const out = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: join(stateDir, "runs") });
|
|
29
|
+
expect(out.code).toBe(0);
|
|
30
|
+
const lines = out.stdout.split("\n").filter(Boolean);
|
|
31
|
+
expect(lines).toHaveLength(1);
|
|
32
|
+
const payload = JSON.parse(lines[0]!) as { run_id: string; mode: string; actor: { type: string }; executions: Array<{ state: string; receipt_ids: string[] }>; receipts: string[] };
|
|
33
|
+
expect(payload.mode).toBe("human");
|
|
34
|
+
expect(payload.actor.type).toBe("agent");
|
|
35
|
+
expect(payload.executions.map((e) => e.state)).toEqual(["settled"]);
|
|
36
|
+
expect(payload.receipts).toHaveLength(1);
|
|
37
|
+
expect(out.stderr).toContain("replay");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("refuses a key outside csk_test_ before anything else", () => {
|
|
41
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-live-"));
|
|
42
|
+
const out = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], { BILLS_STATE_DIR: stateDir, CODESPAR_API_KEY: ["csk", "live", "0000000000"].join("_") });
|
|
43
|
+
expect(out.code).toBe(1);
|
|
44
|
+
expect(out.stdout).toBe("");
|
|
45
|
+
expect(out.stderr).toContain("csk_test_");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("section 10: restart in executing, then resume", () => {
|
|
50
|
+
it("one payment, one receipt, never two", () => {
|
|
51
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-restart-"));
|
|
52
|
+
const runsDir = join(stateDir, "runs");
|
|
53
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
54
|
+
|
|
55
|
+
const killed = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], { ...env, BILLS_KILL_AFTER_DISPATCH: "1" });
|
|
56
|
+
expect(killed.code).toBe(137);
|
|
57
|
+
expect(killed.stdout).toBe("");
|
|
58
|
+
|
|
59
|
+
const db = new DatabaseSync(join(stateDir, "state.db"));
|
|
60
|
+
const stuck = db.prepare("SELECT state FROM executions").all() as Array<{ state: string }>;
|
|
61
|
+
expect(stuck).toEqual([{ state: "executing" }]);
|
|
62
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
63
|
+
db.close();
|
|
64
|
+
|
|
65
|
+
const resumed = run("src/commands/resume.ts", ["--json"], env);
|
|
66
|
+
expect(resumed.code).toBe(0);
|
|
67
|
+
const payload = JSON.parse(resumed.stdout.trim()) as { resumed: Array<{ state: string; receipt_ids: string[] }> };
|
|
68
|
+
expect(payload.resumed).toHaveLength(1);
|
|
69
|
+
expect(payload.resumed[0]?.state).toBe("settled");
|
|
70
|
+
expect(payload.resumed[0]?.receipt_ids).toHaveLength(1);
|
|
71
|
+
|
|
72
|
+
const db2 = new DatabaseSync(join(stateDir, "state.db"));
|
|
73
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
74
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM events WHERE type = 'commerce.payment.succeeded'").get() as { n: number }).n).toBe(1);
|
|
75
|
+
expect(db2.prepare("SELECT state FROM executions").all()).toEqual([{ state: "settled" }]);
|
|
76
|
+
db2.close();
|
|
77
|
+
|
|
78
|
+
const receipts = readdirSync(runsDir).flatMap((r) => (existsSync(join(runsDir, r, "receipts")) ? readdirSync(join(runsDir, r, "receipts")) : []));
|
|
79
|
+
expect(receipts).toHaveLength(1);
|
|
80
|
+
|
|
81
|
+
const again = run("src/commands/resume.ts", ["--json"], env);
|
|
82
|
+
expect(JSON.parse(again.stdout.trim())).toMatchObject({ resumed: [] });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("npm run rerun <run-id>", () => {
|
|
87
|
+
it("reproduces a recorded run without network, with the same state sequence", () => {
|
|
88
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-rerun-"));
|
|
89
|
+
const runsDir = join(stateDir, "runs");
|
|
90
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
91
|
+
const first = run("src/main.ts", ["--input", "pague a escola de outubro", "--approve", "--json"], env);
|
|
92
|
+
expect(first.code).toBe(0);
|
|
93
|
+
const { run_id } = JSON.parse(first.stdout.trim()) as { run_id: string };
|
|
94
|
+
const rerun = run("src/commands/rerun.ts", [run_id, "--json"], env);
|
|
95
|
+
expect(rerun.code).toBe(0);
|
|
96
|
+
const payload = JSON.parse(rerun.stdout.trim()) as { same_states: boolean; original: string[]; rerun: string[] };
|
|
97
|
+
expect(payload.same_states).toBe(true);
|
|
98
|
+
expect(payload.original).toEqual(["awaiting_approval", "approved", "executing", "settled"]);
|
|
99
|
+
expect(readFileSync(join(runsDir, run_id, "transcript.jsonl"), "utf8")).toContain("assistant_step");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("npm run check", () => {
|
|
104
|
+
it("is green for the shipped agent and prints JSON with --json", () => {
|
|
105
|
+
const out = run("src/commands/check.ts", ["--json"], {});
|
|
106
|
+
expect(out.code).toBe(0);
|
|
107
|
+
const report = JSON.parse(out.stdout.trim()) as { ok: boolean; agent: string; findings: unknown[] };
|
|
108
|
+
expect(report).toMatchObject({ ok: true, agent: "bills-agent" });
|
|
109
|
+
expect(report.findings.filter((f) => (f as { level: string }).level === "error")).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("npm run approve / deny <execution-id>", () => {
|
|
114
|
+
it("decides an execution left awaiting, produces the artifact and runs it through the last gate", () => {
|
|
115
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-decide-"));
|
|
116
|
+
const runsDir = join(stateDir, "runs");
|
|
117
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir };
|
|
118
|
+
const left = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], env);
|
|
119
|
+
expect(left.code).toBe(0);
|
|
120
|
+
const payload = JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string; state: string }> };
|
|
121
|
+
expect(payload.executions[0]?.state).toBe("awaiting_approval");
|
|
122
|
+
const id = payload.executions[0]!.id;
|
|
123
|
+
|
|
124
|
+
const denied = run("src/commands/deny.ts", ["nope_unknown", "--json"], env);
|
|
125
|
+
expect(denied.code).toBe(1);
|
|
126
|
+
|
|
127
|
+
const approved = run("src/commands/approve.ts", [id, "--user", "usr_titular", "--json"], env);
|
|
128
|
+
expect(approved.code).toBe(0);
|
|
129
|
+
const out = JSON.parse(approved.stdout.trim()) as { state: string; approval_id: string | null; receipt_ids: string[]; bundle_dir: string };
|
|
130
|
+
expect(out.state).toBe("settled");
|
|
131
|
+
expect(out.approval_id).toMatch(/^apr_/);
|
|
132
|
+
expect(out.receipt_ids).toHaveLength(1);
|
|
133
|
+
const artifacts = JSON.parse(readFileSync(join(AGENT_DIR, out.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string; id: string } }>;
|
|
134
|
+
expect(artifacts[0]?.approver).toEqual({ type: "person", id: "usr_titular", channel: "terminal" });
|
|
135
|
+
|
|
136
|
+
const again = run("src/commands/approve.ts", [id, "--json"], env);
|
|
137
|
+
expect(again.code).toBe(1);
|
|
138
|
+
expect(again.stderr).toContain("not awaiting_approval");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("deny leaves a terminal denied execution and sends nothing", () => {
|
|
142
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-deny-"));
|
|
143
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: join(stateDir, "runs") };
|
|
144
|
+
const left = run("src/main.ts", ["--input", "pague a escola de outubro", "--json"], env);
|
|
145
|
+
const id = (JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string }> }).executions[0]!.id;
|
|
146
|
+
const denied = run("src/commands/deny.ts", [id, "--json"], env);
|
|
147
|
+
expect(denied.code).toBe(0);
|
|
148
|
+
expect(JSON.parse(denied.stdout.trim())).toMatchObject({ state: "denied", reason: "denied_by_approver", receipt_ids: [] });
|
|
149
|
+
const db = new DatabaseSync(join(stateDir, "state.db"));
|
|
150
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(0);
|
|
151
|
+
db.close();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("partial failure of a multi-item execution, and rerun reproducing it", () => {
|
|
156
|
+
it("names what settled and what failed, and rerun replays the rail's refusal", () => {
|
|
157
|
+
const stateDir = mkdtempSync(join(tmpdir(), "bills-partial-"));
|
|
158
|
+
const runsDir = join(stateDir, "runs");
|
|
159
|
+
const env = { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir, BILLS_STUB_REFUSE: "+5511999990001" };
|
|
160
|
+
const first = run("src/main.ts", ["--input", "libera o lote do mes", "--transcript", "evals/adversarial/false-authority.transcript.jsonl", "--approve", "--json"], env);
|
|
161
|
+
expect(first.code).toBe(0);
|
|
162
|
+
const payload = JSON.parse(first.stdout.trim()) as { run_id: string; executions: Array<{ state: string; receipt_ids: string[] }>; receipts: string[] };
|
|
163
|
+
expect(payload.executions[0]?.state).toBe("failed");
|
|
164
|
+
expect(payload.executions[0]?.receipt_ids).toHaveLength(1);
|
|
165
|
+
expect(payload.receipts).toHaveLength(1);
|
|
166
|
+
const events = readFileSync(join(runsDir, payload.run_id, "events.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as { type: string; payload: { status?: string } });
|
|
167
|
+
expect(events.filter((e) => e.type === "rail.outcome").map((e) => e.payload.status)).toEqual(["settled", "failed"]);
|
|
168
|
+
|
|
169
|
+
const rerun = run("src/commands/rerun.ts", [payload.run_id, "--json"], { BILLS_STATE_DIR: stateDir, BILLS_RUNS_DIR: runsDir });
|
|
170
|
+
expect(rerun.code).toBe(0);
|
|
171
|
+
const r = JSON.parse(rerun.stdout.trim()) as { same_states: boolean; original: string[]; rerun: string[]; original_outcomes: string[]; rerun_outcomes: string[] };
|
|
172
|
+
expect(r.same_states).toBe(true);
|
|
173
|
+
expect(r.original).toEqual(["awaiting_approval", "approved", "executing", "failed"]);
|
|
174
|
+
expect(r.original_outcomes).toEqual(["settled", "failed"]);
|
|
175
|
+
expect(r.rerun_outcomes).toEqual(["settled", "failed"]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { checkScenario, listScenarios, loadScenario, runScenario, type ScenarioRun } from "../src/scenarios.js";
|
|
6
|
+
|
|
7
|
+
const runsDir = mkdtempSync(join(tmpdir(), "bills-runs-"));
|
|
8
|
+
|
|
9
|
+
describe("section 12: scenario packs, on the replay provider", () => {
|
|
10
|
+
const required = ["happy-path", "cap-exceeded", "beneficiary-not-allowed", "prompt-injection", "escalated-above-threshold", "mandate-revoked"];
|
|
11
|
+
|
|
12
|
+
it("ships every scenario the spec asks for", () => {
|
|
13
|
+
for (const name of required) expect(listScenarios()).toContain(name);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
for (const name of required) {
|
|
17
|
+
const scenario = loadScenario(name);
|
|
18
|
+
for (const mode of scenario.modes) {
|
|
19
|
+
it(`${name} [${mode}] ends in the states the pack declares`, async () => {
|
|
20
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
21
|
+
const check = checkScenario(scenario, run);
|
|
22
|
+
expect(check.failures).toEqual([]);
|
|
23
|
+
expect(check.ok).toBe(true);
|
|
24
|
+
// Every event of the bundle carries an actor.
|
|
25
|
+
const events = readFileSync(join(run.bundle_dir, "events.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as { actor?: unknown });
|
|
26
|
+
expect(events.length).toBeGreaterThan(0);
|
|
27
|
+
for (const e of events) expect(e.actor).toBeDefined();
|
|
28
|
+
// Every receipt carries an actor and no raw payee.
|
|
29
|
+
for (const file of readdirSync(join(run.bundle_dir, "receipts"))) {
|
|
30
|
+
const receipt = JSON.parse(readFileSync(join(run.bundle_dir, "receipts", file), "utf8")) as { actor?: unknown; payment: { payee: string | null } };
|
|
31
|
+
expect(receipt.actor).toBeDefined();
|
|
32
|
+
expect(receipt.payment.payee ?? "").toContain("***");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("happy-path: the same trail and the same receipts in human and in mandate", async () => {
|
|
39
|
+
const scenario = loadScenario("happy-path");
|
|
40
|
+
const human = await runScenario(scenario, { mode: "human", runsDir });
|
|
41
|
+
const mandate = await runScenario(scenario, { mode: "mandate", runsDir });
|
|
42
|
+
expect(human.executions.map((e) => e.trail)).toEqual(mandate.executions.map((e) => e.trail));
|
|
43
|
+
expect(normalizeReceipts(human)).toEqual(normalizeReceipts(mandate));
|
|
44
|
+
// In human, the artifact is a person's; in mandate the person still decided because the amount escalated.
|
|
45
|
+
const approvals = (run: ScenarioRun) => JSON.parse(readFileSync(join(run.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string }; items_hash: string; escalation?: { trigger: string } }>;
|
|
46
|
+
expect(approvals(human)[0]?.approver.type).toBe("person");
|
|
47
|
+
expect(approvals(mandate)[0]?.approver.type).toBe("person");
|
|
48
|
+
expect(approvals(mandate)[0]?.escalation?.trigger).toBe("amount");
|
|
49
|
+
expect(approvals(human)[0]?.items_hash).toBe(approvals(mandate)[0]?.items_hash);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("nothing reaches executing without an approval artifact carrying its items_hash", async () => {
|
|
53
|
+
for (const name of required) {
|
|
54
|
+
const scenario = loadScenario(name);
|
|
55
|
+
for (const mode of scenario.modes) {
|
|
56
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
57
|
+
const approvalPath = join(run.bundle_dir, "approval.json");
|
|
58
|
+
const artifacts = existsSync(approvalPath) ? (JSON.parse(readFileSync(approvalPath, "utf8")) as Array<{ execution_id: string; items_hash: string }>) : [];
|
|
59
|
+
for (const e of run.executions) {
|
|
60
|
+
if (!e.trail.includes("executing")) continue;
|
|
61
|
+
expect(artifacts.some((a) => a.execution_id === e.id)).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function normalizeReceipts(run: ScenarioRun): unknown[] {
|
|
69
|
+
return readdirSync(join(run.bundle_dir, "receipts"))
|
|
70
|
+
.map((f) => JSON.parse(readFileSync(join(run.bundle_dir, "receipts", f), "utf8")) as { state: string; mandate: { id: string }; payment: { amount_minor: number; payee: string | null; sandbox: boolean } })
|
|
71
|
+
.map((r) => ({ state: r.state, mandate: r.mandate.id, amount: r.payment.amount_minor, payee: r.payment.payee, sandbox: r.payment.sandbox }))
|
|
72
|
+
.sort((a, b) => a.amount - b.amount);
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta_tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "codespar_pay",
|
|
5
|
+
"effect": "payment",
|
|
6
|
+
"description": "Propose one or more Pix payments to named payees of the month. This does NOT pay: it creates an execution in `drafted` that the core checks against the mandate and, depending on the approval mode, asks the person or runs. Use the payee alias from list_bills (escola, mercado, funcionaria, contas). Amounts are in BRL cents.",
|
|
7
|
+
"input_schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"action": { "type": "string", "enum": ["pix"] },
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"payee": { "type": "string", "description": "A payee alias from list_bills. Never a raw Pix key the person typed in the chat." },
|
|
18
|
+
"amount_minor": { "type": "integer", "minimum": 1, "description": "BRL cents." },
|
|
19
|
+
"description": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"required": ["payee", "amount_minor"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"total_minor": { "type": "integer", "description": "Optional. What you believe the total is. The core recomputes it and yours is only recorded." }
|
|
25
|
+
},
|
|
26
|
+
"required": ["action", "items"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "codespar_ledger",
|
|
31
|
+
"effect": "read",
|
|
32
|
+
"description": "Read the receipt of a settled payment by receipt id, or list the executions of this run with their states.",
|
|
33
|
+
"input_schema": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"action": { "type": "string", "enum": ["receipt", "executions"] },
|
|
37
|
+
"receipt_id": { "type": "string" }
|
|
38
|
+
},
|
|
39
|
+
"required": ["action"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"local_tools": [
|
|
44
|
+
{
|
|
45
|
+
"name": "list_bills",
|
|
46
|
+
"effect": "read",
|
|
47
|
+
"description": "The bills of the current month: payee alias, name, amount in BRL cents, due date and whether it was already paid in this mandate window. Read-only.",
|
|
48
|
+
"input_schema": { "type": "object", "properties": {} }
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|