@gmickel/gno 1.28.0 → 1.29.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.
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.0.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.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,242 @@
1
+ /** Closed, getter-safe runtime parsing for collection egress policy services. */
2
+
3
+ import type { EgressPolicy } from "../config/types";
4
+ import type {
5
+ CollectionEgressCheckInput,
6
+ EgressRelaxationConfirmation,
7
+ } from "./collection-egress-policy-service";
8
+
9
+ import { EGRESS_POLICIES } from "../config/types";
10
+ import {
11
+ EGRESS_ACTIONS,
12
+ EGRESS_CONTENT_CLASSES,
13
+ EGRESS_DESTINATION_ZONES,
14
+ } from "./egress-policy";
15
+
16
+ const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
17
+ const MAX_COLLECTIONS = 64;
18
+ const POLICIES = new Set<unknown>(EGRESS_POLICIES);
19
+ const ACTIONS = new Set<unknown>(EGRESS_ACTIONS);
20
+ const DESTINATIONS = new Set<unknown>(EGRESS_DESTINATION_ZONES);
21
+ const CONTENT_CLASSES = new Set<unknown>(EGRESS_CONTENT_CLASSES);
22
+
23
+ export type PolicyValidationResult<T> =
24
+ | { ok: true; value: T }
25
+ | { ok: false; code: "VALIDATION"; error: string };
26
+
27
+ const invalid = <T>(message: string): PolicyValidationResult<T> => ({
28
+ ok: false,
29
+ code: "VALIDATION",
30
+ error: message,
31
+ });
32
+
33
+ /**
34
+ * Snapshot only own data properties. Accessors, exotic prototypes, symbols,
35
+ * proxy traps, arrays, and unknown keys are rejected without invoking getters.
36
+ */
37
+ const closedRecord = (
38
+ value: unknown,
39
+ allowedKeys: readonly string[]
40
+ ): PolicyValidationResult<Record<string, unknown>> => {
41
+ try {
42
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
43
+ return invalid("Expected a JSON object");
44
+ }
45
+ const prototype = Object.getPrototypeOf(value);
46
+ if (prototype !== Object.prototype && prototype !== null) {
47
+ return invalid("Expected a plain JSON object");
48
+ }
49
+ const keys = Reflect.ownKeys(value);
50
+ if (
51
+ keys.length > allowedKeys.length ||
52
+ keys.some((key) => typeof key !== "string" || !allowedKeys.includes(key))
53
+ ) {
54
+ return invalid("Object contains unknown fields");
55
+ }
56
+ const result: Record<string, unknown> = {};
57
+ for (const key of keys) {
58
+ if (typeof key !== "string") return invalid("Invalid object field");
59
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
60
+ if (!descriptor || !("value" in descriptor)) {
61
+ return invalid("Accessor fields are not allowed");
62
+ }
63
+ result[key] = descriptor.value;
64
+ }
65
+ return { ok: true, value: result };
66
+ } catch {
67
+ return invalid("Unreadable input object");
68
+ }
69
+ };
70
+
71
+ export const parseCollectionName = (
72
+ value: unknown
73
+ ): PolicyValidationResult<string> =>
74
+ typeof value === "string" && COLLECTION_PATTERN.test(value)
75
+ ? { ok: true, value }
76
+ : invalid("Invalid collection name");
77
+
78
+ const parsePolicy = (value: unknown): PolicyValidationResult<EgressPolicy> =>
79
+ POLICIES.has(value)
80
+ ? { ok: true, value: value as EgressPolicy }
81
+ : invalid("Invalid collection egress policy");
82
+
83
+ const parseConfirmation = (
84
+ value: unknown
85
+ ): PolicyValidationResult<EgressRelaxationConfirmation | undefined> => {
86
+ if (value === undefined) return { ok: true, value: undefined };
87
+ const record = closedRecord(value, [
88
+ "acknowledged",
89
+ "collection",
90
+ "currentPolicy",
91
+ "currentRevision",
92
+ "targetPolicy",
93
+ ]);
94
+ if (!record.ok) return record;
95
+ const collection = parseCollectionName(record.value.collection);
96
+ const currentPolicy = parsePolicy(record.value.currentPolicy);
97
+ const targetPolicy = parsePolicy(record.value.targetPolicy);
98
+ if (
99
+ !collection.ok ||
100
+ !currentPolicy.ok ||
101
+ !targetPolicy.ok ||
102
+ !Number.isSafeInteger(record.value.currentRevision) ||
103
+ (record.value.currentRevision as number) < 0 ||
104
+ record.value.acknowledged !== true
105
+ ) {
106
+ return invalid("Invalid egress relaxation confirmation");
107
+ }
108
+ return {
109
+ ok: true,
110
+ value: {
111
+ acknowledged: true,
112
+ collection: collection.value,
113
+ currentPolicy: currentPolicy.value,
114
+ currentRevision: record.value.currentRevision as number,
115
+ targetPolicy: targetPolicy.value,
116
+ },
117
+ };
118
+ };
119
+
120
+ export const parsePolicySetInput = (
121
+ value: unknown
122
+ ): PolicyValidationResult<{
123
+ collection: string;
124
+ policy: EgressPolicy;
125
+ confirmation?: EgressRelaxationConfirmation;
126
+ }> => {
127
+ const record = closedRecord(value, ["collection", "confirmation", "policy"]);
128
+ if (!record.ok) return record;
129
+ const collection = parseCollectionName(record.value.collection);
130
+ const policy = parsePolicy(record.value.policy);
131
+ const confirmation = parseConfirmation(record.value.confirmation);
132
+ if (!collection.ok || !policy.ok || !confirmation.ok) {
133
+ return invalid("Invalid collection egress policy mutation");
134
+ }
135
+ return {
136
+ ok: true,
137
+ value: {
138
+ collection: collection.value,
139
+ policy: policy.value,
140
+ confirmation: confirmation.value,
141
+ },
142
+ };
143
+ };
144
+
145
+ export const parsePolicySetBody = (
146
+ value: unknown,
147
+ collectionValue: unknown
148
+ ): PolicyValidationResult<{
149
+ collection: string;
150
+ policy: EgressPolicy;
151
+ confirmation?: EgressRelaxationConfirmation;
152
+ }> => {
153
+ const record = closedRecord(value, ["confirmation", "policy"]);
154
+ if (!record.ok) return record;
155
+ return parsePolicySetInput({
156
+ collection: collectionValue,
157
+ confirmation: record.value.confirmation,
158
+ policy: record.value.policy,
159
+ });
160
+ };
161
+
162
+ const parseCollections = (
163
+ value: unknown
164
+ ): PolicyValidationResult<string[] | undefined> => {
165
+ if (value === undefined) return { ok: true, value: undefined };
166
+ try {
167
+ if (
168
+ !Array.isArray(value) ||
169
+ value.length === 0 ||
170
+ value.length > MAX_COLLECTIONS
171
+ ) {
172
+ return invalid("Invalid collection scope");
173
+ }
174
+ const names: string[] = [];
175
+ const seenNames = new Set<string>();
176
+ for (let index = 0; index < value.length; index += 1) {
177
+ if (!(index in value)) return invalid("Invalid collection scope");
178
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
179
+ if (!descriptor || !("value" in descriptor)) {
180
+ return invalid("Invalid collection scope");
181
+ }
182
+ const name = parseCollectionName(descriptor.value);
183
+ if (!name.ok) return invalid("Invalid collection scope");
184
+ if (seenNames.has(name.value)) {
185
+ return invalid("Invalid collection scope");
186
+ }
187
+ seenNames.add(name.value);
188
+ names.push(name.value);
189
+ }
190
+ return { ok: true, value: names };
191
+ } catch {
192
+ return invalid("Unreadable collection scope");
193
+ }
194
+ };
195
+
196
+ export const parsePolicyCheckInput = (
197
+ value: unknown
198
+ ): PolicyValidationResult<CollectionEgressCheckInput> => {
199
+ const record = closedRecord(value, [
200
+ "action",
201
+ "caller",
202
+ "collections",
203
+ "contentClass",
204
+ "destinationZone",
205
+ "partialResults",
206
+ ]);
207
+ if (!record.ok) return record;
208
+ const caller = closedRecord(record.value.caller, [
209
+ "authenticated",
210
+ "operationAuthorized",
211
+ ]);
212
+ const collections = parseCollections(record.value.collections);
213
+ const partialResults = record.value.partialResults ?? "deny";
214
+ if (
215
+ !caller.ok ||
216
+ !collections.ok ||
217
+ !ACTIONS.has(record.value.action) ||
218
+ !DESTINATIONS.has(record.value.destinationZone) ||
219
+ !CONTENT_CLASSES.has(record.value.contentClass) ||
220
+ (partialResults !== "deny" && partialResults !== "explicit") ||
221
+ typeof caller.value.authenticated !== "boolean" ||
222
+ typeof caller.value.operationAuthorized !== "boolean"
223
+ ) {
224
+ return invalid("Invalid egress policy check");
225
+ }
226
+ return {
227
+ ok: true,
228
+ value: {
229
+ collections: collections.value,
230
+ action: record.value.action as CollectionEgressCheckInput["action"],
231
+ destinationZone: record.value
232
+ .destinationZone as CollectionEgressCheckInput["destinationZone"],
233
+ caller: {
234
+ authenticated: caller.value.authenticated,
235
+ operationAuthorized: caller.value.operationAuthorized,
236
+ },
237
+ contentClass: record.value
238
+ .contentClass as CollectionEgressCheckInput["contentClass"],
239
+ partialResults,
240
+ },
241
+ };
242
+ };
@@ -179,30 +179,42 @@ export async function applyConfigChange<T = void>(
179
179
  };
