@agentguard-run/spend 0.15.2 → 0.15.4
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/CHANGELOG.md +7 -0
- package/dist/dashboard/aggregate.d.ts +90 -0
- package/dist/dashboard/aggregate.d.ts.map +1 -0
- package/dist/dashboard/aggregate.js +147 -0
- package/dist/dashboard/aggregate.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -1
- package/dist/receipts/portability.d.ts +76 -0
- package/dist/receipts/portability.d.ts.map +1 -0
- package/dist/receipts/portability.js +88 -0
- package/dist/receipts/portability.js.map +1 -0
- package/dist/types.d.ts +50 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/dashboard/aggregate.test.ts +103 -0
- package/src/dashboard/aggregate.ts +201 -0
- package/src/receipts/portability.test.ts +68 -0
- package/src/receipts/portability.ts +156 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentGuard® — model-agnostic provenance that survives hot-swap.
|
|
3
|
+
*
|
|
4
|
+
* The "headless" / model-fungibility problem (raised on All-In E280): teams want
|
|
5
|
+
* to hot-swap models — frontier → open-weight → self-hosted — per task to cut
|
|
6
|
+
* cost, but "no one has figured out how to abstract memory/context/AUDIT away
|
|
7
|
+
* from the model." A provider console can't help: it only sees its own tokens,
|
|
8
|
+
* and the trail breaks the moment you route to a different provider.
|
|
9
|
+
*
|
|
10
|
+
* AgentGuard's signed decision log already records, per call, which model served
|
|
11
|
+
* it (provenance) inside one continuous hash chain. This module reads that log
|
|
12
|
+
* and produces a PORTABLE, model-agnostic provenance manifest per workload: the
|
|
13
|
+
* ordered model lineage, every swap, the frontier/dark origin mix, and a single
|
|
14
|
+
* tamper-evidence verdict that spans the whole swap sequence. The audit is keyed
|
|
15
|
+
* to the workload, not the model — so it survives the swap.
|
|
16
|
+
*
|
|
17
|
+
* Honest scope note (same as the dashboard): the signed SpendDecision does not
|
|
18
|
+
* carry a first-class workload/agent id (only `triggeredScopeKey`). Callers pass
|
|
19
|
+
* a `keyOf` selector to group; the default uses `triggeredScopeKey`. We never
|
|
20
|
+
* invent a grouping the ledger does not support.
|
|
21
|
+
*
|
|
22
|
+
* Patent notice: signed hash-chained decision log (U.S. application filed May
|
|
23
|
+
* 2026); composes with DAG Trust Attestation, Patent D §7.3 (App. No.
|
|
24
|
+
* 63/984,626). AgentGuard® is a U.S. registered trademark (Reg. No. 8281464) of
|
|
25
|
+
* Dunecrest Ventures Inc.
|
|
26
|
+
*/
|
|
27
|
+
import type { SignedDecisionLogEntry, SpendDecision } from '../types';
|
|
28
|
+
import { classifyOrigin, decisionCents, type TokenOrigin } from '../dashboard/aggregate';
|
|
29
|
+
|
|
30
|
+
export interface ModelUse {
|
|
31
|
+
model: string;
|
|
32
|
+
origin: TokenOrigin;
|
|
33
|
+
provider: string;
|
|
34
|
+
calls: number;
|
|
35
|
+
cents: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ModelSwap {
|
|
38
|
+
atSequence: number;
|
|
39
|
+
fromModel: string;
|
|
40
|
+
toModel: string;
|
|
41
|
+
fromOrigin: TokenOrigin;
|
|
42
|
+
toOrigin: TokenOrigin;
|
|
43
|
+
/** True when the swap crossed the frontier↔dark boundary (the audit-risky kind). */
|
|
44
|
+
crossesBoundary: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface WorkloadProvenance {
|
|
47
|
+
key: string;
|
|
48
|
+
calls: number;
|
|
49
|
+
spentCents: number;
|
|
50
|
+
frontierCents: number;
|
|
51
|
+
darkCents: number;
|
|
52
|
+
firstAt: string | null;
|
|
53
|
+
lastAt: string | null;
|
|
54
|
+
models: ModelUse[];
|
|
55
|
+
/** Ordered model lineage, e.g. ['claude-5','gpt-5-mini','glm-5.2']. */
|
|
56
|
+
lineage: string[];
|
|
57
|
+
swaps: ModelSwap[];
|
|
58
|
+
}
|
|
59
|
+
export interface WorkloadProvenanceReport {
|
|
60
|
+
workloads: WorkloadProvenance[];
|
|
61
|
+
/** Chain-level tamper evidence spans ALL swaps — the point of the manifest. */
|
|
62
|
+
continuity: { verified: boolean; entries: number; reason?: string };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const defaultKeyOf = (d: SpendDecision): string => d.triggeredScopeKey ?? 'default';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the portable per-workload provenance manifest from signed ledger
|
|
69
|
+
* entries. Pass the async `verifyChain` result as `continuity` so this stays a
|
|
70
|
+
* pure, synchronous function (never null-continuity in production — a manifest
|
|
71
|
+
* without tamper evidence is not the product).
|
|
72
|
+
*/
|
|
73
|
+
export function workloadProvenance(
|
|
74
|
+
entries: SignedDecisionLogEntry[],
|
|
75
|
+
continuity: WorkloadProvenanceReport['continuity'] = { verified: false, entries: 0, reason: 'NOT_CHECKED' },
|
|
76
|
+
keyOf: (d: SpendDecision) => string = defaultKeyOf,
|
|
77
|
+
): WorkloadProvenanceReport {
|
|
78
|
+
interface Group {
|
|
79
|
+
calls: number; spent: number; frontier: number; dark: number;
|
|
80
|
+
first: string | null; last: string | null;
|
|
81
|
+
models: Map<string, ModelUse>; ordered: Array<{ seq: number; model: string; origin: TokenOrigin }>;
|
|
82
|
+
}
|
|
83
|
+
const groups = new Map<string, Group>();
|
|
84
|
+
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const d = entry.decision;
|
|
87
|
+
const kind = d.entryType ?? 'decision';
|
|
88
|
+
if (kind !== 'decision') continue; // settlements/outcomes don't add model steps
|
|
89
|
+
|
|
90
|
+
const key = keyOf(d);
|
|
91
|
+
const grp: Group = groups.get(key) ?? {
|
|
92
|
+
calls: 0, spent: 0, frontier: 0, dark: 0, first: null, last: null,
|
|
93
|
+
models: new Map(), ordered: [],
|
|
94
|
+
};
|
|
95
|
+
const model = d.modelResolved || d.modelRequested || 'unknown';
|
|
96
|
+
const { origin } = classifyOrigin(d);
|
|
97
|
+
const cents = decisionCents(d);
|
|
98
|
+
|
|
99
|
+
grp.calls += 1;
|
|
100
|
+
grp.spent += cents;
|
|
101
|
+
if (origin === 'frontier') grp.frontier += cents;
|
|
102
|
+
else if (origin === 'dark') grp.dark += cents;
|
|
103
|
+
if (d.timestamp) {
|
|
104
|
+
if (!grp.first || d.timestamp < grp.first) grp.first = d.timestamp;
|
|
105
|
+
if (!grp.last || d.timestamp > grp.last) grp.last = d.timestamp;
|
|
106
|
+
}
|
|
107
|
+
const mu = grp.models.get(model) ?? { model, origin, provider: d.provider ?? 'unknown', calls: 0, cents: 0 };
|
|
108
|
+
mu.calls += 1;
|
|
109
|
+
mu.cents += cents;
|
|
110
|
+
grp.models.set(model, mu);
|
|
111
|
+
grp.ordered.push({ seq: entry.sequence, model, origin });
|
|
112
|
+
|
|
113
|
+
groups.set(key, grp);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const workloads: WorkloadProvenance[] = [];
|
|
117
|
+
for (const [key, grp] of groups) {
|
|
118
|
+
grp.ordered.sort((a, b) => a.seq - b.seq);
|
|
119
|
+
const swaps: ModelSwap[] = [];
|
|
120
|
+
for (let i = 1; i < grp.ordered.length; i++) {
|
|
121
|
+
const prev = grp.ordered[i - 1];
|
|
122
|
+
const cur = grp.ordered[i];
|
|
123
|
+
if (cur.model !== prev.model) {
|
|
124
|
+
swaps.push({
|
|
125
|
+
atSequence: cur.seq,
|
|
126
|
+
fromModel: prev.model, toModel: cur.model,
|
|
127
|
+
fromOrigin: prev.origin, toOrigin: cur.origin,
|
|
128
|
+
crossesBoundary: prev.origin !== cur.origin
|
|
129
|
+
&& prev.origin !== 'unknown' && cur.origin !== 'unknown',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// lineage = models in first-appearance order (collapse consecutive repeats)
|
|
134
|
+
const lineage: string[] = [];
|
|
135
|
+
for (const step of grp.ordered) if (lineage[lineage.length - 1] !== step.model) lineage.push(step.model);
|
|
136
|
+
|
|
137
|
+
workloads.push({
|
|
138
|
+
key,
|
|
139
|
+
calls: grp.calls,
|
|
140
|
+
spentCents: grp.spent,
|
|
141
|
+
frontierCents: grp.frontier,
|
|
142
|
+
darkCents: grp.dark,
|
|
143
|
+
firstAt: grp.first,
|
|
144
|
+
lastAt: grp.last,
|
|
145
|
+
models: [...grp.models.values()].sort((a, b) => b.cents - a.cents),
|
|
146
|
+
lineage,
|
|
147
|
+
swaps,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
workloads.sort((a, b) => b.spentCents - a.spentCents);
|
|
152
|
+
return { workloads, continuity };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Human-readable lineage, e.g. "claude-5 → gpt-5-mini → glm-5.2". */
|
|
156
|
+
export const formatLineage = (w: WorkloadProvenance): string => w.lineage.join(' → ');
|