@gmickel/gno 1.27.1 → 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 (195) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +59 -1
  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 +2 -2
  10. package/spec/cli.md +82 -5
  11. package/spec/db/schema.sql +31 -0
  12. package/spec/mcp.md +71 -0
  13. package/spec/output-schemas/ask.schema.json +156 -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 +183 -3
  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/get.schema.json +94 -0
  22. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  23. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  24. package/spec/output-schemas/multi-get.schema.json +128 -0
  25. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  26. package/spec/output-schemas/record-import.schema.json +223 -0
  27. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  28. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  29. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  30. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  31. package/spec/output-schemas/search-result.schema.json +94 -0
  32. package/spec/output-schemas/search-results.schema.json +155 -0
  33. package/spec/project-profile.schema.json +66 -0
  34. package/src/app/context-format.ts +1 -1
  35. package/src/app/context-runtime-contract.ts +16 -7
  36. package/src/cli/commands/ask.ts +7 -0
  37. package/src/cli/commands/collection/index.ts +5 -0
  38. package/src/cli/commands/collection/policy.ts +126 -0
  39. package/src/cli/commands/context-build.ts +5 -0
  40. package/src/cli/commands/daemon.ts +45 -3
  41. package/src/cli/commands/egress-audit.ts +68 -0
  42. package/src/cli/commands/embed.ts +2 -0
  43. package/src/cli/commands/get.ts +12 -2
  44. package/src/cli/commands/index-cmd.ts +13 -0
  45. package/src/cli/commands/multi-get.ts +9 -2
  46. package/src/cli/commands/query.ts +12 -0
  47. package/src/cli/commands/replay.ts +13 -3
  48. package/src/cli/commands/shared.ts +28 -0
  49. package/src/cli/commands/update.ts +5 -0
  50. package/src/cli/commands/vsearch.ts +3 -1
  51. package/src/cli/program.ts +127 -0
  52. package/src/config/index.ts +9 -0
  53. package/src/config/project-profile.ts +2 -0
  54. package/src/config/types.ts +66 -0
  55. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  56. package/src/converters/adapters/browser-export/formats.ts +358 -0
  57. package/src/converters/adapters/email/adapter.ts +429 -0
  58. package/src/converters/adapters/email/html.ts +162 -0
  59. package/src/converters/adapters/email/mime.ts +454 -0
  60. package/src/converters/adapters/email/parameters.ts +77 -0
  61. package/src/converters/adapters/ical/adapter.ts +475 -0
  62. package/src/converters/adapters/ical/recurrence.ts +186 -0
  63. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  64. package/src/converters/adapters/jsonl/config.ts +105 -0
  65. package/src/converters/adapters/shared/html-text.ts +165 -0
  66. package/src/converters/adapters/shared/record-utils.ts +79 -0
  67. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  68. package/src/converters/adapters/transcript/adapter.ts +295 -0
  69. package/src/converters/adapters/transcript/json.ts +184 -0
  70. package/src/converters/adapters/transcript/model.ts +171 -0
  71. package/src/converters/adapters/transcript/text.ts +58 -0
  72. package/src/converters/adapters/transcript/timed.ts +152 -0
  73. package/src/converters/index.ts +11 -1
  74. package/src/converters/mime.ts +8 -0
  75. package/src/converters/pipeline.ts +15 -1
  76. package/src/converters/registry.ts +37 -1
  77. package/src/converters/types.ts +136 -0
  78. package/src/core/browser-clip.ts +16 -1
  79. package/src/core/collection-egress-policy-projection.ts +28 -0
  80. package/src/core/collection-egress-policy-service.ts +443 -0
  81. package/src/core/collection-egress-policy-validation.ts +242 -0
  82. package/src/core/config-mutation.ts +31 -19
  83. package/src/core/context-capsule-schema.ts +198 -1
  84. package/src/core/context-capsule-validation.ts +4 -4
  85. package/src/core/context-capsule-verification.ts +5 -1
  86. package/src/core/context-capsule.ts +177 -112
  87. package/src/core/context-evidence.ts +93 -15
  88. package/src/core/destination-classifier.ts +402 -0
  89. package/src/core/document-capabilities.ts +12 -0
  90. package/src/core/egress-audit.ts +239 -0
  91. package/src/core/egress-authorization.ts +50 -0
  92. package/src/core/egress-enforcement.ts +264 -0
  93. package/src/core/egress-policy.ts +440 -0
  94. package/src/core/egress-provenance.ts +336 -0
  95. package/src/core/job-manager.ts +37 -0
  96. package/src/core/network-boundary-inventory.ts +261 -0
  97. package/src/core/project-profile-apply-state.ts +5 -0
  98. package/src/core/project-profile.ts +4 -0
  99. package/src/core/record-metadata.ts +49 -0
  100. package/src/core/retrieval-qrels.ts +6 -0
  101. package/src/core/retrieval-replay.ts +18 -4
  102. package/src/core/retrieval-trace-export.ts +23 -4
  103. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  104. package/src/core/retrieval-trace-management-types.ts +3 -0
  105. package/src/core/retrieval-trace-management.ts +9 -1
  106. package/src/core/retrieval-trace-session.ts +94 -0
  107. package/src/core/retrieval-trace.ts +22 -0
  108. package/src/ingestion/record-adapter-canonical.ts +433 -0
  109. package/src/ingestion/record-adapter.ts +448 -0
  110. package/src/ingestion/record-container.ts +704 -0
  111. package/src/ingestion/record-path.ts +20 -0
  112. package/src/ingestion/record-sync.ts +70 -0
  113. package/src/ingestion/sync.ts +243 -36
  114. package/src/ingestion/types.ts +71 -1
  115. package/src/ingestion/walker.ts +31 -11
  116. package/src/llm/errors.ts +10 -0
  117. package/src/llm/http-inference.ts +175 -0
  118. package/src/llm/http-policy.ts +537 -0
  119. package/src/llm/httpEmbedding.ts +47 -28
  120. package/src/llm/httpGeneration.ts +31 -18
  121. package/src/llm/httpRerank.ts +30 -18
  122. package/src/llm/index.ts +1 -0
  123. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  124. package/src/llm/pinned-http-connection.ts +177 -0
  125. package/src/mcp/context.ts +53 -1
  126. package/src/mcp/http-egress.ts +220 -0
  127. package/src/mcp/http-security.ts +32 -21
  128. package/src/mcp/http-session.ts +13 -2
  129. package/src/mcp/http-transport.ts +96 -6
  130. package/src/mcp/sync-egress.ts +24 -0
  131. package/src/mcp/tools/add-collection.ts +4 -0
  132. package/src/mcp/tools/ask.ts +1 -1
  133. package/src/mcp/tools/context.ts +9 -2
  134. package/src/mcp/tools/egress.ts +247 -0
  135. package/src/mcp/tools/embed.ts +1 -0
  136. package/src/mcp/tools/get.ts +10 -2
  137. package/src/mcp/tools/index-cmd.ts +7 -0
  138. package/src/mcp/tools/index.ts +98 -0
  139. package/src/mcp/tools/multi-get.ts +9 -2
  140. package/src/mcp/tools/query.ts +10 -0
  141. package/src/mcp/tools/remove-collection.ts +4 -0
  142. package/src/mcp/tools/sync.ts +6 -0
  143. package/src/mcp/tools/trace.ts +7 -1
  144. package/src/mcp/tools/vsearch.ts +1 -0
  145. package/src/mcp/tools/workspace-write.ts +2 -0
  146. package/src/pipeline/egress-lineage.ts +124 -0
  147. package/src/pipeline/filters.ts +1 -1
  148. package/src/pipeline/graph-retrieval.ts +7 -4
  149. package/src/pipeline/hybrid.ts +21 -4
  150. package/src/pipeline/result-context.ts +12 -4
  151. package/src/pipeline/search.ts +23 -4
  152. package/src/pipeline/types.ts +6 -0
  153. package/src/pipeline/vsearch.ts +67 -28
  154. package/src/publish/artifact.ts +31 -3
  155. package/src/publish/export-service.ts +25 -0
  156. package/src/sdk/client.ts +119 -2
  157. package/src/sdk/documents.ts +13 -5
  158. package/src/sdk/embed.ts +1 -0
  159. package/src/sdk/types.ts +33 -0
  160. package/src/serve/browse-tree.ts +4 -2
  161. package/src/serve/clipper-capture.ts +5 -1
  162. package/src/serve/closed-json.ts +61 -0
  163. package/src/serve/config-sync.ts +4 -1
  164. package/src/serve/context.ts +1 -0
  165. package/src/serve/doc-events.ts +110 -39
  166. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  167. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  168. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  169. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  170. package/src/serve/public/globals.built.css +1 -1
  171. package/src/serve/public/pages/Collections.tsx +3 -0
  172. package/src/serve/resident-request.ts +80 -3
  173. package/src/serve/resident-runtime.ts +63 -1
  174. package/src/serve/routes/api.ts +237 -65
  175. package/src/serve/routes/clipper.ts +64 -1
  176. package/src/serve/routes/mcp.ts +6 -0
  177. package/src/serve/routes/traces.ts +27 -4
  178. package/src/serve/server.ts +118 -7
  179. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  180. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  181. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  182. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  183. package/src/store/migrations/index.ts +8 -0
  184. package/src/store/retrieval-trace-codec.ts +15 -0
  185. package/src/store/sqlite/adapter.ts +284 -14
  186. package/src/store/sqlite/change-journal-store.ts +41 -2
  187. package/src/store/sqlite/egress-audit-store.ts +485 -0
  188. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  189. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  190. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  191. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  192. package/src/store/types.ts +166 -1
  193. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
  194. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
  195. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -0,0 +1,440 @@