180
180
  }
181
181
  } else {
182
- const syncCollResult = await ctx.store.syncCollections(
183
- newConfig.collections
184
- );
185
- if (!syncCollResult.ok) {
186
- console.warn(
187
- `Config saved but DB sync failed: ${syncCollResult.error.message}`
182
+ const syncProjection = async (): Promise<void> => {
183
+ const syncCollResult = await ctx.store.syncCollections(
184
+ newConfig.collections
188
185
  );
189
- return {
190
- ok: false,
191
- error: `DB sync failed: ${syncCollResult.error.message}`,
192
- code: "SYNC_ERROR",
193
- };
194
- }
195
-
196
- const syncCtxResult = await ctx.store.syncContexts(
197
- newConfig.contexts ?? []
198
- );
199
- if (!syncCtxResult.ok) {
186
+ if (!syncCollResult.ok) {
187
+ throw new Error(
188
+ `Collection sync failed: ${syncCollResult.error.message}`
189
+ );
190
+ }
191
+ const syncCtxResult = await ctx.store.syncContexts(
192
+ newConfig.contexts ?? []
193
+ );
194
+ if (!syncCtxResult.ok) {
195
+ throw new Error(
196
+ `Context sync failed: ${syncCtxResult.error.message}`
197
+ );
198
+ }
199
+ };
200
+ const transaction = ctx.store.withTransaction
201
+ ? await ctx.store.withTransaction(syncProjection)
202
+ : await syncProjection()
203
+ .then(() => ({ ok: true as const, value: undefined }))
204
+ .catch((cause: unknown) => ({
205
+ ok: false as const,
206
+ error: {
207
+ message:
208
+ cause instanceof Error ? cause.message : "DB sync failed",
209
+ },
210
+ }));
211
+ if (!transaction.ok) {
200
212
  console.warn(
201
- `Config saved but context sync failed: ${syncCtxResult.error.message}`
213
+ `Config saved but DB sync failed: ${transaction.error.message}`
202
214
  );
203
215
  return {
204
216
  ok: false,
205
- error: `Context sync failed: ${syncCtxResult.error.message}`,
217
+ error: `DB sync failed: ${transaction.error.message}`,
206
218
  code: "SYNC_ERROR",
207
219
  };
208
220
  }
@@ -11,10 +11,12 @@ import {
11
11
  sha256Text,
12
12
  validateContextCapsulePayload,
13
13
  } from "./context-capsule-validation";
14
+ import { egressLineageSchema } from "./egress-provenance";
14
15
 
15
16
  export { contextCapsuleIndexSnapshotSchema } from "./context-capsule-index-schema";
16
17
 
17
18
  export const CONTEXT_CAPSULE_SCHEMA_VERSION = "1.0" as const;
19
+ export const CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION = "1.1" as const;
18
20
  export const CONTEXT_CAPSULE_COORDINATE_SPACE = "canonical_mirror" as const;
19
21
 
20
22
  const SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -356,6 +358,7 @@ export const contextCapsuleEvidenceSchema = z
356
358
  "unclassified",
357
359
  "unavailable",
358
360
  ]),
361
+ egressLineage: egressLineageSchema.optional(),
359
362
  record: recordEvidenceMetadataSchema.optional(),
360
363
  })
