@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.
- package/README.md +46 -20
- package/assets/skill/SKILL.md +33 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.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,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
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
213
|
+
`Config saved but DB sync failed: ${transaction.error.message}`
|
|
202
214
|
);
|
|
203
215
|
return {
|
|
204
216
|
ok: false,
|
|
205
|
-
error: `
|
|
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(
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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"),
|