@gmickel/gno 1.28.0 → 1.29.1

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.
Files changed (143) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +33 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +1 -1
  10. package/spec/cli.md +41 -1
  11. package/spec/db/schema.sql +19 -0
  12. package/spec/mcp.md +66 -0
  13. package/spec/output-schemas/ask.schema.json +31 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  22. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  23. package/spec/output-schemas/record-import.schema.json +30 -0
  24. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  25. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  26. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  27. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  28. package/spec/output-schemas/search-results.schema.json +30 -0
  29. package/src/app/context-runtime-contract.ts +16 -7
  30. package/src/cli/commands/ask.ts +7 -0
  31. package/src/cli/commands/collection/index.ts +5 -0
  32. package/src/cli/commands/collection/policy.ts +126 -0
  33. package/src/cli/commands/context-build.ts +5 -0
  34. package/src/cli/commands/daemon.ts +45 -3
  35. package/src/cli/commands/egress-audit.ts +68 -0
  36. package/src/cli/commands/embed.ts +2 -0
  37. package/src/cli/commands/query.ts +12 -0
  38. package/src/cli/commands/replay.ts +13 -3
  39. package/src/cli/commands/vsearch.ts +3 -1
  40. package/src/cli/program.ts +123 -0
  41. package/src/config/index.ts +9 -0
  42. package/src/config/types.ts +50 -0
  43. package/src/core/browser-clip.ts +16 -1
  44. package/src/core/collection-egress-policy-projection.ts +28 -0
  45. package/src/core/collection-egress-policy-service.ts +443 -0
  46. package/src/core/collection-egress-policy-validation.ts +242 -0
  47. package/src/core/config-mutation.ts +31 -19
  48. package/src/core/context-capsule-schema.ts +111 -1
  49. package/src/core/context-capsule-validation.ts +4 -4
  50. package/src/core/context-capsule-verification.ts +5 -1
  51. package/src/core/context-capsule.ts +177 -132
  52. package/src/core/context-evidence.ts +86 -14
  53. package/src/core/destination-classifier.ts +402 -0
  54. package/src/core/egress-audit.ts +239 -0
  55. package/src/core/egress-authorization.ts +50 -0
  56. package/src/core/egress-enforcement.ts +264 -0
  57. package/src/core/egress-policy.ts +440 -0
  58. package/src/core/egress-provenance.ts +336 -0
  59. package/src/core/job-manager.ts +37 -0
  60. package/src/core/network-boundary-inventory.ts +261 -0
  61. package/src/core/retrieval-qrels.ts +6 -0
  62. package/src/core/retrieval-replay.ts +18 -4
  63. package/src/core/retrieval-trace-export.ts +23 -4
  64. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  65. package/src/core/retrieval-trace-management-types.ts +3 -0
  66. package/src/core/retrieval-trace-management.ts +9 -1
  67. package/src/core/retrieval-trace-session.ts +94 -0
  68. package/src/core/retrieval-trace.ts +22 -0
  69. package/src/ingestion/record-adapter.ts +12 -1
  70. package/src/ingestion/record-container.ts +38 -22
  71. package/src/ingestion/sync.ts +15 -0
  72. package/src/ingestion/types.ts +2 -0
  73. package/src/llm/errors.ts +10 -0
  74. package/src/llm/http-inference.ts +175 -0
  75. package/src/llm/http-policy.ts +537 -0
  76. package/src/llm/httpEmbedding.ts +47 -28
  77. package/src/llm/httpGeneration.ts +31 -18
  78. package/src/llm/httpRerank.ts +30 -18
  79. package/src/llm/index.ts +1 -0
  80. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  81. package/src/llm/pinned-http-connection.ts +177 -0
  82. package/src/mcp/context.ts +53 -1
  83. package/src/mcp/http-egress.ts +220 -0
  84. package/src/mcp/http-security.ts +32 -21
  85. package/src/mcp/http-session.ts +13 -2
  86. package/src/mcp/http-transport.ts +96 -6
  87. package/src/mcp/sync-egress.ts +24 -0
  88. package/src/mcp/tools/add-collection.ts +4 -0
  89. package/src/mcp/tools/ask.ts +1 -1
  90. package/src/mcp/tools/context.ts +9 -2
  91. package/src/mcp/tools/egress.ts +247 -0
  92. package/src/mcp/tools/embed.ts +1 -0
  93. package/src/mcp/tools/index-cmd.ts +7 -0
  94. package/src/mcp/tools/index.ts +98 -0
  95. package/src/mcp/tools/query.ts +10 -0
  96. package/src/mcp/tools/remove-collection.ts +4 -0
  97. package/src/mcp/tools/sync.ts +6 -0
  98. package/src/mcp/tools/trace.ts +7 -1
  99. package/src/mcp/tools/vsearch.ts +1 -0
  100. package/src/pipeline/egress-lineage.ts +124 -0
  101. package/src/pipeline/hybrid.ts +14 -0
  102. package/src/pipeline/search.ts +12 -0
  103. package/src/pipeline/types.ts +3 -0
  104. package/src/pipeline/vsearch.ts +41 -20
  105. package/src/publish/artifact.ts +31 -3
  106. package/src/publish/export-service.ts +25 -0
  107. package/src/sdk/client.ts +119 -2
  108. package/src/sdk/embed.ts +1 -0
  109. package/src/sdk/types.ts +33 -0
  110. package/src/serve/clipper-capture.ts +5 -1
  111. package/src/serve/closed-json.ts +61 -0
  112. package/src/serve/config-sync.ts +4 -1
  113. package/src/serve/context.ts +1 -0
  114. package/src/serve/doc-events.ts +110 -39
  115. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  116. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  117. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  118. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  119. package/src/serve/public/globals.built.css +1 -1
  120. package/src/serve/public/pages/Collections.tsx +3 -0
  121. package/src/serve/resident-request.ts +80 -3
  122. package/src/serve/resident-runtime.ts +63 -1
  123. package/src/serve/routes/api.ts +174 -0
  124. package/src/serve/routes/clipper.ts +64 -1
  125. package/src/serve/routes/mcp.ts +6 -0
  126. package/src/serve/routes/traces.ts +27 -4
  127. package/src/serve/server.ts +118 -7
  128. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  129. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  130. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  131. package/src/store/migrations/index.ts +6 -0
  132. package/src/store/retrieval-trace-codec.ts +15 -0
  133. package/src/store/sqlite/adapter.ts +170 -7
  134. package/src/store/sqlite/change-journal-store.ts +41 -2
  135. package/src/store/sqlite/egress-audit-store.ts +485 -0
  136. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  137. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  138. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  139. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  140. package/src/store/types.ts +126 -1
  141. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
  142. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
  143. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -0,0 +1,485 @@