361
364
  .strict()
@@ -406,6 +409,11 @@ export const contextCapsuleEvidenceSchema = z
406
409
  }
407
410
  });
408
411
 
412
+ export const contextCapsuleEvidenceV1_1Schema =
413
+ contextCapsuleEvidenceSchema.and(
414
+ z.object({ egressLineage: egressLineageSchema }).passthrough()
415
+ );
416
+
409
417
  const coveredFacetSchema = z
410
418
  .object({
411
419
  facet: nonEmptyTextSchema.max(512),
@@ -570,8 +578,110 @@ export const contextCapsulePayloadV1Schema = z
570
578
  warnings: z.array(warningSchema).max(32),
571
579
  })
572
580
  .strict()
573
- .superRefine(validateContextCapsulePayload);
581
+ .superRefine((value, context) => {
582
+ validateContextCapsulePayload(value, context);
583
+ if (
584
+ value.evidence.some((evidence) => evidence.egressLineage !== undefined)
585
+ ) {
586
+ context.addIssue({
587
+ code: "custom",
588
+ message: "Context Capsule 1.0 cannot contain egress lineage",
589
+ path: ["evidence"],
590
+ });
591
+ }
592
+ });
574
593
 
575
594
  export type ContextCapsulePayloadV1 = z.infer<
