@agent-finops/core 0.7.3 → 0.8.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.
@@ -1,4 +1,5 @@
1
1
  import { localAgentFormatDescriptors } from "./localAgentFormats/registry.js";
2
+ import { generatedProviderContractStates } from "./providerContractStates.generated.js";
2
3
  /**
3
4
  * How thoroughly an ingestion path itself has been exercised.
4
5
  *
@@ -12,6 +13,7 @@ export const sourceValidationCoverageValues = [
12
13
  "failed"
13
14
  ];
14
15
  export const sourceFreshnessStatusValues = ["fresh", "stale", "not_checked"];
16
+ export const providerContractStateValues = ["current", "stale_contract"];
15
17
  /**
16
18
  * Shipped validation matrix. Keep these claims proof-conservative: a live
17
19
  * authentication check is not the same as a non-empty end-to-end billing
@@ -23,48 +25,67 @@ export const sourceStatusDefinitions = [
23
25
  label: `${descriptor.label} local logs`,
24
26
  validationCoverage: descriptor.confidenceDefaults.validationCoverage,
25
27
  validationNote: descriptor.validationNote,
26
- staleAfterHours: 72
28
+ staleAfterHours: 72,
29
+ ...(descriptor.id === "gemini-cli"
30
+ ? { contractState: generatedProviderContractStates["gemini-cli"] }
31
+ : {})
27
32
  })),
28
33
  {
29
34
  id: "openai",
30
35
  label: "OpenAI Costs and Usage API",
31
36
  validationCoverage: "live_verified",
32
37
  validationNote: "Product connector QA exercised non-empty Admin cost and usage API paths; the tested Costs total reconciled to invoiced API credits less the provider-UI balance with $0.00 variance. This does not reconcile the current user's account; final invoices, tax, discounts, and later adjustments remain separate.",
33
- staleAfterHours: 48
38
+ staleAfterHours: 48,
39
+ contractState: generatedProviderContractStates.openai
34
40
  },
35
41
  {
36
42
  id: "anthropic",
37
43
  label: "Anthropic Cost Report and Claude Code Analytics",
38
44
  validationCoverage: "live_verified",
39
45
  validationNote: "Admin cost and Claude Code usage paths are exercised with non-empty live records.",
40
- staleAfterHours: 48
46
+ staleAfterHours: 48,
47
+ contractState: generatedProviderContractStates.anthropic
41
48
  },
42
49
  {
43
50
  id: "cursor",
44
51
  label: "Cursor Admin API",
45
52
  validationCoverage: "fixture_verified",
46
53
  validationNote: "Canonical teamMemberSpend envelopes, totalPages pagination, completeness failures, and malformed responses pass recorded fixtures; live account QA is pending.",
47
- staleAfterHours: 48
54
+ staleAfterHours: 48,
55
+ contractState: generatedProviderContractStates.cursor
48
56
  },
49
57
  {
50
58
  id: "github-copilot",
51
59
  label: "GitHub Copilot organization APIs",
52
60
  validationCoverage: "fixture_verified",
53
61
  validationNote: "The 2026-03-10 signed-NDJSON metrics workflow, seat pagination, per-seat plan types, and failure paths pass recorded fixtures; live account QA is pending.",
54
- staleAfterHours: 48
62
+ staleAfterHours: 48,
63
+ contractState: generatedProviderContractStates["github-copilot"]
55
64
  }
56
65
  ];
57
- export function buildSourceStatuses(observations = [], now = new Date()) {
66
+ export function buildSourceStatuses(observations = [], now = new Date(), definitions = sourceStatusDefinitions) {
58
67
  const byId = new Map(observations.map((observation) => [observation.id, observation]));
59
- return sourceStatusDefinitions.map((definition) => {
68
+ return definitions.map((definition) => {
60
69
  const observation = byId.get(definition.id);
70
+ const observedContractState = observation?.contractState;
71
+ const contractState = definition.contractState === "stale_contract" || observedContractState === "stale_contract"
72
+ ? "stale_contract"
73
+ : definition.contractState;
74
+ const observedFinancialEvidence = observation?.financialEvidence ?? "missing";
75
+ const financialEvidence = contractState === "stale_contract" && observedFinancialEvidence === "verified"
76
+ ? "missing"
77
+ : observedFinancialEvidence;
78
+ const financialEvidenceNote = contractState === "stale_contract" && observedFinancialEvidence === "verified"
79
+ ? "Provider contract drift is unresolved; the verified financial headline is withheld pending human review."
80
+ : observation?.financialEvidenceNote ?? "No current financial evidence was observed on this machine.";
61
81
  return {
62
82
  id: definition.id,
63
83
  label: definition.label,
64
84
  validationCoverage: observation?.validationCoverage ?? definition.validationCoverage,
65
85
  validationNote: definition.validationNote,
66
- financialEvidence: observation?.financialEvidence ?? "missing",
67
- financialEvidenceNote: observation?.financialEvidenceNote ?? "No current financial evidence was observed on this machine.",
86
+ financialEvidence,
87
+ financialEvidenceNote,
88
+ ...(contractState ? { contractState } : {}),
68
89
  freshness: sourceFreshness(definition, observation, now),
69
90
  ...(observation?.lastError ? { lastError: observation.lastError } : {})
70
91
  };
@@ -86,6 +107,79 @@ export function financialEvidenceForRecords(records) {
86
107
  }
87
108
  return "missing";
88
109
  }
110
+ /**
111
+ * Apply the reviewed provider-contract gate before any connected financial
112
+ * calculation. Local transcript evidence is unchanged. When a provider's
113
+ * financial semantics are stale, its priced rows remain present for audit and
114
+ * attribution but cannot carry a dollar amount or proof-level confidence.
115
+ */
116
+ export function applyProviderContractGate(records, definitions = sourceStatusDefinitions) {
117
+ const contractStates = new Map(definitions
118
+ .filter((definition) => definition.contractState !== undefined)
119
+ .map((definition) => [definition.id, definition.contractState]));
120
+ return records.map((record) => {
121
+ if (record.providerCostType === "local_agent_logs")
122
+ return record;
123
+ const sourceId = providerStatusIdForRecord(record);
124
+ if (!sourceId || contractStates.get(sourceId) !== "stale_contract")
125
+ return record;
126
+ return {
127
+ ...record,
128
+ source: { ...record.source, confidence: "missing" },
129
+ amountUsd: null,
130
+ costConfidence: "missing"
131
+ };
132
+ });
133
+ }
134
+ /**
135
+ * Project persisted source metadata through the same fail-closed contract
136
+ * gate as records. This is read-time only: the signed local receipt remains
137
+ * byte-exact, while an upgraded release cannot repeat an obsolete verified
138
+ * claim from an older sources.json.
139
+ */
140
+ export function applyProviderContractGateToSourceRegistry(registry, definitions = sourceStatusDefinitions) {
141
+ const states = new Map(definitions
142
+ .filter((definition) => definition.contractState !== undefined)
143
+ .map((definition) => [definition.id, definition.contractState]));
144
+ return {
145
+ ...registry,
146
+ approvedSources: registry.approvedSources.map((source) => {
147
+ if (source.type !== "provider_api")
148
+ return source;
149
+ const statusId = providerStatusIdForProvider(source.provider);
150
+ if (!statusId || states.get(statusId) !== "stale_contract")
151
+ return source;
152
+ const baseScope = source.scope.split(" Last successful pull produced ")[0]?.trim() || source.scope;
153
+ return {
154
+ ...source,
155
+ financialEvidence: "missing",
156
+ fieldsMissing: Array.from(new Set([
157
+ ...source.fieldsMissing,
158
+ "provider financial headline (contract review required)"
159
+ ])),
160
+ scope: `${baseScope} Provider contract drift is unresolved; prior financial evidence and headline are withheld.`
161
+ };
162
+ })
163
+ };
164
+ }
165
+ function providerStatusIdForRecord(record) {
166
+ if (record.source.provider === "google" && record.agentId === "gemini-cli")
167
+ return "gemini-cli";
168
+ return providerStatusIdForProvider(record.source.provider);
169
+ }
170
+ function providerStatusIdForProvider(provider) {
171
+ if (provider === "openai")
172
+ return "openai";
173
+ if (provider === "anthropic")
174
+ return "anthropic";
175
+ if (provider === "cursor")
176
+ return "cursor";
177
+ if (provider === "github" || provider === "github-copilot" || provider === "copilot")
178
+ return "github-copilot";
179
+ if (provider === "google" || provider === "gemini")
180
+ return "gemini-cli";
181
+ return undefined;
182
+ }
89
183
  /** Stable, plain-text formatter shared by terminal surfaces. */
