@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
@@ -1,17 +1,20 @@
1
1
  /** Loopback REST handlers for private retrieval trace management. */
2
2
 
3
+ import type { Collection } from "../../config/types";
3
4
  import type {
4
5
  RetrievalTraceExportRequest,
5
6
  RetrievalTraceLabelRequest,
6
7
  } from "../../core/retrieval-trace-management";
7
8
  import type { StoreError, StorePort, StoreResult } from "../../store/types";
8
9
 
10
+ import { authorizeCurrentEgress } from "../../core/egress-authorization";
9
11
  import { RetrievalTraceManagementService } from "../../core/retrieval-trace-management";
10
12
 
11
13
  const errorStatus = (error: StoreError): number => {
12
14
  if (error.code === "NOT_FOUND") return 404;
13
15
  if (error.code === "CONSTRAINT_VIOLATION") return 409;
14
16
  if (error.code === "INVALID_INPUT") return 400;
17
+ if (error.code === "EGRESS_DENIED") return 403;
15
18
  return 500;
16
19
  };
17
20
 
@@ -135,14 +138,34 @@ export const handleTraceLabel = async (
135
138
 
136
139
  export const handleTraceExport = async (
137
140
  store: StorePort,
138
- request: Request
141
+ request: Request,
142
+ collections: readonly Collection[] = []
139
143
  ): Promise<Response> => {
140
144
  const body = await parseJsonObject(request);
141
145
  if (body instanceof Response) return body;
142
146
  return response(
143
- await new RetrievalTraceManagementService(store).export(
144
- body as unknown as RetrievalTraceExportRequest
145
- )
147
+ await new RetrievalTraceManagementService(store, {
148
+ authorizeExport: async (lineage) => {
149
+ const hostname = new URL(request.url).hostname;
150
+ const destinationZone = ["localhost", "127.0.0.1", "::1"].includes(
151
+ hostname
152
+ )
153
+ ? "local_process"
154
+ : "remote";
155
+ return authorizeCurrentEgress({
156
+ store,
157
+ config: { collections },
158
+ lineage,
159
+ action: "export",
160
+ destinationZone,
161
+ caller: {
162
+ authenticated: true,
163
+ operationAuthorized: true,
164
+ },
165
+ contentClass: "retrieval_trace",
166
+ });
167
+ },
168
+ }).export(body as unknown as RetrievalTraceExportRequest)
146
169
  );
147
170
  };
148
171
 
@@ -26,6 +26,8 @@ import {
26
26
  handleBrowseTree,
27
27
  handleCapabilities,
28
28
  handleClearCollectionEmbeddings,
29
+ handleCollectionEgressCheck,
30
+ handleCollectionEgressPolicy,
29
31
  handleCollections,
30
32
  handleConnectors,
31
33
  handleCreateFolder,
@@ -43,6 +45,11 @@ import {
43
45
  handleDuplicateDoc,
44
46
  handleEmbed,
45
47
  handleEmbedStatus,
48
+ handleEgressAuditDelete,
49
+ handleEgressAuditList,
50
+ handleEgressAuditPurge,
51
+ handleEgressAuditShow,
52
+ handleEgressAuditStatus,
46
53
  handleHealth,
47
54
  handleImportPreview,
48
55
  handleInstallConnector,
@@ -66,6 +73,7 @@ import {
66
73
  handleTags,
67
74
  handleTrashDoc,
68
75
  handleUpdateCollection,
76
+ handleUpdateCollectionEgressPolicy,
69
77
  handleUpdateDoc,
70
78
  handleVerifyConnector,
71
79
  } from "./routes/api";
@@ -304,7 +312,7 @@ export async function startServer(
304
312
  }
305
313
  return withSecurityHeaders(
306
314
  await handleResidentRead(runtime as ResidentRuntime, req, () =>
307
- handleTraceExport(store, req)
315
+ handleTraceExport(store, req, ctxHolder.config.collections)
308
316
  ),
309
317
  isDev
310
318
  );
@@ -398,6 +406,53 @@ export async function startServer(
398
406
  );
399
407
  },
400
408
  },
409
+ "/api/egress/check": {
410
+ POST: async (req: Request) =>
411
+ withSecurityHeaders(
412
+ await handleCollectionEgressCheck(ctxHolder, req),
413
+ isDev
414
+ ),
415
+ },
416
+ "/api/egress/audits/status": {
417
+ GET: async () =>
418
+ withSecurityHeaders(await handleEgressAuditStatus(store), isDev),
419
+ },
420
+ "/api/egress/audits": {
421
+ GET: async (req: Request) =>
422
+ withSecurityHeaders(await handleEgressAuditList(store, req), isDev),
423
+ DELETE: async (req: Request) => {
424
+ if (!isRequestAllowed(req, port)) {
425
+ return withSecurityHeaders(forbiddenResponse(), isDev);
426
+ }
427
+ return withSecurityHeaders(
428
+ await handleEgressAuditPurge(store),
429
+ isDev
430
+ );
431
+ },
432
+ },
433
+ "/api/egress/audits/:auditId": {
434
+ GET: async (req: Request) => {
435
+ const auditId = decodeURIComponent(
436
+ new URL(req.url).pathname.split("/").at(-1) ?? ""
437
+ );
438
+ return withSecurityHeaders(
439
+ await handleEgressAuditShow(store, auditId),
440
+ isDev
441
+ );
442
+ },
443
+ DELETE: async (req: Request) => {
444
+ if (!isRequestAllowed(req, port)) {
445
+ return withSecurityHeaders(forbiddenResponse(), isDev);
446
+ }
447
+ const auditId = decodeURIComponent(
448
+ new URL(req.url).pathname.split("/").at(-1) ?? ""
449
+ );
450
+ return withSecurityHeaders(
451
+ await handleEgressAuditDelete(store, auditId),
452
+ isDev
453
+ );
454
+ },
455
+ },
401
456
  "/api/connectors": {
402
457
  GET: async (req: Request) =>
403
458
  withSecurityHeaders(
@@ -707,12 +762,42 @@ export async function startServer(
707
762
  },
708
763
  },
709
764
  "/api/events": {
710
- GET: () =>
711
- withSecurityHeaders(
712
- runtime.eventBus?.createResponse() ??
713
- new Response("event stream unavailable", { status: 503 }),
714
- isDev
715
- ),
765
+ GET: (req: Request) => {
766
+ const residentRuntime = runtime as ResidentRuntime;
767
+ const admitted = residentRuntime.admitRequest(req.signal);
768
+ if (!admitted || !residentRuntime.eventBus) {
769
+ admitted?.finish();
770
+ return withSecurityHeaders(
771
+ Response.json(
772
+ {
773
+ error: {
774
+ code: "UNAVAILABLE",
775
+ message: "Document event stream unavailable",
776
+ },
777
+ },
778
+ { status: 503 }
779
+ ),
780
+ isDev
781
+ );
782
+ }
783
+ try {
784
+ return withSecurityHeaders(
785
+ residentRuntime.eventBus.createResponse({
786
+ authorizationEpoch:
787
+ admitted.authorizationEpoch ??
788
+ residentRuntime.authorizationEpoch,
789
+ isAuthorizationEpochCurrent: () =>
790
+ admitted.isAuthorizationEpochCurrent?.() ?? true,
791
+ onClose: () => admitted.finish(),
792
+ signal: admitted.signal,
793
+ }),
794
+ isDev
795
+ );
796
+ } catch (error) {
797
+ admitted.finish();
798
+ throw error;
799
+ }
800
+ },
716
801
  },
717
802
  "/api/tags": {
718
803
  GET: async (req: Request) => {
@@ -893,6 +978,32 @@ export async function startServer(
893
978
  );
894
979
  },
895
980
  },
981
+ "/api/collections/:name/egress-policy": {
982
+ GET: (req: Request) => {
983
+ const parts = new URL(req.url).pathname.split("/");
984
+ const name = decodeURIComponent(parts.at(-2) ?? "");
985
+ return withSecurityHeaders(
986
+ handleCollectionEgressPolicy(ctxHolder, name),
987
+ isDev
988
+ );
989
+ },
990
+ PUT: async (req: Request) => {
991
+ if (!isRequestAllowed(req, port)) {
992
+ return withSecurityHeaders(forbiddenResponse(), isDev);
993
+ }
994
+ const parts = new URL(req.url).pathname.split("/");
995
+ const name = decodeURIComponent(parts.at(-2) ?? "");
996
+ return withSecurityHeaders(
997
+ await handleUpdateCollectionEgressPolicy(
998
+ ctxHolder,
999
+ store,
1000
+ name,
1001
+ req
1002
+ ),
1003
+ isDev
1004
+ );
1005
+ },
1006
+ },
896
1007
  "/api/collections/:name/embeddings/clear": {
897
1008
  POST: async (req: Request) => {
898
1009
  if (!isRequestAllowed(req, port)) {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Add fail-closed collection egress policy state.
3
+ *
4
+ * Existing rows become local-only with legacy_default provenance. Config sync
5
+ * can later replace that value with config_default or an explicit user choice
6
+ * without affecting local retrieval/indexing.
7
+ */
8
+ import type { Migration } from "./runner";
9
+
10
+ export const migration: Migration = {
11
+ version: 23,
12
+ name: "collection_egress_policy",
13
+
14
+ up(db): void {
15
+ const columns = new Set(
16
+ db
17
+ .query<{ name: string }, []>("PRAGMA table_info(collections)")
18
+ .all()
19
+ .map((row) => row.name)
20
+ );
21
+ if (!columns.has("egress_policy")) {
22
+ db.exec(`
23
+ ALTER TABLE collections
24
+ ADD COLUMN egress_policy TEXT NOT NULL DEFAULT 'local_only'
25
+ CHECK (egress_policy IN ('local_only', 'lan', 'remote'))
26
+ `);
27
+ }
28
+ if (!columns.has("egress_policy_source")) {
29
+ db.exec(`
30
+ ALTER TABLE collections
31
+ ADD COLUMN egress_policy_source TEXT NOT NULL DEFAULT 'legacy_default'
32
+ CHECK (
33
+ egress_policy_source IN (
34
+ 'explicit',
35
+ 'config_default',
36
+ 'legacy_default'
37
+ )
38
+ )
39
+ CHECK (
40
+ egress_policy_source = 'explicit'
41
+ OR egress_policy = 'local_only'
42
+ )
43
+ `);
44
+ }
45
+ },
46
+
47
+ down(db): void {
48
+ const columns = new Set(
49
+ db
50
+ .query<{ name: string }, []>("PRAGMA table_info(collections)")
51
+ .all()
52
+ .map((row) => row.name)
53
+ );
54
+ if (columns.has("egress_policy_source")) {
55
+ db.exec("ALTER TABLE collections DROP COLUMN egress_policy_source");
56
+ }
57
+ if (columns.has("egress_policy")) {
58
+ db.exec("ALTER TABLE collections DROP COLUMN egress_policy");
59
+ }
60
+ },
61
+ };
@@ -0,0 +1,121 @@
1
+ /** Persist derived egress lineage and bounded redacted audit receipts. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type { Migration } from "./runner";
6
+
7
+ const LEGACY_LINEAGE_JSON =
8
+ '{"digest":"87b249b76459c91c172da6be6cbe3f93b61c44869eb196f3470ec36ebb50b8b0","effectivePolicy":"local_only","sources":[{"collection":"legacy","policy":"local_only","source":"legacy_default"}]}';
9
+ const LEGACY_LINEAGE_DIGEST =
10
+ "87b249b76459c91c172da6be6cbe3f93b61c44869eb196f3470ec36ebb50b8b0";
11
+
12
+ export const migration: Migration = {
13
+ version: 24,
14
+ name: "egress_derived_lineage",
15
+
16
+ up(db: Database): void {
17
+ const lineageBytes = new TextEncoder().encode(
18
+ LEGACY_LINEAGE_JSON
19
+ ).byteLength;
20
+ db.exec(`
21
+ ALTER TABLE retrieval_traces
22
+ ADD COLUMN effective_egress_policy TEXT NOT NULL DEFAULT 'local_only'
23
+ CHECK (effective_egress_policy IN ('local_only', 'lan', 'remote'));
24
+ ALTER TABLE retrieval_traces
25
+ ADD COLUMN egress_lineage_digest TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_DIGEST}'
26
+ CHECK (length(egress_lineage_digest) = 64 AND egress_lineage_digest NOT GLOB '*[^0-9a-f]*');
27
+ ALTER TABLE retrieval_traces
28
+ ADD COLUMN egress_lineage_json TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_JSON}';
29
+ ALTER TABLE retrieval_traces
30
+ ADD COLUMN egress_lineage_bytes INTEGER NOT NULL DEFAULT ${lineageBytes}
31
+ CHECK (egress_lineage_bytes > 0 AND egress_lineage_bytes <= 32768);
32
+ ALTER TABLE retrieval_traces
33
+ ADD COLUMN creation_digest_version INTEGER NOT NULL DEFAULT 0
34
+ CHECK (creation_digest_version IN (0, 1));
35
+
36
+ ALTER TABLE retrieval_trace_exports
37
+ ADD COLUMN effective_egress_policy TEXT NOT NULL DEFAULT 'local_only'
38
+ CHECK (effective_egress_policy IN ('local_only', 'lan', 'remote'));
39
+ ALTER TABLE retrieval_trace_exports
40
+ ADD COLUMN egress_lineage_digest TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_DIGEST}'
41
+ CHECK (length(egress_lineage_digest) = 64 AND egress_lineage_digest NOT GLOB '*[^0-9a-f]*');
42
+ ALTER TABLE retrieval_trace_exports
43
+ ADD COLUMN egress_lineage_json TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_JSON}';
44
+ ALTER TABLE retrieval_trace_exports
45
+ ADD COLUMN egress_lineage_bytes INTEGER NOT NULL DEFAULT ${lineageBytes}
46
+ CHECK (egress_lineage_bytes > 0 AND egress_lineage_bytes <= 32768);
47
+
48
+ ALTER TABLE document_changes
49
+ ADD COLUMN effective_egress_policy TEXT NOT NULL DEFAULT 'local_only'
50
+ CHECK (effective_egress_policy IN ('local_only', 'lan', 'remote'));
51
+ ALTER TABLE document_changes
52
+ ADD COLUMN egress_lineage_digest TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_DIGEST}'
53
+ CHECK (length(egress_lineage_digest) = 64 AND egress_lineage_digest NOT GLOB '*[^0-9a-f]*');
54
+ ALTER TABLE document_changes
55
+ ADD COLUMN egress_lineage_json TEXT NOT NULL DEFAULT '${LEGACY_LINEAGE_JSON}';
56
+ ALTER TABLE document_changes
57
+ ADD COLUMN egress_lineage_bytes INTEGER NOT NULL DEFAULT ${lineageBytes}
58
+ CHECK (egress_lineage_bytes > 0 AND egress_lineage_bytes <= 32768);
59
+
60
+ UPDATE document_changes
61
+ SET byte_size = byte_size + egress_lineage_bytes;
62
+ UPDATE document_change_journal_state
63
+ SET retained_bytes = (
64
+ SELECT COALESCE(SUM(byte_size), 0) FROM document_changes
65
+ );
66
+
67
+ CREATE TABLE egress_audit_receipts (
68
+ audit_id TEXT PRIMARY KEY,
69
+ decision TEXT NOT NULL CHECK (decision IN ('allow', 'deny')),
70
+ action TEXT NOT NULL
71
+ CHECK (action IN ('retrieve', 'serve', 'publish', 'remote_inference', 'export', 'clip_write')),
72
+ destination_zone TEXT NOT NULL
73
+ CHECK (destination_zone IN ('local_process', 'loopback', 'lan', 'remote')),
74
+ content_class TEXT NOT NULL
75
+ CHECK (content_class IN ('source', 'snippet', 'metadata', 'attachment', 'embedding', 'capsule', 'audit_log', 'retrieval_trace')),
76
+ effective_policy TEXT NOT NULL
77
+ CHECK (effective_policy IN ('local_only', 'lan', 'remote')),
78
+ reason_code TEXT NOT NULL,
79
+ lineage_digest TEXT NOT NULL
80
+ CHECK (length(lineage_digest) = 64 AND lineage_digest NOT GLOB '*[^0-9a-f]*'),
81
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
82
+ expires_at_ms INTEGER NOT NULL CHECK (expires_at_ms > created_at_ms),
83
+ byte_size INTEGER NOT NULL CHECK (byte_size > 0 AND byte_size <= 4096),
84
+ CHECK (length(audit_id) BETWEEN 1 AND 128),
85
+ CHECK (length(reason_code) BETWEEN 1 AND 64)
86
+ );
87
+
88
+ CREATE INDEX idx_egress_audit_retention
89
+ ON egress_audit_receipts(expires_at_ms, created_at_ms, audit_id);
90
+ `);
91
+ },
92
+
93
+ down(db: Database): void {
94
+ db.exec(`
95
+ DROP TABLE egress_audit_receipts;
96
+
97
+ UPDATE document_changes
98
+ SET byte_size = byte_size - egress_lineage_bytes;
99
+ UPDATE document_change_journal_state
100
+ SET retained_bytes = (
101
+ SELECT COALESCE(SUM(byte_size), 0) FROM document_changes
102
+ );
103
+
104
+ ALTER TABLE document_changes DROP COLUMN egress_lineage_bytes;
105
+ ALTER TABLE document_changes DROP COLUMN egress_lineage_json;
106
+ ALTER TABLE document_changes DROP COLUMN egress_lineage_digest;
107
+ ALTER TABLE document_changes DROP COLUMN effective_egress_policy;
108
+
109
+ ALTER TABLE retrieval_trace_exports DROP COLUMN egress_lineage_bytes;
110
+ ALTER TABLE retrieval_trace_exports DROP COLUMN egress_lineage_json;
111
+ ALTER TABLE retrieval_trace_exports DROP COLUMN egress_lineage_digest;
112
+ ALTER TABLE retrieval_trace_exports DROP COLUMN effective_egress_policy;
113
+
114
+ ALTER TABLE retrieval_traces DROP COLUMN egress_lineage_bytes;
115
+ ALTER TABLE retrieval_traces DROP COLUMN egress_lineage_json;
116
+ ALTER TABLE retrieval_traces DROP COLUMN egress_lineage_digest;
117
+ ALTER TABLE retrieval_traces DROP COLUMN effective_egress_policy;
118
+ ALTER TABLE retrieval_traces DROP COLUMN creation_digest_version;
119
+ `);
120
+ },
121
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Add a durable monotonic collection egress policy revision.
3
+ *
4
+ * Legacy rows start at zero. Canonical config mutations advance the revision
5
+ * whenever the effective policy or its provenance changes.
6
+ */
7
+ import type { Migration } from "./runner";
8
+
9
+ export const migration: Migration = {
10
+ version: 25,
11
+ name: "collection_egress_policy_revision",
12
+
13
+ up(db): void {
14
+ const columns = new Set(
15
+ db
16
+ .query<{ name: string }, []>("PRAGMA table_info(collections)")
17
+ .all()
18
+ .map((row) => row.name)
19
+ );
20
+ if (!columns.has("egress_policy_revision")) {
21
+ db.exec(`
22
+ ALTER TABLE collections
23
+ ADD COLUMN egress_policy_revision INTEGER NOT NULL DEFAULT 0
24
+ CHECK (egress_policy_revision >= 0)
25
+ `);
26
+ }
27
+ },
28
+
29
+ down(db): void {
30
+ const columns = new Set(
31
+ db
32
+ .query<{ name: string }, []>("PRAGMA table_info(collections)")
33
+ .all()
34
+ .map((row) => row.name)
35
+ );
36
+ if (columns.has("egress_policy_revision")) {
37
+ db.exec("ALTER TABLE collections DROP COLUMN egress_policy_revision");
38
+ }
39
+ },
40
+ };
@@ -36,6 +36,9 @@ import { migration as m019 } from "./019-saved-capsule-registration-generation";
36
36
  import { migration as m020 } from "./020-browser-clipper-security";
37
37
  import { migration as m021 } from "./021-multi-context-identity";
38
38
  import { migration as m022 } from "./022-record-export-lineage";
39
+ import { migration as m023 } from "./023-collection-egress-policy";
40
+ import { migration as m024 } from "./024-egress-derived-lineage";
41
+ import { migration as m025 } from "./025-collection-egress-policy-revision";
39
42
 
40
43
  /** All migrations in order */
41
44
  export const migrations = [
@@ -61,4 +64,7 @@ export const migrations = [
61
64
  m020,
62
65
  m021,
63
66
  m022,
67
+ m023,
68
+ m024,
69
+ m025,
64
70
  ];
@@ -10,6 +10,7 @@ import type {
10
10
  RetrievalTraceRunInput,
11
11
  } from "./types";
12
12
 
13
+ import { egressLineageSchema } from "../core/egress-provenance";
13
14
  const MAX_ID_LENGTH = 128;
14
15
  const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
15
16
  const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
@@ -134,6 +135,7 @@ const traceInputBaseSchema = z
134
135
  goalDigest: sha256Schema.nullable(),
135
136
  goalShape: queryShapeSchema,
136
137
  filters: traceJsonObjectSchema,
138
+ egressLineage: egressLineageSchema,
137
139
  fingerprints: z
138
140
  .object({
139
141
  pipeline: sha256Schema,
@@ -362,6 +364,19 @@ export const hashRetrievalTraceCreation = (
362
364
  return hashTraceCanonical(immutable);
363
365
  };
364
366
 
367
+ /** Exact pre-v24 creation projection, used only for marked migrated rows. */
368
+ export const hashLegacyRetrievalTraceCreation = (
369
+ trace: RetrievalTraceInput
370
+ ): string => {
371
+ const {
372
+ status: _status,
373
+ updatedAtMs: _updatedAtMs,
374
+ egressLineage: _egressLineage,
375
+ ...immutable
376
+ } = trace;
377
+ return hashTraceCanonical(immutable);
378
+ };
379
+
365
380
  export const parseRetrievalTraceInput = (
366
381
  input: RetrievalTraceInput
367
382
  ): RetrievalTraceInput => retrievalTraceInputSchema.parse(input);