576
595
  typeof contextCapsulePayloadV1Schema
577
596
  >;
597
+
598
+ const validateContextCapsuleV1_1Lineage = (
599
+ value: {
600
+ scope: { collections: string[] };
601
+ egressLineage: z.infer<typeof egressLineageSchema>;
602
+ evidence: z.infer<typeof contextCapsuleEvidenceV1_1Schema>[];
603
+ },
604
+ context: z.RefinementCtx
605
+ ): void => {
606
+ const aggregateByCollection = new Map(
607
+ value.egressLineage.sources.map((source) => [source.collection, source])
608
+ );
609
+ const requiredCollections = new Set(value.scope.collections);
610
+ for (const [evidenceIndex, evidence] of value.evidence.entries()) {
611
+ const evidenceCollection = evidence.collection;
612
+ const evidenceOwnSource = evidence.egressLineage.sources.find(
613
+ (source) => source.collection === evidenceCollection
614
+ );
615
+ if (!evidenceOwnSource) {
616
+ context.addIssue({
617
+ code: "custom",
618
+ message: "Evidence lineage must include its source collection",
619
+ path: ["evidence", evidenceIndex, "egressLineage"],
620
+ });
621
+ }
622
+ for (const source of evidence.egressLineage.sources) {
623
+ requiredCollections.add(source.collection);
624
+ const aggregateSource = aggregateByCollection.get(source.collection);
625
+ if (
626
+ !aggregateSource ||
627
+ aggregateSource.policy !== source.policy ||
628
+ aggregateSource.source !== source.source
629
+ ) {
630
+ context.addIssue({
631
+ code: "custom",
632
+ message:
633
+ "Aggregate lineage must preserve every evidence policy source",
634
+ path: ["egressLineage"],
635
+ });
636
+ }
637
+ }
638
+ }
639
+ if (value.scope.collections.length > 0) {
640
+ const aggregateCollections = value.egressLineage.sources.map(
641
+ ({ collection }) => collection
642
+ );
643
+ const required = [...requiredCollections].sort(compareCodeUnits);
644
+ if (JSON.stringify(aggregateCollections) !== JSON.stringify(required)) {
645
+ context.addIssue({
646
+ code: "custom",
647
+ message:
648
+ "Aggregate lineage membership must equal requested scope and evidence ownership",
649
+ path: ["egressLineage", "sources"],
650
+ });
651
+ }
652
+ }
653
+ };
654
+
655
+ export const contextCapsulePayloadV1_1Schema = z
656
+ .object({
657
+ schemaVersion: z.literal(CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION),
658
+ coordinateSpace: z.literal(CONTEXT_CAPSULE_COORDINATE_SPACE),
659
+ goal: nonEmptyTextSchema,
660
+ query: nonEmptyTextSchema,
661
+ scope: scopeSchema,
662
+ budget: budgetSchema,
663
+ retrieval: contextCapsuleRetrievalSchema,
664
+ fingerprints: fingerprintsSchema,
665
+ capabilities: capabilitiesSchema,
666
+ fallbacks: z.array(fallbackSchema).max(16),
667
+ guidance: guidanceSchema,
668
+ egressLineage: egressLineageSchema,
669
+ evidence: z.array(contextCapsuleEvidenceV1_1Schema).min(1),
670
+ coverage: coverageSchema,
671
+ omissions: omissionsSchema,
672
+ truncated: z.boolean(),
673
+ warnings: z.array(warningSchema).max(32),
674
+ })
675
+ .strict()
676
+ .superRefine((value, context) => {
677
+ validateContextCapsulePayload(value, context);
678
+ validateContextCapsuleV1_1Lineage(value, context);
679
+ });
680
+
681
+ export type ContextCapsulePayloadV1_1 = z.infer<
682
+ typeof contextCapsulePayloadV1_1Schema
683
+ >;
684
+
685
+ export type ContextCapsulePayload =
686
+ | ContextCapsulePayloadV1
687
+ | ContextCapsulePayloadV1_1;
@@ -1,6 +1,6 @@
1
1
  import type { RefinementCtx } from "zod";
