@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.
- package/README.md +46 -20
- package/assets/skill/SKILL.md +33 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +6 -6
- package/package.json +1 -1
- package/spec/cli.md +41 -1
- package/spec/db/schema.sql +19 -0
- package/spec/mcp.md +66 -0
- package/spec/output-schemas/ask.schema.json +31 -1
- package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
- package/spec/output-schemas/collection-egress-check.schema.json +91 -0
- package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
- package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
- package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
- package/spec/output-schemas/egress-audit-management.schema.json +88 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- package/spec/output-schemas/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +30 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
- package/spec/output-schemas/search-results.schema.json +30 -0
- package/src/app/context-runtime-contract.ts +16 -7
- package/src/cli/commands/ask.ts +7 -0
- package/src/cli/commands/collection/index.ts +5 -0
- package/src/cli/commands/collection/policy.ts +126 -0
- package/src/cli/commands/context-build.ts +5 -0
- package/src/cli/commands/daemon.ts +45 -3
- package/src/cli/commands/egress-audit.ts +68 -0
- package/src/cli/commands/embed.ts +2 -0
- package/src/cli/commands/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +123 -0
- package/src/config/index.ts +9 -0
- package/src/config/types.ts +50 -0
- package/src/core/browser-clip.ts +16 -1
- package/src/core/collection-egress-policy-projection.ts +28 -0
- package/src/core/collection-egress-policy-service.ts +443 -0
- package/src/core/collection-egress-policy-validation.ts +242 -0
- package/src/core/config-mutation.ts +31 -19
- package/src/core/context-capsule-schema.ts +111 -1
- package/src/core/context-capsule-validation.ts +4 -4
- package/src/core/context-capsule-verification.ts +5 -1
- package/src/core/context-capsule.ts +177 -132
- package/src/core/context-evidence.ts +86 -14
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/egress-audit.ts +239 -0
- package/src/core/egress-authorization.ts +50 -0
- package/src/core/egress-enforcement.ts +264 -0
- package/src/core/egress-policy.ts +440 -0
- package/src/core/egress-provenance.ts +336 -0
- package/src/core/job-manager.ts +37 -0
- package/src/core/network-boundary-inventory.ts +261 -0
- package/src/core/retrieval-qrels.ts +6 -0
- package/src/core/retrieval-replay.ts +18 -4
- package/src/core/retrieval-trace-export.ts +23 -4
- package/src/core/retrieval-trace-management-helpers.ts +1 -0
- package/src/core/retrieval-trace-management-types.ts +3 -0
- package/src/core/retrieval-trace-management.ts +9 -1
- package/src/core/retrieval-trace-session.ts +94 -0
- package/src/core/retrieval-trace.ts +22 -0
- package/src/ingestion/record-adapter.ts +12 -1
- package/src/ingestion/record-container.ts +38 -22
- package/src/ingestion/sync.ts +15 -0
- package/src/ingestion/types.ts +2 -0
- package/src/llm/errors.ts +10 -0
- package/src/llm/http-inference.ts +175 -0
- package/src/llm/http-policy.ts +537 -0
- package/src/llm/httpEmbedding.ts +47 -28
- package/src/llm/httpGeneration.ts +31 -18
- package/src/llm/httpRerank.ts +30 -18
- package/src/llm/index.ts +1 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
- package/src/llm/pinned-http-connection.ts +177 -0
- package/src/mcp/context.ts +53 -1
- package/src/mcp/http-egress.ts +220 -0
- package/src/mcp/http-security.ts +32 -21
- package/src/mcp/http-session.ts +13 -2
- package/src/mcp/http-transport.ts +96 -6
- package/src/mcp/sync-egress.ts +24 -0
- package/src/mcp/tools/add-collection.ts +4 -0
- package/src/mcp/tools/ask.ts +1 -1
- package/src/mcp/tools/context.ts +9 -2
- package/src/mcp/tools/egress.ts +247 -0
- package/src/mcp/tools/embed.ts +1 -0
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/query.ts +10 -0
- package/src/mcp/tools/remove-collection.ts +4 -0
- package/src/mcp/tools/sync.ts +6 -0
- package/src/mcp/tools/trace.ts +7 -1
- package/src/mcp/tools/vsearch.ts +1 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/hybrid.ts +14 -0
- package/src/pipeline/search.ts +12 -0
- package/src/pipeline/types.ts +3 -0
- package/src/pipeline/vsearch.ts +41 -20
- package/src/publish/artifact.ts +31 -3
- package/src/publish/export-service.ts +25 -0
- package/src/sdk/client.ts +119 -2
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/clipper-capture.ts +5 -1
- package/src/serve/closed-json.ts +61 -0
- package/src/serve/config-sync.ts +4 -1
- package/src/serve/context.ts +1 -0
- package/src/serve/doc-events.ts +110 -39
- package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
- package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
- package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
- package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +3 -0
- package/src/serve/resident-request.ts +80 -3
- package/src/serve/resident-runtime.ts +63 -1
- package/src/serve/routes/api.ts +174 -0
- package/src/serve/routes/clipper.ts +64 -1
- package/src/serve/routes/mcp.ts +6 -0
- package/src/serve/routes/traces.ts +27 -4
- package/src/serve/server.ts +118 -7
- package/src/store/migrations/023-collection-egress-policy.ts +61 -0
- package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
- package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
- package/src/store/migrations/index.ts +6 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +170 -7
- package/src/store/sqlite/change-journal-store.ts +41 -2
- package/src/store/sqlite/egress-audit-store.ts +485 -0
- package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
- package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
- package/src/store/sqlite/retrieval-trace-store.ts +104 -5
- package/src/store/types.ts +126 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
|