90
184
  export function formatSourceStatuses(statuses) {
91
185
  return statuses.map((status) => {
@@ -93,6 +187,7 @@ export function formatSourceStatuses(statuses) {
93
187
  return [
94
188
  `${status.label} (${status.id})`,
95
189
  ` validation coverage: ${status.validationCoverage}`,
190
+ ...(status.contractState ? [` provider contract: ${status.contractState}`] : []),
96
191
  ` financial evidence: ${status.financialEvidence}`,
97
192
  ` freshness: ${freshness}`,
98
193
  ` last error: ${status.lastError ? singleLineStatusText(status.lastError) : "none recorded"}`,
@@ -39,7 +39,7 @@ export async function verifyConnectedSpendTrustReceipt(rootPath, exactSpendConte
39
39
  if (isNodeError(error, "ENOENT")) {
40
40
  return missingTrust();
41
41
  }
42
- return invalidTrust(error instanceof Error ? error.message : String(error));
42
+ return invalidTrust("the external trust directory failed safety validation");
43
43
  }
44
44
  let rawReceipt;
45
45
  try {
@@ -48,7 +48,7 @@ export async function verifyConnectedSpendTrustReceipt(rootPath, exactSpendConte
48
48
  catch (error) {
49
49
  if (isNodeError(error, "ENOENT"))
50
50
  return missingTrust();
51
- return invalidTrust(error instanceof Error ? error.message : String(error));
51
+ return invalidTrust("the external provider-sync receipt failed safety validation");
52
52
  }
53
53
  let receipt;
54
54
  try {
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
7
14
  "scripts": {
8
15
  "build": "tsc -b",
9
16
  "typecheck": "tsc -b --pretty false",