2
2
 
3
- import type { ContextCapsulePayloadV1 } from "./context-capsule-schema";
3
+ import type { ContextCapsulePayload } from "./context-capsule-schema";
4
4
 
5
5
  import { DEFAULT_INDEX_NAME, parseUri } from "../app/constants";
6
6
  import { canonicalizeIndexName } from "../app/index-name";
@@ -86,7 +86,7 @@ const isWithinPrefix = (
86
86
  uri.path.startsWith(`${prefix.path}/`));
87
87
 
88
88
  const validateCapabilityBindings = (
89
- value: ContextCapsulePayloadV1,
89
+ value: ContextCapsulePayload,
90
90
  fallbackKeys: Set<string>,
91
91
  context: RefinementCtx
92
92
  ): void => {
@@ -165,7 +165,7 @@ const validateCapabilityBindings = (
165
165
  };
166
166
 
167
167
  const validateCoverageBindings = (
168
- value: ContextCapsulePayloadV1,
168
+ value: ContextCapsulePayload,
169
169
  context: RefinementCtx
170
170
  ): void => {
171
171
  const requested = new Set(value.coverage.requestedFacets);
@@ -329,7 +329,7 @@ const validateCoverageBindings = (
329
329
  };
330
330
 
331
331
  export const validateContextCapsulePayload = (
332
- value: ContextCapsulePayloadV1,
332
+ value: ContextCapsulePayload,
333
333
  context: RefinementCtx
334
334
  ): void => {
335
335
  const fallbackKeys = new Set(value.fallbacks.map((item) => item.code));
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
 
3
3
  import {
4
4
  CONTEXT_CAPSULE_COORDINATE_SPACE,
5
+ CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION,
5
6
  CONTEXT_CAPSULE_SCHEMA_VERSION,
6
7
  contextCapsuleGnoUriSchema,
7
8
  contextCapsuleIndexSnapshotSchema,
@@ -120,7 +121,10 @@ export const contextCapsuleVerificationEvidenceSchema = z
120
121
 
121
122
  export const contextCapsuleVerificationSchema = z
122
123
  .object({
123
- schemaVersion: z.literal(CONTEXT_CAPSULE_SCHEMA_VERSION),
124
+ schemaVersion: z.union([
125
+ z.literal(CONTEXT_CAPSULE_SCHEMA_VERSION),
126
+ z.literal(CONTEXT_CAPSULE_CURRENT_SCHEMA_VERSION),
127
+ ]),
124
128
  coordinateSpace: z.literal(CONTEXT_CAPSULE_COORDINATE_SPACE),
125
129
  capsuleId: sha256Schema,
126
130
  operationStatus: z.literal("completed"),