@gmickel/gno 1.28.0 → 1.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +33 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +1 -1
  10. package/spec/cli.md +41 -1
  11. package/spec/db/schema.sql +19 -0
  12. package/spec/mcp.md +66 -0
  13. package/spec/output-schemas/ask.schema.json +31 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  22. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  23. package/spec/output-schemas/record-import.schema.json +30 -0
  24. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  25. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  26. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  27. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  28. package/spec/output-schemas/search-results.schema.json +30 -0
  29. package/src/app/context-runtime-contract.ts +16 -7
  30. package/src/cli/commands/ask.ts +7 -0
  31. package/src/cli/commands/collection/index.ts +5 -0
  32. package/src/cli/commands/collection/policy.ts +126 -0
  33. package/src/cli/commands/context-build.ts +5 -0
  34. package/src/cli/commands/daemon.ts +45 -3
  35. package/src/cli/commands/egress-audit.ts +68 -0
  36. package/src/cli/commands/embed.ts +2 -0
  37. package/src/cli/commands/query.ts +12 -0
  38. package/src/cli/commands/replay.ts +13 -3
  39. package/src/cli/commands/vsearch.ts +3 -1
  40. package/src/cli/program.ts +123 -0
  41. package/src/config/index.ts +9 -0
  42. package/src/config/types.ts +50 -0
  43. package/src/core/browser-clip.ts +16 -1
  44. package/src/core/collection-egress-policy-projection.ts +28 -0
  45. package/src/core/collection-egress-policy-service.ts +443 -0
  46. package/src/core/collection-egress-policy-validation.ts +242 -0
  47. package/src/core/config-mutation.ts +31 -19
  48. package/src/core/context-capsule-schema.ts +111 -1
  49. package/src/core/context-capsule-validation.ts +4 -4
  50. package/src/core/context-capsule-verification.ts +5 -1
  51. package/src/core/context-capsule.ts +177 -132
  52. package/src/core/context-evidence.ts +86 -14
  53. package/src/core/destination-classifier.ts +402 -0
  54. package/src/core/egress-audit.ts +239 -0
  55. package/src/core/egress-authorization.ts +50 -0
  56. package/src/core/egress-enforcement.ts +264 -0
  57. package/src/core/egress-policy.ts +440 -0
  58. package/src/core/egress-provenance.ts +336 -0
  59. package/src/core/job-manager.ts +37 -0
  60. package/src/core/network-boundary-inventory.ts +261 -0
  61. package/src/core/retrieval-qrels.ts +6 -0
  62. package/src/core/retrieval-replay.ts +18 -4
  63. package/src/core/retrieval-trace-export.ts +23 -4
  64. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  65. package/src/core/retrieval-trace-management-types.ts +3 -0
  66. package/src/core/retrieval-trace-management.ts +9 -1
  67. package/src/core/retrieval-trace-session.ts +94 -0
  68. package/src/core/retrieval-trace.ts +22 -0
  69. package/src/ingestion/record-adapter.ts +12 -1
  70. package/src/ingestion/record-container.ts +38 -22
  71. package/src/ingestion/sync.ts +15 -0
  72. package/src/ingestion/types.ts +2 -0
  73. package/src/llm/errors.ts +10 -0
  74. package/src/llm/http-inference.ts +175 -0
  75. package/src/llm/http-policy.ts +537 -0
  76. package/src/llm/httpEmbedding.ts +47 -28
  77. package/src/llm/httpGeneration.ts +31 -18
  78. package/src/llm/httpRerank.ts +30 -18
  79. package/src/llm/index.ts +1 -0
  80. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  81. package/src/llm/pinned-http-connection.ts +177 -0
  82. package/src/mcp/context.ts +53 -1
  83. package/src/mcp/http-egress.ts +220 -0
  84. package/src/mcp/http-security.ts +32 -21
  85. package/src/mcp/http-session.ts +13 -2
  86. package/src/mcp/http-transport.ts +96 -6
  87. package/src/mcp/sync-egress.ts +24 -0
  88. package/src/mcp/tools/add-collection.ts +4 -0
  89. package/src/mcp/tools/ask.ts +1 -1
  90. package/src/mcp/tools/context.ts +9 -2
  91. package/src/mcp/tools/egress.ts +247 -0
  92. package/src/mcp/tools/embed.ts +1 -0
  93. package/src/mcp/tools/index-cmd.ts +7 -0
  94. package/src/mcp/tools/index.ts +98 -0
  95. package/src/mcp/tools/query.ts +10 -0
  96. package/src/mcp/tools/remove-collection.ts +4 -0
  97. package/src/mcp/tools/sync.ts +6 -0
  98. package/src/mcp/tools/trace.ts +7 -1
  99. package/src/mcp/tools/vsearch.ts +1 -0
  100. package/src/pipeline/egress-lineage.ts +124 -0
  101. package/src/pipeline/hybrid.ts +14 -0
  102. package/src/pipeline/search.ts +12 -0
  103. package/src/pipeline/types.ts +3 -0
  104. package/src/pipeline/vsearch.ts +41 -20
  105. package/src/publish/artifact.ts +31 -3
  106. package/src/publish/export-service.ts +25 -0
  107. package/src/sdk/client.ts +119 -2
  108. package/src/sdk/embed.ts +1 -0
  109. package/src/sdk/types.ts +33 -0
  110. package/src/serve/clipper-capture.ts +5 -1
  111. package/src/serve/closed-json.ts +61 -0
  112. package/src/serve/config-sync.ts +4 -1
  113. package/src/serve/context.ts +1 -0
  114. package/src/serve/doc-events.ts +110 -39
  115. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  116. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  117. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  118. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  119. package/src/serve/public/globals.built.css +1 -1
  120. package/src/serve/public/pages/Collections.tsx +3 -0
  121. package/src/serve/resident-request.ts +80 -3
  122. package/src/serve/resident-runtime.ts +63 -1
  123. package/src/serve/routes/api.ts +174 -0
  124. package/src/serve/routes/clipper.ts +64 -1
  125. package/src/serve/routes/mcp.ts +6 -0
  126. package/src/serve/routes/traces.ts +27 -4
  127. package/src/serve/server.ts +118 -7
  128. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  129. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  130. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  131. package/src/store/migrations/index.ts +6 -0
  132. package/src/store/retrieval-trace-codec.ts +15 -0
  133. package/src/store/sqlite/adapter.ts +170 -7
  134. package/src/store/sqlite/change-journal-store.ts +41 -2
  135. package/src/store/sqlite/egress-audit-store.ts +485 -0
  136. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  137. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  138. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  139. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  140. package/src/store/types.ts +126 -1
  141. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
  142. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
  143. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -0,0 +1,336 @@
