@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,126 @@
1
+ /** Collection egress policy management commands. */
2
+
3
+ import type { EgressPolicy } from "../../../config/types";
4
+ import type {
5
+ EgressAction,
6
+ EgressContentClass,
7
+ EgressDestinationZone,
8
+ } from "../../../core/egress-policy";
9
+
10
+ import { loadConfig } from "../../../config";
11
+ import { projectCollectionEgressPolicy } from "../../../core/collection-egress-policy-projection";
12
+ import { CollectionEgressPolicyService } from "../../../core/collection-egress-policy-service";
13
+ import { applyConfigChange } from "../../../core/config-mutation";
14
+ import { CliError } from "../../errors";
15
+ import { initStore } from "../shared";
16
+
17
+ interface PolicyOptions {
18
+ configPath?: string;
19
+ indexName?: string;
20
+ }
21
+
22
+ const writeJson = (value: unknown): void => {
23
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
24
+ };
25
+
26
+ export const collectionPolicyGet = async (
27
+ name: string,
28
+ options: PolicyOptions = {}
29
+ ): Promise<void> => {
30
+ const loaded = await loadConfig(options.configPath);
31
+ if (!loaded.ok) throw new CliError("RUNTIME", loaded.error.message);
32
+ const state = new CollectionEgressPolicyService({
33
+ getConfig: () => loaded.value,
34
+ }).get(name);
35
+ if (!state.ok) throw new CliError("VALIDATION", state.error);
36
+ writeJson(state.value);
37
+ };
38
+
39
+ export const collectionPolicySet = async (
40
+ name: string,
41
+ policy: EgressPolicy,
42
+ options: PolicyOptions & { confirmRelaxation?: string } = {}
43
+ ): Promise<void> => {
44
+ const initialized = await initStore({
45
+ configPath: options.configPath,
46
+ indexName: options.indexName,
47
+ });
48
+ if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
49
+ let config = initialized.config;
50
+ try {
51
+ const currentResult = new CollectionEgressPolicyService({
52
+ getConfig: () => config,
53
+ }).get(name);
54
+ const service = new CollectionEgressPolicyService({
55
+ getConfig: () => config,
56
+ mutateConfig: async (mutate) => {
57
+ const result = await applyConfigChange(
58
+ {
59
+ store: initialized.store,
60
+ configPath: initialized.actualConfigPath,
61
+ onConfigUpdated: (next) => {
62
+ config = next;
63
+ },
64
+ projectStore: (store, next) =>
65
+ projectCollectionEgressPolicy(store, next, name),
66
+ },
67
+ mutate
68
+ );
69
+ return result;
70
+ },
71
+ });
72
+ const result = await service.set({
73
+ collection: name,
74
+ policy,
75
+ confirmation:
76
+ currentResult.ok && options.confirmRelaxation
77
+ ? {
78
+ collection: currentResult.value.collection,
79
+ currentPolicy: currentResult.value.effectivePolicy,
80
+ currentRevision: Number(options.confirmRelaxation),
81
+ targetPolicy: policy,
82
+ acknowledged: true,
83
+ }
84
+ : undefined,
85
+ });
86
+ if (!result.ok) {
87
+ throw new CliError("VALIDATION", result.error, {
88
+ details: { policyCode: result.code },
89
+ });
90
+ }
91
+ writeJson(result.value);
92
+ } finally {
93
+ await initialized.store.close();
94
+ }
95
+ };
96
+
97
+ export const collectionPolicyCheck = async (
98
+ input: {
99
+ collections?: string[];
100
+ action: EgressAction;
101
+ destinationZone: EgressDestinationZone;
102
+ contentClass: EgressContentClass;
103
+ authenticated: boolean;
104
+ authorized: boolean;
105
+ partial?: boolean;
106
+ },
107
+ options: PolicyOptions = {}
108
+ ): Promise<void> => {
109
+ const loaded = await loadConfig(options.configPath);
110
+ if (!loaded.ok) throw new CliError("RUNTIME", loaded.error.message);
111
+ const result = new CollectionEgressPolicyService({
112
+ getConfig: () => loaded.value,
113
+ }).explain({
114
+ collections: input.collections,
115
+ action: input.action,
116
+ destinationZone: input.destinationZone,
117
+ caller: {
118
+ authenticated: input.authenticated,
119
+ operationAuthorized: input.authorized,
120
+ },
121
+ contentClass: input.contentClass,
122
+ partialResults: input.partial ? "explicit" : "deny",
123
+ });
124
+ if (!result.ok) throw new CliError("VALIDATION", result.error);
125
+ writeJson(result.value);
126
+ };
@@ -149,7 +149,11 @@ export const contextBuild = async (
149
149
  const progress = showProgress
150
150
  ? createThrottledProgressRenderer(createProgressRenderer())
151
151
  : undefined;
152
+ const egressCollections = options.collections?.length
153
+ ? options.collections
154
+ : ("all" as const);
152
155
  const embedResult = await llm.createEmbeddingPort(embedUri, {
156
+ egressCollections,
153
157
  policy,
154
158
  onProgress: progress ? (value) => progress("embed", value) : undefined,
155
159
  });
@@ -165,6 +169,7 @@ export const contextBuild = async (
165
169
  }
166
170
  }
