@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.
- package/README.md +46 -20
- package/assets/skill/SKILL.md +33 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.1.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,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
|
+
});
|
package/src/core/job-manager.ts
CHANGED
|
@@ -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[];
|