/** * AgentGuard® — CFO cost dashboard, read side. * * Aggregates the signed, hash-chained decision log into a CFO-legible view: * spend over time, by model, by provider, frontier-vs-"dark" (local / * open-weight / self-hosted) tokens, reserved-vs-settled true-up, an * enforcement/savings summary, and a tamper-evidence check. Everything is * derived IN-PROCESS from the customer-held ledger — no proxy, no data plane, * no provider billing API required. * * The "dark token" view is the differentiator: open-source / self-hosted usage * never appears on any provider's bill, so a provider console structurally * cannot show it. The signed ledger can, because the receipt is written where * the call is made. * * Honest scope note: the signed SpendDecision carries time, provider, model, * cents, action, builderCode, entryType and a provenance block — but NOT full * tenant/agent identity (only the scope key a cap matched on). Per-agent / * per-tenant breakdown therefore requires recording a metadata-only scope * digest on the decision; see `scopeKeyBreakdown` (best-effort from * triggeredScopeKey) and DASHBOARD.md. We do not invent attribution the ledger * does not carry. * * Patent notice: signed hash-chained decision log (U.S. application filed May * 2026); composes with DAG Trust Attestation, Patent D §7.3 (App. No. * 63/984,626). AgentGuard® is a U.S. registered trademark (Reg. No. 8281464) * of Dunecrest Ventures Inc. */ import type { Provider, SignedDecisionLogEntry, SpendDecision } from '../types'; import type { ProvenanceProvider } from '../receipts/schema'; /** Model families that are open-weight / self-hostable → "dark" when self-hosted. */ const OPEN_WEIGHT_FAMILIES = new Set([ 'meta', 'mistral', 'cohere', 'fireworks', 'baseten', 'together', 'deepseek', 'moonshot', 'alibaba', 'self_hosted', ]); const FRONTIER_PROVIDERS = new Set(['openai', 'anthropic', 'gemini', 'bedrock']); export type TokenOrigin = 'frontier' | 'dark' | 'unknown'; /** Classify a decision as frontier vs dark (local/open-weight) spend. * Prefers the signed provenance block; falls back to the coarse provider * field and labels the result `inferred` so the UI never overstates. */ export function classifyOrigin(decision: SpendDecision): { origin: TokenOrigin; inferred: boolean } { const prov = decision.provenance; if (prov?.model_identity) { const fam = prov.model_identity.provider; if (fam === 'self_hosted') return { origin: 'dark', inferred: false }; const selfHosted = /self|local|on[-_]?prem|in[-_]?house/i.test(prov.hosting?.provider_route ?? ''); if (selfHosted) return { origin: 'dark', inferred: false }; if (OPEN_WEIGHT_FAMILIES.has(fam)) return { origin: 'dark', inferred: false }; if (fam === 'anthropic' || fam === 'openai' || fam === 'google') return { origin: 'frontier', inferred: false }; } if (FRONTIER_PROVIDERS.has(decision.provider)) return { origin: 'frontier', inferred: true }; if (decision.provider === 'unknown') return { origin: 'dark', inferred: true }; return { origin: 'unknown', inferred: true }; } /** Cents this decision actually cost the org: settled actuals win over the * pre-dispatch projection; blocked calls cost zero. */ export function decisionCents(decision: SpendDecision): number { if (decision.action === 'block') return 0; if (typeof decision.actualCents === 'number') return decision.actualCents; return decision.projectedCents ?? 0; } export interface BucketSpend { key: string; cents: number; calls: number; } export interface CfoDashboard { generatedAt: string; windowStart: string | null; windowEnd: string | null; totals: { calls: number; spentCents: number; frontierCents: number; darkCents: number; darkSharePct: number; // dark / total spend, 0..100 inferredCents: number; // spend whose origin was inferred, not from provenance }; enforcement: { allowed: number; downgraded: number; blocked: number; shadow: number; /** Cents NOT spent because a call was blocked or downgraded — the ROI line. */ estimatedSavedCents: number; }; trueUp: { reservedCents: number; // sum of projections on decision entries settledCents: number; // sum of actuals on settlement entries driftCents: number; // settled - reserved (over/under-estimate) }; byHour: BucketSpend[]; // ISO hour → spend byModel: BucketSpend[]; // modelResolved → spend, desc byProvider: BucketSpend[]; byBuilderCode: BucketSpend[]; scopeKeyBreakdown: BucketSpend[]; // best-effort from triggeredScopeKey (may be sparse) integrity: { verified: boolean; entries: number; reason?: string; sequence?: number; /** Completeness signal: missing per-signer sequence numbers (possible omitted actions). */ sequenceGaps?: number[]; }; } function topBuckets(map: Map, limit = 50): BucketSpend[] { return [...map.values()].sort((a, b) => b.cents - a.cents).slice(0, limit); } function bump(map: Map, key: string, cents: number) { const cur = map.get(key) ?? { key, cents: 0, calls: 0 }; cur.cents += cents; cur.calls += 1; map.set(key, cur); } /** * Build the CFO dashboard from signed ledger entries. Pure + synchronous. * `integrity` is passed in (compute via verifyChain, which is async) so this * stays a pure function; pass null to skip. */ export function aggregateLedger( entries: SignedDecisionLogEntry[], integrity: CfoDashboard['integrity'] | null = null, now: string = new Date().toISOString(), ): CfoDashboard { const byHour = new Map(); const byModel = new Map(); const byProvider = new Map(); const byBuilder = new Map(); const byScopeKey = new Map(); let calls = 0, spent = 0, frontier = 0, dark = 0, inferred = 0; let allowed = 0, downgraded = 0, blocked = 0, shadow = 0, saved = 0; let reserved = 0, settled = 0; let minTs: string | null = null, maxTs: string | null = null; for (const entry of entries) { const d = entry.decision; const kind = d.entryType ?? 'decision'; if (kind === 'settlement') { if (typeof d.actualCents === 'number') settled += d.actualCents; // settlement entries adjust true-up only; not double-counted as calls continue; } if (kind === 'outcome' || kind === 'governance') continue; // provider enforcement decision calls += 1; const cents = decisionCents(d); spent += cents; reserved += d.projectedCents ?? 0; const { origin, inferred: wasInferred } = classifyOrigin(d); if (origin === 'frontier') frontier += cents; else if (origin === 'dark') dark += cents; if (wasInferred) inferred += cents; switch (d.action) { case 'allow': allowed += 1; break; case 'downgrade': downgraded += 1; saved += Math.max(0, (d.projectedCents ?? 0) - cents); break; case 'block': blocked += 1; saved += d.projectedCents ?? 0; break; case 'shadow': shadow += 1; break; } const ts = d.timestamp; if (ts) { if (!minTs || ts < minTs) minTs = ts; if (!maxTs || ts > maxTs) maxTs = ts; bump(byHour, ts.slice(0, 13) + ':00', cents); // ISO hour bucket } bump(byModel, d.modelResolved || d.modelRequested || 'unknown', cents); bump(byProvider, d.provider || 'unknown', cents); if (d.builderCode) bump(byBuilder, d.builderCode, cents); if (d.triggeredScopeKey) bump(byScopeKey, d.triggeredScopeKey, cents); } return { generatedAt: now, windowStart: minTs, windowEnd: maxTs, totals: { calls, spentCents: spent, frontierCents: frontier, darkCents: dark, darkSharePct: spent > 0 ? Math.round((dark / spent) * 1000) / 10 : 0, inferredCents: inferred, }, enforcement: { allowed, downgraded, blocked, shadow, estimatedSavedCents: saved }, trueUp: { reservedCents: reserved, settledCents: settled, driftCents: settled - reserved }, byHour: [...byHour.values()].sort((a, b) => a.key.localeCompare(b.key)), byModel: topBuckets(byModel), byProvider: topBuckets(byProvider), byBuilderCode: topBuckets(byBuilder), scopeKeyBreakdown: topBuckets(byScopeKey), integrity: integrity ?? { verified: false, entries: entries.length, reason: 'NOT_CHECKED' }, }; } export const centsToUsd = (c: number): string => `$${(c / 100).toFixed(2)}`;