167
171
  const rerankResult = await llm.createRerankPort(rerankUri, {
172
+ egressCollections,
168
173
  policy,
169
174
  onProgress: progress ? (value) => progress("rerank", value) : undefined,
170
175
  });
@@ -3,6 +3,11 @@ import type { HttpGatewayOverrides } from "../../mcp/http-security";
3
3
  import type { BackgroundRuntimeResult } from "../../serve/background-runtime";
4
4
  import type { ResidentRuntime } from "../../serve/resident-runtime";
5
5
 
6
+ import {
7
+ enforceCollectionEgress,
8
+ EGRESS_DENIED_MESSAGE,
9
+ EgressDeniedError,
10
+ } from "../../core/egress-enforcement";
6
11
  import {
7
12
  DEFAULT_HTTP_GATEWAY_PORT,
8
13
  isHttpGatewayLoopbackBind,
@@ -42,6 +47,40 @@ function formatCollectionSyncSummary(result: CollectionSyncResult): string {
42
47
  return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
43
48
  }
44
49
 
50
+ export const authorizeDaemonStatus = async (
51
+ runtime: ResidentRuntime,
52
+ gateway: Awaited<ReturnType<typeof createMcpHttpGateway>>,
53
+ request: Request,
54
+ server: Parameters<typeof gateway.route>[1]
55
+ ): Promise<Response> => {
56
+ const authorization = await gateway.security.authorize(request, server);
57
+ if (!authorization.ok) return authorization.response;
58
+ try {
59
+ enforceCollectionEgress({
60
+ collections: runtime.config.collections,
61
+ action: "serve",
62
+ destinationZone: authorization.value.peerClassification.zone,
63
+ caller: {
64
+ authenticated: authorization.value.authenticated,
65
+ operationAuthorized: true,
66
+ },
67
+ contentClass: "metadata",
68
+ });
69
+ return handleResidentStatus(() => runtime.getStatus());
70
+ } catch (error) {
71
+ if (!(error instanceof EgressDeniedError)) throw error;
72
+ return Response.json(
73
+ {
74
+ error: "EGRESS_DENIED",
75
+ message: EGRESS_DENIED_MESSAGE,
76
+ reason: error.decision.reason,
77
+ audit: error.decision.audit,
78
+ },
79
+ { status: 403 }
80
+ );
81
+ }
82
+ };
83
+
45
84
  export function handleDaemonAppStatus(
46
85
  runtime: ResidentRuntime,
47
86
  host: string
@@ -161,9 +200,12 @@ export async function daemon(
161
200
  ),
162
201
  },
163
202
  "/api/resident/status": {
164
- GET: () =>
165
- handleResidentStatus(() =>
166
- (runtime as ResidentRuntime).getStatus()
203
+ GET: (request, peerServer) =>
204
+ authorizeDaemonStatus(
205
+ runtime as ResidentRuntime,
206
+ gateway as Awaited<ReturnType<typeof createMcpHttpGateway>>,
207
+ request,
208
+ peerServer
167
209
  ),
168
210
  },
169
211
  },
@@ -0,0 +1,68 @@
1
+ /** Local, content-free egress audit management commands. */
2
+
3
+ import { EgressAuditService } from "../../core/egress-audit";
4
+ import { CliError } from "../errors";
5
+ import { initStore } from "./shared";
6
+
7
+ interface AuditOptions {
8
+ configPath?: string;
9
+ indexName?: string;
10
+ }
11
+
12
+ const run = async (
13
+ options: AuditOptions,
14
+ operation: (service: EgressAuditService) => Promise<unknown>
15
+ ): Promise<void> => {
16
+ const initialized = await initStore({
17
+ configPath: options.configPath,
18
+ indexName: options.indexName,
19
+ syncConfig: false,
20
+ allowEmptyCollections: true,
21
+ });
22
+ if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
23
+ try {
24
+ const result = await operation(new EgressAuditService(initialized.store));
25
+ if (
26
+ typeof result === "object" &&
27
+ result !== null &&
28
+ "ok" in result &&
29
+ !(result as { ok: boolean }).ok
30
+ ) {
31
+ const failure = result as {
32
+ ok: false;
33
+ error: { code: string; message: string };
34
+ };
35
+ throw new CliError("RUNTIME", failure.error.message, {
36
+ details: { auditCode: failure.error.code },
37
+ });
38
+ }
39
+ const value =
40
+ typeof result === "object" && result !== null && "value" in result
41
+ ? (result as { value: unknown }).value
42
+ : result;
43
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
44
+ } finally {
45
+ await initialized.store.close();
46
+ }
47
+ };
48
+
49
+ export const egressAuditList = (
50
+ input: { limit?: number; cursor?: string },
51
+ options: AuditOptions = {}
52
+ ): Promise<void> => run(options, (service) => service.list(input));
53
+
54
+ export const egressAuditShow = (
55
+ auditId: string,
56
+ options: AuditOptions = {}
57
+ ): Promise<void> => run(options, (service) => service.show(auditId));
58
+
59
+ export const egressAuditStatus = (options: AuditOptions = {}): Promise<void> =>
60
+ run(options, (service) => service.status());
61
+
62
+ export const egressAuditDelete = (
63
+ auditId: string,
64
+ options: AuditOptions = {}
65
+ ): Promise<void> => run(options, (service) => service.delete(auditId));
66
+
67
+ export const egressAuditPurge = (options: AuditOptions = {}): Promise<void> =>
68
+ run(options, (service) => service.purge());
@@ -492,6 +492,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
492
492
  }
