@codespar/cli 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/commands/agent.js +13 -7
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/templates/bills-agent/LICENSE +21 -0
- package/templates/bills-agent/README.md +21 -0
- package/templates/bills-agent/_gitignore +10 -0
- package/templates/bills-agent/agents/bills-agent/.env.example +7 -0
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +16 -0
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +16 -0
- package/templates/bills-agent/agents/bills-agent/README.md +89 -0
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +23 -0
- package/templates/bills-agent/agents/bills-agent/agent.yaml +26 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +108 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/eval.yaml +9 -0
- package/templates/bills-agent/agents/bills-agent/guardrails.json +15 -0
- package/templates/bills-agent/agents/bills-agent/mandate.example.json +46 -0
- package/templates/bills-agent/agents/bills-agent/package.json +24 -0
- package/templates/bills-agent/agents/bills-agent/runbook.md +19 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/src/adversarial.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/src/bills.ts +28 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +9 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +13 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +26 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +45 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +7 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +37 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +38 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +65 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +30 -0
- package/templates/bills-agent/agents/bills-agent/src/main.ts +185 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +125 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/pix-out.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/src/scenarios.ts +196 -0
- package/templates/bills-agent/agents/bills-agent/src/setup.ts +207 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +40 -0
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +85 -0
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/tools.json +51 -0
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +11 -0
- package/templates/bills-agent/package.json +34 -0
- package/templates/bills-agent/packages/agent-core/README.md +19 -0
- package/templates/bills-agent/packages/agent-core/package.json +24 -0
- package/templates/bills-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/bills-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/bills-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/bills-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/bills-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/bills-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/bills-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/bills-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/bills-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/bills-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/bills-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/bills-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/bills-agent/tsconfig.base.json +16 -0
- package/templates/bills-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/LICENSE +21 -0
- package/templates/collections-agent/README.md +20 -0
- package/templates/collections-agent/_gitignore +10 -0
- package/templates/collections-agent/agents/collections-agent/.env.example +4 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +19 -0
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +19 -0
- package/templates/collections-agent/agents/collections-agent/README.md +94 -0
- package/templates/collections-agent/agents/collections-agent/SYSTEM_PROMPT.md +31 -0
- package/templates/collections-agent/agents/collections-agent/agent.yaml +28 -0
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +178 -0
- package/templates/collections-agent/agents/collections-agent/channels/webhook/index.ts +131 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap-mandate.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.json +19 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.json +13 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/eval.yaml +11 -0
- package/templates/collections-agent/agents/collections-agent/guardrails.json +20 -0
- package/templates/collections-agent/agents/collections-agent/mandate.example.json +40 -0
- package/templates/collections-agent/agents/collections-agent/package.json +29 -0
- package/templates/collections-agent/agents/collections-agent/runbook.md +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.json +36 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.transcript.jsonl +5 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.json +60 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.transcript.jsonl +6 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/src/adversarial.ts +190 -0
- package/templates/collections-agent/agents/collections-agent/src/agreements.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +10 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +13 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +48 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +7 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +39 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +35 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +38 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +30 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +36 -0
- package/templates/collections-agent/agents/collections-agent/src/envelope.ts +103 -0
- package/templates/collections-agent/agents/collections-agent/src/main.ts +193 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +105 -0
- package/templates/collections-agent/agents/collections-agent/src/poll.ts +94 -0
- package/templates/collections-agent/agents/collections-agent/src/scenarios.ts +252 -0
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +250 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +51 -0
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +172 -0
- package/templates/collections-agent/agents/collections-agent/test/envelope.test.ts +53 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1103.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +95 -0
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +100 -0
- package/templates/collections-agent/agents/collections-agent/tools.json +40 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +11 -0
- package/templates/collections-agent/package.json +35 -0
- package/templates/collections-agent/packages/agent-core/README.md +19 -0
- package/templates/collections-agent/packages/agent-core/package.json +24 -0
- package/templates/collections-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/collections-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/collections-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/collections-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/collections-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/collections-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/collections-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/collections-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/collections-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/collections-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/collections-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/collections-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/collections-agent/tsconfig.base.json +16 -0
- package/templates/collections-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +41 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.4: `escalate_above` only tightens. Each trigger sends a
|
|
3
|
+
* `mandate` execution to `awaiting_approval`; none of them raises a cap or
|
|
4
|
+
* adds a payee. The first trigger that fires is the one the artifact records.
|
|
5
|
+
*/
|
|
6
|
+
import type { Guardrails } from "./guardrails.js";
|
|
7
|
+
import type { EscalateAbove } from "./manifest.js";
|
|
8
|
+
import type { EscalationTrigger, ExecutionItem } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export interface EscalationContext {
|
|
11
|
+
now: Date;
|
|
12
|
+
timezone: string;
|
|
13
|
+
/** Payees this agent has already settled a payment to, under this mandate. */
|
|
14
|
+
knownPayees: ReadonlySet<string>;
|
|
15
|
+
/** Minor units already executed (settled or in flight) per payee inside the velocity window. */
|
|
16
|
+
recentByPayee: ReadonlyMap<string, number>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Escalation {
|
|
20
|
+
trigger: EscalationTrigger;
|
|
21
|
+
detail: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function evaluateEscalation(
|
|
25
|
+
rule: EscalateAbove | undefined,
|
|
26
|
+
guardrails: Guardrails,
|
|
27
|
+
items: readonly ExecutionItem[],
|
|
28
|
+
ctx: EscalationContext,
|
|
29
|
+
): Escalation | undefined {
|
|
30
|
+
if (!rule) return undefined;
|
|
31
|
+
|
|
32
|
+
if (rule.amount !== undefined) {
|
|
33
|
+
const total = items.reduce((sum, i) => sum + i.amount, 0);
|
|
34
|
+
if (total > rule.amount) {
|
|
35
|
+
return { trigger: "amount", detail: `total ${total} is above the escalation threshold ${rule.amount}` };
|
|
36
|
+
}
|
|
37
|
+
// Fractioning (section 9): the same payee, several parts inside the window, adds up.
|
|
38
|
+
if (guardrails.velocity) {
|
|
39
|
+
const perPayee = new Map<string, number>();
|
|
40
|
+
for (const item of items) perPayee.set(item.payee, (perPayee.get(item.payee) ?? 0) + item.amount);
|
|
41
|
+
for (const [payee, amount] of perPayee) {
|
|
42
|
+
const recent = ctx.recentByPayee.get(payee) ?? 0;
|
|
43
|
+
if (recent + amount > rule.amount) {
|
|
44
|
+
return {
|
|
45
|
+
trigger: "amount",
|
|
46
|
+
detail: `payments to ${payee} inside the last ${guardrails.velocity.window_hours}h add up to ${recent + amount}, above the escalation threshold ${rule.amount}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (rule.new_beneficiary) {
|
|
54
|
+
const fresh = items.find((i) => !ctx.knownPayees.has(i.payee));
|
|
55
|
+
if (fresh) {
|
|
56
|
+
return { trigger: "new_beneficiary", detail: `first payment to ${fresh.beneficiary} (${fresh.payee}) under this mandate` };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (rule.outside_hours && isOutsideHours(rule.outside_hours, ctx.now, ctx.timezone)) {
|
|
61
|
+
return { trigger: "outside_hours", detail: `requested at ${localClock(ctx.now, ctx.timezone)}, inside the ${rule.outside_hours} window` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function localClock(now: Date, timezone: string): string {
|
|
68
|
+
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: timezone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(now);
|
|
69
|
+
const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
|
|
70
|
+
const minute = parts.find((p) => p.type === "minute")?.value ?? "00";
|
|
71
|
+
return `${hour}:${minute}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `"22:00-07:00"` is the CLOSED window: inside it, the trigger fires. Crosses midnight when start > end. */
|
|
75
|
+
export function isOutsideHours(window: string, now: Date, timezone: string): boolean {
|
|
76
|
+
const [start, end] = window.split("-") as [string, string];
|
|
77
|
+
const minutes = (hhmm: string): number => {
|
|
78
|
+
const [h, m] = hhmm.split(":").map(Number) as [number, number];
|
|
79
|
+
return h * 60 + m;
|
|
80
|
+
};
|
|
81
|
+
const current = minutes(localClock(now, timezone));
|
|
82
|
+
const s = minutes(start);
|
|
83
|
+
const e = minutes(end);
|
|
84
|
+
return s <= e ? current >= s && current < e : current >= s || current < e;
|
|
85
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events the CodeSpar API publishes, as an agent may declare them under
|
|
3
|
+
* `events:` in `agent.yaml`. The list is the one place the kits keep it;
|
|
4
|
+
* `npm run check` refuses a manifest entry outside it.
|
|
5
|
+
*
|
|
6
|
+
* Source: codespar-enterprise `packages/api/src` (the publishers behind
|
|
7
|
+
* `/v1/triggers`), measured 2026-09-23. Update it against that tree, never
|
|
8
|
+
* from memory.
|
|
9
|
+
*/
|
|
10
|
+
export const PUBLISHED_EVENTS = [
|
|
11
|
+
"commerce.payment.succeeded",
|
|
12
|
+
"commerce.payment.failed",
|
|
13
|
+
"commerce.payment.pending",
|
|
14
|
+
"commerce.payment.refunded",
|
|
15
|
+
"commerce.payment.updated",
|
|
16
|
+
"commerce.pix_out.succeeded",
|
|
17
|
+
"commerce.pix_out.failed",
|
|
18
|
+
"commerce.charge.created",
|
|
19
|
+
"commerce.charge.paid",
|
|
20
|
+
"commerce.charge.expired",
|
|
21
|
+
"commerce.charge.cancelled",
|
|
22
|
+
"commerce.mandate.granted",
|
|
23
|
+
"commerce.mandate.paused",
|
|
24
|
+
"commerce.mandate.resumed",
|
|
25
|
+
"commerce.mandate.revoked",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export type PublishedEvent = (typeof PUBLISHED_EVENTS)[number];
|
|
29
|
+
|
|
30
|
+
/** The rail's answer to one attempt, as the core records it locally and as the API publishes it. */
|
|
31
|
+
export const PAYMENT_SUCCEEDED: PublishedEvent = "commerce.payment.succeeded";
|
|
32
|
+
export const PAYMENT_FAILED: PublishedEvent = "commerce.payment.failed";
|
|
33
|
+
/** The payer's answer to a receivable, as the API publishes it: paid settles, expired and cancelled fail. */
|
|
34
|
+
export const CHARGE_PAID: PublishedEvent = "commerce.charge.paid";
|
|
35
|
+
export const CHARGE_EXPIRED: PublishedEvent = "commerce.charge.expired";
|
|
36
|
+
export const CHARGE_CANCELLED: PublishedEvent = "commerce.charge.cancelled";
|
|
37
|
+
|
|
38
|
+
export function isPublishedEvent(name: string): name is PublishedEvent {
|
|
39
|
+
return (PUBLISHED_EVENTS as readonly string[]).includes(name);
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `guardrails.json`: what the agent applies on its own, before the mandate.
|
|
3
|
+
* Everything here can only tighten the mandate; nothing here widens it.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { EscalateAboveSchema } from "./manifest.js";
|
|
8
|
+
|
|
9
|
+
export const GuardrailsSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
approval: z.enum(["human", "mandate"]),
|
|
12
|
+
/** Must equal the manifest's `escalate_above`; `npm run check` compares them. */
|
|
13
|
+
escalate_above: EscalateAboveSchema.optional(),
|
|
14
|
+
/** Section 9, fractioning: the window over which amounts to one payee add up towards `escalate_above.amount`. */
|
|
15
|
+
velocity: z
|
|
16
|
+
.object({
|
|
17
|
+
window_hours: z.number().int().positive(),
|
|
18
|
+
/** Optional hard ceiling on the number of executions to one payee in the window. */
|
|
19
|
+
max_per_payee: z.number().int().positive().optional(),
|
|
20
|
+
})
|
|
21
|
+
.strict()
|
|
22
|
+
.optional(),
|
|
23
|
+
/** When an `outside_hours` trigger fires: ask a human now, or refuse and let the person retry inside the window. */
|
|
24
|
+
outside_hours_action: z.enum(["escalate", "refuse"]).default("escalate"),
|
|
25
|
+
/** When the model's stated total differs from the core's: execute the core's number, or refuse. Never the model's. */
|
|
26
|
+
model_total_mismatch: z.enum(["use_core", "refuse"]).default("use_core"),
|
|
27
|
+
/** Minutes an approval artifact stays valid. */
|
|
28
|
+
approval_ttl_minutes: z.number().int().positive().default(15),
|
|
29
|
+
/** IANA zone the `outside_hours` window is read in. */
|
|
30
|
+
timezone: z.string().default("America/Sao_Paulo"),
|
|
31
|
+
/**
|
|
32
|
+
* The agent-specific envelope the kit's own deterministic policy reads
|
|
33
|
+
* (a collections agent: discount ceiling, instalments, due-date window).
|
|
34
|
+
* The core carries it and runs the kit's `policyExtension` at every gate;
|
|
35
|
+
* it does not interpret the keys.
|
|
36
|
+
*/
|
|
37
|
+
envelope: z.record(z.string(), z.unknown()).optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
|
|
41
|
+
export type Guardrails = z.infer<typeof GuardrailsSchema>;
|
|
42
|
+
|
|
43
|
+
export function loadGuardrails(path: string): Guardrails {
|
|
44
|
+
return GuardrailsSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { ExecutionItem } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/** JSON with keys sorted at every level, so the same value always hashes the same. */
|
|
5
|
+
export function canonicalJson(value: unknown): string {
|
|
6
|
+
return JSON.stringify(sortKeys(value));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sortKeys(value: unknown): unknown {
|
|
10
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
const out: Record<string, unknown> = {};
|
|
13
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
14
|
+
const v = (value as Record<string, unknown>)[key];
|
|
15
|
+
if (v !== undefined) out[key] = sortKeys(v);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sha256Hex(input: string | Buffer): string {
|
|
23
|
+
return createHash("sha256").update(input).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Section 4.2: the hash of the canonical list. Only the fields that decide
|
|
28
|
+
* where money goes take part: payee, amount, currency, and the due date of a
|
|
29
|
+
* receivable when it has one. Aliases, names and descriptions are
|
|
30
|
+
* presentation and may differ without changing the hash.
|
|
31
|
+
*/
|
|
32
|
+
export function itemsHash(items: readonly ExecutionItem[]): string {
|
|
33
|
+
const canonical = items.map((item) => ({
|
|
34
|
+
payee: item.payee,
|
|
35
|
+
amount: item.amount,
|
|
36
|
+
currency: item.currency,
|
|
37
|
+
...(item.due_date ? { due_date: item.due_date } : {}),
|
|
38
|
+
}));
|
|
39
|
+
return `sha256:${sha256Hex(canonicalJson(canonical))}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hmacSha256Hex(key: Buffer, payload: string): string {
|
|
43
|
+
return createHmac("sha256", key).update(payload).digest("hex");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hexEqual(a: string, b: string): boolean {
|
|
47
|
+
if (a.length !== b.length) return false;
|
|
48
|
+
return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function newId(prefix: string, bytes = 8): string {
|
|
4
|
+
return `${prefix}_${randomBytes(bytes).toString("hex")}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function newRunId(label?: string): string {
|
|
8
|
+
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
9
|
+
return label ? `run_${stamp}_${label}_${randomBytes(3).toString("hex")}` : `run_${stamp}_${randomBytes(3).toString("hex")}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./state-machine.js";
|
|
3
|
+
export * from "./hash.js";
|
|
4
|
+
export * from "./ids.js";
|
|
5
|
+
export * from "./approval.js";
|
|
6
|
+
export * from "./manifest.js";
|
|
7
|
+
export * from "./guardrails.js";
|
|
8
|
+
export * from "./mandate.js";
|
|
9
|
+
export * from "./escalate.js";
|
|
10
|
+
export * from "./secrets.js";
|
|
11
|
+
export * from "./revocation.js";
|
|
12
|
+
export * from "./rail.js";
|
|
13
|
+
export * from "./tools.js";
|
|
14
|
+
export * from "./bundle.js";
|
|
15
|
+
export * from "./engine.js";
|
|
16
|
+
export * from "./agent.js";
|
|
17
|
+
export * from "./check.js";
|
|
18
|
+
export * from "./events.js";
|
|
19
|
+
export * from "./state/store.js";
|
|
20
|
+
export * from "./stubs/mandate-status.js";
|
|
21
|
+
export * from "./stubs/rail.js";
|
|
22
|
+
export * from "./stubs/charge-rail.js";
|
|
23
|
+
export * from "./api/client.js";
|
|
24
|
+
export * from "./api/rail.js";
|
|
25
|
+
export * from "./api/mandate-status.js";
|
|
26
|
+
export * from "./api/charge-rail.js";
|
|
27
|
+
export * from "./api/sandbox-payer.js";
|
|
28
|
+
export * from "./providers/types.js";
|
|
29
|
+
export * from "./providers/replay.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local view of a consumer mandate: the fields the core checks before
|
|
3
|
+
* anything reaches the API, plus the named payees the kit presents to the
|
|
4
|
+
* person. The API re-verifies all of it server-side on every spend; the
|
|
5
|
+
* local checks exist so a refusal is readable and happens before any call.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
export const BeneficiarySchema = z
|
|
11
|
+
.object({
|
|
12
|
+
/** Short handle the model may use: `escola`, `mercado`. */
|
|
13
|
+
alias: z.string().regex(/^[a-z0-9_-]+$/),
|
|
14
|
+
name: z.string().min(1),
|
|
15
|
+
/** The pinned payee key. Must appear in `merchant_allowlist`. */
|
|
16
|
+
payee: z.string().min(1),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
20
|
+
export const MandateSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
id: z.string().min(1),
|
|
23
|
+
/** Local version counter for the approval artifact; bumps when the mandate is re-signed. */
|
|
24
|
+
version: z.number().int().positive(),
|
|
25
|
+
consumer_id: z.string().min(1),
|
|
26
|
+
agent_id: z.string().min(1),
|
|
27
|
+
purpose: z.string().min(1),
|
|
28
|
+
currency: z.string().min(1),
|
|
29
|
+
/** Section 4.3: total over the window, in minor units. */
|
|
30
|
+
cap_minor: z.number().int().positive(),
|
|
31
|
+
per_tx_cap_minor: z.number().int().positive(),
|
|
32
|
+
periodic_cap: z.object({ window: z.enum(["day", "month"]), cap_minor: z.number().int().positive() }).strict().optional(),
|
|
33
|
+
/** `document` is the receivable side: the list names the debtors (CPF/CNPJ) with an open agreement, and a charge may only be issued against one of them. */
|
|
34
|
+
merchant_pin_kind: z.enum(["pix-key", "merchant-id", "mcc", "document"]),
|
|
35
|
+
/** Concrete keys. The kit never treats `"*"` as authorizing a payee. */
|
|
36
|
+
merchant_allowlist: z.array(z.string().min(1)).nonempty(),
|
|
37
|
+
beneficiaries: z.array(BeneficiarySchema),
|
|
38
|
+
status: z.enum(["active", "paused", "revoked", "expired"]),
|
|
39
|
+
/** ISO 8601. */
|
|
40
|
+
expires_at: z.string().datetime(),
|
|
41
|
+
/** The signature the API returned at consent, when the mandate is a real one. Never required locally. */
|
|
42
|
+
signature: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
43
|
+
/** The canonical mandate the API returned, presented on `/v1/consumer-payments/execute`. */
|
|
44
|
+
canonical: z.record(z.string(), z.unknown()).optional(),
|
|
45
|
+
/** Where this mandate came from. */
|
|
46
|
+
source: z.enum(["example", "consent"]).default("example"),
|
|
47
|
+
})
|
|
48
|
+
.strict()
|
|
49
|
+
.superRefine((m, ctx) => {
|
|
50
|
+
if (m.per_tx_cap_minor > m.cap_minor) {
|
|
51
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["per_tx_cap_minor"], message: "per_tx_cap_minor cannot exceed cap_minor" });
|
|
52
|
+
}
|
|
53
|
+
// The API's own rule (`periodic_cap_never_binds`): the window cap must sit below the lifetime cap, or it is a limit the consumer was shown and never enforced.
|
|
54
|
+
if (m.periodic_cap && m.periodic_cap.cap_minor >= m.cap_minor) {
|
|
55
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["periodic_cap", "cap_minor"], message: "periodic_cap.cap_minor must be below cap_minor (the lifetime cap)" });
|
|
56
|
+
}
|
|
57
|
+
for (const [i, b] of m.beneficiaries.entries()) {
|
|
58
|
+
if (!m.merchant_allowlist.includes(b.payee)) {
|
|
59
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["beneficiaries", i, "payee"], message: `payee ${b.payee} is not in merchant_allowlist` });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Mandate = z.infer<typeof MandateSchema>;
|
|
65
|
+
export type Beneficiary = z.infer<typeof BeneficiarySchema>;
|
|
66
|
+
|
|
67
|
+
export function loadMandate(path: string): Mandate {
|
|
68
|
+
return MandateSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The window the monthly cap is counted over. */
|
|
72
|
+
export function windowStart(mandate: Mandate, now: Date): Date {
|
|
73
|
+
const window = mandate.periodic_cap?.window ?? "month";
|
|
74
|
+
const d = new Date(now);
|
|
75
|
+
if (window === "day") {
|
|
76
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
77
|
+
} else {
|
|
78
|
+
d.setUTCDate(1);
|
|
79
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
80
|
+
}
|
|
81
|
+
return d;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function windowCap(mandate: Mandate): number {
|
|
85
|
+
return mandate.periodic_cap?.cap_minor ?? mandate.cap_minor;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A payee is allowed only when the SIGNED list names it. A wildcard names nobody here. */
|
|
89
|
+
export function payeeAllowed(mandate: Mandate, payee: string): boolean {
|
|
90
|
+
return mandate.merchant_allowlist.some((entry) => entry !== "*" && entry === payee);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveBeneficiary(mandate: Mandate, aliasOrPayee: string): Beneficiary | undefined {
|
|
94
|
+
const needle = aliasOrPayee.trim().toLowerCase();
|
|
95
|
+
return mandate.beneficiaries.find((b) => b.alias === needle || b.payee.toLowerCase() === needle || b.name.toLowerCase() === needle);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function mandateExpired(mandate: Mandate, now: Date): boolean {
|
|
99
|
+
return new Date(mandate.expires_at).getTime() <= now.getTime();
|
|
100
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.3: the `agent.yaml` manifest, `schema: 1`.
|
|
3
|
+
*
|
|
4
|
+
* The schema promises stability from now on: a field is removed or changes
|
|
5
|
+
* meaning only under a new `schema` number.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
const PINNED_PACKAGE = /^@codespar\/(mcp|cli)@\d+\.\d+\.\d+$/;
|
|
13
|
+
const HOURS_WINDOW = /^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$/;
|
|
14
|
+
|
|
15
|
+
export const MaturitySchema = z.enum(["live", "sandbox", "blocked"]);
|
|
16
|
+
|
|
17
|
+
export const EscalateAboveSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
/** Minor units. A payment above this, still inside the mandate cap, asks a human. */
|
|
20
|
+
amount: z.number().int().positive().optional(),
|
|
21
|
+
/** The first payment to a payee that is already on the allowlist asks a human. */
|
|
22
|
+
new_beneficiary: z.boolean().optional(),
|
|
23
|
+
/** "HH:MM-HH:MM", may cross midnight. Execution requested inside it asks a human. */
|
|
24
|
+
outside_hours: z.string().regex(HOURS_WINDOW, 'expected "HH:MM-HH:MM"').optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
|
|
28
|
+
export const ManifestSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
schema: z.literal(1),
|
|
31
|
+
name: z.string().regex(/^[a-z0-9-]+$/),
|
|
32
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
33
|
+
approval: z.array(z.enum(["human", "mandate"])).nonempty(),
|
|
34
|
+
default_approval: z.enum(["human", "mandate"]),
|
|
35
|
+
escalate_above: EscalateAboveSchema.optional(),
|
|
36
|
+
mcp: z.string().regex(PINNED_PACKAGE, "mcp must pin an exact version, e.g. @codespar/mcp@0.5.8"),
|
|
37
|
+
cli: z.string().regex(PINNED_PACKAGE, "cli must pin an exact version, e.g. @codespar/cli@0.13.0"),
|
|
38
|
+
tools: z.string(),
|
|
39
|
+
guardrails: z.string(),
|
|
40
|
+
mandate_schema: z.string(),
|
|
41
|
+
events: z.array(z.string().regex(/^commerce\.[a-z_.]+$/)),
|
|
42
|
+
channels: z.array(z.enum(["terminal", "whatsapp"])).nonempty(),
|
|
43
|
+
maturity: z.record(z.string(), MaturitySchema),
|
|
44
|
+
scenarios: z.string(),
|
|
45
|
+
evals: z.string(),
|
|
46
|
+
agents_md: z.string(),
|
|
47
|
+
})
|
|
48
|
+
.strict()
|
|
49
|
+
.superRefine((m, ctx) => {
|
|
50
|
+
if (!m.approval.includes(m.default_approval)) {
|
|
51
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["default_approval"], message: "default_approval must be one of approval" });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type Manifest = z.infer<typeof ManifestSchema>;
|
|
56
|
+
export type EscalateAbove = z.infer<typeof EscalateAboveSchema>;
|
|
57
|
+
|
|
58
|
+
export interface LoadedManifest {
|
|
59
|
+
manifest: Manifest;
|
|
60
|
+
/** Absolute path of the manifest file. */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Directory the relative paths in the manifest resolve against. */
|
|
63
|
+
dir: string;
|
|
64
|
+
resolvePath(relative: string): string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseManifest(text: string): Manifest {
|
|
68
|
+
return ManifestSchema.parse(parseYaml(text));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadManifest(path: string): LoadedManifest {
|
|
72
|
+
const absolute = resolve(path);
|
|
73
|
+
const manifest = parseManifest(readFileSync(absolute, "utf8"));
|
|
74
|
+
const dir = dirname(absolute);
|
|
75
|
+
return { manifest, path: absolute, dir, resolvePath: (relative) => resolve(dir, relative) };
|
|
76
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference harness: a tool-use loop on `@anthropic-ai/sdk`. It only
|
|
3
|
+
* turns messages into a request and the response into tool calls or a
|
|
4
|
+
* reply; it never decides anything about money.
|
|
5
|
+
*/
|
|
6
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
7
|
+
import type { ContentBlockParam, MessageParam, Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages";
|
|
8
|
+
import type { AgentRuntime, ConversationMessage, Reply, ToolCall, ToolSpec, Turn } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_MODEL = "claude-sonnet-5";
|
|
11
|
+
|
|
12
|
+
export interface AnthropicRuntimeOptions {
|
|
13
|
+
apiKey?: string | undefined;
|
|
14
|
+
model?: string;
|
|
15
|
+
maxTokens?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AnthropicRuntime implements AgentRuntime {
|
|
19
|
+
readonly name = "anthropic";
|
|
20
|
+
private readonly client: Anthropic;
|
|
21
|
+
private readonly model: string;
|
|
22
|
+
private readonly maxTokens: number;
|
|
23
|
+
|
|
24
|
+
constructor(options: AnthropicRuntimeOptions = {}) {
|
|
25
|
+
const apiKey = options.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
26
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not set; use the replay provider for runs without a model");
|
|
27
|
+
this.client = new Anthropic({ apiKey });
|
|
28
|
+
this.model = options.model ?? process.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODEL;
|
|
29
|
+
this.maxTokens = options.maxTokens ?? 1024;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async step(input: Turn, tools: ToolSpec[]): Promise<ToolCall[] | Reply> {
|
|
33
|
+
const response = await this.client.messages.create({
|
|
34
|
+
model: this.model,
|
|
35
|
+
max_tokens: this.maxTokens,
|
|
36
|
+
system: input.system,
|
|
37
|
+
tools: tools.map(toAnthropicTool),
|
|
38
|
+
messages: toAnthropicMessages(input.messages),
|
|
39
|
+
});
|
|
40
|
+
const calls: ToolCall[] = [];
|
|
41
|
+
const text: string[] = [];
|
|
42
|
+
for (const block of response.content) {
|
|
43
|
+
if (block.type === "tool_use") calls.push({ id: block.id, name: block.name, input: (block.input ?? {}) as Record<string, unknown> });
|
|
44
|
+
else if (block.type === "text") text.push(block.text);
|
|
45
|
+
}
|
|
46
|
+
if (calls.length > 0) return calls;
|
|
47
|
+
return { text: text.join("\n").trim() };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toAnthropicTool(tool: ToolSpec): AnthropicTool {
|
|
52
|
+
return { name: tool.name, description: tool.description, input_schema: tool.input_schema as AnthropicTool["input_schema"] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toAnthropicMessages(messages: ConversationMessage[]): MessageParam[] {
|
|
56
|
+
const out: MessageParam[] = [];
|
|
57
|
+
for (const m of messages) {
|
|
58
|
+
if (m.role === "user") {
|
|
59
|
+
out.push({ role: "user", content: m.content });
|
|
60
|
+
} else if (m.role === "assistant") {
|
|
61
|
+
const content: ContentBlockParam[] = [];
|
|
62
|
+
if (m.content) content.push({ type: "text", text: m.content });
|
|
63
|
+
for (const call of m.tool_calls ?? []) content.push({ type: "tool_use", id: call.id, name: call.name, input: call.input });
|
|
64
|
+
out.push({ role: "assistant", content });
|
|
65
|
+
} else {
|
|
66
|
+
const last = out[out.length - 1];
|
|
67
|
+
const block = { type: "tool_result" as const, tool_use_id: m.tool_call_id, content: m.content, ...(m.is_error ? { is_error: true } : {}) };
|
|
68
|
+
if (last && last.role === "user" && Array.isArray(last.content) && last.content.every((b) => (b as { type: string }).type === "tool_result")) {
|
|
69
|
+
(last.content as Array<typeof block>).push(block);
|
|
70
|
+
} else {
|
|
71
|
+
out.push({ role: "user", content: [block] });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic provider. It reads the `assistant_step` lines of a
|
|
3
|
+
* recorded transcript and answers them in order, so the CI, the scenarios
|
|
4
|
+
* and `npm run rerun` run with no model and no network. The recorded lines
|
|
5
|
+
* are the MODEL's outputs only; everything the core decided is recomputed
|
|
6
|
+
* live, which is what makes a rerun a test rather than a printout.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import type { AgentRuntime, Reply, ToolCall, ToolSpec, Turn } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export interface RecordedStep {
|
|
12
|
+
kind: "assistant_step";
|
|
13
|
+
tool_calls?: ToolCall[];
|
|
14
|
+
reply?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ReplayExhaustedError extends Error {
|
|
18
|
+
constructor(consumed: number) {
|
|
19
|
+
super(`replay transcript exhausted after ${consumed} step(s); the run asked the model more than the recording holds`);
|
|
20
|
+
this.name = "ReplayExhaustedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ReplayRuntime implements AgentRuntime {
|
|
25
|
+
readonly name = "replay";
|
|
26
|
+
private cursor = 0;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly steps: RecordedStep[]) {}
|
|
29
|
+
|
|
30
|
+
static fromFile(path: string): ReplayRuntime {
|
|
31
|
+
return new ReplayRuntime(parseTranscript(readFileSync(path, "utf8")));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static fromLines(lines: Array<Record<string, unknown>>): ReplayRuntime {
|
|
35
|
+
return new ReplayRuntime(lines.filter((l) => l["kind"] === "assistant_step") as unknown as RecordedStep[]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get consumed(): number {
|
|
39
|
+
return this.cursor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get remaining(): number {
|
|
43
|
+
return this.steps.length - this.cursor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async step(_input: Turn, _tools: ToolSpec[]): Promise<ToolCall[] | Reply> {
|
|
47
|
+
const next = this.steps[this.cursor];
|
|
48
|
+
if (!next) throw new ReplayExhaustedError(this.cursor);
|
|
49
|
+
this.cursor += 1;
|
|
50
|
+
if (next.tool_calls && next.tool_calls.length > 0) {
|
|
51
|
+
return next.tool_calls.map((c, i) => ({ id: c.id ?? `tc_${this.cursor}_${i}`, name: c.name, input: c.input ?? {} }));
|
|
52
|
+
}
|
|
53
|
+
return { text: next.reply ?? "" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseTranscript(text: string): RecordedStep[] {
|
|
58
|
+
return text
|
|
59
|
+
.split("\n")
|
|
60
|
+
.filter((l) => l.trim().length > 0)
|
|
61
|
+
.map((l) => JSON.parse(l) as Record<string, unknown>)
|
|
62
|
+
.filter((l) => l["kind"] === "assistant_step") as unknown as RecordedStep[];
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 13: the contract of the harness file. Policy stays outside the
|
|
3
|
+
* provider: the state machine, the mandate, the guardrails and `tools.json`
|
|
4
|
+
* are the core's. Changing the model changes nothing about what can be paid.
|
|
5
|
+
*/
|
|
6
|
+
export interface ToolSpec {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
input_schema: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
input: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Reply {
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ConversationMessage =
|
|
23
|
+
| { role: "user"; content: string }
|
|
24
|
+
| { role: "assistant"; content: string; tool_calls?: ToolCall[] }
|
|
25
|
+
| { role: "tool"; tool_call_id: string; name: string; content: string; is_error?: boolean };
|
|
26
|
+
|
|
27
|
+
export interface Turn {
|
|
28
|
+
system: string;
|
|
29
|
+
messages: ConversationMessage[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentRuntime {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
step(input: Turn, tools: ToolSpec[]): Promise<ToolCall[] | Reply>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isReply(out: ToolCall[] | Reply): out is Reply {
|
|
38
|
+
return !Array.isArray(out);
|
|
39
|
+
}
|