@agent-finops/core 0.5.9 → 0.6.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.
@@ -0,0 +1,277 @@
1
+ import { constants } from "node:fs";
2
+ import { chmod, lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
6
+ const trustDirectoryEnvironmentVariable = "AI_SPEND_STATE_TRUST_DIR";
7
+ export async function writeConnectedSpendTrustReceipt(rootPath, exactSpendContents, options = {}) {
8
+ const canonicalRoot = await realpath(resolve(rootPath));
9
+ const trustDirectory = await resolveTrustDirectory(canonicalRoot, true, options.trustDirectory);
10
+ const spendSha256 = sha256(exactSpendContents);
11
+ const sourceRegistrySha256 = options.sourceRegistryContents === undefined
12
+ ? undefined
13
+ : sha256(options.sourceRegistryContents);
14
+ const trustedAt = new Date().toISOString();
15
+ const receipt = {
16
+ version: 1,
17
+ canonicalRoot,
18
+ mode: "connected_provider",
19
+ trustedAt,
20
+ spendSha256,
21
+ ...(sourceRegistrySha256 ? { sourceRegistrySha256 } : {}),
22
+ stateDigest: connectedStateDigest(canonicalRoot, spendSha256, sourceRegistrySha256)
23
+ };
24
+ await writePrivateFile(trustDirectory, receiptFileName(canonicalRoot), `${JSON.stringify(receipt, null, 2)}\n`);
25
+ }
26
+ export async function verifyConnectedSpendTrustReceipt(rootPath, exactSpendContents, options = {}) {
27
+ let canonicalRoot;
28
+ try {
29
+ canonicalRoot = await realpath(resolve(rootPath));
30
+ }
31
+ catch {
32
+ return invalidTrust("the project root no longer resolves");
33
+ }
34
+ let trustDirectory;
35
+ try {
36
+ trustDirectory = await resolveTrustDirectory(canonicalRoot, false, options.trustDirectory);
37
+ }
38
+ catch (error) {
39
+ if (isNodeError(error, "ENOENT")) {
40
+ return missingTrust();
41
+ }
42
+ return invalidTrust(error instanceof Error ? error.message : String(error));
43
+ }
44
+ let rawReceipt;
45
+ try {
46
+ rawReceipt = await readPrivateFile(trustDirectory, receiptFileName(canonicalRoot));
47
+ }
48
+ catch (error) {
49
+ if (isNodeError(error, "ENOENT"))
50
+ return missingTrust();
51
+ return invalidTrust(error instanceof Error ? error.message : String(error));
52
+ }
53
+ let receipt;
54
+ try {
55
+ receipt = JSON.parse(rawReceipt);
56
+ }
57
+ catch {
58
+ return invalidTrust("the external provider-sync receipt is not valid JSON");
59
+ }
60
+ if (!isTrustReceipt(receipt)) {
61
+ return invalidTrust("the external provider-sync receipt has an invalid shape");
62
+ }
63
+ const spendSha256 = sha256(exactSpendContents);
64
+ const expectedStateDigest = connectedStateDigest(canonicalRoot, spendSha256, receipt.sourceRegistrySha256);
65
+ if (receipt.canonicalRoot !== canonicalRoot ||
66
+ receipt.spendSha256 !== spendSha256 ||
67
+ receipt.stateDigest !== expectedStateDigest) {
68
+ return {
69
+ trusted: false,
70
+ reason: "mismatch",
71
+ message: connectedTrustFailureMessage("the exact spend.json contents do not match the last successful provider sync")
72
+ };
73
+ }
74
+ return {
75
+ trusted: true,
76
+ trustedAt: receipt.trustedAt,
77
+ spendSha256,
78
+ ...(receipt.sourceRegistrySha256
79
+ ? { sourceRegistrySha256: receipt.sourceRegistrySha256 }
80
+ : {})
81
+ };
82
+ }
83
+ /**
84
+ * Provider/source truth axes are trusted only when the same external receipt
85
+ * binds both the connected spend state and the exact persisted sources.json.
86
+ * A repository clone or edit can therefore register a boundary, but cannot
87
+ * self-assert live validation or verified financial evidence.
88
+ */
89
+ export async function verifyConnectedSourceRegistryTrustReceipt(rootPath, exactSpendContents, exactSourceRegistryContents, options = {}) {
90
+ const spendTrust = await verifyConnectedSpendTrustReceipt(rootPath, exactSpendContents, options);
91
+ if (!spendTrust.trusted)
92
+ return spendTrust;
93
+ if (!spendTrust.sourceRegistrySha256) {
94
+ return {
95
+ trusted: false,
96
+ reason: "missing",
97
+ message: connectedTrustFailureMessage("its external provider-sync receipt does not bind the persisted source registry")
98
+ };
99
+ }
100
+ if (spendTrust.sourceRegistrySha256 !== sha256(exactSourceRegistryContents)) {
101
+ return {
102
+ trusted: false,
103
+ reason: "mismatch",
104
+ message: connectedTrustFailureMessage("the exact sources.json contents do not match the last successful provider sync")
105
+ };
106
+ }
107
+ return spendTrust;
108
+ }
109
+ /** Remove stale trust whenever connected state is reset or replaced locally. */
110
+ export async function invalidateConnectedSpendTrustReceipt(rootPath, options = {}) {
111
+ const canonicalRoot = await realpath(resolve(rootPath));
112
+ let trustDirectory;
113
+ try {
114
+ trustDirectory = await resolveTrustDirectory(canonicalRoot, false, options.trustDirectory);
115
+ }
116
+ catch (error) {
117
+ if (isNodeError(error, "ENOENT"))
118
+ return;
119
+ throw error;
120
+ }
121
+ const receiptPath = join(trustDirectory, receiptFileName(canonicalRoot));
122
+ const receiptInfo = await lstat(receiptPath).catch((error) => {
123
+ if (isNodeError(error, "ENOENT"))
124
+ return undefined;
125
+ throw error;
126
+ });
127
+ if (!receiptInfo)
128
+ return;
129
+ // unlink removes a malicious link itself rather than touching its target.
130
+ if (!receiptInfo.isFile() && !receiptInfo.isSymbolicLink()) {
131
+ throw new Error(`Refusing to remove non-file provider trust receipt: ${receiptPath}`);
132
+ }
133
+ await unlink(receiptPath);
134
+ }
135
+ export function connectedTrustFailureMessage(reason) {
136
+ return [
137
+ `Connected provider state is not trusted on this machine because ${reason}.`,
138
+ "Re-run the provider sync with the original environment credential reference before using connected totals or Apply actions."
139
+ ].join(" ");
140
+ }
141
+ async function resolveTrustDirectory(canonicalRoot, create, override) {
142
+ const configured = override?.trim() || process.env[trustDirectoryEnvironmentVariable]?.trim();
143
+ const requested = resolve(configured && configured.length > 0
144
+ ? configured
145
+ : join(homedir(), ".aibill", "state-receipts"));
146
+ let info = await lstat(requested).catch((error) => {
147
+ if (isNodeError(error, "ENOENT"))
148
+ return undefined;
149
+ throw error;
150
+ });
151
+ if (!info && create) {
152
+ await mkdir(requested, { recursive: true, mode: 0o700 });
153
+ info = await lstat(requested);
154
+ }
155
+ if (!info) {
156
+ const error = new Error(`Provider trust directory does not exist: ${requested}`);
157
+ error.code = "ENOENT";
158
+ throw error;
159
+ }
160
+ if (info.isSymbolicLink() || !info.isDirectory()) {
161
+ throw new Error(`Refusing provider trust directory ${requested}: it is not a real directory.`);
162
+ }
163
+ const canonicalTrustDirectory = await realpath(requested);
164
+ if (isSameOrDescendant(canonicalTrustDirectory, canonicalRoot)) {
165
+ throw new Error(`Refusing provider trust directory ${canonicalTrustDirectory}: it must stay outside the approved repository.`);
166
+ }
167
+ if (create)
168
+ await chmod(canonicalTrustDirectory, 0o700);
169
+ return canonicalTrustDirectory;
170
+ }
171
+ async function readPrivateFile(directory, fileName) {
172
+ const filePath = directChild(directory, fileName);
173
+ const info = await lstat(filePath);
174
+ if (info.isSymbolicLink() || !info.isFile()) {
175
+ throw new Error(`Refusing provider trust receipt ${filePath}: it is not a regular file.`);
176
+ }
177
+ let handle;
178
+ try {
179
+ handle = await open(filePath, constants.O_RDONLY | noFollowFlag());
180
+ const openedInfo = await handle.stat();
181
+ if (!openedInfo.isFile()) {
182
+ throw new Error(`Refusing provider trust receipt ${filePath}: it is not a regular file.`);
183
+ }
184
+ return await handle.readFile("utf8");
185
+ }
186
+ finally {
187
+ await handle?.close();
188
+ }
189
+ }
190
+ async function writePrivateFile(directory, fileName, contents) {
191
+ const filePath = directChild(directory, fileName);
192
+ const existing = await lstat(filePath).catch((error) => {
193
+ if (isNodeError(error, "ENOENT"))
194
+ return undefined;
195
+ throw error;
196
+ });
197
+ if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
198
+ throw new Error(`Refusing provider trust receipt ${filePath}: it is not a regular file.`);
199
+ }
200
+ const temporaryPath = join(directory, `.${fileName}.${process.pid}.${randomUUID()}.tmp`);
201
+ let handle;
202
+ try {
203
+ handle = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
204
+ await handle.writeFile(contents, "utf8");
205
+ await handle.sync();
206
+ await handle.close();
207
+ handle = undefined;
208
+ await rename(temporaryPath, filePath);
209
+ await chmod(filePath, 0o600);
210
+ }
211
+ catch (error) {
212
+ await handle?.close().catch(() => undefined);
213
+ await unlink(temporaryPath).catch(() => undefined);
214
+ throw error;
215
+ }
216
+ }
217
+ function directChild(directory, fileName) {
218
+ if (!fileName || basename(fileName) !== fileName || fileName === "." || fileName === "..") {
219
+ throw new Error("Provider trust receipt filenames must be one direct child name.");
220
+ }
221
+ return join(directory, fileName);
222
+ }
223
+ function receiptFileName(canonicalRoot) {
224
+ return `${sha256(canonicalRoot)}.json`;
225
+ }
226
+ function connectedStateDigest(canonicalRoot, spendSha256, sourceRegistrySha256) {
227
+ return sourceRegistrySha256
228
+ ? sha256(`aibill-connected-state-v1\0${canonicalRoot}\0${spendSha256}\0${sourceRegistrySha256}`)
229
+ : sha256(`aibill-connected-state-v1\0${canonicalRoot}\0${spendSha256}`);
230
+ }
231
+ function sha256(value) {
232
+ return createHash("sha256").update(value, "utf8").digest("hex");
233
+ }
234
+ function isTrustReceipt(value) {
235
+ if (!isRecord(value))
236
+ return false;
237
+ return value.version === 1 &&
238
+ value.mode === "connected_provider" &&
239
+ typeof value.canonicalRoot === "string" &&
240
+ typeof value.trustedAt === "string" &&
241
+ Number.isFinite(Date.parse(value.trustedAt)) &&
242
+ isSha256(value.spendSha256) &&
243
+ (value.sourceRegistrySha256 === undefined || isSha256(value.sourceRegistrySha256)) &&
244
+ isSha256(value.stateDigest);
245
+ }
246
+ function isSha256(value) {
247
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
248
+ }
249
+ function isRecord(value) {
250
+ return typeof value === "object" && value !== null && !Array.isArray(value);
251
+ }
252
+ function isSameOrDescendant(candidate, parent) {
253
+ const pathFromParent = relative(parent, candidate);
254
+ return pathFromParent === "" ||
255
+ (pathFromParent !== ".." && !pathFromParent.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(pathFromParent));
256
+ }
257
+ function noFollowFlag() {
258
+ return typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
259
+ }
260
+ function missingTrust() {
261
+ return {
262
+ trusted: false,
263
+ reason: "missing",
264
+ message: connectedTrustFailureMessage("its external provider-sync receipt is missing")
265
+ };
266
+ }
267
+ function invalidTrust(reason) {
268
+ return {
269
+ trusted: false,
270
+ reason: "invalid",
271
+ message: connectedTrustFailureMessage(`its external provider-sync receipt is invalid (${reason})`)
272
+ };
273
+ }
274
+ function isNodeError(error, code) {
275
+ return error instanceof Error && error.code === code;
276
+ }
277
+ //# sourceMappingURL=stateTrust.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,7 +1,7 @@
1
1
  id,timestamp,source_id,source_name,provider,source_confidence,observed_from,model,input_tokens,output_tokens,amount_usd,cost_confidence,client_id,project_id,agent_id,user_id,workspace_id,api_key_id,provider_cost_type,operation,usage_granularity,stable_input_fingerprint,batch_eligible,downgrade_safe