493
493
  await llm.getManager().dispose(modelUri);
494
494
  const recreated = await llm.createEmbeddingPort(modelUri, {
495
+ egressCollections: options.collection ? [options.collection] : "all",
495
496
  policy,
496
497
  onProgress: downloadProgress
497
498
  ? (progress) => downloadProgress("embed", progress)
@@ -508,6 +509,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
508
509
  return { ok: true as const, value: recreated.value };
509
510
  };
510
511
  const embedResult = await llm.createEmbeddingPort(modelUri, {
512
+ egressCollections: options.collection ? [options.collection] : "all",
511
513
  policy,
512
514
  onProgress: downloadProgress
513
515
  ? (progress) => downloadProgress("embed", progress)
@@ -195,6 +195,9 @@ export async function query(
195
195
  }
196
196
  traceSession = traceStart.value ?? undefined;
197
197
  const llm = new LlmAdapter(config);
198
+ const egressCollections = options.collection
199
+ ? [options.collection]
200
+ : ("all" as const);
198
201
 
199
202
  // Resolve download policy from env/flags
200
203
  const globals = getGlobals();
@@ -210,6 +213,7 @@ export async function query(
210
213
 
211
214
  // Create embedding port (for vector search)
212
215
  const embedResult = await llm.createEmbeddingPort(embedUri, {
216
+ egressCollections,
213
217
  policy,
214
218
  onProgress: downloadProgress
215
219
  ? (progress) => downloadProgress("embed", progress)
@@ -223,6 +227,7 @@ export async function query(
223
227
  // Skip when structured query modes are provided.
224
228
  if (expandUri) {
225
229
  const genResult = await llm.createExpansionPort(expandUri, {
230
+ egressCollections,
226
231
  policy,
227
232
  onProgress: downloadProgress
228
233
  ? (progress) => downloadProgress("expand", progress)
@@ -236,6 +241,7 @@ export async function query(
236
241
  // Create rerank port - optional
237
242
  if (rerankUri) {
238
243
  const rerankResult = await llm.createRerankPort(rerankUri, {
244
+ egressCollections,
239
245
  policy,
240
246
  onProgress: downloadProgress
241
247
  ? (progress) => downloadProgress("rerank", progress)
@@ -354,12 +360,16 @@ export async function queryDiagnose(
354
360
  const shouldCreateAnyModel =
355
361
  shouldCreateEmbeddingPort || !options.noExpand || !options.noRerank;
356
362
  const llm = shouldCreateAnyModel ? new LlmAdapter(config) : null;
363
+ const egressCollections = options.collection
364
+ ? [options.collection]
365
+ : ("all" as const);
357
366
 
358
367
  const embedUri = shouldCreateEmbeddingPort
359
368
  ? resolveModelUri(config, "embed", options.embedModel, options.collection)
360
369
  : null;
361
370
  if (embedUri && llm) {
362
371
  const embedResult = await llm.createEmbeddingPort(embedUri, {
372
+ egressCollections,
363
373
  policy,
364
374
  onProgress: downloadProgress
365
375
  ? (progress) => downloadProgress("embed", progress)
@@ -378,6 +388,7 @@ export async function queryDiagnose(
378
388
  options.collection
379
389
  );
380
390
  const genResult = await llm.createExpansionPort(expandUri, {
391
+ egressCollections,
381
392
  policy,
382
393
  onProgress: downloadProgress
383
394
  ? (progress) => downloadProgress("expand", progress)
@@ -396,6 +407,7 @@ export async function queryDiagnose(
396
407
  options.collection
397
408
  );
398
409
  const rerankResult = await llm.createRerankPort(rerankUri, {
410
+ egressCollections,
399
411
  policy,
400
412
  onProgress: downloadProgress
401
413
  ? (progress) => downloadProgress("rerank", progress)
@@ -91,16 +91,26 @@ export const traceReplay = async (
91
91
  const policy = resolveDownloadPolicy(process.env, {
92
92
  offline: options.offline,
93
93
  });
94
+ const egressCollections = "all" as const;
94
95
  if (embedUri) {
95
- const created = await llm.createEmbeddingPort(embedUri, { policy });
96
+ const created = await llm.createEmbeddingPort(embedUri, {
97
+ egressCollections,
98
+ policy,
99
+ });
96
100
  if (created.ok) embedPort = created.value;
97
101
  }
98
102
  if (expandUri) {
99
- const created = await llm.createExpansionPort(expandUri, { policy });
103
+ const created = await llm.createExpansionPort(expandUri, {
104
+ egressCollections,
105
+ policy,
106
+ });
100
107
  if (created.ok) expandPort = created.value;
101
108
  }
102
109
  if (rerankUri) {
103
- const created = await llm.createRerankPort(rerankUri, { policy });
110
+ const created = await llm.createRerankPort(rerankUri, {
111
+ egressCollections,
112
+ policy,
113
+ });
104
114
  if (created.ok) rerankPort = created.value;
105
115
  }
106
116
  if (embedPort && embedUri) {
@@ -128,7 +128,9 @@ export async function vsearch(
128
128
 
129
129
  // Create LLM adapter for embeddings
130
130
  const llm = new LlmAdapter(config);
131
- const embedResult = await llm.createEmbeddingPort(modelUri);
131
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
132
+ egressCollections: options.collection ? [options.collection] : "all",
133
+ });
132
134
  if (!embedResult.ok) {
133
135
  await traceSession?.finish("failed");
134
136
  return { success: false, error: embedResult.error.message };
@@ -2105,6 +2105,129 @@ function wireManagementCommands(program: Command): void {
2105
2105
  });
2106
2106
  });
2107
2107
 
2108
+ const collectionPolicyCmd = collectionCmd
2109
+ .command("policy")
2110
+ .description("Inspect and change collection egress policy");
2111
+
2112
+ collectionPolicyCmd
2113
+ .command("get <name>")
2114
+ .description("Show effective policy, source, and confirmation revision")
2115
+ .action(async (name: string) => {
2116
+ const { collectionPolicyGet } = await import("./commands/collection");
2117
+ await collectionPolicyGet(name, { configPath: getGlobals().config });
2118
+ });
2119
+
2120
+ collectionPolicyCmd
2121
+ .command("set <name> <policy>")
2122
+ .description("Set local_only, lan, or remote policy")
2123
+ .option(
2124
+ "--confirm-relaxation <current-revision>",
2125
+ "confirm relaxation against the current policy revision"
2126
+ )
2127
+ .action(
2128
+ async (
2129
+ name: string,
2130
+ policy: string,
2131
+ cmdOpts: Record<string, unknown>
2132
+ ) => {
2133
+ if (!["local_only", "lan", "remote"].includes(policy)) {
2134
+ throw new CliError(
2135
+ "VALIDATION",
2136
+ "Policy must be local_only, lan, or remote"
2137
+ );
2138
+ }
2139
+ const { collectionPolicySet } = await import("./commands/collection");
2140
+ await collectionPolicySet(
2141
+ name,
2142
+ policy as "local_only" | "lan" | "remote",
2143
+ {
2144
+ configPath: getGlobals().config,
2145
+ confirmRelaxation: cmdOpts.confirmRelaxation as string | undefined,
2146
+ }
2147
+ );
2148
+ }
2149
+ );
2150
+
2151
+ collectionPolicyCmd
2152
+ .command("check")
2153
+ .description("Check or explain an exact egress decision without acting")
2154
+ .requiredOption("--action <action>", "egress action")
2155
+ .requiredOption(
2156
+ "--destination <zone>",
2157
+ "local_process, loopback, lan, remote"
2158
+ )
2159
+ .requiredOption("--content-class <class>", "content class")
2160
+ .option(
2161
+ "-c, --collection <name>",
2162
+ "collection scope (repeatable)",
2163
+ collectRepeatableValue,
2164
+ []
2165
+ )
2166
+ .option("--authenticated", "caller authentication succeeded")
2167
+ .option("--authorized", "operation authorization succeeded")
2168
+ .option("--partial", "explicitly permit partial collection results")
2169
+ .option(
2170
+ "--explain-egress",
2171
+ "return the same decision contract without performing an action"
2172
+ )
2173
+ .action(async (cmdOpts: Record<string, unknown>) => {
2174
+ const { collectionPolicyCheck } = await import("./commands/collection");
2175
+ await collectionPolicyCheck(
2176
+ {
2177
+ collections: cmdOpts.collection as string[],
2178
+ action: cmdOpts.action as Parameters<
2179
+ typeof collectionPolicyCheck
2180
+ >[0]["action"],
2181
+ destinationZone: cmdOpts.destination as Parameters<
2182
+ typeof collectionPolicyCheck
2183
+ >[0]["destinationZone"],
2184
+ contentClass: cmdOpts.contentClass as Parameters<
2185
+ typeof collectionPolicyCheck
2186
+ >[0]["contentClass"],
2187
+ authenticated: Boolean(cmdOpts.authenticated),
2188
+ authorized: Boolean(cmdOpts.authorized),
2189
+ partial: Boolean(cmdOpts.partial),
2190
+ },
2191
+ { configPath: getGlobals().config }
2192
+ );
2193
+ });
2194
+
2195
+ const egressAuditCmd = program
2196
+ .command("egress-audit")
2197
+ .description("Manage local content-free egress audit receipts");
2198
+ egressAuditCmd
2199
+ .command("list")
2200
+ .option("--limit <count>", "page size", Number)
2201
+ .option("--cursor <cursor>", "opaque next-page cursor")
2202
+ .action(async (cmdOpts: Record<string, unknown>) => {
2203
+ const { egressAuditList } = await import("./commands/egress-audit");
2204
+ await egressAuditList(
2205
+ {
2206
+ limit: cmdOpts.limit as number | undefined,
2207
+ cursor: cmdOpts.cursor as string | undefined,
2208
+ },
2209
+ { configPath: getGlobals().config }
2210
+ );
2211
+ });
2212
+ egressAuditCmd.command("show <audit-id>").action(async (auditId: string) => {
2213
+ const { egressAuditShow } = await import("./commands/egress-audit");
2214
+ await egressAuditShow(auditId, { configPath: getGlobals().config });
2215
+ });
2216
+ egressAuditCmd.command("status").action(async () => {
2217
+ const { egressAuditStatus } = await import("./commands/egress-audit");
2218
+ await egressAuditStatus({ configPath: getGlobals().config });
2219
+ });
2220
+ egressAuditCmd
2221
+ .command("delete <audit-id>")
2222
+ .action(async (auditId: string) => {
2223
+ const { egressAuditDelete } = await import("./commands/egress-audit");
2224
+ await egressAuditDelete(auditId, { configPath: getGlobals().config });
2225
+ });
2226
+ egressAuditCmd.command("purge").action(async () => {
2227
+ const { egressAuditPurge } = await import("./commands/egress-audit");
2228
+ await egressAuditPurge({ configPath: getGlobals().config });
2229
+ });
2230
+
2108
2231
  // context subcommands
2109
2232
  const contextCmd = program
2110
2233
  .command("context")
@@ -59,6 +59,7 @@ export {
59
59
  CollectionSchema,
60
60
  type Config,
61
61
  ConfigSchema,
62
+ DEFAULT_EGRESS_POLICY,
62
63
  HttpGatewayConfigSchema,
63
64
  HttpGatewayLimitsSchema,
64
65
  CONTENT_TYPE_GRAPH_HINTS,
@@ -70,11 +71,19 @@ export {
70
71
  DEFAULT_EXCLUDES,
71
72
  DEFAULT_FTS_TOKENIZER,
72
73
  DEFAULT_PATTERN,
74
+ type EffectiveConfiguredEgressPolicy,
75
+ EGRESS_POLICIES,
76
+ EGRESS_POLICY_SOURCES,
77
+ type EgressPolicy,
78
+ EgressPolicySchema,
79
+ type EgressPolicySource,
80
+ EgressPolicySourceSchema,
73
81
  FTS_TOKENIZERS,
74
82
  type FtsTokenizer,
75
83
  getCollectionFromScope,
76
84
  isValidLanguageHint,
77
85
  parseScope,
86
+ resolveConfiguredEgressPolicy,
78
87
  type ProjectProfileBinding,
79
88
  ProjectProfileBindingSchema,
80
89
  type ScopeType,
@@ -48,6 +48,28 @@ export type FtsTokenizer = (typeof FTS_TOKENIZERS)[number];
48
48
  /** Default FTS tokenizer - snowball english for multilingual stemming */
49
49
  export const DEFAULT_FTS_TOKENIZER: FtsTokenizer = "snowball english";
50
50
 
51
+ /** Collection-owned boundary for where indexed content may travel. */
52
+ export const EGRESS_POLICIES = ["local_only", "lan", "remote"] as const;
53
+ export const EgressPolicySchema = z.enum(EGRESS_POLICIES);
54
+ export type EgressPolicy = z.infer<typeof EgressPolicySchema>;
55
+
56
+ /** Missing policy is always interpreted as the fail-closed local-only default. */
57
+ export const DEFAULT_EGRESS_POLICY: EgressPolicy = "local_only";
58
+
59
+ /** Provenance for an effective collection egress policy. */
60
+ export const EGRESS_POLICY_SOURCES = [
61
+ "explicit",
62
+ "config_default",
63
+ "legacy_default",
64
+ ] as const;
65
+ export const EgressPolicySourceSchema = z.enum(EGRESS_POLICY_SOURCES);
66
+ export type EgressPolicySource = z.infer<typeof EgressPolicySourceSchema>;
67
+
68
+ export interface EffectiveConfiguredEgressPolicy {
69
+ policy: EgressPolicy;
70
+ source: Extract<EgressPolicySource, "explicit" | "config_default">;
71
+ }
72
+
51
73
  /**
52
74
  * BCP-47 language tag pattern (simplified, case-insensitive).
53
75
  * Matches: en, de, fr, zh-CN, zh-Hans, und, en-US, etc.
@@ -104,6 +126,18 @@ export const CollectionSchema = z.object({
104
126
  })
105
127
  .optional(),
106
128
 
129
+ /**
130
+ * Explicit collection egress boundary. Absence remains valid for legacy
131
+ * configs and resolves to local_only rather than relaxing access.
132
+ */
133
+ egressPolicy: EgressPolicySchema.optional(),
134
+
135
+ /**
136
+ * Durable monotonic token for egress policy/source changes. Legacy configs
137
+ * start at zero; guarded mutations advance it under the config write lock.
138
+ */
139
+ egressPolicyRevision: z.number().int().nonnegative().optional(),
140
+
107
141
  /** Optional per-collection model overrides */
108
142
  models: z
109
143
  .object({
@@ -133,6 +167,22 @@ export const CollectionSchema = z.object({
133
167
  export type Collection = z.infer<typeof CollectionSchema>;
134
168
  export type CollectionModelOverrides = NonNullable<Collection["models"]>;
135
169
 
170
+ /** Resolve config input without erasing whether the user made an explicit choice. */
171
+ export function resolveConfiguredEgressPolicy(
172
+ collection: Pick<Collection, "egressPolicy">
173
+ ): EffectiveConfiguredEgressPolicy {
174
+ if (collection.egressPolicy === undefined) {
175
+ return {
176
+ policy: DEFAULT_EGRESS_POLICY,
177
+ source: "config_default",
178
+ };
179
+ }
180
+ return {
181
+ policy: collection.egressPolicy,
182
+ source: "explicit",
183
+ };
184
+ }
185
+
136
186
  // ─────────────────────────────────────────────────────────────────────────────
137
187
  // Project Affinity Input
138
188
  // ─────────────────────────────────────────────────────────────────────────────