1
+ import {
2
+ EGRESS_POLICIES,
3
+ EGRESS_POLICY_SOURCES,
4
+ type EgressPolicy,
5
+ type EgressPolicySource,
6
+ } from "../config/types";
7
+
8
+ export const EGRESS_ACTIONS = [
9
+ "retrieve",
10
+ "serve",
11
+ "publish",
12
+ "remote_inference",
13
+ "export",
14
+ "clip_write",
15
+ ] as const;
16
+ export type EgressAction = (typeof EGRESS_ACTIONS)[number];
17
+
18
+ export const EGRESS_DESTINATION_ZONES = [
19
+ "local_process",
20
+ "loopback",
21
+ "lan",
22
+ "remote",
23
+ ] as const;
24
+ export type EgressDestinationZone = (typeof EGRESS_DESTINATION_ZONES)[number];
25
+
26
+ /**
27
+ * Source and derived classes retain the same collection-owned policy.
28
+ * Transformation never declassifies indexed content.
29
+ */
30
+ export const EGRESS_CONTENT_CLASSES = [
31
+ "source",
32
+ "snippet",
33
+ "metadata",
34
+ "attachment",
35
+ "embedding",
36
+ "capsule",
37
+ "audit_log",
38
+ "retrieval_trace",
39
+ ] as const;
40
+ export type EgressContentClass = (typeof EGRESS_CONTENT_CLASSES)[number];
41
+
42
+ /** Hard cap for one decision and its bounded audit projection. */
43
+ export const EGRESS_MAX_COLLECTIONS = 64;
44
+
45
+ export const EGRESS_REASON_CODES = [
46
+ "LOCAL_DESTINATION",
47
+ "LAN_POLICY_AUTHENTICATED",
48
+ "REMOTE_POLICY_AUTHENTICATED",
49
+ "INVALID_INPUT",
50
+ "NO_COLLECTION_POLICY",
51
+ "COLLECTION_LIMIT_EXCEEDED",
52
+ "INVALID_COLLECTION",
53
+ "UNKNOWN_ACTION",
54
+ "UNKNOWN_DESTINATION",
55
+ "ACTION_DESTINATION_MISMATCH",
56
+ "UNKNOWN_CONTENT_CLASS",
57
+ "UNKNOWN_POLICY",
58
+ "UNKNOWN_POLICY_SOURCE",
59
+ "INVALID_POLICY_SOURCE_PAIR",
60
+ "INVALID_CALLER",
61
+ "CALLER_NOT_AUTHORIZED",
62
+ "AUTHENTICATION_REQUIRED",
63
+ "POLICY_LOCAL_ONLY",
64
+ "POLICY_LAN_ONLY",
65
+ ] as const;
66
+ export type EgressReasonCode = (typeof EGRESS_REASON_CODES)[number];
67
+
68
+ export interface CollectionEgressState {
69
+ /** Stable configured collection identifier; never a filesystem path. */
70
+ collection: string;
71
+ policy: EgressPolicy;
72
+ source: EgressPolicySource;
73
+ }
74
+
75
+ export interface EgressCallerContext {
76
+ /**
77
+ * Transport or feature authentication succeeded. Network destinations
78
+ * always require this; policy cannot make authentication optional.
79
+ */
80
+ authenticated: boolean;
81
+ /**
82
+ * The surrounding authorization layer permits this operation. A false value
83
+ * narrows access even for a policy that would otherwise allow it.
84
+ */
85
+ operationAuthorized: boolean;
86
+ }
87
+
88
+ export interface EgressDestination {
89
+ zone: EgressDestinationZone;
90
+ /**
91
+ * Optional classifier inputs are deliberately excluded from decisions and
92
+ * audit metadata so hosts, provider names, and credentials cannot leak.
93
+ */
94
+ host?: string;
95
+ provider?: string;
96
+ }
97
+
98
+ export interface EgressEvaluationInput {
99
+ collections: readonly CollectionEgressState[];
100
+ action: EgressAction;
101
+ destination: EgressDestination;
102
+ caller: EgressCallerContext;
103
+ contentClass: EgressContentClass;
104
+ }
105
+
106
+ export interface EgressDecisionMetadata {
107
+ action: EgressAction | "unknown";
108
+ destinationZone: EgressDestinationZone | "unknown";
109
+ contentClass: EgressContentClass | "unknown";
110
+ collectionCount: number;
111
+ collections: readonly string[];
112
+ effectivePolicy: EgressPolicy | "unknown";
113
+ effectivePolicySource: EgressPolicySource | "mixed" | "unknown";
114
+ callerAuthenticated: boolean | null;
115
+ callerOperationAuthorized: boolean | null;
116
+ }
117
+
118
+ export interface EgressDecision {
119
+ allowed: boolean;
120
+ code: "EGRESS_ALLOWED" | "EGRESS_DENIED";
121
+ reason: EgressReasonCode;
122
+ audit: EgressDecisionMetadata;
123
+ }
124
+
125
+ export interface EgressPolicyPort {
126
+ evaluate(input: EgressEvaluationInput): EgressDecision;
127
+ }
128
+
129
+ interface RuntimeCollectionEgressState {
130
+ collection: unknown;
131
+ policy: unknown;
132
+ source: unknown;
133
+ }
134
+
135
+ interface EgressEvaluationSnapshot {
136
+ collections: RuntimeCollectionEgressState[];
137
+ action: unknown;
138
+ destinationZone: unknown;
139
+ callerAuthenticated: unknown;
140
+ callerOperationAuthorized: unknown;
141
+ contentClass: unknown;
142
+ }
143
+
144
+ type EgressSnapshotResult =
145
+ | { kind: "invalid" }
146
+ | { kind: "limit_exceeded" }
147
+ | { kind: "snapshot"; value: EgressEvaluationSnapshot };
148
+
149
+ const ACTIONS = new Set<unknown>(EGRESS_ACTIONS);
150
+ const DESTINATION_ZONES = new Set<unknown>(EGRESS_DESTINATION_ZONES);
151
+ const CONTENT_CLASSES = new Set<unknown>(EGRESS_CONTENT_CLASSES);
152
+ const POLICIES = new Set<unknown>(EGRESS_POLICIES);
153
+ const POLICY_SOURCES = new Set<unknown>(EGRESS_POLICY_SOURCES);
154
+ const COLLECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
155
+
156
+ const POLICY_ORDER: Record<EgressPolicy, number> = {
157
+ local_only: 0,
158
+ lan: 1,
159
+ remote: 2,
160
+ };
161
+
162
+ const isAction = (value: unknown): value is EgressAction => ACTIONS.has(value);
163
+ const isDestinationZone = (value: unknown): value is EgressDestinationZone =>
164
+ DESTINATION_ZONES.has(value);
165
+ const isContentClass = (value: unknown): value is EgressContentClass =>
166
+ CONTENT_CLASSES.has(value);
167
+ const isPolicy = (value: unknown): value is EgressPolicy => POLICIES.has(value);
168
+ const isPolicySource = (value: unknown): value is EgressPolicySource =>
169
+ POLICY_SOURCES.has(value);
170
+
171
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
172
+ typeof value === "object" && value !== null;
173
+
174
+ function invalidInputAudit(): EgressDecisionMetadata {
175
+ return {
176
+ action: "unknown",
177
+ destinationZone: "unknown",
178
+ contentClass: "unknown",
179
+ collectionCount: 0,
180
+ collections: [],
181
+ effectivePolicy: "unknown",
182
+ effectivePolicySource: "unknown",
183
+ callerAuthenticated: null,
184
+ callerOperationAuthorized: null,
185
+ };
186
+ }
187
+
188
+ function limitExceededAudit(): EgressDecisionMetadata {
189
+ return {
190
+ ...invalidInputAudit(),
191
+ collectionCount: EGRESS_MAX_COLLECTIONS + 1,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Read untrusted object graphs once into bounded plain data. Any throwing
197
+ * getter or revoked proxy escapes to the evaluator's fixed fail-closed catch.
198
+ */
199
+ function snapshotInput(input: unknown): EgressSnapshotResult {
200
+ if (!isRecord(input)) return { kind: "invalid" };
201
+
202
+ const rawCollections = input.collections;
203
+ if (!Array.isArray(rawCollections)) return { kind: "invalid" };
204
+ const collectionCount = rawCollections.length;
205
+ if (collectionCount > EGRESS_MAX_COLLECTIONS) {
206
+ return { kind: "limit_exceeded" };
207
+ }
208
+
209
+ const collections: RuntimeCollectionEgressState[] = [];
210
+ for (let index = 0; index < collectionCount; index += 1) {
211
+ const collection = rawCollections[index];
212
+ if (!isRecord(collection)) {
213
+ collections.push({
214
+ collection: undefined,
215
+ policy: undefined,
216
+ source: undefined,
217
+ });
218
+ continue;
219
+ }
220
+ collections.push({
221
+ collection: collection.collection,
222
+ policy: collection.policy,
223
+ source: collection.source,
224
+ });
225
+ }
226
+
227
+ const destination = input.destination;
228
+ const caller = input.caller;
229
+ return {
230
+ kind: "snapshot",
231
+ value: {
232
+ collections,
233
+ action: input.action,
234
+ destinationZone: isRecord(destination) ? destination.zone : undefined,
235
+ callerAuthenticated: isRecord(caller) ? caller.authenticated : undefined,
236
+ callerOperationAuthorized: isRecord(caller)
237
+ ? caller.operationAuthorized
238
+ : undefined,
239
+ contentClass: input.contentClass,
240
+ },
241
+ };
242
+ }
243
+
244
+ function snapshotAudit(
245
+ snapshot: EgressEvaluationSnapshot
246
+ ): EgressDecisionMetadata {
247
+ const collections = snapshot.collections
248
+ .map(({ collection }) =>
249
+ typeof collection === "string" && COLLECTION_NAME_PATTERN.test(collection)
250
+ ? collection
251
+ : "unknown"
252
+ )
253
+ .sort();
254
+ return {
255
+ action: isAction(snapshot.action) ? snapshot.action : "unknown",
256
+ destinationZone: isDestinationZone(snapshot.destinationZone)
257
+ ? snapshot.destinationZone
258
+ : "unknown",
259
+ contentClass: isContentClass(snapshot.contentClass)
260
+ ? snapshot.contentClass
261
+ : "unknown",
262
+ collectionCount: snapshot.collections.length,
263
+ collections,
264
+ effectivePolicy: "unknown",
265
+ effectivePolicySource: "unknown",
266
+ callerAuthenticated:
267
+ typeof snapshot.callerAuthenticated === "boolean"
268
+ ? snapshot.callerAuthenticated
269
+ : null,
270
+ callerOperationAuthorized:
271
+ typeof snapshot.callerOperationAuthorized === "boolean"
272
+ ? snapshot.callerOperationAuthorized
273
+ : null,
274
+ };
275
+ }
276
+
277
+ function decision(
278
+ allowed: boolean,
279
+ reason: EgressReasonCode,
280
+ audit: EgressDecisionMetadata
281
+ ): EgressDecision {
282
+ return {
283
+ allowed,
284
+ code: allowed ? "EGRESS_ALLOWED" : "EGRESS_DENIED",
285
+ reason,
286
+ audit,
287
+ };
288
+ }
289
+
290
+ function effectiveCollectionPolicy(
291
+ collections: readonly CollectionEgressState[]
292
+ ): {
293
+ policy: EgressPolicy;
294
+ source: EgressPolicySource | "mixed";
295
+ } {
296
+ let policy: EgressPolicy = "remote";
297
+ for (const collection of collections) {
298
+ if (POLICY_ORDER[collection.policy] < POLICY_ORDER[policy]) {
299
+ policy = collection.policy;
300
+ }
301
+ }
302
+
303
+ const restrictiveSources = new Set(
304
+ collections
305
+ .filter((collection) => collection.policy === policy)
306
+ .map((collection) => collection.source)
307
+ );
308
+ const source =
309
+ restrictiveSources.size === 1
310
+ ? (restrictiveSources.values().next().value ?? "mixed")
311
+ : "mixed";
312
+ return { policy, source };
313
+ }
314
+
315
+ function actionAllowsDestination(
316
+ action: EgressAction,
317
+ destination: EgressDestinationZone
318
+ ): boolean {
319
+ switch (action) {
320
+ case "publish":
321
+ return destination === "remote";
322
+ case "remote_inference":
323
+ return destination !== "local_process";
324
+ case "serve":
325
+ return destination !== "local_process";
326
+ case "export":
327
+ return destination !== "loopback";
328
+ case "retrieve":
329
+ case "clip_write":
330
+ return true;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Evaluate one content-transfer boundary. Runtime guards intentionally accept
336
+ * malformed JavaScript callers fail-closed even though TypeScript callers get
337
+ * the narrower contract.
338
+ */
339
+ function evaluateSnapshot(snapshot: EgressEvaluationSnapshot): EgressDecision {
340
+ const audit = snapshotAudit(snapshot);
341
+ if (snapshot.collections.length === 0) {
342
+ return decision(false, "NO_COLLECTION_POLICY", audit);
343
+ }
344
+ if (!isAction(snapshot.action)) {
345
+ return decision(false, "UNKNOWN_ACTION", audit);
346
+ }
347
+ if (!isDestinationZone(snapshot.destinationZone)) {
348
+ return decision(false, "UNKNOWN_DESTINATION", audit);
349
+ }
350
+ if (!actionAllowsDestination(snapshot.action, snapshot.destinationZone)) {
351
+ return decision(false, "ACTION_DESTINATION_MISMATCH", audit);
352
+ }
353
+ if (!isContentClass(snapshot.contentClass)) {
354
+ return decision(false, "UNKNOWN_CONTENT_CLASS", audit);
355
+ }
356
+ if (
357
+ typeof snapshot.callerAuthenticated !== "boolean" ||
358
+ typeof snapshot.callerOperationAuthorized !== "boolean"
359
+ ) {
360
+ return decision(false, "INVALID_CALLER", audit);
361
+ }
362
+
363
+ const collections: CollectionEgressState[] = [];
364
+ const collectionNames = new Set<string>();
365
+ for (const collection of snapshot.collections) {
366
+ if (
367
+ typeof collection.collection !== "string" ||
368
+ !COLLECTION_NAME_PATTERN.test(collection.collection)
369
+ ) {
370
+ return decision(false, "INVALID_COLLECTION", audit);
371
+ }
372
+ if (!isPolicy(collection.policy)) {
373
+ return decision(false, "UNKNOWN_POLICY", audit);
374
+ }
375
+ if (!isPolicySource(collection.source)) {
376
+ return decision(false, "UNKNOWN_POLICY_SOURCE", audit);
377
+ }
378
+ if (
379
+ collection.source !== "explicit" &&
380
+ collection.policy !== "local_only"
381
+ ) {
382
+ return decision(false, "INVALID_POLICY_SOURCE_PAIR", audit);
383
+ }
384
+ if (collectionNames.has(collection.collection)) {
385
+ return decision(false, "INVALID_COLLECTION", audit);
386
+ }
387
+ collectionNames.add(collection.collection);
388
+ collections.push({
389
+ collection: collection.collection,
390
+ policy: collection.policy,
391
+ source: collection.source,
392
+ });
393
+ }
394
+
395
+ const effective = effectiveCollectionPolicy(collections);
396
+ audit.effectivePolicy = effective.policy;
397
+ audit.effectivePolicySource = effective.source;
398
+
399
+ if (!snapshot.callerOperationAuthorized) {
400
+ return decision(false, "CALLER_NOT_AUTHORIZED", audit);
401
+ }
402
+ if (
403
+ snapshot.destinationZone === "local_process" ||
404
+ snapshot.destinationZone === "loopback"
405
+ ) {
406
+ return decision(true, "LOCAL_DESTINATION", audit);
407
+ }
408
+ if (effective.policy === "local_only") {
409
+ return decision(false, "POLICY_LOCAL_ONLY", audit);
410
+ }
411
+ if (snapshot.destinationZone === "remote" && effective.policy === "lan") {
412
+ return decision(false, "POLICY_LAN_ONLY", audit);
413
+ }
414
+ if (!snapshot.callerAuthenticated) {
415
+ return decision(false, "AUTHENTICATION_REQUIRED", audit);
416
+ }
417
+ if (snapshot.destinationZone === "lan") {
418
+ return decision(true, "LAN_POLICY_AUTHENTICATED", audit);
419
+ }
420
+ return decision(true, "REMOTE_POLICY_AUTHENTICATED", audit);
421
+ }
422
+
423
+ export function evaluateEgressPolicy(input: unknown): EgressDecision {
424
+ try {
425
+ const result = snapshotInput(input);
426
+ if (result.kind === "invalid") {
427
+ return decision(false, "INVALID_INPUT", invalidInputAudit());
428
+ }
429
+ if (result.kind === "limit_exceeded") {
430
+ return decision(false, "COLLECTION_LIMIT_EXCEEDED", limitExceededAudit());
431
+ }
432
+ return evaluateSnapshot(result.value);
433
+ } catch {
434
+ return decision(false, "INVALID_INPUT", invalidInputAudit());
435
+ }
436
+ }
437
+
438
+ export const defaultEgressPolicyPort: EgressPolicyPort = Object.freeze({
439
+ evaluate: evaluateEgressPolicy,
440
+ });