1
+ /** Canonical, content-free policy lineage for source and derived artifacts. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import type { EgressPolicy, EgressPolicySource } from "../config/types";
6
+
7
+ import { EgressPolicySchema, EgressPolicySourceSchema } from "../config/types";
8
+
9
+ export interface EgressLineageSource {
10
+ collection: string;
11
+ policy: EgressPolicy;
12
+ source: EgressPolicySource;
13
+ }
14
+
15
+ export interface EgressLineage {
16
+ effectivePolicy: EgressPolicy;
17
+ digest: string;
18
+ sources: EgressLineageSource[];
19
+ }
20
+
21
+ export type EgressLineageInput = Readonly<{
22
+ collection: string;
23
+ policy: EgressPolicy;
24
+ source: EgressPolicySource;
25
+ }>;
26
+
27
+ const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
28
+ const MAX_EGRESS_LINEAGE_SOURCES = 128;
29
+ const POLICY_RESTRICTIVENESS: Readonly<Record<EgressPolicy, number>> = {
30
+ local_only: 0,
31
+ lan: 1,
32
+ remote: 2,
33
+ };
34
+
35
+ export class EgressProvenanceError extends Error {
36
+ readonly code: "EMPTY_EGRESS_LINEAGE" | "INVALID_EGRESS_LINEAGE";
37
+
38
+ constructor(
39
+ code: "EMPTY_EGRESS_LINEAGE" | "INVALID_EGRESS_LINEAGE",
40
+ message: string
41
+ ) {
42
+ super(message);
43
+ this.name = "EgressProvenanceError";
44
+ this.code = code;
45
+ }
46
+ }
47
+
48
+ const compareCodeUnits = (left: string, right: string): number =>
49
+ left < right ? -1 : left > right ? 1 : 0;
50
+
51
+ const canonicalize = (value: unknown): unknown => {
52
+ if (Array.isArray(value)) return value.map(canonicalize);
53
+ if (value !== null && typeof value === "object") {
54
+ return Object.fromEntries(
55
+ Object.entries(value)
56
+ .sort(([left], [right]) => compareCodeUnits(left, right))
57
+ .map(([key, child]) => [key, canonicalize(child)])
58
+ );
59
+ }
60
+ return value;
61
+ };
62
+
63
+ const canonicalJson = (value: unknown): string =>
64
+ JSON.stringify(canonicalize(value));
65
+ const sha256Text = (value: string): string =>
66
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
67
+
68
+ const invalidLineageInput = (): EgressProvenanceError =>
69
+ new EgressProvenanceError(
70
+ "INVALID_EGRESS_LINEAGE",
71
+ `Source policy lineage must be a readable dense array with at most ${MAX_EGRESS_LINEAGE_SOURCES} entries`
72
+ );
73
+
74
+ const snapshotInputs = (
75
+ inputs: readonly EgressLineageInput[]
76
+ ): readonly EgressLineageInput[] => {
77
+ try {
78
+ if (!Array.isArray(inputs)) throw invalidLineageInput();
79
+ const { length } = inputs;
80
+ if (!Number.isSafeInteger(length) || length > MAX_EGRESS_LINEAGE_SOURCES) {
81
+ throw invalidLineageInput();
82
+ }
83
+ if (length === 0) {
84
+ throw new EgressProvenanceError(
85
+ "EMPTY_EGRESS_LINEAGE",
86
+ "Source policy lineage must contain at least one collection"
87
+ );
88
+ }
89
+
90
+ const snapshots: EgressLineageInput[] = [];
91
+ for (let index = 0; index < length; index += 1) {
92
+ if (!(index in inputs)) throw invalidLineageInput();
93
+ const input: unknown = inputs[index];
94
+ if (input === null || typeof input !== "object") {
95
+ throw invalidLineageInput();
96
+ }
97
+ const candidate = input as Partial<EgressLineageInput>;
98
+ snapshots.push({
99
+ collection: candidate.collection as string,
100
+ policy: candidate.policy as EgressPolicy,
101
+ source: candidate.source as EgressPolicySource,
102
+ });
103
+ }
104
+ return snapshots;
105
+ } catch (error) {
106
+ if (error instanceof EgressProvenanceError) throw error;
107
+ throw invalidLineageInput();
108
+ }
109
+ };
110
+
111
+ const canonicalSources = (
112
+ inputs: readonly EgressLineageInput[]
113
+ ): readonly EgressLineageSource[] => {
114
+ const snapshots = snapshotInputs(inputs);
115
+ const byCollection = new Map<string, EgressLineageSource>();
116
+ for (const input of snapshots) {
117
+ const collectionInput = input.collection;
118
+ const policyInput = input.policy;
119
+ const sourceInput = input.source;
120
+ if (
121
+ typeof collectionInput !== "string" ||
122
+ typeof policyInput !== "string" ||
123
+ typeof sourceInput !== "string"
124
+ ) {
125
+ throw new EgressProvenanceError(
126
+ "INVALID_EGRESS_LINEAGE",
127
+ "Source policy lineage contains an invalid collection or policy"
128
+ );
129
+ }
130
+ const collection = collectionInput.trim().toLowerCase();
131
+ const policy = EgressPolicySchema.safeParse(policyInput);
132
+ const source = EgressPolicySourceSchema.safeParse(sourceInput);
133
+ if (
134
+ collectionInput !== collection ||
135
+ !COLLECTION_PATTERN.test(collection) ||
136
+ !policy.success ||
137
+ !source.success ||
138
+ (source.data !== "explicit" && policy.data !== "local_only")
139
+ ) {
140
+ throw new EgressProvenanceError(
141
+ "INVALID_EGRESS_LINEAGE",
142
+ "Source policy lineage contains an invalid collection or policy"
143
+ );
144
+ }
145
+ const previous = byCollection.get(collection);
146
+ const current = {
147
+ collection,
148
+ policy: policy.data,
149
+ source: source.data,
150
+ };
151
+ if (previous) {
152
+ throw new EgressProvenanceError(
153
+ "INVALID_EGRESS_LINEAGE",
154
+ previous.policy === current.policy && previous.source === current.source
155
+ ? `Duplicate policy lineage for collection ${collection}`
156
+ : `Conflicting policy lineage for collection ${collection}`
157
+ );
158
+ }
159
+ byCollection.set(collection, current);
160
+ }
161
+ return Object.freeze(
162
+ [...byCollection.values()]
163
+ .sort((left, right) =>
164
+ compareCodeUnits(left.collection, right.collection)
165
+ )
166
+ .map((source) => Object.freeze(source))
167
+ );
168
+ };
169
+
170
+ /** Build deterministic lineage. Transformations can aggregate but never relax. */
171
+ export const createEgressLineage = (
172
+ inputs: readonly EgressLineageInput[]
173
+ ): EgressLineage => {
174
+ const sources = canonicalSources(inputs);
175
+ const effectivePolicy = sources.reduce<EgressPolicy>(
176
+ (mostRestrictive, source) =>
177
+ POLICY_RESTRICTIVENESS[source.policy] <
178
+ POLICY_RESTRICTIVENESS[mostRestrictive]
179
+ ? source.policy
180
+ : mostRestrictive,
181
+ "remote"
182
+ );
183
+ const digest = sha256Text(
184
+ canonicalJson({
185
+ effectivePolicy,
186
+ sources: sources.map(({ collection, policy, source }) => ({
187
+ collection,
188
+ policy,
189
+ source,
190
+ })),
191
+ })
192
+ );
193
+ return Object.freeze({ effectivePolicy, digest, sources }) as EgressLineage;
194
+ };
195
+
196
+ /**
197
+ * Canonically merge already-bounded lineage projections. Exact duplicates are
198
+ * collapsed here, before the strict provenance construction boundary.
199
+ */
200
+ export const mergeEgressLineages = (
201
+ lineages: readonly Pick<EgressLineage, "sources">[]
202
+ ): EgressLineage => {
203
+ const byCollection = new Map<string, EgressLineageInput>();
204
+ for (const lineage of lineages) {
205
+ for (const input of lineage.sources) {
206
+ const collection = input.collection;
207
+ const previous = byCollection.get(collection);
208
+ if (
209
+ previous &&
210
+ (previous.policy !== input.policy || previous.source !== input.source)
211
+ ) {
212
+ throw new EgressProvenanceError(
213
+ "INVALID_EGRESS_LINEAGE",
214
+ `Conflicting policy lineage for collection ${collection}`
215
+ );
216
+ }
217
+ if (!previous) byCollection.set(collection, input);
218
+ }
219
+ }
220
+ return createEgressLineage([...byCollection.values()]);
221
+ };
222
+
223
+ /** Resolve an exact requested scope; empty and unknown names fail closed. */
224
+ export const resolveEgressLineage = (
225
+ inputs: readonly EgressLineageInput[],
226
+ requestedNames?: readonly string[]
227
+ ): EgressLineage => {
228
+ const canonical = canonicalSources(inputs);
229
+ if (requestedNames === undefined) return createEgressLineage(canonical);
230
+ let requested: string[];
231
+ try {
232
+ if (!Array.isArray(requestedNames)) throw invalidLineageInput();
233
+ const { length } = requestedNames;
234
+ if (!Number.isSafeInteger(length) || length > MAX_EGRESS_LINEAGE_SOURCES) {
235
+ throw invalidLineageInput();
236
+ }
237
+ const names: string[] = [];
238
+ for (let index = 0; index < length; index += 1) {
239
+ if (!(index in requestedNames)) throw invalidLineageInput();
240
+ const name: unknown = requestedNames[index];
241
+ if (
242
+ typeof name !== "string" ||
243
+ name !== name.trim().toLowerCase() ||
244
+ !COLLECTION_PATTERN.test(name)
245
+ ) {
246
+ throw new EgressProvenanceError(
247
+ "INVALID_EGRESS_LINEAGE",
248
+ "Requested policy scope contains an invalid collection"
249
+ );
250
+ }
251
+ names.push(name);
252
+ }
253
+ requested = [...new Set(names)].sort();
254
+ } catch (error) {
255
+ if (error instanceof EgressProvenanceError) throw error;
256
+ throw new EgressProvenanceError(
257
+ "INVALID_EGRESS_LINEAGE",
258
+ "Requested policy scope must be a readable dense bounded array"
259
+ );
260
+ }
261
+ if (requested.length === 0) {
262
+ throw new EgressProvenanceError(
263
+ "EMPTY_EGRESS_LINEAGE",
264
+ "Requested policy scope must contain at least one collection"
265
+ );
266
+ }
267
+ const byCollection = new Map(
268
+ canonical.map((input) => [input.collection, input])
269
+ );
270
+ const missing = requested.filter((name) => !byCollection.has(name));
271
+ if (missing.length > 0) {
272
+ throw new EgressProvenanceError(
273
+ "INVALID_EGRESS_LINEAGE",
274
+ `Unknown collection policy scope: ${missing.join(", ")}`
275
+ );
276
+ }
277
+ return createEgressLineage(
278
+ requested.map((name) => byCollection.get(name) as EgressLineageInput)
279
+ );
280
+ };
281
+
282
+ /** Conservative lineage for persisted legacy artifacts without provenance. */
283
+ export const legacyLocalOnlyEgressLineage = (
284
+ collection = "legacy"
285
+ ): EgressLineage =>
286
+ createEgressLineage([
287
+ { collection, policy: "local_only", source: "legacy_default" },
288
+ ]);
289
+
290
+ /** True only for the exact migration projection used by pre-lineage rows. */
291
+ export const isMigratedLegacyEgressLineage = (
292
+ lineage: EgressLineage
293
+ ): boolean => lineage.digest === legacyLocalOnlyEgressLineage().digest;
294
+
295
+ export const egressLineageByteSize = (lineage: EgressLineage): number =>
296
+ new TextEncoder().encode(canonicalJson(lineage)).byteLength;
297
+
298
+ export const egressLineageSourceSchema = z
299
+ .object({
300
+ collection: z.string().regex(COLLECTION_PATTERN),
301
+ policy: EgressPolicySchema,
302
+ source: EgressPolicySourceSchema,
303
+ })
304
+ .strict();
305
+
306
+ export const egressLineageSchema = z
307
+ .object({
308
+ effectivePolicy: EgressPolicySchema,
309
+ digest: z.string().regex(/^[a-f0-9]{64}$/),
310
+ sources: z
311
+ .array(egressLineageSourceSchema)
312
+ .min(1)
313
+ .max(MAX_EGRESS_LINEAGE_SOURCES),
314
+ })
315
+ .strict()
316
+ .superRefine((value, context) => {
317
+ try {
318
+ const canonical = createEgressLineage(value.sources);
319
+ if (
320
+ canonical.effectivePolicy !== value.effectivePolicy ||
321
+ canonical.digest !== value.digest ||
322
+ JSON.stringify(canonical.sources) !== JSON.stringify(value.sources)
323
+ ) {
324
+ context.addIssue({
325
+ code: "custom",
326
+ message: "Egress lineage must be canonical and digest-bound",
327
+ });
328
+ }
329
+ } catch (error) {
330
+ context.addIssue({
331
+ code: "custom",
332
+ message:
333
+ error instanceof Error ? error.message : "Invalid egress lineage",
334
+ });
335
+ }
336
+ });
@@ -76,7 +76,9 @@ export class JobManager {
76
76
  #lockTimeoutMs: number;
77
77
  #activeJobId: string | null = null;
78
78
  #jobs = new Map<string, JobRecord>();
79
+ #jobAuthorizationEpochs = new Map<string, string>();
79
80
  #activeJobs = new Set<Promise<void>>();
81
+ #authorizationEpoch = "egress-epoch-uninitialized";
80
82
 
81
83
  constructor(options: JobManagerOptions) {
82
84
  this.#lockPath = options.lockPath;
@@ -154,6 +156,25 @@ export class JobManager {
154
156
  return this.#jobs.get(this.#activeJobId) ?? null;
155
157
  }
156
158
 
159
+ /**
160
+ * Rotate the authorization snapshot used by queued work. Jobs re-check this
161
+ * after acquiring the shared tool mutex, immediately before any work runs.
162
+ */
163
+ setAuthorizationEpoch(epoch: string): number {
164
+ if (epoch === this.#authorizationEpoch) return 0;
165
+ this.#authorizationEpoch = epoch;
166
+ let invalidated = 0;
167
+ for (const job of this.#jobs.values()) {
168
+ if (
169
+ job.status === "running" &&
170
+ this.#jobAuthorizationEpochs.get(job.id) !== epoch
171
+ ) {
172
+ invalidated += 1;
173
+ }
174
+ }
175
+ return invalidated;
176
+ }
177
+
157
178
  updateJobProgress(
158
179
  jobId: string,
159
180
  progress: { current: number; total: number; currentFile?: string }
@@ -164,6 +185,7 @@ export class JobManager {
164
185
 
165
186
  clear(): void {
166
187
  this.#jobs.clear();
188
+ this.#jobAuthorizationEpochs.clear();
167
189
  this.#activeJobId = null;
168
190
  }
169
191
 
@@ -203,6 +225,7 @@ export class JobManager {
203
225
  try {
204
226
  const release = await this.#toolMutex.acquire();
205
227
  try {
228
+ this.#assertAuthorizationEpoch(job);
206
229
  const result = await fn();
207
230
  job.status = "completed";
208
231
  job.result = result;
@@ -238,6 +261,7 @@ export class JobManager {
238
261
  };
239
262
 
240
263
  this.#jobs.set(jobId, job);
264
+ this.#jobAuthorizationEpochs.set(jobId, this.#authorizationEpoch);
241
265
  this.#activeJobId = jobId;
242
266
 
243
267
  const jobPromise = this.#runJob(job, fn, lock);
@@ -261,6 +285,7 @@ export class JobManager {
261
285
  };
262
286
 
263
287
  this.#jobs.set(jobId, job);
288
+ this.#jobAuthorizationEpochs.set(jobId, this.#authorizationEpoch);
264
289
  this.#activeJobId = jobId;
265
290
 
266
291
  const jobPromise = this.#runTypedJob(job, fn, lock);
@@ -277,6 +302,7 @@ export class JobManager {
277
302
  try {
278
303
  const release = await this.#toolMutex.acquire();
279
304
  try {
305
+ this.#assertAuthorizationEpoch(job);
280
306
  const result = await fn();
281
307
  job.status = "completed";
282
308
  job.typedResult = result;
@@ -305,6 +331,7 @@ export class JobManager {
305
331
  const completedAt = job.completedAt ?? job.startedAt;
306
332
  if (now - completedAt > JOB_EXPIRATION_MS) {
307
333
  this.#jobs.delete(id);
334
+ this.#jobAuthorizationEpochs.delete(id);
308
335
  }
309
336
  }
310
337
 
@@ -321,7 +348,17 @@ export class JobManager {
321
348
  const job = completed[i];
322
349
  if (job) {
323
350
  this.#jobs.delete(job.id);
351
+ this.#jobAuthorizationEpochs.delete(job.id);
324
352
  }
325
353
  }
326
354
  }
355
+
356
+ #assertAuthorizationEpoch(job: JobRecord): void {
357
+ if (this.#jobAuthorizationEpochs.get(job.id) === this.#authorizationEpoch) {
358
+ return;
359
+ }
360
+ throw new Error(
361
+ "STALE_EGRESS_POLICY: collection policy changed; retry to re-authorize"
362
+ );
363
+ }
327
364
  }
@@ -0,0 +1,261 @@
1
+ /** Checked callsite-level inventory of shipped network/process boundaries. */
2
+
3
+ import type { EgressAction } from "./egress-policy";
4
+
5
+ export type NetworkBoundaryPrimitive =
6
+ | "bun_connect"
7
+ | "bun_dns_lookup"
8
+ | "bun_serve"
9
+ | "child_process"
10
+ | "event_source"
11
+ | "fetch"
12
+ | "http_inference"
13
+ | "web_socket";
14
+
15
+ export interface NetworkBoundaryInventoryEntry {
16
+ id: string;
17
+ key: string;
18
+ path: string;
19
+ primitive: NetworkBoundaryPrimitive | "logical";
20
+ action: EgressAction | null;
21
+ enforcement:
22
+ | "collection_policy"
23
+ | "client_transport"
24
+ | "disabled"
25
+ | "local_process_only"
26
+ | "loopback_only"
27
+ | "no_collection_data";
28
+ serverBoundary?: string;
29
+ }
30
+
31
+ export const NETWORK_BOUNDARY_INVENTORY = [
32
+ {
33
+ id: "daemon-listener",
34
+ key: "src/cli/commands/daemon.ts::bun_serve#1",
35
+ path: "src/cli/commands/daemon.ts",
36
+ primitive: "bun_serve",
37
+ action: "serve",
38
+ enforcement: "collection_policy",
39
+ },
40
+ {
41
+ id: "semantic-setup-process",
42
+ key: "src/cli/commands/setup-semantic.ts::child_process#1",
43
+ path: "src/cli/commands/setup-semantic.ts",
44
+ primitive: "child_process",
45
+ action: null,
46
+ enforcement: "no_collection_data",
47
+ },
48
+ {
49
+ id: "detached-runtime-process",
50
+ key: "src/cli/detach.ts::child_process#1",
51
+ path: "src/cli/detach.ts",
52
+ primitive: "child_process",
53
+ action: null,
54
+ enforcement: "no_collection_data",
55
+ },
56
+ {
57
+ id: "detached-runtime-health",
58
+ key: "src/cli/detach.ts::fetch#1",
59
+ path: "src/cli/detach.ts",
60
+ primitive: "fetch",
61
+ action: null,
62
+ enforcement: "no_collection_data",
63
+ },
64
+ {
65
+ id: "terminal-pager",
66
+ key: "src/cli/pager.ts::child_process#1",
67
+ path: "src/cli/pager.ts",
68
+ primitive: "child_process",
69
+ action: null,
70
+ enforcement: "local_process_only",
71
+ },
72
+ {
73
+ id: "file-lock-process-probe",
74
+ key: "src/core/file-lock.ts::child_process#1",
75
+ path: "src/core/file-lock.ts",
76
+ primitive: "child_process",
77
+ action: null,
78
+ enforcement: "no_collection_data",
79
+ },
80
+ {
81
+ id: "local-file-operations",
82
+ key: "src/core/file-ops.ts::child_process#1",
83
+ path: "src/core/file-ops.ts",
84
+ primitive: "child_process",
85
+ action: null,
86
+ enforcement: "local_process_only",
87
+ },
88
+ {
89
+ id: "sync-update-command",
90
+ key: "src/ingestion/sync.ts::child_process#1",
91
+ path: "src/ingestion/sync.ts",
92
+ primitive: "child_process",
93
+ action: "export",
94
+ enforcement: "collection_policy",
95
+ },
96
+ {
97
+ id: "sync-git-repository-check",
98
+ key: "src/ingestion/sync.ts::child_process#2",
99
+ path: "src/ingestion/sync.ts",
100
+ primitive: "child_process",
101
+ action: null,
102
+ enforcement: "local_process_only",
103
+ },
104
+ {
105
+ id: "sync-git-pull",
106
+ key: "src/ingestion/sync.ts::child_process#3",
107
+ path: "src/ingestion/sync.ts",
108
+ primitive: "child_process",
109
+ action: "export",
110
+ enforcement: "collection_policy",
111
+ },
112
+ {
113
+ id: "inference-dns-classification",
114
+ key: "src/llm/http-policy.ts::bun_dns_lookup#1",
115
+ path: "src/llm/http-policy.ts",
116
+ primitive: "bun_dns_lookup",
117
+ action: "remote_inference",
118
+ enforcement: "collection_policy",
119
+ },
120
+ {
121
+ id: "http-embedding-single",
122
+ key: "src/llm/httpEmbedding.ts::http_inference#1",
123
+ path: "src/llm/httpEmbedding.ts",
124
+ primitive: "http_inference",
125
+ action: "remote_inference",
126
+ enforcement: "collection_policy",
127
+ },
128
+ {
129
+ id: "http-embedding-batch",
130
+ key: "src/llm/httpEmbedding.ts::http_inference#2",
131
+ path: "src/llm/httpEmbedding.ts",
132
+ primitive: "http_inference",
133
+ action: "remote_inference",
134
+ enforcement: "collection_policy",
135
+ },
136
+ {
137
+ id: "http-generation",
138
+ key: "src/llm/httpGeneration.ts::http_inference#1",
139
+ path: "src/llm/httpGeneration.ts",
140
+ primitive: "http_inference",
141
+ action: "remote_inference",
142
+ enforcement: "collection_policy",
143
+ },
144
+ {
145
+ id: "http-rerank",
146
+ key: "src/llm/httpRerank.ts::http_inference#1",
147
+ path: "src/llm/httpRerank.ts",
148
+ primitive: "http_inference",
149
+ action: "remote_inference",
150
+ enforcement: "collection_policy",
151
+ },
152
+ {
153
+ id: "pinned-http-fetch",
154
+ key: "src/llm/pinned-http-connection.ts::fetch#1",
155
+ path: "src/llm/pinned-http-connection.ts",
156
+ primitive: "fetch",
157
+ action: "remote_inference",
158
+ enforcement: "collection_policy",
159
+ },
160
+ {
161
+ id: "browser-blob-read",
162
+ key: "src/serve/public/components/ai-elements/prompt-input.tsx::fetch#1",
163
+ path: "src/serve/public/components/ai-elements/prompt-input.tsx",
164
+ primitive: "fetch",
165
+ action: null,
166
+ enforcement: "local_process_only",
167
+ },
168
+ {
169
+ id: "browser-api-hook",
170
+ key: "src/serve/public/hooks/use-api.ts::fetch#1",
171
+ path: "src/serve/public/hooks/use-api.ts",
172
+ primitive: "fetch",
173
+ action: "serve",
174
+ enforcement: "client_transport",
175
+ serverBoundary: "src/serve/server.ts",
176
+ },
177
+ {
178
+ id: "browser-api-helper",
179
+ key: "src/serve/public/hooks/use-api.ts::fetch#2",
180
+ path: "src/serve/public/hooks/use-api.ts",
181
+ primitive: "fetch",
182
+ action: "serve",
183
+ enforcement: "client_transport",
184
+ serverBoundary: "src/serve/server.ts",
185
+ },
186
+ {
187
+ id: "browser-document-events",
188
+ key: "src/serve/public/hooks/use-doc-events.ts::event_source#1",
189
+ path: "src/serve/public/hooks/use-doc-events.ts",
190
+ primitive: "event_source",
191
+ action: "serve",
192
+ enforcement: "client_transport",
193
+ serverBoundary: "src/serve/server.ts",
194
+ },
195
+ {
196
+ id: "clipper-pair-csrf",
197
+ key: "src/serve/public/lib/clipper-approval.ts::fetch#1",
198
+ path: "src/serve/public/lib/clipper-approval.ts",
199
+ primitive: "fetch",
200
+ action: "clip_write",
201
+ enforcement: "client_transport",
202
+ serverBoundary: "src/serve/routes/clipper.ts",
203
+ },
204
+ {
205
+ id: "clipper-pair-approval",
206
+ key: "src/serve/public/lib/clipper-approval.ts::fetch#2",
207
+ path: "src/serve/public/lib/clipper-approval.ts",
208
+ primitive: "fetch",
209
+ action: "clip_write",
210
+ enforcement: "client_transport",
211
+ serverBoundary: "src/serve/routes/clipper.ts",
212
+ },
213
+ {
214
+ id: "serve-listener",
215
+ key: "src/serve/server.ts::bun_serve#1",
216
+ path: "src/serve/server.ts",
217
+ primitive: "bun_serve",
218
+ action: "serve",
219
+ enforcement: "loopback_only",
220
+ },
221
+ {
222
+ id: "http-mcp-tools",
223
+ key: "logical::http-mcp-tools",
224
+ path: "src/mcp/http-egress.ts",
225
+ primitive: "logical",
226
+ action: "serve",
227
+ enforcement: "collection_policy",
228
+ },
229
+ {
230
+ id: "http-mcp-resources",
231
+ key: "logical::http-mcp-resources",
232
+ path: "src/mcp/http-egress.ts",
233
+ primitive: "logical",
234
+ action: "serve",
235
+ enforcement: "collection_policy",
236
+ },
237
+ {
238
+ id: "local-publish-artifact",
239
+ key: "logical::local-publish-artifact",
240
+ path: "src/publish/export-service.ts",
241
+ primitive: "logical",
242
+ action: "export",
243
+ enforcement: "local_process_only",
244
+ },
245
+ {
246
+ id: "remote-publish-upload",
247
+ key: "logical::remote-publish-upload",
248
+ path: "src/publish/export-service.ts",
249
+ primitive: "logical",
250
+ action: null,
251
+ enforcement: "disabled",
252
+ },
253
+ {
254
+ id: "private-agent-access",
255
+ key: "logical::private-agent-access",
256
+ path: "src/publish/encrypted-export.ts",
257
+ primitive: "logical",
258
+ action: null,
259
+ enforcement: "disabled",
260
+ },
261
+ ] as const satisfies readonly NetworkBoundaryInventoryEntry[];