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