2
2
  oai-001,2026-05-17T10:00:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5,60000,7000,4.80,estimated,client-acme,project-support,agent-triage,user-ops-lead,workspace-acme,key-support,sample_call,ticket_triage,call,,false,true
3
3
  oai-002,2026-05-17T13:30:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5-mini,32000,5000,2.70,estimated,client-acme,project-support,agent-triage,user-support-rep,workspace-acme,key-support,sample_call,ticket_triage,call,,false,true
4
- oai-003,2026-05-18T09:10:00.000Z,openai-sample,OpenAI sample export,openai,verified,sample_csv,gpt-5.5,70000,8500,6.10,verified,client-beta,project-research,agent-analyst,user-research-lead,workspace-beta,key-research,sample_call,research_summary,call,research-summary-v1,true,true
5
- oai-004,2026-05-18T15:45:00.000Z,openai-sample,OpenAI sample export,openai,verified,sample_csv,gpt-5.5-mini,24000,4200,2.00,verified,client-acme,project-support,agent-triage,user-support-rep,workspace-acme,key-support,sample_call,ticket_triage,call,,false,true
4
+ oai-003,2026-05-18T09:10:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5,70000,8500,6.10,estimated,client-beta,project-research,agent-analyst,user-research-lead,workspace-beta,key-research,sample_call,research_summary,call,research-summary-v1,true,true
5
+ oai-004,2026-05-18T15:45:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5-mini,24000,4200,2.00,estimated,client-acme,project-support,agent-triage,user-support-rep,workspace-acme,key-support,sample_call,ticket_triage,call,,false,true
6
6
  oai-005,2026-05-19T11:15:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5,180000,14000,22.40,estimated,client-beta,project-research,agent-analyst,user-research-lead,workspace-beta,key-research,sample_call,research_summary,call,research-summary-v1,true,true
7
7
  oai-006,2026-05-19T14:40:00.000Z,openai-sample,OpenAI sample export,openai,estimated,sample_csv,gpt-5.5,160000,12000,18.60,estimated,client-beta,project-research,agent-analyst,user-research-lead,workspace-beta,key-research,sample_call,research_summary,call,research-summary-v1,true,true