@gmickel/gno 1.27.1 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -20
- package/assets/skill/SKILL.md +59 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.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 +2 -2
- package/spec/cli.md +82 -5
- package/spec/db/schema.sql +31 -0
- package/spec/mcp.md +71 -0
- package/spec/output-schemas/ask.schema.json +156 -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 +183 -3
- 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/get.schema.json +94 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +223 -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-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +155 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- 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/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +127 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +66 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -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 +198 -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 -112
- package/src/core/context-evidence.ts +93 -15
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/document-capabilities.ts +12 -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/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -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-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +448 -0
- package/src/ingestion/record-container.ts +704 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +243 -36
- package/src/ingestion/types.ts +71 -1
- package/src/ingestion/walker.ts +31 -11
- 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/get.ts +10 -2
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/multi-get.ts +9 -2
- 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/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +21 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +23 -4
- package/src/pipeline/types.ts +6 -0
- package/src/pipeline/vsearch.ts +67 -28
- 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/documents.ts +13 -5
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/browse-tree.ts +4 -2
- 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 +237 -65
- 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/022-record-export-lineage.ts +42 -0
- 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 +8 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +284 -14
- 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 +166 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-vn5f663b.js +0 -50
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/** DNS-pinned, redirect-aware destination policy for outbound HTTP inference. */
|
|
2
|
+
|
|
3
|
+
import type { DestinationClassification } from "../core/destination-classifier";
|
|
4
|
+
import type { HttpDestinationPolicyAudit } from "./pinned-http-connection";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
classifyDestination,
|
|
8
|
+
isNetworkAddress,
|
|
9
|
+
} from "../core/destination-classifier";
|
|
10
|
+
import { PinnedHttpConnection } from "./pinned-http-connection";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
HttpDestinationPolicyAudit,
|
|
14
|
+
PinnedHttpFetch,
|
|
15
|
+
} from "./pinned-http-connection";
|
|
16
|
+
export {
|
|
17
|
+
PinnedHttpConnection,
|
|
18
|
+
PinnedHttpRequestError,
|
|
19
|
+
} from "./pinned-http-connection";
|
|
20
|
+
|
|
21
|
+
export const MAX_HTTP_DESTINATION_ADDRESSES = 32;
|
|
22
|
+
export const MAX_HTTP_DESTINATION_REDIRECTS = 5;
|
|
23
|
+
|
|
24
|
+
export const HTTP_DESTINATION_POLICY_REASONS = [
|
|
25
|
+
"DESTINATION_ALLOWED",
|
|
26
|
+
"INVALID_DESTINATION",
|
|
27
|
+
"UNSUPPORTED_PROTOCOL",
|
|
28
|
+
"CREDENTIALS_IN_URL",
|
|
29
|
+
"DNS_UNRESOLVED",
|
|
30
|
+
"DNS_INVALID_ANSWER",
|
|
31
|
+
"DNS_RESULT_LIMIT",
|
|
32
|
+
"ZONE_NOT_ALLOWED",
|
|
33
|
+
"PROVIDER_ADDRESS_NOT_PUBLIC",
|
|
34
|
+
"PROVIDER_HTTPS_REQUIRED",
|
|
35
|
+
"PROXY_ENVIRONMENT_ACTIVE",
|
|
36
|
+
"DNS_REBINDING",
|
|
37
|
+
"INVALID_REDIRECT_COUNT",
|
|
38
|
+
"REDIRECT_LIMIT",
|
|
39
|
+
"REDIRECT_ZONE_CHANGED",
|
|
40
|
+
"PROVIDER_REDIRECT_ORIGIN_CHANGED",
|
|
41
|
+
"HTTPS_DOWNGRADE",
|
|
42
|
+
] as const;
|
|
43
|
+
export type HttpDestinationPolicyReason =
|
|
44
|
+
(typeof HTTP_DESTINATION_POLICY_REASONS)[number];
|
|
45
|
+
|
|
46
|
+
export type HttpDestinationMaximumZone = "loopback" | "lan" | "remote";
|
|
47
|
+
|
|
48
|
+
export interface HttpDestinationPolicyDenial {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: Exclude<HttpDestinationPolicyReason, "DESTINATION_ALLOWED">;
|
|
51
|
+
classification: DestinationClassification;
|
|
52
|
+
audit: HttpDestinationPolicyAudit;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface HttpDestinationResolver {
|
|
56
|
+
lookup(hostname: string, port: number): Promise<readonly string[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface HttpDestinationPolicyOptions {
|
|
60
|
+
maximumZone: HttpDestinationMaximumZone;
|
|
61
|
+
resolver?: HttpDestinationResolver;
|
|
62
|
+
remoteProvider?: boolean;
|
|
63
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type HttpDestinationConnectionResult =
|
|
67
|
+
| { ok: true; value: PinnedHttpConnection }
|
|
68
|
+
| HttpDestinationPolicyDenial;
|
|
69
|
+
|
|
70
|
+
export type HttpDestinationPolicyResult =
|
|
71
|
+
| { ok: true; reason: "DESTINATION_ALLOWED"; value: HttpDestinationPin }
|
|
72
|
+
| HttpDestinationPolicyDenial;
|
|
73
|
+
|
|
74
|
+
export type HttpDestinationClassificationResult =
|
|
75
|
+
| {
|
|
76
|
+
ok: true;
|
|
77
|
+
reason: "DESTINATION_ALLOWED";
|
|
78
|
+
classification: DestinationClassification;
|
|
79
|
+
}
|
|
80
|
+
| HttpDestinationPolicyDenial;
|
|
81
|
+
|
|
82
|
+
const ZONE_ORDER: Record<HttpDestinationMaximumZone, number> = {
|
|
83
|
+
loopback: 0,
|
|
84
|
+
lan: 1,
|
|
85
|
+
remote: 2,
|
|
86
|
+
};
|
|
87
|
+
const PROXY_ENV_NAMES = [
|
|
88
|
+
"HTTP_PROXY",
|
|
89
|
+
"HTTPS_PROXY",
|
|
90
|
+
"ALL_PROXY",
|
|
91
|
+
"http_proxy",
|
|
92
|
+
"https_proxy",
|
|
93
|
+
"all_proxy",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
const defaultResolver: HttpDestinationResolver = {
|
|
97
|
+
async lookup(hostname: string, port: number): Promise<readonly string[]> {
|
|
98
|
+
const results = await Bun.dns.lookup(hostname, {
|
|
99
|
+
family: "any",
|
|
100
|
+
socketType: "tcp",
|
|
101
|
+
port,
|
|
102
|
+
});
|
|
103
|
+
return results.map(({ address }) => address);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function normalizeHostname(hostname: string): string {
|
|
108
|
+
return hostname.startsWith("[") && hostname.endsWith("]")
|
|
109
|
+
? hostname.slice(1, -1).toLowerCase()
|
|
110
|
+
: hostname.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeAddresses(addresses: readonly string[]): string[] {
|
|
114
|
+
return [...new Set(addresses.map((address) => address.trim().toLowerCase()))]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.sort();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function protocolFor(url: URL): HttpDestinationPolicyAudit["protocol"] {
|
|
120
|
+
if (url.protocol === "http:") return "http";
|
|
121
|
+
if (url.protocol === "https:") return "https";
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function portFor(url: URL): number {
|
|
126
|
+
if (url.port) return Number(url.port);
|
|
127
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasActiveProxy(
|
|
131
|
+
env: Readonly<Record<string, string | undefined>>
|
|
132
|
+
): boolean {
|
|
133
|
+
return PROXY_ENV_NAMES.some((name) => Boolean(env[name]?.trim()));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function invalidClassification(): DestinationClassification {
|
|
137
|
+
return classifyDestination({ kind: "network" });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createAudit(
|
|
141
|
+
classification: DestinationClassification,
|
|
142
|
+
url: URL | null,
|
|
143
|
+
redirectCount: number
|
|
144
|
+
): HttpDestinationPolicyAudit {
|
|
145
|
+
return {
|
|
146
|
+
protocol: url ? protocolFor(url) : "unknown",
|
|
147
|
+
port: url ? portFor(url) : null,
|
|
148
|
+
redirectCount,
|
|
149
|
+
classification: classification.audit,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function denial(
|
|
154
|
+
reason: HttpDestinationPolicyDenial["reason"],
|
|
155
|
+
classification: DestinationClassification,
|
|
156
|
+
url: URL | null,
|
|
157
|
+
redirectCount: number
|
|
158
|
+
): HttpDestinationPolicyDenial {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason,
|
|
162
|
+
classification,
|
|
163
|
+
audit: createAudit(classification, url, redirectCount),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function zoneAllowed(
|
|
168
|
+
actual: DestinationClassification["zone"],
|
|
169
|
+
maximum: HttpDestinationMaximumZone
|
|
170
|
+
): boolean {
|
|
171
|
+
if (actual === "local_process") return true;
|
|
172
|
+
return ZONE_ORDER[actual] <= ZONE_ORDER[maximum];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function resolveAddresses(
|
|
176
|
+
url: URL,
|
|
177
|
+
resolver: HttpDestinationResolver
|
|
178
|
+
): Promise<
|
|
179
|
+
| { ok: true; hostname: string; addresses: string[] }
|
|
180
|
+
| {
|
|
181
|
+
ok: false;
|
|
182
|
+
reason: "DNS_UNRESOLVED" | "DNS_INVALID_ANSWER" | "DNS_RESULT_LIMIT";
|
|
183
|
+
}
|
|
184
|
+
> {
|
|
185
|
+
const hostname = normalizeHostname(url.hostname);
|
|
186
|
+
if (isNetworkAddress(hostname)) {
|
|
187
|
+
return { ok: true, hostname, addresses: [hostname] };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const raw = await resolver.lookup(hostname, portFor(url));
|
|
191
|
+
if (raw.length > MAX_HTTP_DESTINATION_ADDRESSES) {
|
|
192
|
+
return { ok: false, reason: "DNS_RESULT_LIMIT" };
|
|
193
|
+
}
|
|
194
|
+
const addresses = normalizeAddresses(raw);
|
|
195
|
+
if (addresses.length === 0) {
|
|
196
|
+
return { ok: false, reason: "DNS_UNRESOLVED" };
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
addresses.some(
|
|
200
|
+
(address) => address.includes("%") || !isNetworkAddress(address)
|
|
201
|
+
)
|
|
202
|
+
) {
|
|
203
|
+
return { ok: false, reason: "DNS_INVALID_ANSWER" };
|
|
204
|
+
}
|
|
205
|
+
return { ok: true, hostname, addresses };
|
|
206
|
+
} catch {
|
|
207
|
+
return { ok: false, reason: "DNS_UNRESOLVED" };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseHttpUrl(rawUrl: string): URL | null {
|
|
212
|
+
try {
|
|
213
|
+
return new URL(rawUrl);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function providerAddressesArePublic(
|
|
220
|
+
classification: DestinationClassification
|
|
221
|
+
): boolean {
|
|
222
|
+
return (
|
|
223
|
+
classification.audit.addressClasses.length === 1 &&
|
|
224
|
+
classification.audit.addressClasses[0] === "public"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface PreparedDestination {
|
|
229
|
+
url: URL;
|
|
230
|
+
hostname: string;
|
|
231
|
+
addresses: string[];
|
|
232
|
+
classification: DestinationClassification;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function freezeClassification(
|
|
236
|
+
classification: DestinationClassification
|
|
237
|
+
): DestinationClassification {
|
|
238
|
+
const audit = Object.freeze({
|
|
239
|
+
...classification.audit,
|
|
240
|
+
addressClasses: Object.freeze([...classification.audit.addressClasses]),
|
|
241
|
+
});
|
|
242
|
+
return Object.freeze({ ...classification, audit });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isPolicyDenial(
|
|
246
|
+
value: PreparedDestination | HttpDestinationPolicyDenial
|
|
247
|
+
): value is HttpDestinationPolicyDenial {
|
|
248
|
+
return "ok" in value && value.ok === false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function prepareDestination(
|
|
252
|
+
rawUrl: string,
|
|
253
|
+
options: Required<
|
|
254
|
+
Pick<HttpDestinationPolicyOptions, "maximumZone" | "remoteProvider" | "env">
|
|
255
|
+
> & { resolver: HttpDestinationResolver },
|
|
256
|
+
redirectCount: number
|
|
257
|
+
): Promise<PreparedDestination | HttpDestinationPolicyDenial> {
|
|
258
|
+
const url = parseHttpUrl(rawUrl);
|
|
259
|
+
const invalid = invalidClassification();
|
|
260
|
+
if (!url) return denial("INVALID_DESTINATION", invalid, null, redirectCount);
|
|
261
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
262
|
+
return denial("UNSUPPORTED_PROTOCOL", invalid, url, redirectCount);
|
|
263
|
+
}
|
|
264
|
+
if (url.username || url.password) {
|
|
265
|
+
return denial("CREDENTIALS_IN_URL", invalid, url, redirectCount);
|
|
266
|
+
}
|
|
267
|
+
if (options.remoteProvider && url.protocol !== "https:") {
|
|
268
|
+
return denial("PROVIDER_HTTPS_REQUIRED", invalid, url, redirectCount);
|
|
269
|
+
}
|
|
270
|
+
if (options.maximumZone !== "remote" && hasActiveProxy(options.env)) {
|
|
271
|
+
return denial("PROXY_ENVIRONMENT_ACTIVE", invalid, url, redirectCount);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const resolved = await resolveAddresses(url, options.resolver);
|
|
275
|
+
if (!resolved.ok) {
|
|
276
|
+
return denial(resolved.reason, invalid, url, redirectCount);
|
|
277
|
+
}
|
|
278
|
+
const classification = classifyDestination({
|
|
279
|
+
kind: "network",
|
|
280
|
+
hostname: resolved.hostname,
|
|
281
|
+
addresses: resolved.addresses,
|
|
282
|
+
remoteProvider: options.remoteProvider,
|
|
283
|
+
});
|
|
284
|
+
if (options.remoteProvider && !providerAddressesArePublic(classification)) {
|
|
285
|
+
return denial(
|
|
286
|
+
"PROVIDER_ADDRESS_NOT_PUBLIC",
|
|
287
|
+
classification,
|
|
288
|
+
url,
|
|
289
|
+
redirectCount
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!zoneAllowed(classification.zone, options.maximumZone)) {
|
|
293
|
+
return denial("ZONE_NOT_ALLOWED", classification, url, redirectCount);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
url,
|
|
297
|
+
hostname: resolved.hostname,
|
|
298
|
+
addresses: resolved.addresses,
|
|
299
|
+
classification,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Opaque DNS pin. JSON/log projection is intentionally redacted; raw connection
|
|
305
|
+
* details are released only by acquireConnection() after an exact DNS recheck.
|
|
306
|
+
*/
|
|
307
|
+
export class HttpDestinationPin {
|
|
308
|
+
readonly #classification: DestinationClassification;
|
|
309
|
+
readonly #classificationZone: DestinationClassification["zone"];
|
|
310
|
+
readonly #url: URL;
|
|
311
|
+
readonly #hostname: string;
|
|
312
|
+
readonly #addresses: readonly string[];
|
|
313
|
+
readonly #maximumZone: HttpDestinationMaximumZone;
|
|
314
|
+
readonly #resolver: HttpDestinationResolver;
|
|
315
|
+
readonly #remoteProvider: boolean;
|
|
316
|
+
readonly #env: Readonly<Record<string, string | undefined>>;
|
|
317
|
+
readonly #redirectCount: number;
|
|
318
|
+
|
|
319
|
+
constructor(
|
|
320
|
+
prepared: PreparedDestination,
|
|
321
|
+
options: Required<
|
|
322
|
+
Pick<
|
|
323
|
+
HttpDestinationPolicyOptions,
|
|
324
|
+
"maximumZone" | "remoteProvider" | "env"
|
|
325
|
+
>
|
|
326
|
+
> & { resolver: HttpDestinationResolver },
|
|
327
|
+
redirectCount: number
|
|
328
|
+
) {
|
|
329
|
+
this.#classification = freezeClassification(prepared.classification);
|
|
330
|
+
this.#classificationZone = prepared.classification.zone;
|
|
331
|
+
this.#url = prepared.url;
|
|
332
|
+
this.#hostname = prepared.hostname;
|
|
333
|
+
this.#addresses = prepared.addresses;
|
|
334
|
+
this.#maximumZone = options.maximumZone;
|
|
335
|
+
this.#resolver = options.resolver;
|
|
336
|
+
this.#remoteProvider = options.remoteProvider;
|
|
337
|
+
this.#env = options.env;
|
|
338
|
+
this.#redirectCount = redirectCount;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
toJSON(): {
|
|
342
|
+
classification: DestinationClassification;
|
|
343
|
+
audit: HttpDestinationPolicyAudit;
|
|
344
|
+
} {
|
|
345
|
+
return {
|
|
346
|
+
classification: this.#classification,
|
|
347
|
+
audit: createAudit(this.#classification, this.#url, this.#redirectCount),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
get classification(): DestinationClassification {
|
|
352
|
+
return this.#classification;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async acquireConnection(): Promise<HttpDestinationConnectionResult> {
|
|
356
|
+
const prepared = await prepareDestination(
|
|
357
|
+
this.#url.href,
|
|
358
|
+
{
|
|
359
|
+
maximumZone: this.#maximumZone,
|
|
360
|
+
resolver: this.#resolver,
|
|
361
|
+
remoteProvider: this.#remoteProvider,
|
|
362
|
+
env: this.#env,
|
|
363
|
+
},
|
|
364
|
+
this.#redirectCount
|
|
365
|
+
);
|
|
366
|
+
if (isPolicyDenial(prepared)) {
|
|
367
|
+
return denial(
|
|
368
|
+
"DNS_REBINDING",
|
|
369
|
+
prepared.classification,
|
|
370
|
+
this.#url,
|
|
371
|
+
this.#redirectCount
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (
|
|
375
|
+
prepared.addresses.length !== this.#addresses.length ||
|
|
376
|
+
prepared.addresses.some(
|
|
377
|
+
(address, index) => address !== this.#addresses[index]
|
|
378
|
+
)
|
|
379
|
+
) {
|
|
380
|
+
return denial(
|
|
381
|
+
"DNS_REBINDING",
|
|
382
|
+
prepared.classification,
|
|
383
|
+
this.#url,
|
|
384
|
+
this.#redirectCount
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const address = this.#addresses[0];
|
|
389
|
+
if (!address) {
|
|
390
|
+
return denial(
|
|
391
|
+
"DNS_UNRESOLVED",
|
|
392
|
+
this.#classification,
|
|
393
|
+
this.#url,
|
|
394
|
+
this.#redirectCount
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const target = new URL(this.#url.href);
|
|
398
|
+
target.hostname = address.includes(":") ? `[${address}]` : address;
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
value: new PinnedHttpConnection(
|
|
402
|
+
target.href,
|
|
403
|
+
this.#url.host,
|
|
404
|
+
isNetworkAddress(this.#hostname) ? undefined : this.#hostname,
|
|
405
|
+
createAudit(this.#classification, this.#url, this.#redirectCount)
|
|
406
|
+
),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async followRedirect(location: string): Promise<HttpDestinationPolicyResult> {
|
|
411
|
+
if (this.#redirectCount >= MAX_HTTP_DESTINATION_REDIRECTS) {
|
|
412
|
+
return denial(
|
|
413
|
+
"REDIRECT_LIMIT",
|
|
414
|
+
this.#classification,
|
|
415
|
+
this.#url,
|
|
416
|
+
this.#redirectCount
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
let nextUrl: URL;
|
|
420
|
+
try {
|
|
421
|
+
nextUrl = new URL(location, this.#url);
|
|
422
|
+
} catch {
|
|
423
|
+
return denial(
|
|
424
|
+
"INVALID_DESTINATION",
|
|
425
|
+
invalidClassification(),
|
|
426
|
+
null,
|
|
427
|
+
this.#redirectCount + 1
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (this.#url.protocol === "https:" && nextUrl.protocol === "http:") {
|
|
431
|
+
return denial(
|
|
432
|
+
"HTTPS_DOWNGRADE",
|
|
433
|
+
invalidClassification(),
|
|
434
|
+
nextUrl,
|
|
435
|
+
this.#redirectCount + 1
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (
|
|
439
|
+
this.#remoteProvider &&
|
|
440
|
+
nextUrl.origin.toLowerCase() !== this.#url.origin.toLowerCase()
|
|
441
|
+
) {
|
|
442
|
+
return denial(
|
|
443
|
+
"PROVIDER_REDIRECT_ORIGIN_CHANGED",
|
|
444
|
+
invalidClassification(),
|
|
445
|
+
nextUrl,
|
|
446
|
+
this.#redirectCount + 1
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const next = await prepareHttpDestination(nextUrl.href, {
|
|
450
|
+
maximumZone: this.#maximumZone,
|
|
451
|
+
resolver: this.#resolver,
|
|
452
|
+
remoteProvider: this.#remoteProvider,
|
|
453
|
+
env: this.#env,
|
|
454
|
+
redirectCount: this.#redirectCount + 1,
|
|
455
|
+
});
|
|
456
|
+
if (!next.ok) return next;
|
|
457
|
+
if (next.value.classification.zone !== this.#classificationZone) {
|
|
458
|
+
return denial(
|
|
459
|
+
"REDIRECT_ZONE_CHANGED",
|
|
460
|
+
next.value.classification,
|
|
461
|
+
nextUrl,
|
|
462
|
+
this.#redirectCount + 1
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return next;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function prepareHttpDestination(
|
|
470
|
+
rawUrl: string,
|
|
471
|
+
options: HttpDestinationPolicyOptions & { redirectCount?: number }
|
|
472
|
+
): Promise<HttpDestinationPolicyResult> {
|
|
473
|
+
const redirectCount =
|
|
474
|
+
options.redirectCount === undefined ? 0 : options.redirectCount;
|
|
475
|
+
if (!Number.isSafeInteger(redirectCount) || redirectCount < 0) {
|
|
476
|
+
return denial("INVALID_REDIRECT_COUNT", invalidClassification(), null, 0);
|
|
477
|
+
}
|
|
478
|
+
if (redirectCount > MAX_HTTP_DESTINATION_REDIRECTS) {
|
|
479
|
+
return denial(
|
|
480
|
+
"REDIRECT_LIMIT",
|
|
481
|
+
invalidClassification(),
|
|
482
|
+
null,
|
|
483
|
+
redirectCount
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const resolvedOptions = {
|
|
487
|
+
maximumZone: options.maximumZone,
|
|
488
|
+
resolver: options.resolver ?? defaultResolver,
|
|
489
|
+
remoteProvider: options.remoteProvider ?? false,
|
|
490
|
+
env: options.env ?? process.env,
|
|
491
|
+
};
|
|
492
|
+
const prepared = await prepareDestination(
|
|
493
|
+
rawUrl,
|
|
494
|
+
resolvedOptions,
|
|
495
|
+
redirectCount
|
|
496
|
+
);
|
|
497
|
+
if (isPolicyDenial(prepared)) return prepared;
|
|
498
|
+
return {
|
|
499
|
+
ok: true,
|
|
500
|
+
reason: "DESTINATION_ALLOWED",
|
|
501
|
+
value: new HttpDestinationPin(prepared, resolvedOptions, redirectCount),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Resolve and classify an HTTP destination without opening a socket or
|
|
507
|
+
* transferring request headers/body. This bounded DNS-only phase lets the
|
|
508
|
+
* collection policy evaluate a hostname's proven zone before transport setup.
|
|
509
|
+
*/
|
|
510
|
+
export async function classifyHttpDestination(
|
|
511
|
+
rawUrl: string,
|
|
512
|
+
options: Pick<HttpDestinationPolicyOptions, "resolver">
|
|
513
|
+
): Promise<HttpDestinationClassificationResult> {
|
|
514
|
+
const url = parseHttpUrl(rawUrl);
|
|
515
|
+
const invalid = invalidClassification();
|
|
516
|
+
if (!url) return denial("INVALID_DESTINATION", invalid, null, 0);
|
|
517
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
518
|
+
return denial("UNSUPPORTED_PROTOCOL", invalid, url, 0);
|
|
519
|
+
}
|
|
520
|
+
if (url.username || url.password) {
|
|
521
|
+
return denial("CREDENTIALS_IN_URL", invalid, url, 0);
|
|
522
|
+
}
|
|
523
|
+
const resolved = await resolveAddresses(
|
|
524
|
+
url,
|
|
525
|
+
options.resolver ?? defaultResolver
|
|
526
|
+
);
|
|
527
|
+
if (!resolved.ok) return denial(resolved.reason, invalid, url, 0);
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
reason: "DESTINATION_ALLOWED",
|
|
531
|
+
classification: classifyDestination({
|
|
532
|
+
kind: "network",
|
|
533
|
+
hostname: resolved.hostname,
|
|
534
|
+
addresses: resolved.addresses,
|
|
535
|
+
}),
|
|
536
|
+
};
|
|
537
|
+
}
|
package/src/llm/httpEmbedding.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* @module src/llm/httpEmbedding
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { HttpInferenceOptions } from "./http-inference";
|
|
8
9
|
import type { EmbeddingPort, LlmResult } from "./types";
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { EgressDeniedError } from "../core/egress-enforcement";
|
|
12
|
+
import { egressDeniedInferenceError, inferenceFailedError } from "./errors";
|
|
13
|
+
import { requestHttpInference } from "./http-inference";
|
|
11
14
|
|
|
12
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
16
|
// Types
|
|
@@ -34,11 +37,13 @@ interface OpenAIEmbeddingResponse {
|
|
|
34
37
|
export class HttpEmbedding implements EmbeddingPort {
|
|
35
38
|
private readonly apiUrl: string;
|
|
36
39
|
private readonly modelName: string;
|
|
40
|
+
private readonly requestOptions: HttpInferenceOptions;
|
|
37
41
|
private dims: number | null = null;
|
|
38
42
|
readonly modelUri: string;
|
|
39
43
|
|
|
40
|
-
constructor(modelUri: string) {
|
|
44
|
+
constructor(modelUri: string, requestOptions: HttpInferenceOptions) {
|
|
41
45
|
this.modelUri = modelUri;
|
|
46
|
+
this.requestOptions = requestOptions;
|
|
42
47
|
// Parse URI: http://host:port/v1/embeddings#modelname or just http://host:port
|
|
43
48
|
const hashIndex = modelUri.indexOf("#");
|
|
44
49
|
if (hashIndex > 0) {
|
|
@@ -64,16 +69,20 @@ export class HttpEmbedding implements EmbeddingPort {
|
|
|
64
69
|
|
|
65
70
|
async embed(text: string): Promise<LlmResult<number[]>> {
|
|
66
71
|
try {
|
|
67
|
-
const response = await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const response = await requestHttpInference(
|
|
73
|
+
this.apiUrl,
|
|
74
|
+
{
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
input: text,
|
|
81
|
+
model: this.modelName,
|
|
82
|
+
}),
|
|
71
83
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
model: this.modelName,
|
|
75
|
-
}),
|
|
76
|
-
});
|
|
84
|
+
this.requestOptions
|
|
85
|
+
);
|
|
77
86
|
|
|
78
87
|
if (!response.ok) {
|
|
79
88
|
const errorText = await response.text();
|
|
@@ -108,26 +117,33 @@ export class HttpEmbedding implements EmbeddingPort {
|
|
|
108
117
|
} catch (e) {
|
|
109
118
|
return {
|
|
110
119
|
ok: false,
|
|
111
|
-
error:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
error:
|
|
121
|
+
e instanceof EgressDeniedError
|
|
122
|
+
? egressDeniedInferenceError(e)
|
|
123
|
+
: inferenceFailedError(
|
|
124
|
+
this.modelUri,
|
|
125
|
+
e instanceof Error ? e : new Error(String(e))
|
|
126
|
+
),
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
async embedBatch(texts: string[]): Promise<LlmResult<number[][]>> {
|
|
120
132
|
try {
|
|
121
|
-
const response = await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
const response = await requestHttpInference(
|
|
134
|
+
this.apiUrl,
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
input: texts,
|
|
142
|
+
model: this.modelName,
|
|
143
|
+
}),
|
|
125
144
|
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
model: this.modelName,
|
|
129
|
-
}),
|
|
130
|
-
});
|
|
145
|
+
this.requestOptions
|
|
146
|
+
);
|
|
131
147
|
|
|
132
148
|
if (!response.ok) {
|
|
133
149
|
const errorText = await response.text();
|
|
@@ -168,10 +184,13 @@ export class HttpEmbedding implements EmbeddingPort {
|
|
|
168
184
|
} catch (e) {
|
|
169
185
|
return {
|
|
170
186
|
ok: false,
|
|
171
|
-
error:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
error:
|
|
188
|
+
e instanceof EgressDeniedError
|
|
189
|
+
? egressDeniedInferenceError(e)
|
|
190
|
+
: inferenceFailedError(
|
|
191
|
+
this.modelUri,
|
|
192
|
+
e instanceof Error ? e : new Error(String(e))
|
|
193
|
+
),
|
|
175
194
|
};
|
|
176
195
|
}
|
|
177
196
|
}
|