@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
|
@@ -91,6 +91,7 @@ interface CollectionStats {
|
|
|
91
91
|
"override" | "preset" | "default"
|
|
92
92
|
>;
|
|
93
93
|
pattern?: string;
|
|
94
|
+
egressPolicy?: CollectionModelDetails["egressPolicy"];
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
interface StatusResponse {
|
|
@@ -411,6 +412,7 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
411
412
|
models: config?.models,
|
|
412
413
|
modelSources: config?.modelSources,
|
|
413
414
|
pattern: config?.pattern,
|
|
415
|
+
egressPolicy: config?.egressPolicy,
|
|
414
416
|
};
|
|
415
417
|
});
|
|
416
418
|
|
|
@@ -722,6 +724,7 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
722
724
|
/>
|
|
723
725
|
|
|
724
726
|
<CollectionModelDialog
|
|
727
|
+
availableCollections={collections.map(({ name }) => name)}
|
|
725
728
|
collection={modelDialogCollection}
|
|
726
729
|
onOpenChange={(open) => {
|
|
727
730
|
if (!open) {
|
|
@@ -18,6 +18,69 @@ function saturatedResponse(): Response {
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function policyChangedResponse(): Response {
|
|
22
|
+
return Response.json(
|
|
23
|
+
{
|
|
24
|
+
error: {
|
|
25
|
+
code: "EGRESS_POLICY_CHANGED",
|
|
26
|
+
message: "Collection policy changed; retry",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ status: 409 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function wrapResidentStream(
|
|
34
|
+
response: Response,
|
|
35
|
+
isCurrent: () => boolean,
|
|
36
|
+
finish: () => void
|
|
37
|
+
): Response {
|
|
38
|
+
if (!response.body) {
|
|
39
|
+
finish();
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
const reader = response.body.getReader();
|
|
43
|
+
let finished = false;
|
|
44
|
+
const finishOnce = (): void => {
|
|
45
|
+
if (finished) return;
|
|
46
|
+
finished = true;
|
|
47
|
+
finish();
|
|
48
|
+
};
|
|
49
|
+
const body = new ReadableStream<Uint8Array>({
|
|
50
|
+
async pull(controller) {
|
|
51
|
+
try {
|
|
52
|
+
if (!isCurrent()) {
|
|
53
|
+
await reader.cancel("EGRESS_POLICY_CHANGED");
|
|
54
|
+
controller.close();
|
|
55
|
+
finishOnce();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = await reader.read();
|
|
59
|
+
if (!isCurrent()) {
|
|
60
|
+
await reader.cancel("EGRESS_POLICY_CHANGED");
|
|
61
|
+
controller.close();
|
|
62
|
+
finishOnce();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (result.done) {
|
|
66
|
+
controller.close();
|
|
67
|
+
finishOnce();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
controller.enqueue(result.value);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
controller.error(error);
|
|
73
|
+
finishOnce();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async cancel(reason) {
|
|
77
|
+
finishOnce();
|
|
78
|
+
await reader.cancel(reason);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return new Response(body, response);
|
|
82
|
+
}
|
|
83
|
+
|
|
21
84
|
export async function handleResidentRead(
|
|
22
85
|
runtime: ResidentRuntime,
|
|
23
86
|
request: Request | undefined,
|
|
@@ -27,13 +90,28 @@ export async function handleResidentRead(
|
|
|
27
90
|
if (!admitted) return unavailableResponse();
|
|
28
91
|
|
|
29
92
|
let releaseReader: (() => void) | undefined;
|
|
93
|
+
let deferredFinish = false;
|
|
94
|
+
const isAuthorizationEpochCurrent = (): boolean =>
|
|
95
|
+
admitted.isAuthorizationEpochCurrent?.() ?? true;
|
|
96
|
+
const finish = (): void => {
|
|
97
|
+
releaseReader?.();
|
|
98
|
+
admitted.finish();
|
|
99
|
+
};
|
|
30
100
|
try {
|
|
31
101
|
releaseReader = await runtime.readerGate.acquire(admitted.signal);
|
|
32
102
|
if (admitted.signal.aborted) return unavailableResponse();
|
|
33
103
|
const response = await runtime.withModelLease(() =>
|
|
34
104
|
Promise.resolve(operation(admitted.signal))
|
|
35
105
|
);
|
|
36
|
-
|
|
106
|
+
if (admitted.signal.aborted) return unavailableResponse();
|
|
107
|
+
if (!isAuthorizationEpochCurrent()) {
|
|
108
|
+
return policyChangedResponse();
|
|
109
|
+
}
|
|
110
|
+
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
111
|
+
deferredFinish = true;
|
|
112
|
+
return wrapResidentStream(response, isAuthorizationEpochCurrent, finish);
|
|
113
|
+
}
|
|
114
|
+
return response;
|
|
37
115
|
} catch (error) {
|
|
38
116
|
if (
|
|
39
117
|
admitted.signal.aborted ||
|
|
@@ -49,7 +127,6 @@ export async function handleResidentRead(
|
|
|
49
127
|
}
|
|
50
128
|
throw error;
|
|
51
129
|
} finally {
|
|
52
|
-
|
|
53
|
-
admitted.finish();
|
|
130
|
+
if (!deferredFinish) finish();
|
|
54
131
|
}
|
|
55
132
|
}
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
loadConfig,
|
|
33
33
|
} from "../config";
|
|
34
34
|
import { SavedCapsuleReverificationScheduler } from "../core/capsule-reverification-scheduler";
|
|
35
|
+
import { collectionEgressPolicyEpoch } from "../core/collection-egress-policy-service";
|
|
36
|
+
import { authorizeCurrentEgress } from "../core/egress-authorization";
|
|
35
37
|
import { acquireWriteLock } from "../core/file-lock";
|
|
36
38
|
import { JobManager } from "../core/job-manager";
|
|
37
39
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
@@ -78,8 +80,10 @@ export interface ResidentGeneration {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export interface ResidentRequestHandle {
|
|
83
|
+
authorizationEpoch?: string;
|
|
81
84
|
id: string;
|
|
82
85
|
signal: AbortSignal;
|
|
86
|
+
isAuthorizationEpochCurrent?(): boolean;
|
|
83
87
|
finish(): void;
|
|
84
88
|
}
|
|
85
89
|
|
|
@@ -101,12 +105,14 @@ export interface ResidentRuntime {
|
|
|
101
105
|
readonly generations: ResidentGeneration;
|
|
102
106
|
readonly activeRequests: number;
|
|
103
107
|
readonly activeSessions: number;
|
|
108
|
+
readonly authorizationEpoch: string;
|
|
104
109
|
readonly isShuttingDown: boolean;
|
|
105
110
|
getStatus(): ResidentStatus;
|
|
106
111
|
setListenerPort(port: number | null): void;
|
|
107
112
|
setTransportStatusProvider(
|
|
108
113
|
provider: (() => HttpMcpTransportStatus) | null
|
|
109
114
|
): void;
|
|
115
|
+
setPolicySessionInvalidator(invalidator: (() => Promise<void>) | null): void;
|
|
110
116
|
admitRequest(signal?: AbortSignal): ResidentRequestHandle | null;
|
|
111
117
|
withModelLease<T>(operation: () => Promise<T>): Promise<T>;
|
|
112
118
|
markContentMutation(): void;
|
|
@@ -306,6 +312,8 @@ export async function startResidentRuntime(
|
|
|
306
312
|
serverInstanceId,
|
|
307
313
|
toolMutex,
|
|
308
314
|
});
|
|
315
|
+
let authorizationEpoch = collectionEgressPolicyEpoch(initialConfig);
|
|
316
|
+
jobManager.setAuthorizationEpoch(authorizationEpoch);
|
|
309
317
|
ctxHolder.jobManager = jobManager;
|
|
310
318
|
const admission = new AdmissionController();
|
|
311
319
|
const readerGate = new ReaderGate(
|
|
@@ -315,6 +323,7 @@ export async function startResidentRuntime(
|
|
|
315
323
|
const startedAt = Date.now();
|
|
316
324
|
let listenerPort: number | null = null;
|
|
317
325
|
let transportStatusProvider: (() => HttpMcpTransportStatus) | null = null;
|
|
326
|
+
let policySessionInvalidator: (() => Promise<void>) | null = null;
|
|
318
327
|
let shutdownState: ResidentStatus["shutdown"]["state"] = "none";
|
|
319
328
|
let admissionState: ResidentStatus["admission"]["state"] = "accepting";
|
|
320
329
|
let disposed = false;
|
|
@@ -359,7 +368,32 @@ export async function startResidentRuntime(
|
|
|
359
368
|
markIndexMutation: () => {
|
|
360
369
|
generations.index += 1;
|
|
361
370
|
},
|
|
371
|
+
invalidateEgressPolicy: async () =>
|
|
372
|
+
(await ctxHolder.invalidateEgressPolicy?.()) ?? {
|
|
373
|
+
policyEpoch: collectionEgressPolicyEpoch(ctxHolder.config),
|
|
374
|
+
queuedJobsInvalidated: 0,
|
|
375
|
+
sessionsInvalidated: 0,
|
|
376
|
+
staleWorkMustRetry: true,
|
|
377
|
+
},
|
|
362
378
|
});
|
|
379
|
+
mcpContext.authorizeTraceExport = async (lineage) => {
|
|
380
|
+
const egress = mcpContext.getEgressContext?.();
|
|
381
|
+
return authorizeCurrentEgress({
|
|
382
|
+
store,
|
|
383
|
+
config: ctxHolder.config,
|
|
384
|
+
lineage,
|
|
385
|
+
action: "export",
|
|
386
|
+
destinationZone:
|
|
387
|
+
egress?.destinationZone === "loopback"
|
|
388
|
+
? "local_process"
|
|
389
|
+
: (egress?.destinationZone ?? "local_process"),
|
|
390
|
+
caller: egress?.caller ?? {
|
|
391
|
+
authenticated: true,
|
|
392
|
+
operationAuthorized: true,
|
|
393
|
+
},
|
|
394
|
+
contentClass: "retrieval_trace",
|
|
395
|
+
});
|
|
396
|
+
};
|
|
363
397
|
|
|
364
398
|
const runtime: ResidentRuntime = {
|
|
365
399
|
mode: options.mode ?? "serve",
|
|
@@ -385,10 +419,22 @@ export async function startResidentRuntime(
|
|
|
385
419
|
get activeSessions() {
|
|
386
420
|
return transportStatusProvider?.().activeSessions ?? 0;
|
|
387
421
|
},
|
|
422
|
+
get authorizationEpoch() {
|
|
423
|
+
return authorizationEpoch;
|
|
424
|
+
},
|
|
388
425
|
get isShuttingDown() {
|
|
389
426
|
return disposed || !admission.accepting;
|
|
390
427
|
},
|
|
391
|
-
admitRequest: (signal) =>
|
|
428
|
+
admitRequest: (signal) => {
|
|
429
|
+
const admitted = admission.admit(signal);
|
|
430
|
+
if (!admitted) return null;
|
|
431
|
+
const requestEpoch = authorizationEpoch;
|
|
432
|
+
return {
|
|
433
|
+
...admitted,
|
|
434
|
+
authorizationEpoch: requestEpoch,
|
|
435
|
+
isAuthorizationEpochCurrent: () => requestEpoch === authorizationEpoch,
|
|
436
|
+
};
|
|
437
|
+
},
|
|
392
438
|
async withModelLease<T>(operation: () => Promise<T>): Promise<T> {
|
|
393
439
|
const lease = modelManager.acquireLease();
|
|
394
440
|
try {
|
|
@@ -450,6 +496,9 @@ export async function startResidentRuntime(
|
|
|
450
496
|
setTransportStatusProvider(provider) {
|
|
451
497
|
transportStatusProvider = provider;
|
|
452
498
|
},
|
|
499
|
+
setPolicySessionInvalidator(invalidator) {
|
|
500
|
+
policySessionInvalidator = invalidator;
|
|
501
|
+
},
|
|
453
502
|
async syncAll(syncOptions = {}) {
|
|
454
503
|
const config = ctxHolder.config;
|
|
455
504
|
const syncAllService = deps.syncAllService
|
|
@@ -508,6 +557,19 @@ export async function startResidentRuntime(
|
|
|
508
557
|
};
|
|
509
558
|
ctxHolder.startBackgroundWork = (operation) =>
|
|
510
559
|
runtime.startBackgroundWork(operation);
|
|
560
|
+
ctxHolder.invalidateEgressPolicy = async () => {
|
|
561
|
+
const sessionsInvalidated = transportStatusProvider?.().activeSessions ?? 0;
|
|
562
|
+
const policyEpoch = collectionEgressPolicyEpoch(ctxHolder.config);
|
|
563
|
+
authorizationEpoch = policyEpoch;
|
|
564
|
+
const queuedJobsInvalidated = jobManager.setAuthorizationEpoch(policyEpoch);
|
|
565
|
+
await policySessionInvalidator?.();
|
|
566
|
+
return {
|
|
567
|
+
policyEpoch,
|
|
568
|
+
queuedJobsInvalidated,
|
|
569
|
+
sessionsInvalidated,
|
|
570
|
+
staleWorkMustRetry: true,
|
|
571
|
+
};
|
|
572
|
+
};
|
|
511
573
|
mcpContext.getResidentStatus = () => runtime.getStatus();
|
|
512
574
|
return { success: true, runtime };
|
|
513
575
|
}
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Collection,
|
|
15
15
|
CollectionModelOverrides,
|
|
16
16
|
Config,
|
|
17
|
+
EgressPolicy,
|
|
17
18
|
ModelPreset,
|
|
18
19
|
} from "../../config/types";
|
|
19
20
|
import type { JobManager } from "../../core/job-manager";
|
|
@@ -49,6 +50,13 @@ import {
|
|
|
49
50
|
normalizeContentTypes,
|
|
50
51
|
} from "../../config";
|
|
51
52
|
import { type PublicCaptureInput } from "../../core/capture";
|
|
53
|
+
import { projectCollectionEgressPolicy } from "../../core/collection-egress-policy-projection";
|
|
54
|
+
import {
|
|
55
|
+
CollectionEgressPolicyService,
|
|
56
|
+
type CollectionEgressPolicyState,
|
|
57
|
+
type EgressRelaxationConfirmation,
|
|
58
|
+
} from "../../core/collection-egress-policy-service";
|
|
59
|
+
import { parsePolicySetBody } from "../../core/collection-egress-policy-validation";
|
|
52
60
|
import {
|
|
53
61
|
type ConnectorVerificationCode,
|
|
54
62
|
getConnectorVerificationRemediation,
|
|
@@ -58,6 +66,7 @@ import {
|
|
|
58
66
|
deriveEditableCopyRelPath,
|
|
59
67
|
getDocumentCapabilities,
|
|
60
68
|
} from "../../core/document-capabilities";
|
|
69
|
+
import { EgressAuditService } from "../../core/egress-audit";
|
|
61
70
|
import {
|
|
62
71
|
atomicWrite,
|
|
63
72
|
copyFilePath,
|
|
@@ -144,6 +153,7 @@ import {
|
|
|
144
153
|
executeResidentCapturePlan,
|
|
145
154
|
planResidentCapture,
|
|
146
155
|
} from "../capture-service";
|
|
156
|
+
import { parseClosedJson } from "../closed-json";
|
|
147
157
|
import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
|
|
148
158
|
import {
|
|
149
159
|
getConnectorStatuses,
|
|
@@ -176,6 +186,12 @@ export interface ContextHolder {
|
|
|
176
186
|
jobManager?: JobManager;
|
|
177
187
|
markContentMutation?: () => void;
|
|
178
188
|
markIndexMutation?: () => void;
|
|
189
|
+
invalidateEgressPolicy?: () => Promise<{
|
|
190
|
+
policyEpoch: string;
|
|
191
|
+
queuedJobsInvalidated: number;
|
|
192
|
+
sessionsInvalidated: number;
|
|
193
|
+
staleWorkMustRetry: true;
|
|
194
|
+
}>;
|
|
179
195
|
startBackgroundWork?: (
|
|
180
196
|
operation: (signal: AbortSignal) => Promise<void>
|
|
181
197
|
) => boolean;
|
|
@@ -361,6 +377,11 @@ export interface UpdateCollectionRequestBody {
|
|
|
361
377
|
};
|
|
362
378
|
}
|
|
363
379
|
|
|
380
|
+
export interface UpdateCollectionEgressPolicyRequestBody {
|
|
381
|
+
policy: EgressPolicy;
|
|
382
|
+
confirmation?: EgressRelaxationConfirmation;
|
|
383
|
+
}
|
|
384
|
+
|
|
364
385
|
export interface ClearCollectionEmbeddingsRequestBody {
|
|
365
386
|
mode?: "stale" | "all";
|
|
366
387
|
}
|
|
@@ -387,6 +408,7 @@ export interface CollectionResponse {
|
|
|
387
408
|
gen: "override" | "preset" | "default";
|
|
388
409
|
};
|
|
389
410
|
activePresetId: string;
|
|
411
|
+
egressPolicy: CollectionEgressPolicyState | null;
|
|
390
412
|
}
|
|
391
413
|
|
|
392
414
|
function serializeCollection(
|
|
@@ -406,6 +428,12 @@ function serializeCollection(
|
|
|
406
428
|
effectiveModels: getCollectionEffectiveModels(config, collection.name),
|
|
407
429
|
modelSources: getCollectionModelSources(config, collection.name),
|
|
408
430
|
activePresetId: modelConfig.activePreset,
|
|
431
|
+
egressPolicy: (() => {
|
|
432
|
+
const result = new CollectionEgressPolicyService({
|
|
433
|
+
getConfig: () => config,
|
|
434
|
+
}).get(collection.name);
|
|
435
|
+
return result.ok ? result.value : null;
|
|
436
|
+
})(),
|
|
409
437
|
};
|
|
410
438
|
}
|
|
411
439
|
|
|
@@ -1001,6 +1029,150 @@ export async function handleCollections(config: Config): Promise<Response> {
|
|
|
1001
1029
|
);
|
|
1002
1030
|
}
|
|
1003
1031
|
|
|
1032
|
+
const collectionPolicyService = (
|
|
1033
|
+
ctxHolder: ContextHolder,
|
|
1034
|
+
store: SqliteAdapter,
|
|
1035
|
+
collection: string
|
|
1036
|
+
): CollectionEgressPolicyService =>
|
|
1037
|
+
new CollectionEgressPolicyService({
|
|
1038
|
+
getConfig: () => ctxHolder.config,
|
|
1039
|
+
mutateConfig: (mutate) =>
|
|
1040
|
+
applyConfigChangeTyped(
|
|
1041
|
+
ctxHolder,
|
|
1042
|
+
store,
|
|
1043
|
+
(config) => mutate(config),
|
|
1044
|
+
undefined,
|
|
1045
|
+
{
|
|
1046
|
+
projectStore: (targetStore, config) =>
|
|
1047
|
+
projectCollectionEgressPolicy(targetStore, config, collection),
|
|
1048
|
+
}
|
|
1049
|
+
),
|
|
1050
|
+
onPolicyChanged: async () =>
|
|
1051
|
+
(await ctxHolder.invalidateEgressPolicy?.()) ?? {
|
|
1052
|
+
policyEpoch: "egress-epoch-standalone",
|
|
1053
|
+
queuedJobsInvalidated: 0,
|
|
1054
|
+
sessionsInvalidated: 0,
|
|
1055
|
+
staleWorkMustRetry: true,
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
export function handleCollectionEgressPolicy(
|
|
1060
|
+
ctxHolder: ContextHolder,
|
|
1061
|
+
name: string
|
|
1062
|
+
): Response {
|
|
1063
|
+
const state = new CollectionEgressPolicyService({
|
|
1064
|
+
getConfig: () => ctxHolder.config,
|
|
1065
|
+
}).get(name);
|
|
1066
|
+
if (state.ok) return jsonResponse(state.value);
|
|
1067
|
+
return errorResponse(
|
|
1068
|
+
state.code === "NOT_FOUND" ? "NOT_FOUND" : "VALIDATION",
|
|
1069
|
+
state.error,
|
|
1070
|
+
state.code === "NOT_FOUND" ? 404 : 400
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export async function handleUpdateCollectionEgressPolicy(
|
|
1075
|
+
ctxHolder: ContextHolder,
|
|
1076
|
+
store: SqliteAdapter,
|
|
1077
|
+
name: string,
|
|
1078
|
+
req: Request
|
|
1079
|
+
): Promise<Response> {
|
|
1080
|
+
const parsed = await parseClosedJson(req);
|
|
1081
|
+
if (!parsed.ok) return errorResponse("VALIDATION", parsed.error, 400);
|
|
1082
|
+
const input = parsePolicySetBody(parsed.value, name);
|
|
1083
|
+
if (!input.ok) return errorResponse("VALIDATION", input.error, 400);
|
|
1084
|
+
const result = await collectionPolicyService(ctxHolder, store, name).set(
|
|
1085
|
+
input.value
|
|
1086
|
+
);
|
|
1087
|
+
if (!result.ok) {
|
|
1088
|
+
const status =
|
|
1089
|
+
result.code === "NOT_FOUND"
|
|
1090
|
+
? 404
|
|
1091
|
+
: result.code === "EGRESS_RELAXATION_CONFIRMATION_REQUIRED"
|
|
1092
|
+
? 409
|
|
1093
|
+
: 400;
|
|
1094
|
+
return errorResponse(result.code, result.error, status);
|
|
1095
|
+
}
|
|
1096
|
+
return jsonResponse(result.value);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export async function handleCollectionEgressCheck(
|
|
1100
|
+
ctxHolder: ContextHolder,
|
|
1101
|
+
req: Request
|
|
1102
|
+
): Promise<Response> {
|
|
1103
|
+
const parsed = await parseClosedJson(req);
|
|
1104
|
+
if (!parsed.ok) return errorResponse("VALIDATION", parsed.error, 400);
|
|
1105
|
+
const result = new CollectionEgressPolicyService({
|
|
1106
|
+
getConfig: () => ctxHolder.config,
|
|
1107
|
+
}).check(parsed.value);
|
|
1108
|
+
return result.ok
|
|
1109
|
+
? jsonResponse(result.value)
|
|
1110
|
+
: errorResponse("VALIDATION", result.error, 400);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export async function handleEgressAuditList(
|
|
1114
|
+
store: SqliteAdapter,
|
|
1115
|
+
req: Request
|
|
1116
|
+
): Promise<Response> {
|
|
1117
|
+
const url = new URL(req.url);
|
|
1118
|
+
const limit = url.searchParams.has("limit")
|
|
1119
|
+
? Number(url.searchParams.get("limit"))
|
|
1120
|
+
: undefined;
|
|
1121
|
+
const result = await new EgressAuditService(store).list({
|
|
1122
|
+
limit,
|
|
1123
|
+
cursor: url.searchParams.get("cursor") ?? undefined,
|
|
1124
|
+
});
|
|
1125
|
+
return result.ok
|
|
1126
|
+
? jsonResponse(result.value)
|
|
1127
|
+
: errorResponse(result.error.code, result.error.message);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export async function handleEgressAuditShow(
|
|
1131
|
+
store: SqliteAdapter,
|
|
1132
|
+
auditId: string
|
|
1133
|
+
): Promise<Response> {
|
|
1134
|
+
const result = await new EgressAuditService(store).show(auditId);
|
|
1135
|
+
return result.ok
|
|
1136
|
+
? jsonResponse(result.value)
|
|
1137
|
+
: errorResponse(
|
|
1138
|
+
result.error.code,
|
|
1139
|
+
result.error.message,
|
|
1140
|
+
result.error.code === "NOT_FOUND" ? 404 : 400
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export async function handleEgressAuditStatus(
|
|
1145
|
+
store: SqliteAdapter
|
|
1146
|
+
): Promise<Response> {
|
|
1147
|
+
const result = await new EgressAuditService(store).status();
|
|
1148
|
+
return result.ok
|
|
1149
|
+
? jsonResponse(result.value)
|
|
1150
|
+
: errorResponse(result.error.code, result.error.message);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export async function handleEgressAuditDelete(
|
|
1154
|
+
store: SqliteAdapter,
|
|
1155
|
+
auditId: string
|
|
1156
|
+
): Promise<Response> {
|
|
1157
|
+
const result = await new EgressAuditService(store).delete(auditId);
|
|
1158
|
+
return result.ok
|
|
1159
|
+
? jsonResponse(result.value)
|
|
1160
|
+
: errorResponse(
|
|
1161
|
+
result.error.code,
|
|
1162
|
+
result.error.message,
|
|
1163
|
+
result.error.code === "NOT_FOUND" ? 404 : 400
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export async function handleEgressAuditPurge(
|
|
1168
|
+
store: SqliteAdapter
|
|
1169
|
+
): Promise<Response> {
|
|
1170
|
+
const result = await new EgressAuditService(store).purge();
|
|
1171
|
+
return result.ok
|
|
1172
|
+
? jsonResponse(result.value)
|
|
1173
|
+
: errorResponse(result.error.code, result.error.message);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1004
1176
|
/**
|
|
1005
1177
|
* POST /api/publish/export
|
|
1006
1178
|
* Build a gno.sh-compatible publish artifact for a collection or single doc.
|
|
@@ -1166,6 +1338,7 @@ export async function handleCreateCollection(
|
|
|
1166
1338
|
if (!collection) {
|
|
1167
1339
|
return errorResponse("RUNTIME", "Collection not found after add", 500);
|
|
1168
1340
|
}
|
|
1341
|
+
await ctxHolder.invalidateEgressPolicy?.();
|
|
1169
1342
|
const jobResult = await startJob(
|
|
1170
1343
|
"add",
|
|
1171
1344
|
async (): Promise<SyncResult> => {
|
|
@@ -1281,6 +1454,7 @@ export async function handleDeleteCollection(
|
|
|
1281
1454
|
const status = statusMap[syncResult.code] ?? 500;
|
|
1282
1455
|
return errorResponse(syncResult.code, syncResult.error, status);
|
|
1283
1456
|
}
|
|
1457
|
+
await ctxHolder.invalidateEgressPolicy?.();
|
|
1284
1458
|
ctxHolder.markContentMutation?.();
|
|
1285
1459
|
ctxHolder.markIndexMutation?.();
|
|
1286
1460
|
return jsonResponse({
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/** Dedicated loopback browser-clipper HTTP gateway. */
|
|
2
2
|
|
|
3
|
+
import type { EgressLineage } from "../../core/egress-provenance";
|
|
3
4
|
import type { HttpMcpPeerServer } from "../../mcp/http-security";
|
|
4
5
|
import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
5
6
|
import type { ContextHolder } from "./api";
|
|
6
7
|
|
|
7
8
|
import { prepareBrowserClip } from "../../core/browser-clip";
|
|
9
|
+
import {
|
|
10
|
+
enforceCollectionEgressWithAudit,
|
|
11
|
+
EGRESS_DENIED_MESSAGE,
|
|
12
|
+
EgressDeniedError,
|
|
13
|
+
} from "../../core/egress-enforcement";
|
|
8
14
|
import {
|
|
9
15
|
planResidentCapture,
|
|
10
16
|
type ResidentCapturePlanResult,
|
|
@@ -83,6 +89,40 @@ const pollStatusCode = (result: ClipperPairPollResult): number => {
|
|
|
83
89
|
return 200;
|
|
84
90
|
};
|
|
85
91
|
|
|
92
|
+
const enforceClipWrite = async (
|
|
93
|
+
ctxHolder: ContextHolder,
|
|
94
|
+
store: SqliteAdapter,
|
|
95
|
+
body: unknown
|
|
96
|
+
): Promise<EgressLineage | undefined> => {
|
|
97
|
+
const record =
|
|
98
|
+
typeof body === "object" && body !== null
|
|
99
|
+
? (body as Record<string, unknown>)
|
|
100
|
+
: null;
|
|
101
|
+
const payload =
|
|
102
|
+
typeof record?.payload === "object" && record.payload !== null
|
|
103
|
+
? (record.payload as Record<string, unknown>)
|
|
104
|
+
: record;
|
|
105
|
+
const destination =
|
|
106
|
+
typeof payload?.destination === "object" && payload.destination !== null
|
|
107
|
+
? (payload.destination as Record<string, unknown>)
|
|
108
|
+
: null;
|
|
109
|
+
const collection =
|
|
110
|
+
typeof destination?.collection === "string"
|
|
111
|
+
? destination.collection
|
|
112
|
+
: undefined;
|
|
113
|
+
if (!collection) return undefined;
|
|
114
|
+
const { lineage } = await enforceCollectionEgressWithAudit({
|
|
115
|
+
collections: ctxHolder.config.collections,
|
|
116
|
+
collectionNames: [collection],
|
|
117
|
+
action: "clip_write",
|
|
118
|
+
destinationZone: "loopback",
|
|
119
|
+
caller: { authenticated: true, operationAuthorized: true },
|
|
120
|
+
contentClass: "source",
|
|
121
|
+
store,
|
|
122
|
+
});
|
|
123
|
+
return lineage;
|
|
124
|
+
};
|
|
125
|
+
|
|
86
126
|
export function createClipperRouteGateway(
|
|
87
127
|
ctxHolder: ContextHolder,
|
|
88
128
|
store: SqliteAdapter,
|
|
@@ -279,7 +319,12 @@ export function createClipperRouteGateway(
|
|
|
279
319
|
if (!authenticated.ok) return authenticated.response;
|
|
280
320
|
const { admission, grant } = authenticated.value;
|
|
281
321
|
try {
|
|
282
|
-
const
|
|
322
|
+
const egressLineage = await enforceClipWrite(
|
|
323
|
+
ctxHolder,
|
|
324
|
+
store,
|
|
325
|
+
admission.body
|
|
326
|
+
);
|
|
327
|
+
const prepared = prepareBrowserClip(admission.body, { egressLineage });
|
|
283
328
|
const planned = await planResidentCapture(
|
|
284
329
|
ctxHolder,
|
|
285
330
|
store,
|
|
@@ -303,6 +348,12 @@ export function createClipperRouteGateway(
|
|
|
303
348
|
admission.origin
|
|
304
349
|
);
|
|
305
350
|
} catch (error) {
|
|
351
|
+
if (error instanceof EgressDeniedError) {
|
|
352
|
+
return withClipperCors(
|
|
353
|
+
clipperErrorResponse("EGRESS_DENIED", EGRESS_DENIED_MESSAGE, 403),
|
|
354
|
+
admission.origin
|
|
355
|
+
);
|
|
356
|
+
}
|
|
306
357
|
return withClipperCors(
|
|
307
358
|
clipperErrorResponse(
|
|
308
359
|
"CLIPPER_INVALID_REQUEST",
|
|
@@ -321,6 +372,11 @@ export function createClipperRouteGateway(
|
|
|
321
372
|
if (!authenticated.ok) return authenticated.response;
|
|
322
373
|
const { admission, grant } = authenticated.value;
|
|
323
374
|
try {
|
|
375
|
+
const egressLineage = await enforceClipWrite(
|
|
376
|
+
ctxHolder,
|
|
377
|
+
store,
|
|
378
|
+
admission.body
|
|
379
|
+
);
|
|
324
380
|
return withClipperCors(
|
|
325
381
|
await executeClipperCapture({
|
|
326
382
|
request,
|
|
@@ -330,10 +386,17 @@ export function createClipperRouteGateway(
|
|
|
330
386
|
context: ctxHolder,
|
|
331
387
|
store,
|
|
332
388
|
pairing,
|
|
389
|
+
egressLineage,
|
|
333
390
|
}),
|
|
334
391
|
admission.origin
|
|
335
392
|
);
|
|
336
393
|
} catch (error) {
|
|
394
|
+
if (error instanceof EgressDeniedError) {
|
|
395
|
+
return withClipperCors(
|
|
396
|
+
clipperErrorResponse("EGRESS_DENIED", EGRESS_DENIED_MESSAGE, 403),
|
|
397
|
+
admission.origin
|
|
398
|
+
);
|
|
399
|
+
}
|
|
337
400
|
return withClipperCors(
|
|
338
401
|
clipperErrorResponse(
|
|
339
402
|
"CLIPPER_CAPTURE_FAILED",
|
package/src/serve/routes/mcp.ts
CHANGED
|
@@ -44,6 +44,9 @@ export async function createMcpHttpGateway(
|
|
|
44
44
|
(runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(() =>
|
|
45
45
|
transport.getStatus()
|
|
46
46
|
);
|
|
47
|
+
(runtime as Partial<ResidentRuntime>).setPolicySessionInvalidator?.(() =>
|
|
48
|
+
transport.invalidateAuthenticatedSessions()
|
|
49
|
+
);
|
|
47
50
|
|
|
48
51
|
const route: McpHttpRoute = async (request, server) => {
|
|
49
52
|
const authorization = await security.authorize(request, server);
|
|
@@ -52,8 +55,10 @@ export async function createMcpHttpGateway(
|
|
|
52
55
|
// POST responses and GET streams may both be long-lived SSE responses.
|
|
53
56
|
server.timeout(request, 0);
|
|
54
57
|
return transport.handleRequest(authorization.value.request, {
|
|
58
|
+
authenticated: authorization.value.authenticated,
|
|
55
59
|
identity: authorization.value.identity,
|
|
56
60
|
parsedBody: authorization.value.parsedBody,
|
|
61
|
+
peerClassification: authorization.value.peerClassification,
|
|
57
62
|
});
|
|
58
63
|
};
|
|
59
64
|
|
|
@@ -64,6 +69,7 @@ export async function createMcpHttpGateway(
|
|
|
64
69
|
close: async () => {
|
|
65
70
|
await transport.close();
|
|
66
71
|
(runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(null);
|
|
72
|
+
(runtime as Partial<ResidentRuntime>).setPolicySessionInvalidator?.(null);
|
|
67
73
|
},
|
|
68
74
|
};
|
|
69
75
|
}
|