@gmickel/gno 1.28.0 → 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.
Files changed (143) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +33 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +1 -1
  10. package/spec/cli.md +41 -1
  11. package/spec/db/schema.sql +19 -0
  12. package/spec/mcp.md +66 -0
  13. package/spec/output-schemas/ask.schema.json +31 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +89 -2
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  22. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  23. package/spec/output-schemas/record-import.schema.json +30 -0
  24. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  25. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  26. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  27. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  28. package/spec/output-schemas/search-results.schema.json +30 -0
  29. package/src/app/context-runtime-contract.ts +16 -7
  30. package/src/cli/commands/ask.ts +7 -0
  31. package/src/cli/commands/collection/index.ts +5 -0
  32. package/src/cli/commands/collection/policy.ts +126 -0
  33. package/src/cli/commands/context-build.ts +5 -0
  34. package/src/cli/commands/daemon.ts +45 -3
  35. package/src/cli/commands/egress-audit.ts +68 -0
  36. package/src/cli/commands/embed.ts +2 -0
  37. package/src/cli/commands/query.ts +12 -0
  38. package/src/cli/commands/replay.ts +13 -3
  39. package/src/cli/commands/vsearch.ts +3 -1
  40. package/src/cli/program.ts +123 -0
  41. package/src/config/index.ts +9 -0
  42. package/src/config/types.ts +50 -0
  43. package/src/core/browser-clip.ts +16 -1
  44. package/src/core/collection-egress-policy-projection.ts +28 -0
  45. package/src/core/collection-egress-policy-service.ts +443 -0
  46. package/src/core/collection-egress-policy-validation.ts +242 -0
  47. package/src/core/config-mutation.ts +31 -19
  48. package/src/core/context-capsule-schema.ts +111 -1
  49. package/src/core/context-capsule-validation.ts +4 -4
  50. package/src/core/context-capsule-verification.ts +5 -1
  51. package/src/core/context-capsule.ts +177 -132
  52. package/src/core/context-evidence.ts +86 -14
  53. package/src/core/destination-classifier.ts +402 -0
  54. package/src/core/egress-audit.ts +239 -0
  55. package/src/core/egress-authorization.ts +50 -0
  56. package/src/core/egress-enforcement.ts +264 -0
  57. package/src/core/egress-policy.ts +440 -0
  58. package/src/core/egress-provenance.ts +336 -0
  59. package/src/core/job-manager.ts +37 -0
  60. package/src/core/network-boundary-inventory.ts +261 -0
  61. package/src/core/retrieval-qrels.ts +6 -0
  62. package/src/core/retrieval-replay.ts +18 -4
  63. package/src/core/retrieval-trace-export.ts +23 -4
  64. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  65. package/src/core/retrieval-trace-management-types.ts +3 -0
  66. package/src/core/retrieval-trace-management.ts +9 -1
  67. package/src/core/retrieval-trace-session.ts +94 -0
  68. package/src/core/retrieval-trace.ts +22 -0
  69. package/src/ingestion/record-adapter.ts +12 -1
  70. package/src/ingestion/record-container.ts +38 -22
  71. package/src/ingestion/sync.ts +15 -0
  72. package/src/ingestion/types.ts +2 -0
  73. package/src/llm/errors.ts +10 -0
  74. package/src/llm/http-inference.ts +175 -0
  75. package/src/llm/http-policy.ts +537 -0
  76. package/src/llm/httpEmbedding.ts +47 -28
  77. package/src/llm/httpGeneration.ts +31 -18
  78. package/src/llm/httpRerank.ts +30 -18
  79. package/src/llm/index.ts +1 -0
  80. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  81. package/src/llm/pinned-http-connection.ts +177 -0
  82. package/src/mcp/context.ts +53 -1
  83. package/src/mcp/http-egress.ts +220 -0
  84. package/src/mcp/http-security.ts +32 -21
  85. package/src/mcp/http-session.ts +13 -2
  86. package/src/mcp/http-transport.ts +96 -6
  87. package/src/mcp/sync-egress.ts +24 -0
  88. package/src/mcp/tools/add-collection.ts +4 -0
  89. package/src/mcp/tools/ask.ts +1 -1
  90. package/src/mcp/tools/context.ts +9 -2
  91. package/src/mcp/tools/egress.ts +247 -0
  92. package/src/mcp/tools/embed.ts +1 -0
  93. package/src/mcp/tools/index-cmd.ts +7 -0
  94. package/src/mcp/tools/index.ts +98 -0
  95. package/src/mcp/tools/query.ts +10 -0
  96. package/src/mcp/tools/remove-collection.ts +4 -0
  97. package/src/mcp/tools/sync.ts +6 -0
  98. package/src/mcp/tools/trace.ts +7 -1
  99. package/src/mcp/tools/vsearch.ts +1 -0
  100. package/src/pipeline/egress-lineage.ts +124 -0
  101. package/src/pipeline/hybrid.ts +14 -0
  102. package/src/pipeline/search.ts +12 -0
  103. package/src/pipeline/types.ts +3 -0
  104. package/src/pipeline/vsearch.ts +41 -20
  105. package/src/publish/artifact.ts +31 -3
  106. package/src/publish/export-service.ts +25 -0
  107. package/src/sdk/client.ts +119 -2
  108. package/src/sdk/embed.ts +1 -0
  109. package/src/sdk/types.ts +33 -0
  110. package/src/serve/clipper-capture.ts +5 -1
  111. package/src/serve/closed-json.ts +61 -0
  112. package/src/serve/config-sync.ts +4 -1
  113. package/src/serve/context.ts +1 -0
  114. package/src/serve/doc-events.ts +110 -39
  115. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  116. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  117. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  118. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  119. package/src/serve/public/globals.built.css +1 -1
  120. package/src/serve/public/pages/Collections.tsx +3 -0
  121. package/src/serve/resident-request.ts +80 -3
  122. package/src/serve/resident-runtime.ts +63 -1
  123. package/src/serve/routes/api.ts +174 -0
  124. package/src/serve/routes/clipper.ts +64 -1
  125. package/src/serve/routes/mcp.ts +6 -0
  126. package/src/serve/routes/traces.ts +27 -4
  127. package/src/serve/server.ts +118 -7
  128. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  129. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  130. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  131. package/src/store/migrations/index.ts +6 -0
  132. package/src/store/retrieval-trace-codec.ts +15 -0
  133. package/src/store/sqlite/adapter.ts +170 -7
  134. package/src/store/sqlite/change-journal-store.ts +41 -2
  135. package/src/store/sqlite/egress-audit-store.ts +485 -0
  136. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  137. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  138. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  139. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  140. package/src/store/types.ts +126 -1
  141. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip +0 -0
  142. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +0 -1
  143. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -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 { inferenceFailedError } from "./errors";
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 fetch(this.apiUrl, {
68
- method: "POST",
69
- headers: {
70
- "Content-Type": "application/json",
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
- body: JSON.stringify({
73
- input: text,
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: inferenceFailedError(
112
- this.modelUri,
113
- e instanceof Error ? e : new Error(String(e))
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 fetch(this.apiUrl, {
122
- method: "POST",
123
- headers: {
124
- "Content-Type": "application/json",
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
- body: JSON.stringify({
127
- input: texts,
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: inferenceFailedError(
172
- this.modelUri,
173
- e instanceof Error ? e : new Error(String(e))
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
  }
@@ -5,12 +5,16 @@
5
5
  * @module src/llm/httpGeneration
6
6
  */
7
7
 
8
+ import type { HttpInferenceOptions } from "./http-inference";
8
9
  import type { GenerationPort, GenParams, LlmResult } from "./types";
9
10
 
11
+ import { EgressDeniedError } from "../core/egress-enforcement";
10
12
  import {
13
+ egressDeniedInferenceError,
11
14
  inferenceFailedError,
12
15
  structuredOutputUnavailableError,
13
16
  } from "./errors";
17
+ import { requestHttpInference } from "./http-inference";
14
18
 
15
19
  // ─────────────────────────────────────────────────────────────────────────────
16
20
  // Types
@@ -44,11 +48,13 @@ interface OpenAIChatResponse {
44
48
  export class HttpGeneration implements GenerationPort {
45
49
  private readonly apiUrl: string;
46
50
  private readonly modelName: string;
51
+ private readonly requestOptions: HttpInferenceOptions;
47
52
  readonly modelUri: string;
48
53
  readonly structuredOutput = "none" as const;
49
54
 
50
- constructor(modelUri: string) {
55
+ constructor(modelUri: string, requestOptions: HttpInferenceOptions) {
51
56
  this.modelUri = modelUri;
57
+ this.requestOptions = requestOptions;
52
58
  // Parse URI: http://host:port/v1/chat/completions#modelname
53
59
  const hashIndex = modelUri.indexOf("#");
54
60
  if (hashIndex > 0) {
@@ -74,20 +80,24 @@ export class HttpGeneration implements GenerationPort {
74
80
  };
75
81
  }
76
82
  try {
77
- const response = await fetch(this.apiUrl, {
78
- method: "POST",
79
- headers: {
80
- "Content-Type": "application/json",
83
+ const response = await requestHttpInference(
84
+ this.apiUrl,
85
+ {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ },
90
+ body: JSON.stringify({
91
+ model: this.modelName,
92
+ messages: [{ role: "user", content: prompt }],
93
+ temperature: params?.temperature ?? 0,
94
+ max_tokens: params?.maxTokens ?? 256,
95
+ stop: params?.stop,
96
+ seed: params?.seed,
97
+ }),
81
98
  },
82
- body: JSON.stringify({
83
- model: this.modelName,
84
- messages: [{ role: "user", content: prompt }],
85
- temperature: params?.temperature ?? 0,
86
- max_tokens: params?.maxTokens ?? 256,
87
- stop: params?.stop,
88
- seed: params?.seed,
89
- }),
90
- });
99
+ this.requestOptions
100
+ );
91
101
 
92
102
  if (!response.ok) {
93
103
  const errorText = await response.text();
@@ -107,10 +117,13 @@ export class HttpGeneration implements GenerationPort {
107
117
  } catch (e) {
108
118
  return {
109
119
  ok: false,
110
- error: inferenceFailedError(
111
- this.modelUri,
112
- e instanceof Error ? e : new Error(String(e))
113
- ),
120
+ error:
121
+ e instanceof EgressDeniedError
122
+ ? egressDeniedInferenceError(e)
123
+ : inferenceFailedError(
124
+ this.modelUri,
125
+ e instanceof Error ? e : new Error(String(e))
126
+ ),
114
127
  };
115
128
  }
116
129
  }
@@ -5,9 +5,12 @@
5
5
  * @module src/llm/httpRerank
6
6
  */
7
7
 
8
+ import type { HttpInferenceOptions } from "./http-inference";
8
9
  import type { LlmResult, RerankPort, RerankScore } from "./types";
9
10
 
10
- import { inferenceFailedError } from "./errors";
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
@@ -36,10 +39,12 @@ export class HttpRerank implements RerankPort {
36
39
  private readonly apiUrl: string;
37
40
  private readonly modelName: string;
38
41
  private readonly instruction: string;
42
+ private readonly requestOptions: HttpInferenceOptions;
39
43
  readonly modelUri: string;
40
44
 
41
- constructor(modelUri: string) {
45
+ constructor(modelUri: string, requestOptions: HttpInferenceOptions) {
42
46
  this.modelUri = modelUri;
47
+ this.requestOptions = requestOptions;
43
48
  // Parse URI: http://host:port/v1/completions#modelname
44
49
  const hashIndex = modelUri.indexOf("#");
45
50
  if (hashIndex > 0) {
@@ -109,19 +114,23 @@ export class HttpRerank implements RerankPort {
109
114
 
110
115
  private async scoreBatch(prompts: string[]): Promise<LlmResult<number[]>> {
111
116
  try {
112
- const response = await fetch(this.apiUrl, {
113
- method: "POST",
114
- headers: {
115
- "Content-Type": "application/json",
117
+ const response = await requestHttpInference(
118
+ this.apiUrl,
119
+ {
120
+ method: "POST",
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ },
124
+ body: JSON.stringify({
125
+ model: this.modelName,
126
+ prompt: prompts, // Array of prompts for batching
127
+ max_tokens: 10, // Just need the score
128
+ temperature: 0, // Deterministic
129
+ stop: ["\n", "<"],
130
+ }),
116
131
  },
117
- body: JSON.stringify({
118
- model: this.modelName,
119
- prompt: prompts, // Array of prompts for batching
120
- max_tokens: 10, // Just need the score
121
- temperature: 0, // Deterministic
122
- stop: ["\n", "<"],
123
- }),
124
- });
132
+ this.requestOptions
133
+ );
125
134
 
126
135
  if (!response.ok) {
127
136
  const errorText = await response.text();
@@ -154,10 +163,13 @@ export class HttpRerank implements RerankPort {
154
163
  } catch (e) {
155
164
  return {
156
165
  ok: false,
157
- error: inferenceFailedError(
158
- this.modelUri,
159
- e instanceof Error ? e : new Error(String(e))
160
- ),
166
+ error:
167
+ e instanceof EgressDeniedError
168
+ ? egressDeniedInferenceError(e)
169
+ : inferenceFailedError(
170
+ this.modelUri,
171
+ e instanceof Error ? e : new Error(String(e))
172
+ ),
161
173
  };
162
174
  }
163
175
  }
package/src/llm/index.ts CHANGED
@@ -14,6 +14,7 @@ export type { LlmError, LlmErrorCode } from "./errors";
14
14
  export {
15
15
  corruptedError,
16
16
  downloadFailedError,
17
+ egressDeniedInferenceError,
17
18
  inferenceFailedError,
18
19
  invalidUriError,
19
20
  isRetryable,
@@ -43,11 +43,22 @@ export interface CreatePortOptions {
43
43
  policy?: DownloadPolicy;
44
44
  /** Progress callback for downloads */
45
45
  onProgress?: ProgressCallback;
46
+ /**
47
+ * Participating collection scope for outbound HTTP inference.
48
+ * `"all"` is an explicit corpus-wide decision, never an omission fallback.
49
+ */
50
+ egressCollections: readonly string[] | "all";
46
51
  }
47
52
 
48
53
  /** Default policy: no auto-download (backwards compatible) */
49
54
  const DEFAULT_POLICY: DownloadPolicy = { offline: false, allowDownload: false };
50
55
 
56
+ const resolveEgressCollectionNames = (
57
+ config: Config,
58
+ scope: CreatePortOptions["egressCollections"]
59
+ ): readonly string[] =>
60
+ scope === "all" ? config.collections.map(({ name }) => name) : scope;
61
+
51
62
  // ─────────────────────────────────────────────────────────────────────────────
52
63
  // Adapter
53
64
  // ─────────────────────────────────────────────────────────────────────────────
@@ -70,16 +81,22 @@ export class LlmAdapter {
70
81
  * With options.policy.allowDownload=true, auto-downloads if not cached.
71
82
  */
72
83
  async createEmbeddingPort(
73
- modelUri?: string,
74
- options?: CreatePortOptions
84
+ modelUri: string | undefined,
85
+ options: CreatePortOptions
75
86
  ): Promise<LlmResult<EmbeddingPort>> {
76
87
  const preset = getActivePreset(this.config);
77
88
  const uri = modelUri ?? preset.embed;
78
- const policy = options?.policy ?? DEFAULT_POLICY;
89
+ const policy = options.policy ?? DEFAULT_POLICY;
79
90
 
80
91
  // Use HTTP embedding for remote endpoints
81
92
  if (isHttpModelUri(uri)) {
82
- const httpEmbed = new HttpEmbedding(uri);
93
+ const httpEmbed = new HttpEmbedding(uri, {
94
+ collections: this.config.collections,
95
+ collectionNames: resolveEgressCollectionNames(
96
+ this.config,
97
+ options.egressCollections
98
+ ),
99
+ });
83
100
  // Initialize to verify connection and get dimensions
84
101
  const initResult = await httpEmbed.init();
85
102
  if (!initResult.ok) {
@@ -93,7 +110,7 @@ export class LlmAdapter {
93
110
  uri,
94
111
  "embed",
95
112
  policy,
96
- options?.onProgress
113
+ options.onProgress
97
114
  );
98
115
  if (!resolved.ok) {
99
116
  return resolved;
@@ -111,15 +128,21 @@ export class LlmAdapter {
111
128
  * With options.policy.allowDownload=true, auto-downloads if not cached.
112
129
  */
113
130
  async createGenerationPort(
114
- modelUri?: string,
115
- options?: CreatePortOptions
131
+ modelUri: string | undefined,
132
+ options: CreatePortOptions
116
133
  ): Promise<LlmResult<GenerationPort>> {
117
134
  const uri = getAnswerModelUri(this.config, modelUri);
118
- const policy = options?.policy ?? DEFAULT_POLICY;
135
+ const policy = options.policy ?? DEFAULT_POLICY;
119
136
 
120
137
  // Use HTTP generation for remote endpoints
121
138
  if (isHttpGenUri(uri)) {
122
- const httpGen = new HttpGeneration(uri);
139
+ const httpGen = new HttpGeneration(uri, {
140
+ collections: this.config.collections,
141
+ collectionNames: resolveEgressCollectionNames(
142
+ this.config,
143
+ options.egressCollections
144
+ ),
145
+ });
123
146
  return { ok: true, value: httpGen };
124
147
  }
125
148
 
@@ -128,7 +151,7 @@ export class LlmAdapter {
128
151
  uri,
129
152
  "gen",
130
153
  policy,
131
- options?.onProgress
154
+ options.onProgress
132
155
  );
133
156
  if (!resolved.ok) {
134
157
  return resolved;
@@ -145,14 +168,20 @@ export class LlmAdapter {
145
168
  * Uses preset.expand when configured, else falls back to preset.gen.
146
169
  */
147
170
  async createExpansionPort(
148
- modelUri?: string,
149
- options?: CreatePortOptions
171
+ modelUri: string | undefined,
172
+ options: CreatePortOptions
150
173
  ): Promise<LlmResult<GenerationPort>> {
151
174
  const uri = getExpandModelUri(this.config, modelUri);
152
- const policy = options?.policy ?? DEFAULT_POLICY;
175
+ const policy = options.policy ?? DEFAULT_POLICY;
153
176
 
154
177
  if (isHttpGenUri(uri)) {
155
- const httpGen = new HttpGeneration(uri);
178
+ const httpGen = new HttpGeneration(uri, {
179
+ collections: this.config.collections,
180
+ collectionNames: resolveEgressCollectionNames(
181
+ this.config,
182
+ options.egressCollections
183
+ ),
184
+ });
156
185
  return { ok: true, value: httpGen };
157
186
  }
158
187
 
@@ -160,7 +189,7 @@ export class LlmAdapter {
160
189
  uri,
161
190
  "expand",
162
191
  policy,
163
- options?.onProgress
192
+ options.onProgress
164
193
  );
165
194
  if (!resolved.ok) {
166
195
  return resolved;
@@ -178,16 +207,22 @@ export class LlmAdapter {
178
207
  * With options.policy.allowDownload=true, auto-downloads if not cached.
179
208
  */
180
209
  async createRerankPort(
181
- modelUri?: string,
182
- options?: CreatePortOptions
210
+ modelUri: string | undefined,
211
+ options: CreatePortOptions
183
212
  ): Promise<LlmResult<RerankPort>> {
184
213
  const preset = getActivePreset(this.config);
185
214
  const uri = modelUri ?? preset.rerank;
186
- const policy = options?.policy ?? DEFAULT_POLICY;
215
+ const policy = options.policy ?? DEFAULT_POLICY;
187
216
 
188
217
  // Use HTTP rerank for remote endpoints
189
218
  if (isHttpRerankUri(uri)) {
190
- const httpRerank = new HttpRerank(uri);
219
+ const httpRerank = new HttpRerank(uri, {
220
+ collections: this.config.collections,
221
+ collectionNames: resolveEgressCollectionNames(
222
+ this.config,
223
+ options.egressCollections
224
+ ),
225
+ });
191
226
  return { ok: true, value: httpRerank };
192
227
  }
193
228
 
@@ -196,7 +231,7 @@ export class LlmAdapter {
196
231
  uri,
197
232
  "rerank",
198
233
  policy,
199
- options?.onProgress
234
+ options.onProgress
200
235
  );
201
236
  if (!resolved.ok) {
202
237
  return resolved;
@@ -0,0 +1,177 @@
1
+ /** Opaque request seam for one DNS-pinned HTTP connection. */
2
+
3
+ import type { DestinationClassificationAudit } from "../core/destination-classifier";
4
+
5
+ export interface HttpDestinationPolicyAudit {
6
+ protocol: "http" | "https" | "unknown";
7
+ port: number | null;
8
+ redirectCount: number;
9
+ classification: DestinationClassificationAudit;
10
+ }
11
+
12
+ export type PinnedHttpFetch = (
13
+ input: string | URL | Request,
14
+ init?: BunFetchRequestInit
15
+ ) => Promise<Response>;
16
+
17
+ export class PinnedHttpRequestError extends Error {
18
+ readonly code = "PINNED_HTTP_REQUEST_FAILED";
19
+ readonly aborted: boolean;
20
+
21
+ constructor(aborted: boolean) {
22
+ super(
23
+ aborted ? "Pinned HTTP request aborted" : "Pinned HTTP request failed"
24
+ );
25
+ this.name = "PinnedHttpRequestError";
26
+ this.aborted = aborted;
27
+ this.stack = `${this.name}: ${this.message}`;
28
+ }
29
+
30
+ toJSON(): { code: string; message: string; aborted: boolean } {
31
+ return { code: this.code, message: this.message, aborted: this.aborted };
32
+ }
33
+ }
34
+
35
+ function freezeAudit(
36
+ audit: HttpDestinationPolicyAudit
37
+ ): HttpDestinationPolicyAudit {
38
+ const classification = Object.freeze({
39
+ ...audit.classification,
40
+ addressClasses: Object.freeze([...audit.classification.addressClasses]),
41
+ });
42
+ return Object.freeze({ ...audit, classification });
43
+ }
44
+
45
+ function normalizedRequestError(
46
+ signal?: AbortSignal | null
47
+ ): PinnedHttpRequestError {
48
+ return new PinnedHttpRequestError(signal?.aborted === true);
49
+ }
50
+
51
+ function releaseReaderLock(
52
+ reader: ReadableStreamDefaultReader<Uint8Array>
53
+ ): void {
54
+ try {
55
+ reader.releaseLock();
56
+ } catch {
57
+ // The sanitized stream is already terminal; never replace its safe error.
58
+ }
59
+ }
60
+
61
+ function sanitizeResponseBody(
62
+ body: ReadableStream<Uint8Array> | null,
63
+ signal?: AbortSignal | null
64
+ ): ReadableStream<Uint8Array> | null {
65
+ if (!body) return null;
66
+ const reader = body.getReader();
67
+ return new ReadableStream<Uint8Array>({
68
+ async pull(controller): Promise<void> {
69
+ try {
70
+ const chunk = await reader.read();
71
+ if (chunk.done) {
72
+ releaseReaderLock(reader);
73
+ controller.close();
74
+ return;
75
+ }
76
+ controller.enqueue(chunk.value);
77
+ } catch {
78
+ releaseReaderLock(reader);
79
+ controller.error(normalizedRequestError(signal));
80
+ }
81
+ },
82
+ async cancel(reason): Promise<void> {
83
+ try {
84
+ await reader.cancel(reason);
85
+ releaseReaderLock(reader);
86
+ } catch {
87
+ releaseReaderLock(reader);
88
+ throw normalizedRequestError(signal);
89
+ }
90
+ },
91
+ });
92
+ }
93
+
94
+ function buildPinnedRequestInit(
95
+ init: BunFetchRequestInit,
96
+ headers: Headers,
97
+ tls: BunFetchRequestInitTLS
98
+ ): BunFetchRequestInit {
99
+ return {
100
+ body: init.body,
101
+ cache: init.cache,
102
+ credentials: init.credentials,
103
+ decompress: init.decompress,
104
+ headers,
105
+ integrity: init.integrity,
106
+ keepalive: init.keepalive,
107
+ method: init.method,
108
+ mode: init.mode,
109
+ priority: init.priority,
110
+ redirect: "manual",
111
+ referrer: init.referrer,
112
+ referrerPolicy: init.referrerPolicy,
113
+ signal: init.signal,
114
+ tls,
115
+ verbose: false,
116
+ window: init.window,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * JSON/log projection is redacted. request() is the only raw target consumer;
122
+ * it preserves Host/SNI while forcing an IP URL and manual redirects.
123
+ */
124
+ export class PinnedHttpConnection {
125
+ readonly #targetUrl: string;
126
+ readonly #hostHeader: string;
127
+ readonly #tlsServerName?: string;
128
+ readonly #audit: HttpDestinationPolicyAudit;
129
+
130
+ constructor(
131
+ targetUrl: string,
132
+ hostHeader: string,
133
+ tlsServerName: string | undefined,
134
+ audit: HttpDestinationPolicyAudit
135
+ ) {
136
+ this.#targetUrl = targetUrl;
137
+ this.#hostHeader = hostHeader;
138
+ this.#tlsServerName = tlsServerName;
139
+ this.#audit = freezeAudit(audit);
140
+ }
141
+
142
+ toJSON(): { audit: HttpDestinationPolicyAudit; pinned: true } {
143
+ return { audit: this.#audit, pinned: true };
144
+ }
145
+
146
+ async request(
147
+ init: BunFetchRequestInit = {},
148
+ fetchFn: PinnedHttpFetch = fetch
149
+ ): Promise<Response> {
150
+ const headers = new Headers(init.headers);
151
+ headers.set("host", this.#hostHeader);
152
+ const {
153
+ checkServerIdentity: _ignoredIdentityOverride,
154
+ rejectUnauthorized: _ignoredVerificationOverride,
155
+ serverName: _ignoredServerNameOverride,
156
+ ...callerTls
157
+ } = init.tls ?? {};
158
+ const tls = {
159
+ ...callerTls,
160
+ rejectUnauthorized: true,
161
+ serverName: this.#tlsServerName,
162
+ };
163
+ try {
164
+ const response = await fetchFn(
165
+ this.#targetUrl,
166
+ buildPinnedRequestInit(init, headers, tls)
167
+ );
168
+ return new Response(sanitizeResponseBody(response.body, init.signal), {
169
+ status: response.status,
170
+ statusText: response.statusText,
171
+ headers: response.headers,
172
+ });
173
+ } catch {
174
+ throw normalizedRequestError(init.signal);
175
+ }
176
+ }
177
+ }