@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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool-use loop. The provider says what the model wants; this file
|
|
3
|
+
* dispatches only the tools `tools.json` names, records every step in the
|
|
4
|
+
* transcript, and hands payment tools to the engine, which is the only
|
|
5
|
+
* thing that can move an execution past `drafted`.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProofBundle } from "./bundle.js";
|
|
8
|
+
import type { ExecutionEngine } from "./engine.js";
|
|
9
|
+
import type { AgentRuntime, ConversationMessage, ToolCall, ToolSpec } from "./providers/types.js";
|
|
10
|
+
import { isReply } from "./providers/types.js";
|
|
11
|
+
import type { Execution } from "./state-machine.js";
|
|
12
|
+
import type { ToolsFile } from "./tools.js";
|
|
13
|
+
import { allowedToolNames, toolSpecs } from "./tools.js";
|
|
14
|
+
|
|
15
|
+
export interface ToolContext {
|
|
16
|
+
engine: ExecutionEngine;
|
|
17
|
+
/** Fires when a payment tool created an execution; the channel decides what to do with it (ask, or run). */
|
|
18
|
+
onExecution: (execution: Execution) => Promise<Execution>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ToolHandler = (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
22
|
+
|
|
23
|
+
export interface AgentLoopOptions {
|
|
24
|
+
runtime: AgentRuntime;
|
|
25
|
+
tools: ToolsFile;
|
|
26
|
+
handlers: Record<string, ToolHandler>;
|
|
27
|
+
system: string;
|
|
28
|
+
bundle: ProofBundle;
|
|
29
|
+
engine: ExecutionEngine;
|
|
30
|
+
onExecution: ToolContext["onExecution"];
|
|
31
|
+
/** A hard stop on tool round-trips per turn, so a looping model cannot run forever. */
|
|
32
|
+
maxSteps?: number;
|
|
33
|
+
clock?: () => Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TurnResult {
|
|
37
|
+
reply: string;
|
|
38
|
+
tool_calls: Array<{ name: string; refused: boolean }>;
|
|
39
|
+
executions: Execution[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AgentLoop {
|
|
43
|
+
private readonly messages: ConversationMessage[] = [];
|
|
44
|
+
private readonly allowed: Set<string>;
|
|
45
|
+
private readonly specs: ToolSpec[];
|
|
46
|
+
private readonly clock: () => Date;
|
|
47
|
+
|
|
48
|
+
constructor(private readonly options: AgentLoopOptions) {
|
|
49
|
+
this.allowed = allowedToolNames(options.tools);
|
|
50
|
+
this.specs = toolSpecs(options.tools);
|
|
51
|
+
this.clock = options.clock ?? (() => new Date());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get history(): readonly ConversationMessage[] {
|
|
55
|
+
return this.messages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async turn(userText: string): Promise<TurnResult> {
|
|
59
|
+
const { runtime, bundle } = this.options;
|
|
60
|
+
const maxSteps = this.options.maxSteps ?? 8;
|
|
61
|
+
const calls: TurnResult["tool_calls"] = [];
|
|
62
|
+
const executions: Execution[] = [];
|
|
63
|
+
|
|
64
|
+
this.messages.push({ role: "user", content: userText });
|
|
65
|
+
bundle.transcript({ at: this.now(), kind: "user", text: userText });
|
|
66
|
+
|
|
67
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
68
|
+
const out = await runtime.step({ system: this.options.system, messages: this.messages }, this.specs);
|
|
69
|
+
if (isReply(out)) {
|
|
70
|
+
this.messages.push({ role: "assistant", content: out.text });
|
|
71
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", reply: out.text });
|
|
72
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text: out.text });
|
|
73
|
+
return { reply: out.text, tool_calls: calls, executions };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.messages.push({ role: "assistant", content: "", tool_calls: out });
|
|
77
|
+
bundle.transcript({ at: this.now(), kind: "assistant_step", tool_calls: out });
|
|
78
|
+
|
|
79
|
+
for (const call of out) {
|
|
80
|
+
const result = await this.dispatch(call, executions);
|
|
81
|
+
calls.push({ name: call.name, refused: result.refused });
|
|
82
|
+
this.messages.push({ role: "tool", tool_call_id: call.id, name: call.name, content: JSON.stringify(result.content), ...(result.refused ? { is_error: true } : {}) });
|
|
83
|
+
bundle.transcript({ at: this.now(), kind: "tool_result", tool_call_id: call.id, name: call.name, refused: result.refused, content: result.content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const text = "I stopped: too many tool steps in one turn. Nothing was paid beyond what the trail shows.";
|
|
88
|
+
this.messages.push({ role: "assistant", content: text });
|
|
89
|
+
bundle.transcript({ at: this.now(), kind: "assistant", text, truncated: true });
|
|
90
|
+
return { reply: text, tool_calls: calls, executions };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async dispatch(call: ToolCall, executions: Execution[]): Promise<{ refused: boolean; content: unknown }> {
|
|
94
|
+
const { bundle, handlers, engine, onExecution } = this.options;
|
|
95
|
+
bundle.transcript({ at: this.now(), kind: "tool_call", tool_call_id: call.id, name: call.name, input: call.input });
|
|
96
|
+
|
|
97
|
+
if (!this.allowed.has(call.name)) {
|
|
98
|
+
// Section 15, closed tools: a tool outside tools.json is refused, and the refusal is in the trail.
|
|
99
|
+
bundle.event({ at: this.now(), type: "tool.refused", tool: call.name, reason: "tool_not_allowed", actor: engine.agentActor });
|
|
100
|
+
return { refused: true, content: { error: "tool_not_allowed", message: `${call.name} is not in this agent's tools.json` } };
|
|
101
|
+
}
|
|
102
|
+
const handler = handlers[call.name];
|
|
103
|
+
if (!handler) return { refused: true, content: { error: "tool_not_implemented", message: `${call.name} has no handler` } };
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await handler(call.input, {
|
|
107
|
+
engine,
|
|
108
|
+
onExecution: async (execution) => {
|
|
109
|
+
const settledOrNot = await onExecution(execution);
|
|
110
|
+
executions.push(settledOrNot);
|
|
111
|
+
return settledOrNot;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { refused: false, content };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
bundle.event({ at: this.now(), type: "tool.error", tool: call.name, message, actor: engine.agentActor });
|
|
118
|
+
return { refused: true, content: { error: "tool_failed", message } };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private now(): string {
|
|
123
|
+
return this.clock().toISOString();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeSpar receivable rail: the merchant COLLECTS. `pay()` issues a
|
|
3
|
+
* cobranca com vencimento through `POST /v1/charges` (the REST form of the
|
|
4
|
+
* `codespar_charge` meta-tool, action=create): `method: "boleto"` plus a
|
|
5
|
+
* `due_date` is what makes it ONE receivable the payer settles either by
|
|
6
|
+
* boleto or by Pix; `idempotency_key` is REQUIRED on that create and is our
|
|
7
|
+
* attempt id, so a retry returns the same charge instead of a second
|
|
8
|
+
* receivable the same debtor could pay twice. An immediate Pix
|
|
9
|
+
* (`method: "pix"`) is deliberately not used: the API does not replay its
|
|
10
|
+
* create on a key and does not read it back, so it cannot close a loop.
|
|
11
|
+
*
|
|
12
|
+
* `lookup()` is `GET /v1/charges/{id}`, which accepts our idempotency key as
|
|
13
|
+
* the id; the kit polls it to close the cycle when no webhook can reach a
|
|
14
|
+
* terminal. Branching is on `local_status` and the normalized `status`, never
|
|
15
|
+
* on prose. Amounts cross the wire in MAJOR units (`amount: 1080.5`), which
|
|
16
|
+
* is what `coerceMetaChargeArgs` reads; the answer's `amount_minor` is
|
|
17
|
+
* compared with what was asked and a mismatch is a failure, not a charge.
|
|
18
|
+
*
|
|
19
|
+
* There is no receipt object for a receivable; the paid charge as the API
|
|
20
|
+
* reports it is what the bundle keeps, marked `kind: "charge"` and unsealed.
|
|
21
|
+
*/
|
|
22
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
23
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
24
|
+
import type { Actor, ChargeInstrument } from "../types.js";
|
|
25
|
+
import { describeApiError, isUncertain } from "./client.js";
|
|
26
|
+
|
|
27
|
+
/** The `/v1/charges` answer, as the SDK types it. */
|
|
28
|
+
export interface ChargeView {
|
|
29
|
+
id: string | null;
|
|
30
|
+
status: string;
|
|
31
|
+
local_status: string;
|
|
32
|
+
status_conflict: boolean;
|
|
33
|
+
method: string;
|
|
34
|
+
currency: string;
|
|
35
|
+
amount: number;
|
|
36
|
+
amount_minor: number;
|
|
37
|
+
due_date: string | null;
|
|
38
|
+
payable: boolean;
|
|
39
|
+
boleto_bar_code: string | null;
|
|
40
|
+
boleto_bank_line: string | null;
|
|
41
|
+
pix_copy_paste: string | null;
|
|
42
|
+
credit_correlation_armed: boolean;
|
|
43
|
+
payment_in_flight: boolean;
|
|
44
|
+
settlement: "confirmed" | "pending" | "unconfirmable" | null;
|
|
45
|
+
issuance_unconfirmed: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function instrumentOf(view: ChargeView): ChargeInstrument {
|
|
49
|
+
return {
|
|
50
|
+
payable: view.payable,
|
|
51
|
+
status: view.status,
|
|
52
|
+
pix_copy_paste: view.pix_copy_paste,
|
|
53
|
+
boleto_bank_line: view.boleto_bank_line,
|
|
54
|
+
boleto_bar_code: view.boleto_bar_code,
|
|
55
|
+
due_date: view.due_date,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a charge view means for the attempt. The row's own status wins over the issuer's normalized one where they name a terminal state. */
|
|
60
|
+
export function outcomeOf(view: ChargeView): RailOutcome | { status: "in_flight" } {
|
|
61
|
+
if (view.issuance_unconfirmed || !view.id) return { status: "in_flight" };
|
|
62
|
+
const id = view.id;
|
|
63
|
+
if (view.local_status === "settled" || view.status === "CONFIRMED" || view.settlement === "confirmed") {
|
|
64
|
+
return { status: "settled", transaction_id: id, receipt_id: id, money_moved: false, sandbox: true, raw: view };
|
|
65
|
+
}
|
|
66
|
+
if (view.local_status === "expired" || view.status === "EXPIRED") {
|
|
67
|
+
return { status: "failed", code: "charge_expired", message: `charge ${id} reached its due date unpaid`, raw: view };
|
|
68
|
+
}
|
|
69
|
+
if (view.status === "CANCELLED" || view.local_status === "cancelled") {
|
|
70
|
+
return { status: "failed", code: "charge_cancelled", message: `charge ${id} was withdrawn`, raw: view };
|
|
71
|
+
}
|
|
72
|
+
return { status: "accepted", transaction_id: id, instrument: instrumentOf(view), sandbox: true, raw: view };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class CodeSparChargeRail implements PaymentRail {
|
|
76
|
+
readonly name = "codespar-charge" as const;
|
|
77
|
+
|
|
78
|
+
constructor(private readonly api: ApiClient) {}
|
|
79
|
+
|
|
80
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
81
|
+
if (!payment.due_date) return { status: "failed", code: "due_date_required", message: "a receivable needs a due_date; the immediate Pix charge cannot be read back or replayed" };
|
|
82
|
+
let view: ChargeView;
|
|
83
|
+
try {
|
|
84
|
+
view = (await this.api.post("/v1/charges", {
|
|
85
|
+
body: {
|
|
86
|
+
amount: payment.amount_minor / 100,
|
|
87
|
+
currency: payment.currency,
|
|
88
|
+
method: "boleto",
|
|
89
|
+
description: payment.description ?? payment.purpose,
|
|
90
|
+
buyer: { name: payment.beneficiary ?? payment.payee, document: payment.payee },
|
|
91
|
+
due_date: payment.due_date,
|
|
92
|
+
idempotency_key: payment.attempt_id,
|
|
93
|
+
},
|
|
94
|
+
})) as ChargeView;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const failure = describeApiError(err);
|
|
97
|
+
if (isUncertain(failure) || failure.code === "issuance_unconfirmed") return { status: "uncertain", code: failure.code, message: failure.message };
|
|
98
|
+
return { status: "failed", code: failure.code, message: failure.message };
|
|
99
|
+
}
|
|
100
|
+
if (view.issuance_unconfirmed || !view.id) return { status: "uncertain", code: "issuance_unconfirmed", message: "the issuer's answer to the create was lost; the key is reserved and a later read resolves it" };
|
|
101
|
+
if (view.amount_minor !== payment.amount_minor) {
|
|
102
|
+
// The API understood another amount than the one approved. The receivable exists; withdraw it rather than leave a debt nobody approved.
|
|
103
|
+
const withdrawn = await this.withdraw(view.id);
|
|
104
|
+
return { status: "failed", code: "amount_mismatch", message: `the API issued ${view.amount_minor} for a request of ${payment.amount_minor} (charge ${view.id}); ${withdrawn ? "withdrawn" : "withdrawal refused, an operator must cancel it"}` };
|
|
105
|
+
}
|
|
106
|
+
const outcome = outcomeOf(view);
|
|
107
|
+
// `in_flight` is excluded above (`issuance_unconfirmed` / no id); the create answers a state, never "still running".
|
|
108
|
+
return outcome.status === "in_flight" ? { status: "uncertain", code: "issuance_unconfirmed", message: "the create answered no readable state" } : outcome;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async lookup(attemptId: string, _payment: RailPayment): Promise<RailLookup> {
|
|
112
|
+
try {
|
|
113
|
+
const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId: attemptId } })) as ChargeView;
|
|
114
|
+
return outcomeOf(view);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const failure = describeApiError(err);
|
|
117
|
+
if (failure.status === 404) return undefined;
|
|
118
|
+
if (failure.status === 409) return { status: "in_flight" };
|
|
119
|
+
return { status: "uncertain", code: failure.code, message: failure.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async receipt(chargeId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
124
|
+
try {
|
|
125
|
+
const view = (await this.api.get("/v1/charges/{chargeId}", { path: { chargeId } })) as ChargeView;
|
|
126
|
+
const raw = view as ChargeView & { settled_at?: string };
|
|
127
|
+
return {
|
|
128
|
+
receipt_id: chargeId,
|
|
129
|
+
kind: "charge",
|
|
130
|
+
state: view.local_status === "settled" ? "paid" : view.local_status,
|
|
131
|
+
mandate: { id: "n/a" },
|
|
132
|
+
// The read names no buyer, so the payee is not on this record; the execution's item names the debtor.
|
|
133
|
+
payment: { amount_minor: view.amount_minor, payee: null, attempt_id: chargeId, money_moved: false, sandbox: true, at: raw.settled_at ?? new Date().toISOString() },
|
|
134
|
+
chain: null,
|
|
135
|
+
receipt_sig: null,
|
|
136
|
+
actor,
|
|
137
|
+
raw: view,
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const failure = describeApiError(err);
|
|
141
|
+
if (failure.status === 404) return undefined;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async withdraw(chargeId: string): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
await this.api.post("/v1/charges/{chargeId}/cancel", { path: { chargeId } });
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place the kit talks to the CodeSpar API. The key is checked for
|
|
3
|
+
* the `csk_test_` prefix before the client is built, so no live key ever
|
|
4
|
+
* reaches the network from this repository.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiClient, CodesparApiError, TimeoutError } from "@codespar/sdk";
|
|
7
|
+
import { assertTestKey } from "../secrets.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_BASE_URL = "https://api.codespar.dev";
|
|
10
|
+
|
|
11
|
+
export interface CodeSparClientOptions {
|
|
12
|
+
apiKey: string | undefined;
|
|
13
|
+
baseUrl?: string | undefined;
|
|
14
|
+
projectId?: string | undefined;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createCodeSparClient(options: CodeSparClientOptions): ApiClient {
|
|
19
|
+
const apiKey = assertTestKey(options.apiKey);
|
|
20
|
+
return new ApiClient({
|
|
21
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
22
|
+
apiKey,
|
|
23
|
+
...(options.projectId ? { projectId: options.projectId } : {}),
|
|
24
|
+
timeout: options.timeoutMs ?? 30_000,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ApiFailure {
|
|
29
|
+
status: number;
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
body?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Normalise the SDK's two error classes into one readable shape without leaking the body into logs. */
|
|
36
|
+
export function describeApiError(err: unknown): ApiFailure {
|
|
37
|
+
if (err instanceof TimeoutError) return { status: 0, code: "timeout", message: `request timed out after ${err.timeoutMs}ms` };
|
|
38
|
+
if (err instanceof CodesparApiError) {
|
|
39
|
+
const body = err.body as { error?: { code?: string; message?: string } } | undefined;
|
|
40
|
+
return {
|
|
41
|
+
status: err.status,
|
|
42
|
+
code: err.code ?? body?.error?.code ?? (err.status === 0 ? "network" : `http_${err.status}`),
|
|
43
|
+
message: body?.error?.message ?? err.message,
|
|
44
|
+
body: err.body,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { status: 0, code: "unknown", message: err instanceof Error ? err.message : String(err) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 5xx, timeouts and `psp_dispatch_uncertain` mean "the money may have moved". */
|
|
51
|
+
export function isUncertain(failure: ApiFailure): boolean {
|
|
52
|
+
return failure.status === 0 || failure.status >= 500 || failure.code === "psp_dispatch_uncertain" || failure.code === "psp_attempt_uncertain";
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandate status from the API: `GET /v1/mandates/{id}`, the one registered
|
|
3
|
+
* spelling of the consumer-allowance point read (the SDK's OpenAPI says the
|
|
4
|
+
* path has no alias; `/v1/consumers/mandates/{id}` answers 404 on staging,
|
|
5
|
+
* measured 2026-09-23). Consulted before `executing` (section 4.7), through
|
|
6
|
+
* the same `csk_test_` client every other call of the kit uses.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed: this source never throws and never assumes. A transport
|
|
9
|
+
* failure, a timeout, a 404, a 5xx, an unreadable body or a status outside
|
|
10
|
+
* `active | paused | revoked | expired` is reported as `unknown`, and the
|
|
11
|
+
* engine executes nothing on `unknown`.
|
|
12
|
+
*
|
|
13
|
+
* The organization kill switch (`org pauseAll`) has no API surface, so
|
|
14
|
+
* `org_paused` is `false` here: the API cannot say it, and this source does
|
|
15
|
+
* not invent it.
|
|
16
|
+
*/
|
|
17
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import type { MandateStatusReport, MandateStatusSource } from "../revocation.js";
|
|
20
|
+
import { describeApiError } from "./client.js";
|
|
21
|
+
|
|
22
|
+
/** The two fields of the 14-field projection the gate reads; the rest is passed through untouched. */
|
|
23
|
+
const MandateReadSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
status: z.enum(["active", "paused", "revoked", "expired"]),
|
|
26
|
+
expires_at: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.passthrough();
|
|
29
|
+
|
|
30
|
+
export class ApiMandateStatusSource implements MandateStatusSource {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly api: ApiClient,
|
|
33
|
+
private readonly clock: () => Date = () => new Date(),
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async check(mandateId: string): Promise<MandateStatusReport> {
|
|
37
|
+
const base = { mandate_id: mandateId, org_paused: false, checked_at: this.clock().toISOString(), source: "api" as const };
|
|
38
|
+
let body: unknown;
|
|
39
|
+
try {
|
|
40
|
+
body = await this.api.get("/v1/mandates/{id}", { path: { id: mandateId } });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const failure = describeApiError(err);
|
|
43
|
+
const detail =
|
|
44
|
+
failure.status === 404
|
|
45
|
+
? `the API has no mandate ${mandateId} for this key (${failure.code})`
|
|
46
|
+
: `GET /v1/mandates/${mandateId} did not answer (${failure.code}): ${failure.message}`;
|
|
47
|
+
return { ...base, status: "unknown", detail };
|
|
48
|
+
}
|
|
49
|
+
const parsed = MandateReadSchema.safeParse(body);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
const status = body && typeof body === "object" && "status" in body ? String((body as { status: unknown }).status) : "missing";
|
|
52
|
+
return { ...base, status: "unknown", detail: `GET /v1/mandates/${mandateId} answered a status this kit does not read (${status})` };
|
|
53
|
+
}
|
|
54
|
+
const expiresAt = new Date(parsed.data.expires_at).getTime();
|
|
55
|
+
const expired = Number.isFinite(expiresAt) && expiresAt <= this.clock().getTime();
|
|
56
|
+
return { ...base, status: expired && parsed.data.status === "active" ? "expired" : parsed.data.status };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeSpar sandbox rail. With the signed envelope the consent returned
|
|
3
|
+
* (`mandate.canonical` + `mandate.signature`) it spends through
|
|
4
|
+
* `POST /v1/consumer-payments/execute`; without it, by mandate id through
|
|
5
|
+
* `POST /v1/consumers/mandates/{id}/spend`. Both are idempotent on
|
|
6
|
+
* `attempt_id`. The by-id route answered `bad_signature` on staging for a
|
|
7
|
+
* mandate carrying `periodic_cap` (OPEN_QUESTIONS §14), which is why the
|
|
8
|
+
* envelope is preferred when it exists. Receipts come back from
|
|
9
|
+
* `GET /v1/consumers/receipts/{id}`.
|
|
10
|
+
*
|
|
11
|
+
* The API has no `actor` field on the wire today (OPEN_QUESTIONS §2). The
|
|
12
|
+
* spend carries `agent_id`, which the mandate binds; the full actor is
|
|
13
|
+
* stamped on the local receipt copy and on every event of the bundle.
|
|
14
|
+
*/
|
|
15
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
16
|
+
import type { Mandate } from "../mandate.js";
|
|
17
|
+
import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
|
|
18
|
+
import type { Actor } from "../types.js";
|
|
19
|
+
import { describeApiError, isUncertain } from "./client.js";
|
|
20
|
+
|
|
21
|
+
export class CodeSparRail implements PaymentRail {
|
|
22
|
+
readonly name = "codespar" as const;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly api: ApiClient,
|
|
26
|
+
private readonly envelope?: Pick<Mandate, "canonical" | "signature"> | undefined,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
30
|
+
try {
|
|
31
|
+
const outcome =
|
|
32
|
+
this.envelope?.canonical && this.envelope.signature
|
|
33
|
+
? await this.api.post("/v1/consumer-payments/execute", {
|
|
34
|
+
body: {
|
|
35
|
+
mandate: this.envelope.canonical,
|
|
36
|
+
signature: this.envelope.signature,
|
|
37
|
+
amount_minor: payment.amount_minor,
|
|
38
|
+
purpose: payment.purpose,
|
|
39
|
+
agent_id: payment.agent_id,
|
|
40
|
+
payee: payment.payee,
|
|
41
|
+
attempt_id: payment.attempt_id,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
: await this.api.post("/v1/consumers/mandates/{id}/spend", {
|
|
45
|
+
path: { id: payment.mandate_id },
|
|
46
|
+
body: { amount_minor: payment.amount_minor, payee: payment.payee, agent_id: payment.agent_id, attempt_id: payment.attempt_id },
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
status: "settled",
|
|
50
|
+
transaction_id: outcome.payment.transactionId,
|
|
51
|
+
receipt_id: outcome.receipt?.id ?? null,
|
|
52
|
+
money_moved: outcome.payment.moneyMoved,
|
|
53
|
+
sandbox: !outcome.payment.moneyMoved,
|
|
54
|
+
raw: outcome,
|
|
55
|
+
};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const failure = describeApiError(err);
|
|
58
|
+
if (isUncertain(failure)) return { status: "uncertain", code: failure.code, message: failure.message };
|
|
59
|
+
return { status: "failed", code: failure.code, message: failure.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* There is no read route for an attempt. The documented way is to present
|
|
65
|
+
* the same `attempt_id` again: the lifecycle is idempotent on it and
|
|
66
|
+
* answers the state it reached. `psp_attempt_in_flight` means the first
|
|
67
|
+
* presentation is still running.
|
|
68
|
+
*/
|
|
69
|
+
async lookup(_attemptId: string, payment: RailPayment): Promise<RailLookup> {
|
|
70
|
+
const outcome = await this.pay(payment);
|
|
71
|
+
if (outcome.status === "failed" && outcome.code === "psp_attempt_in_flight") return { status: "in_flight" };
|
|
72
|
+
return outcome;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
76
|
+
try {
|
|
77
|
+
const r = await this.api.get("/v1/consumers/receipts/{id}", { path: { id: receiptId } });
|
|
78
|
+
return {
|
|
79
|
+
receipt_id: r.receipt_id,
|
|
80
|
+
state: r.state,
|
|
81
|
+
mandate: { id: r.mandate.id },
|
|
82
|
+
payment: {
|
|
83
|
+
amount_minor: r.payment.amount_minor,
|
|
84
|
+
payee: r.quote?.payee ?? null,
|
|
85
|
+
attempt_id: r.payment.attempt_id,
|
|
86
|
+
money_moved: r.payment.money_moved,
|
|
87
|
+
sandbox: r.payment.sandbox === true,
|
|
88
|
+
at: r.payment.at,
|
|
89
|
+
},
|
|
90
|
+
chain: r.chain,
|
|
91
|
+
receipt_sig: r.receipt_sig,
|
|
92
|
+
actor,
|
|
93
|
+
raw: r,
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const failure = describeApiError(err);
|
|
97
|
+
if (failure.status === 404) return undefined;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox PAYER: `POST /v1/test/charges/{chargeId}/pay` (alias
|
|
3
|
+
* `POST /v1/charges/{chargeId}/sandbox/pay`), test environment only. It
|
|
4
|
+
* plays the debtor: the charge goes through the same settlement path a
|
|
5
|
+
* provider `charge-in` webhook takes, `commerce.charge.paid` fans out to the
|
|
6
|
+
* project's triggers, and every record it leaves carries `simulated: true`
|
|
7
|
+
* plus `settled_against: "sandbox_fixture"`. No money moves anywhere. A
|
|
8
|
+
* live-environment key is refused with `sandbox_pay_not_permitted` before
|
|
9
|
+
* anything is read. The route is not in the SDK's typed operations yet
|
|
10
|
+
* (merged 2026-09-23), so it is called by path.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
13
|
+
import { describeApiError, type ApiFailure } from "./client.js";
|
|
14
|
+
|
|
15
|
+
export interface SandboxPaidState {
|
|
16
|
+
charge_id: string;
|
|
17
|
+
status: "paid";
|
|
18
|
+
local_status: string;
|
|
19
|
+
currency: string;
|
|
20
|
+
quoted_minor: number;
|
|
21
|
+
paid_minor: number;
|
|
22
|
+
payment: "full" | "partial" | "over";
|
|
23
|
+
paid_via: string;
|
|
24
|
+
wallet_id: string;
|
|
25
|
+
ledger_entry_id: string | null;
|
|
26
|
+
event: { id: string; type: string } | null;
|
|
27
|
+
simulated: boolean;
|
|
28
|
+
settled_against: string | null;
|
|
29
|
+
money_moved: false;
|
|
30
|
+
idempotent_replay: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SandboxPayResult = { ok: true; state: SandboxPaidState } | { ok: false; failure: ApiFailure };
|
|
34
|
+
|
|
35
|
+
export async function paySandboxCharge(api: ApiClient, chargeRef: string, amountMinor?: number): Promise<SandboxPayResult> {
|
|
36
|
+
try {
|
|
37
|
+
const state = (await api.request("post", "/v1/test/charges/{chargeId}/pay" as never, {
|
|
38
|
+
path: { chargeId: chargeRef },
|
|
39
|
+
body: amountMinor !== undefined ? { amount_minor: amountMinor } : {},
|
|
40
|
+
} as never)) as SandboxPaidState;
|
|
41
|
+
return { ok: true, state };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { ok: false, failure: describeApiError(err) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.2: the approval artifact.
|
|
3
|
+
*
|
|
4
|
+
* STUB, on purpose: the artifact is signed with a LOCAL development key
|
|
5
|
+
* (`.codespar/approval.key`, generated on first use, never committed). The
|
|
6
|
+
* CodeSpar API does not sign approval lists; its HMAC is the mandate proof
|
|
7
|
+
* and the receipt seal. Until an API-side signature exists the artifact
|
|
8
|
+
* proves what was approved to whoever runs the agent, and to nobody else.
|
|
9
|
+
* See docs/OPEN_QUESTIONS.md.
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { canonicalJson, hexEqual, hmacSha256Hex, itemsHash } from "./hash.js";
|
|
15
|
+
import type { Execution } from "./state-machine.js";
|
|
16
|
+
import type { Actor, ApprovalArtifact, EscalationTrigger } from "./types.js";
|
|
17
|
+
import { newId } from "./ids.js";
|
|
18
|
+
|
|
19
|
+
export const APPROVAL_KEY_ID = "local-dev-stub";
|
|
20
|
+
export const DEFAULT_APPROVAL_TTL_MS = 15 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
export interface ApprovalSigner {
|
|
23
|
+
keyId: string;
|
|
24
|
+
sign(payload: string): string;
|
|
25
|
+
verify(payload: string, signature: string): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadOrCreateLocalApprovalKey(stateDir: string): ApprovalSigner {
|
|
29
|
+
const path = join(stateDir, "approval.key");
|
|
30
|
+
let key: Buffer;
|
|
31
|
+
if (existsSync(path)) {
|
|
32
|
+
key = Buffer.from(readFileSync(path, "utf8").trim(), "hex");
|
|
33
|
+
} else {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
key = randomBytes(32);
|
|
36
|
+
writeFileSync(path, key.toString("hex") + "\n", { mode: 0o600 });
|
|
37
|
+
chmodSync(path, 0o600);
|
|
38
|
+
}
|
|
39
|
+
return hmacSigner(APPROVAL_KEY_ID, key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hmacSigner(keyId: string, key: Buffer): ApprovalSigner {
|
|
43
|
+
return {
|
|
44
|
+
keyId,
|
|
45
|
+
sign: (payload) => hmacSha256Hex(key, payload),
|
|
46
|
+
verify: (payload, signature) => hexEqual(hmacSha256Hex(key, payload), signature),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unsignedPayload(artifact: Omit<ApprovalArtifact, "signature">): string {
|
|
51
|
+
return canonicalJson(artifact);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CreateApprovalInput {
|
|
55
|
+
execution: Execution;
|
|
56
|
+
approver: ApprovalArtifact["approver"];
|
|
57
|
+
actor: Actor;
|
|
58
|
+
now: Date;
|
|
59
|
+
ttlMs?: number;
|
|
60
|
+
escalation?: { trigger: EscalationTrigger; detail: string };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createApprovalArtifact(signer: ApprovalSigner, input: CreateApprovalInput): ApprovalArtifact {
|
|
64
|
+
const { execution } = input;
|
|
65
|
+
const approvedAt = input.now.toISOString();
|
|
66
|
+
const expiresAt = new Date(input.now.getTime() + (input.ttlMs ?? DEFAULT_APPROVAL_TTL_MS)).toISOString();
|
|
67
|
+
const unsigned: Omit<ApprovalArtifact, "signature"> = {
|
|
68
|
+
approval_id: newId("apr"),
|
|
69
|
+
execution_id: execution.id,
|
|
70
|
+
mode: execution.mode,
|
|
71
|
+
approver: input.approver,
|
|
72
|
+
approved_at: approvedAt,
|
|
73
|
+
expires_at: expiresAt,
|
|
74
|
+
mandate: execution.mandate,
|
|
75
|
+
items: execution.items,
|
|
76
|
+
items_hash: itemsHash(execution.items),
|
|
77
|
+
...(input.escalation ? { escalation: input.escalation } : {}),
|
|
78
|
+
actor: input.actor,
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
...unsigned,
|
|
82
|
+
signature: { alg: "HMAC-SHA256", key_id: signer.keyId, value: signer.sign(unsignedPayload(unsigned)) },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ApprovalCheck =
|
|
87
|
+
| { ok: true }
|
|
88
|
+
| { ok: false; problem: "signature_invalid" | "items_hash_mismatch" | "expired" | "wrong_execution" | "wrong_mandate" };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The rule that makes the artifact proof: before `executing`, recompute the
|
|
92
|
+
* hash of what is about to be executed and compare it with the artifact.
|
|
93
|
+
*/
|
|
94
|
+
export function checkApprovalArtifact(
|
|
95
|
+
signer: ApprovalSigner,
|
|
96
|
+
artifact: ApprovalArtifact,
|
|
97
|
+
execution: Execution,
|
|
98
|
+
now: Date,
|
|
99
|
+
): ApprovalCheck {
|
|
100
|
+
const { signature, ...unsigned } = artifact;
|
|
101
|
+
if (!signer.verify(unsignedPayload(unsigned), signature.value)) return { ok: false, problem: "signature_invalid" };
|
|
102
|
+
if (artifact.execution_id !== execution.id) return { ok: false, problem: "wrong_execution" };
|
|
103
|
+
if (artifact.mandate.id !== execution.mandate.id || artifact.mandate.version !== execution.mandate.version) {
|
|
104
|
+
return { ok: false, problem: "wrong_mandate" };
|
|
105
|
+
}
|
|
106
|
+
if (itemsHash(execution.items) !== artifact.items_hash) return { ok: false, problem: "items_hash_mismatch" };
|
|
107
|
+
if (new Date(artifact.expires_at).getTime() <= now.getTime()) return { ok: false, problem: "expired" };
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|