1
+ /** Bounded, content-free SQLite egress decision receipts. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type {
6
+ EgressAuditCursor,
7
+ EgressAuditDeleteResult,
8
+ EgressAuditPage,
9
+ EgressAuditPurgeResult,
10
+ EgressAuditReceiptInput,
11
+ EgressAuditReceiptRow,
12
+ EgressAuditRetentionPolicy,
13
+ EgressAuditRetentionResult,
14
+ EgressAuditStatusResult,
15
+ RetrievalTraceAppendResult,
16
+ StoreResult,
17
+ } from "../types";
18
+
19
+ import {
20
+ EGRESS_ACTIONS,
21
+ EGRESS_CONTENT_CLASSES,
22
+ EGRESS_DESTINATION_ZONES,
23
+ EGRESS_REASON_CODES,
24
+ } from "../../core/egress-policy";
25
+ import { err, ok } from "../types";
26
+
27
+ const DAY_MS = 86_400_000;
28
+ const MAX_LIMIT = 1_000;
29
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
30
+ const UTF8 = new TextEncoder();
31
+
32
+ interface DbAuditRow {
33
+ audit_id: string;
34
+ decision: EgressAuditReceiptRow["decision"];
35
+ action: EgressAuditReceiptRow["action"];
36
+ destination_zone: EgressAuditReceiptRow["destinationZone"];
37
+ content_class: EgressAuditReceiptRow["contentClass"];
38
+ effective_policy: EgressAuditReceiptRow["effectivePolicy"];
39
+ reason_code: EgressAuditReceiptRow["reasonCode"];
40
+ lineage_digest: string;
41
+ created_at_ms: number;
42
+ expires_at_ms: number;
43
+ byte_size: number;
44
+ }
45
+
46
+ const mapRow = (row: DbAuditRow): EgressAuditReceiptRow => ({
47
+ auditId: row.audit_id,
48
+ decision: row.decision,
49
+ action: row.action,
50
+ destinationZone: row.destination_zone,
51
+ contentClass: row.content_class,
52
+ effectivePolicy: row.effective_policy,
53
+ reasonCode: row.reason_code,
54
+ lineageDigest: row.lineage_digest,
55
+ createdAtMs: row.created_at_ms,
56
+ expiresAtMs: row.expires_at_ms,
57
+ byteSize: row.byte_size,
58
+ });
59
+
60
+ const receiptBytes = (receipt: EgressAuditReceiptInput): number =>
61
+ [
62
+ receipt.auditId,
63
+ receipt.decision,
64
+ receipt.action,
65
+ receipt.destinationZone,
66
+ receipt.contentClass,
67
+ receipt.effectivePolicy,
68
+ receipt.reasonCode,
69
+ receipt.lineageDigest,
70
+ ].reduce((total, value) => total + UTF8.encode(value).byteLength, 16);
71
+
72
+ const validateReceipt = (receipt: EgressAuditReceiptInput): void => {
73
+ if (
74
+ receipt.auditId.length < 1 ||
75
+ receipt.auditId.length > 128 ||
76
+ !["allow", "deny"].includes(receipt.decision) ||
77
+ !EGRESS_ACTIONS.includes(receipt.action) ||
78
+ !EGRESS_DESTINATION_ZONES.includes(receipt.destinationZone) ||
79
+ !EGRESS_CONTENT_CLASSES.includes(receipt.contentClass) ||
80
+ !["local_only", "lan", "remote"].includes(receipt.effectivePolicy) ||
81
+ !EGRESS_REASON_CODES.includes(receipt.reasonCode) ||
82
+ !SHA256_PATTERN.test(receipt.lineageDigest) ||
83
+ !Number.isSafeInteger(receipt.createdAtMs) ||
84
+ receipt.createdAtMs < 0 ||
85
+ !Number.isSafeInteger(receipt.expiresAtMs) ||
86
+ receipt.expiresAtMs <= receipt.createdAtMs
87
+ ) {
88
+ throw new RangeError("Invalid egress audit receipt");
89
+ }
90
+ };
91
+
92
+ const readSecureDelete = (db: Database): number =>
93
+ db.query<{ secure_delete: number }, []>("PRAGMA secure_delete").get()
94
+ ?.secure_delete ?? 0;
95
+
96
+ const restoreSecureDelete = (db: Database, value: number): void => {
97
+ const mode = value === 0 ? "OFF" : value === 2 ? "FAST" : "ON";
98
+ db.exec(`PRAGMA secure_delete = ${mode}`);
99
+ };
100
+
101
+ export const appendEgressAuditReceipt = (
102
+ db: Database,
103
+ receipt: EgressAuditReceiptInput
104
+ ): StoreResult<RetrievalTraceAppendResult> => {
105
+ try {
106
+ validateReceipt(receipt);
107
+ const byteSize = receiptBytes(receipt);
108
+ const inserted = db.run(
109
+ `INSERT OR IGNORE INTO egress_audit_receipts (
110
+ audit_id, decision, action, destination_zone, content_class,
111
+ effective_policy, reason_code, lineage_digest, created_at_ms,
112
+ expires_at_ms, byte_size
113
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
114
+ [
115
+ receipt.auditId,
116
+ receipt.decision,
117
+ receipt.action,
118
+ receipt.destinationZone,
119
+ receipt.contentClass,
120
+ receipt.effectivePolicy,
121
+ receipt.reasonCode,
122
+ receipt.lineageDigest,
123
+ receipt.createdAtMs,
124
+ receipt.expiresAtMs,
125
+ byteSize,
126
+ ]
127
+ );
128
+ if (inserted.changes > 0) return ok("inserted");
129
+ const stored = db
130
+ .query<DbAuditRow, [string]>(
131
+ "SELECT * FROM egress_audit_receipts WHERE audit_id = ?"
132
+ )
133
+ .get(receipt.auditId);
134
+ return stored &&
135
+ JSON.stringify(mapRow(stored)) ===
136
+ JSON.stringify({ ...receipt, byteSize })
137
+ ? ok("duplicate")
138
+ : err(
139
+ "CONSTRAINT_VIOLATION",
140
+ "Egress audit ID already exists with different content"
141
+ );
142
+ } catch (cause) {
143
+ return err(
144
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
145
+ cause instanceof Error ? cause.message : "Failed to append egress audit",
146
+ cause
147
+ );
148
+ }
149
+ };
150
+
151
+ export const appendEgressAuditReceiptWithRetention = (
152
+ db: Database,
153
+ receipt: EgressAuditReceiptInput,
154
+ policy: EgressAuditRetentionPolicy,
155
+ nowMs: number
156
+ ): StoreResult<RetrievalTraceAppendResult> => {
157
+ const prior = readSecureDelete(db);
158
+ try {
159
+ validateReceipt(receipt);
160
+ validateRetention(policy, nowMs);
161
+ db.exec("PRAGMA secure_delete = ON");
162
+ const transaction = db.transaction((): RetrievalTraceAppendResult => {
163
+ const appended = appendEgressAuditReceipt(db, receipt);
164
+ if (!appended.ok) {
165
+ throw new Error(appended.error.message, {
166
+ cause: appended.error.cause,
167
+ });
168
+ }
169
+ const rows = db
170
+ .query<
171
+ { audit_id: string; created_at_ms: number; byte_size: number },
172
+ []
173
+ >(
174
+ `SELECT audit_id, created_at_ms, byte_size
175
+ FROM egress_audit_receipts
176
+ ORDER BY created_at_ms ASC, audit_id ASC`
177
+ )
178
+ .all();
179
+ let remainingReceipts = rows.length;
180
+ let remainingBytes = rows.reduce(
181
+ (total, row) => total + row.byte_size,
182
+ 0
183
+ );
184
+ const ageBoundary = nowMs - policy.maxAgeDays * DAY_MS;
185
+ for (const row of rows) {
186
+ if (
187
+ row.created_at_ms > ageBoundary &&
188
+ remainingReceipts <= policy.maxReceipts &&
189
+ remainingBytes <= policy.maxBytes
190
+ ) {
191
+ break;
192
+ }
193
+ db.run("DELETE FROM egress_audit_receipts WHERE audit_id = ?", [
194
+ row.audit_id,
195
+ ]);
196
+ remainingReceipts -= 1;
197
+ remainingBytes -= row.byte_size;
198
+ }
199
+ return appended.value;
200
+ });
201
+ return ok(transaction());
202
+ } catch (cause) {
203
+ return err(
204
+ "QUERY_FAILED",
205
+ cause instanceof Error
206
+ ? cause.message
207
+ : "Failed to atomically append egress audit",
208
+ cause
209
+ );
210
+ } finally {
211
+ restoreSecureDelete(db, prior);
212
+ }
213
+ };
214
+
215
+ export const listEgressAuditReceipts = (
216
+ db: Database,
217
+ limit: number,
218
+ cursor?: EgressAuditCursor
219
+ ): StoreResult<EgressAuditPage> => {
220
+ try {
221
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
222
+ throw new RangeError(`Egress audit limit must be from 1 to ${MAX_LIMIT}`);
223
+ }
224
+ if (
225
+ cursor &&
226
+ (!Number.isSafeInteger(cursor.createdAtMs) ||
227
+ cursor.createdAtMs < 0 ||
228
+ cursor.auditId.length < 1 ||
229
+ cursor.auditId.length > 128)
230
+ ) {
231
+ throw new RangeError("Invalid egress audit cursor");
232
+ }
233
+ const rows = cursor
234
+ ? db
235
+ .query<DbAuditRow, [number, number, string, number]>(
236
+ `SELECT * FROM egress_audit_receipts
237
+ WHERE created_at_ms < ?
238
+ OR (created_at_ms = ? AND audit_id < ?)
239
+ ORDER BY created_at_ms DESC, audit_id DESC LIMIT ?`
240
+ )
241
+ .all(
242
+ cursor.createdAtMs,
243
+ cursor.createdAtMs,
244
+ cursor.auditId,
245
+ limit + 1
246
+ )
247
+ : db
248
+ .query<DbAuditRow, [number]>(
249
+ `SELECT * FROM egress_audit_receipts
250
+ ORDER BY created_at_ms DESC, audit_id DESC LIMIT ?`
251
+ )
252
+ .all(limit + 1);
253
+ const selected = rows.slice(0, limit).map(mapRow);
254
+ const last = selected.at(-1);
255
+ return ok({
256
+ receipts: selected,
257
+ nextCursor:
258
+ rows.length > limit && last
259
+ ? { createdAtMs: last.createdAtMs, auditId: last.auditId }
260
+ : null,
261
+ });
262
+ } catch (cause) {
263
+ return err(
264
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
265
+ cause instanceof Error ? cause.message : "Failed to list egress audits",
266
+ cause
267
+ );
268
+ }
269
+ };
270
+
271
+ export const getEgressAuditReceipt = (
272
+ db: Database,
273
+ auditId: string
274
+ ): StoreResult<EgressAuditReceiptRow | null> => {
275
+ try {
276
+ if (auditId.length < 1 || auditId.length > 128) {
277
+ throw new RangeError("Invalid egress audit ID");
278
+ }
279
+ const row = db
280
+ .query<DbAuditRow, [string]>(
281
+ "SELECT * FROM egress_audit_receipts WHERE audit_id = ?"
282
+ )
283
+ .get(auditId);
284
+ return ok(row ? mapRow(row) : null);
285
+ } catch (cause) {
286
+ return err(
287
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
288
+ cause instanceof Error ? cause.message : "Failed to get egress audit",
289
+ cause
290
+ );
291
+ }
292
+ };
293
+
294
+ const checkpointAfterDelete = (
295
+ db: Database,
296
+ deleted: number
297
+ ): Omit<EgressAuditPurgeResult, "deleted"> & { deleted: number } => {
298
+ try {
299
+ const checkpoint =
300
+ db
301
+ .query<{ busy: number; log: number; checkpointed: number }, []>(
302
+ "PRAGMA wal_checkpoint(TRUNCATE)"
303
+ )
304
+ .get() ?? undefined;
305
+ return {
306
+ deleted,
307
+ physicalCleanup:
308
+ checkpoint?.busy === 0 &&
309
+ (checkpoint.log === 0 || checkpoint.log === -1)
310
+ ? "completed"
311
+ : "wal_busy",
312
+ checkpointedFrames: checkpoint?.checkpointed ?? 0,
313
+ remainingWalFrames: checkpoint?.log ?? -1,
314
+ };
315
+ } catch {
316
+ return {
317
+ deleted,
318
+ physicalCleanup: "failed",
319
+ checkpointedFrames: 0,
320
+ remainingWalFrames: -1,
321
+ };
322
+ }
323
+ };
324
+
325
+ export const deleteEgressAuditReceipt = (
326
+ db: Database,
327
+ auditId: string
328
+ ): StoreResult<EgressAuditDeleteResult> => {
329
+ const prior = readSecureDelete(db);
330
+ try {
331
+ if (auditId.length < 1 || auditId.length > 128) {
332
+ throw new RangeError("Invalid egress audit ID");
333
+ }
334
+ db.exec("PRAGMA secure_delete = ON");
335
+ const deleted = db.run(
336
+ "DELETE FROM egress_audit_receipts WHERE audit_id = ?",
337
+ [auditId]
338
+ ).changes;
339
+ return ok({ auditId, ...checkpointAfterDelete(db, deleted) });
340
+ } catch (cause) {
341
+ return err(
342
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
343
+ cause instanceof Error ? cause.message : "Failed to delete egress audit",
344
+ cause
345
+ );
346
+ } finally {
347
+ restoreSecureDelete(db, prior);
348
+ }
349
+ };
350
+
351
+ export const getEgressAuditStatus = (
352
+ db: Database
353
+ ): StoreResult<EgressAuditStatusResult> => {
354
+ try {
355
+ const row = db
356
+ .query<
357
+ {
358
+ receipts: number;
359
+ bytes: number | null;
360
+ oldest_created_at_ms: number | null;
361
+ newest_created_at_ms: number | null;
362
+ },
363
+ []
364
+ >(
365
+ `SELECT COUNT(*) AS receipts,
366
+ SUM(byte_size) AS bytes,
367
+ MIN(created_at_ms) AS oldest_created_at_ms,
368
+ MAX(created_at_ms) AS newest_created_at_ms
369
+ FROM egress_audit_receipts`
370
+ )
371
+ .get();
372
+ return ok({
373
+ receipts: row?.receipts ?? 0,
374
+ bytes: row?.bytes ?? 0,
375
+ oldestCreatedAtMs: row?.oldest_created_at_ms ?? null,
376
+ newestCreatedAtMs: row?.newest_created_at_ms ?? null,
377
+ });
378
+ } catch (cause) {
379
+ return err(
380
+ "QUERY_FAILED",
381
+ cause instanceof Error ? cause.message : "Failed to inspect egress audit",
382
+ cause
383
+ );
384
+ }
385
+ };
386
+
387
+ const validateRetention = (
388
+ policy: EgressAuditRetentionPolicy,
389
+ nowMs: number
390
+ ): void => {
391
+ if (
392
+ !Number.isSafeInteger(nowMs) ||
393
+ nowMs < 0 ||
394
+ !Number.isSafeInteger(policy.maxAgeDays) ||
395
+ policy.maxAgeDays < 1 ||
396
+ policy.maxAgeDays > 3650 ||
397
+ !Number.isSafeInteger(policy.maxReceipts) ||
398
+ policy.maxReceipts < 1 ||
399
+ policy.maxReceipts > 1_000_000 ||
400
+ !Number.isSafeInteger(policy.maxBytes) ||
401
+ policy.maxBytes < 1 ||
402
+ policy.maxBytes > 1024 * 1024 * 1024
403
+ ) {
404
+ throw new RangeError("Invalid egress audit retention policy");
405
+ }
406
+ };
407
+
408
+ export const enforceEgressAuditRetention = (
409
+ db: Database,
410
+ policy: EgressAuditRetentionPolicy,
411
+ nowMs: number
412
+ ): StoreResult<EgressAuditRetentionResult> => {
413
+ const prior = readSecureDelete(db);
414
+ try {
415
+ validateRetention(policy, nowMs);
416
+ db.exec("PRAGMA secure_delete = ON");
417
+ const transaction = db.transaction((): EgressAuditRetentionResult => {
418
+ const rows = db
419
+ .query<
420
+ { audit_id: string; created_at_ms: number; byte_size: number },
421
+ []
422
+ >(
423
+ `SELECT audit_id, created_at_ms, byte_size
424
+ FROM egress_audit_receipts
425
+ ORDER BY created_at_ms ASC, audit_id ASC`
426
+ )
427
+ .all();
428
+ let remainingReceipts = rows.length;
429
+ let remainingBytes = rows.reduce(
430
+ (total, row) => total + row.byte_size,
431
+ 0
432
+ );
433
+ let deleted = 0;
434
+ const ageBoundary = nowMs - policy.maxAgeDays * DAY_MS;
435
+ for (const row of rows) {
436
+ if (
437
+ row.created_at_ms > ageBoundary &&
438
+ remainingReceipts <= policy.maxReceipts &&
439
+ remainingBytes <= policy.maxBytes
440
+ ) {
441
+ break;
442
+ }
443
+ db.run("DELETE FROM egress_audit_receipts WHERE audit_id = ?", [
444
+ row.audit_id,
445
+ ]);
446
+ deleted += 1;
447
+ remainingReceipts -= 1;
448
+ remainingBytes -= row.byte_size;
449
+ }
450
+ return { deleted, remainingReceipts, remainingBytes };
451
+ });
452
+ return ok(transaction());
453
+ } catch (cause) {
454
+ return err(
455
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
456
+ cause instanceof Error
457
+ ? cause.message
458
+ : "Failed to enforce egress audit retention",
459
+ cause
460
+ );
461
+ } finally {
462
+ restoreSecureDelete(db, prior);
463
+ }
464
+ };
465
+
466
+ export const purgeEgressAuditReceipts = (
467
+ db: Database
468
+ ): StoreResult<EgressAuditPurgeResult> => {
469
+ const prior = readSecureDelete(db);
470
+ try {
471
+ db.exec("PRAGMA secure_delete = ON");
472
+ const deleted = db.transaction(
473
+ () => db.run("DELETE FROM egress_audit_receipts").changes
474
+ )();
475
+ return ok(checkpointAfterDelete(db, deleted));
476
+ } catch (cause) {
477
+ return err(
478
+ "QUERY_FAILED",
479
+ cause instanceof Error ? cause.message : "Failed to purge egress audits",
480
+ cause
481
+ );
482
+ } finally {
483
+ restoreSecureDelete(db, prior);
484
+ }
485
+ };
@@ -11,6 +11,11 @@ import type {
11
11
  StoreResult,
12
12
  } from "../types";
13
13
 
14
+ import {
15
+ egressLineageSchema,
16
+ mergeEgressLineages,
17
+ } from "../../core/egress-provenance";
18
+ import { canonicalTraceJson, traceUtf8Bytes } from "../retrieval-trace-codec";
14
19
  import { ok } from "../types";
15
20
  import {
16
21
  type DbRetrievalTraceEventRow,
@@ -140,7 +145,11 @@ const normalizeManifest = (
140
145
  }
141
146
  const traceIds = [...new Set(input.traceIds)].sort();
142
147
  for (const traceId of traceIds) validateTraceId(traceId, "traceId");
143
- return { ...input, traceIds };
148
+ return {
149
+ ...input,
150
+ traceIds,
151
+ egressLineage: egressLineageSchema.parse(input.egressLineage),
152
+ };
144
153
  };
145
154
 
146
155
  const readManifest = (
@@ -154,10 +163,12 @@ const readManifest = (
154
163
  format: RetrievalTraceExportManifestRow["format"];
155
164
  artifact_hash: string;
156
165
  created_at_ms: number;
166
+ egress_lineage_json: string;
157
167
  },
158
168
  [string]
159
169
  >(
160
- `SELECT export_id, format, artifact_hash, created_at_ms
170
+ `SELECT export_id, format, artifact_hash, created_at_ms,
171
+ egress_lineage_json
161
172
  FROM retrieval_trace_exports WHERE export_id = ?`
162
173
  )
163
174
  .get(exportId);
@@ -174,6 +185,9 @@ const readManifest = (
174
185
  traceIds,
175
186
  format: row.format,
176
187
  artifactHash: row.artifact_hash,
188
+ egressLineage: egressLineageSchema.parse(
189
+ JSON.parse(row.egress_lineage_json)
190
+ ),
177
191
  createdAtMs: row.created_at_ms,
178
192
  };
179
193
  };
@@ -228,10 +242,12 @@ export const appendExportManifest = (
228
242
  "Export artifact already belongs to a different manifest"
229
243
  );
230
244
  }
245
+ const sourceLineages = [];
231
246
  for (const traceId of manifest.traceIds) {
232
247
  const trace = db
233
- .query<{ status: string }, [string]>(
234
- "SELECT status FROM retrieval_traces WHERE trace_id = ?"
248
+ .query<{ status: string; egress_lineage_json: string }, [string]>(
249
+ `SELECT status, egress_lineage_json
250
+ FROM retrieval_traces WHERE trace_id = ?`
235
251
  )
236
252
  .get(traceId);
237
253
  if (!trace) {
@@ -244,22 +260,47 @@ export const appendExportManifest = (
244
260
  "Open retrieval traces cannot be exported"
245
261
  );
246
262
  }
263
+ sourceLineages.push(
264
+ egressLineageSchema.parse(JSON.parse(trace.egress_lineage_json))
265
+ );
266
+ }
267
+ const aggregateLineage = mergeEgressLineages(sourceLineages);
268
+ const historicalMembership = aggregateLineage.sources
269
+ .map(({ collection }) => collection)
270
+ .join("\0");
271
+ const manifestMembership = manifest.egressLineage.sources
272
+ .map(({ collection }) => collection)
273
+ .join("\0");
274
+ if (historicalMembership !== manifestMembership) {
275
+ throw new RetrievalTraceConflictError(
276
+ "Export manifest collection lineage does not match trace membership"
277
+ );
247
278
  }
279
+ const egressLineageJson = canonicalTraceJson(manifest.egressLineage);
280
+ const egressLineageBytes = traceUtf8Bytes(egressLineageJson);
248
281
  const inserted = db.run(
249
282
  `INSERT OR IGNORE INTO retrieval_trace_exports
250
- (export_id, format, artifact_hash, created_at_ms) VALUES (?, ?, ?, ?)`,
283
+ (export_id, format, artifact_hash, created_at_ms,
284
+ effective_egress_policy, egress_lineage_digest,
285
+ egress_lineage_json, egress_lineage_bytes)
286
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
251
287
  [
252
288
  manifest.exportId,
253
289
  manifest.format,
254
290
  manifest.artifactHash,
255
291
  manifest.createdAtMs,
292
+ manifest.egressLineage.effectivePolicy,
293
+ manifest.egressLineage.digest,
294
+ egressLineageJson,
295
+ egressLineageBytes,
256
296
  ]
257
297
  );
258
298
  const existing = readManifest(db, manifest.exportId);
259
299
  if (
260
300
  !existing ||
261
301
  existing.format !== manifest.format ||
262
- existing.artifactHash !== manifest.artifactHash
302
+ existing.artifactHash !== manifest.artifactHash ||
303
+ existing.egressLineage.digest !== manifest.egressLineage.digest
263
304
  ) {
264
305
  throw new RetrievalTraceConflictError(
265
306
  "Export ID already exists with different content"
@@ -47,6 +47,7 @@ const storageRows = (db: Database): StorageRow[] =>
47
47
  + length(CAST(COALESCE(t.goal_digest, '') AS BLOB))
48
48
  + length(CAST(t.goal_shape_json AS BLOB))
49
49
  + length(CAST(t.filters_json AS BLOB))
50
+ + t.egress_lineage_bytes
50
51
  + length(CAST(t.pipeline_fingerprint AS BLOB))
51
52
  + length(CAST(t.model_fingerprint AS BLOB))
52
53
  + length(CAST(t.config_fingerprint AS BLOB))
@@ -101,6 +102,7 @@ const storageRows = (db: Database): StorageRow[] =>
101
102
  + length(CAST(et.trace_id AS BLOB))
102
103
  + length(CAST(x.format AS BLOB))
103
104
  + length(CAST(x.artifact_hash AS BLOB))
105
+ + x.egress_lineage_bytes
104
106
  + 8
105
107
  )
106
108
  FROM retrieval_trace_export_traces et
@@ -16,6 +16,7 @@ import type {
16
16
  StoreResult,
17
17
  } from "../types";
18
18
 
19
+ import { egressLineageSchema } from "../../core/egress-provenance";
19
20
  import { traceJsonObjectSchema } from "../retrieval-trace-codec";
20
21
  import { err } from "../types";
21
22
 
@@ -31,6 +32,10 @@ export interface DbRetrievalTraceRow {
31
32
  goal_digest: string | null;
32
33
  goal_shape_json: string;
33
34
  filters_json: string;
35
+ effective_egress_policy: RetrievalTraceRow["egressLineage"]["effectivePolicy"];
36
+ egress_lineage_digest: string;
37
+ egress_lineage_json: string;
38
+ egress_lineage_bytes: number;
34
39
  pipeline_fingerprint: string;
35
40
  model_fingerprint: string;
36
41
  config_fingerprint: string;
@@ -41,6 +46,7 @@ export interface DbRetrievalTraceRow {
41
46
  expires_at_ms: number;
42
47
  byte_size: number;
43
48
  creation_digest: string;
49
+ creation_digest_version: 0 | 1;
44
50
  }
45
51
 
46
52
  export interface DbRetrievalTraceRunRow {
@@ -105,6 +111,7 @@ export const mapTraceRow = (row: DbRetrievalTraceRow): RetrievalTraceRow => ({
105
111
  goalDigest: row.goal_digest,
106
112
  goalShape: JSON.parse(row.goal_shape_json) as RetrievalTraceRow["goalShape"],
107
113
  filters: parseJsonObject(row.filters_json),
114
+ egressLineage: egressLineageSchema.parse(JSON.parse(row.egress_lineage_json)),
108
115
  fingerprints: {
109
116
  pipeline: row.pipeline_fingerprint,
110
117
  model: row.model_fingerprint,
@@ -115,7 +122,7 @@ export const mapTraceRow = (row: DbRetrievalTraceRow): RetrievalTraceRow => ({
115
122
  createdAtMs: row.created_at_ms,
116
123
  updatedAtMs: row.updated_at_ms,
117
124
  expiresAtMs: row.expires_at_ms,
118
- byteSize: row.byte_size,
125
+ byteSize: row.byte_size + row.egress_lineage_bytes,
119
126
  creationDigest: row.creation_digest,
120
127
  });
121
128