@runuai/host 0.9.171 → 0.9.172
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/lib/agents/claude.ts +13 -0
- package/lib/agents/codex.ts +11 -5
- package/lib/agents/session-state.ts +77 -0
- package/lib/agents/transport.ts +2 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +3 -25
- package/lib/assignment-evidence.ts +2 -1
- package/lib/capture-flow.ts +14 -1
- package/lib/cloud-credential-leases.ts +9 -3
- package/lib/cloud-credential-runtime.ts +8 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/credential-cleanup-inventory.ts +2 -1
- package/lib/credential-consumers/engines.ts +29 -5
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +299 -0
- package/lib/custom-connection-runtime.ts +70 -0
- package/lib/engine-accounts.ts +1 -1
- package/lib/engine-credential-env.ts +2 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/obs.ts +2 -1
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +308 -26
- package/lib/secrets-proxy/address.ts +23 -0
- package/lib/task-credential-cleanup.ts +13 -3
- package/package.json +1 -1
- package/runner/runner.mjs +231 -3
- package/src/capture-wire.ts +102 -0
- package/src/cloud-credentials.ts +36 -372
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +207 -0
- package/src/main.ts +104 -3
- package/src/protocol.ts +18 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/** ADR-164 sign-in-box discovery and protocol admission. Provider response
|
|
2
|
+
* bodies and error text never leave this module. All production I/O goes
|
|
3
|
+
* through the DNS-pinned safeRequest transport. */
|
|
4
|
+
import {
|
|
5
|
+
customConnectionEnabled,
|
|
6
|
+
validConnectionModels,
|
|
7
|
+
parseCustomConnectionMaterial,
|
|
8
|
+
parseEngineConnectCustomDiscoverFrame,
|
|
9
|
+
parseEngineConnectCustomSaveFrame,
|
|
10
|
+
parseEngineConnectCustomManualSaveFrame,
|
|
11
|
+
type EngineConnectCustomManualSaveFrame,
|
|
12
|
+
type CustomConnectionAuthMode,
|
|
13
|
+
type CustomConnectionDiscoverResult,
|
|
14
|
+
type CustomConnectionMaterial,
|
|
15
|
+
type CustomConnectionReason,
|
|
16
|
+
type CustomConnectionSaveResult,
|
|
17
|
+
type EngineConnectCustomDiscoverFrame,
|
|
18
|
+
type EngineConnectCustomSaveFrame,
|
|
19
|
+
} from "../src/custom-connections";
|
|
20
|
+
import { buildEngineCapture, parseCaptureFlow, writeCapture } from "./capture-flow";
|
|
21
|
+
import type { CredentialStore } from "./credential-store";
|
|
22
|
+
import { readOpenRouterBudget } from "./openrouter-budget";
|
|
23
|
+
import { readSafeBody, safeRequest, SafeHttpRefusal, type SafeHttpRequest, type SafeHttpResponse } from "./safe-http";
|
|
24
|
+
|
|
25
|
+
export const CUSTOM_CONNECTION_DISCOVERY_MAX_BYTES = 1024 * 1024;
|
|
26
|
+
export const CUSTOM_CONNECTION_DISCOVERY_MAX_PAGES = 5;
|
|
27
|
+
export const CUSTOM_CONNECTION_DISCOVERY_MAX_MODELS = 2000;
|
|
28
|
+
export const CUSTOM_CONNECTION_ATTEMPT_MS = 20_000;
|
|
29
|
+
const PROBE_MAX_BYTES = 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
export interface CustomConnectionCaptureOptions {
|
|
32
|
+
/** Tests may replace the transport. Production always defaults safeRequest. */
|
|
33
|
+
request?: typeof safeRequest;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ConnectionFailure extends Error {
|
|
39
|
+
constructor(readonly reason: CustomConnectionReason, readonly retryable = reason === "timeout") {
|
|
40
|
+
super(reason);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function object(value: unknown): Record<string, unknown> | null {
|
|
45
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function failure(error: unknown, signal?: AbortSignal): ConnectionFailure {
|
|
49
|
+
if (error instanceof ConnectionFailure) return error;
|
|
50
|
+
if (signal?.aborted || (error instanceof SafeHttpRefusal && error.reason === "timeout")) return new ConnectionFailure("timeout", true);
|
|
51
|
+
if (error instanceof SafeHttpRefusal) {
|
|
52
|
+
if (error.reason === "non_public_address") return new ConnectionFailure("private-address");
|
|
53
|
+
if (["url", "request_too_large", "response_too_large"].includes(error.reason)) return new ConnectionFailure("wrong-protocol");
|
|
54
|
+
}
|
|
55
|
+
return new ConnectionFailure("unreachable");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function statusFailure(status: number): ConnectionFailure {
|
|
59
|
+
if (status === 401) return new ConnectionFailure("unauthorized");
|
|
60
|
+
if (status === 403) return new ConnectionFailure("forbidden");
|
|
61
|
+
if (status === 404) return new ConnectionFailure("model-not-found");
|
|
62
|
+
if (status === 429) return new ConnectionFailure("rate-limited");
|
|
63
|
+
if (status >= 500 && status <= 599) return new ConnectionFailure("upstream-error", true);
|
|
64
|
+
return new ConnectionFailure("wrong-protocol");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function headers(settings: CustomConnectionMaterial, auth: CustomConnectionAuthMode = settings.authMode): Record<string, string> {
|
|
68
|
+
return {
|
|
69
|
+
...(auth === "bearer" ? { authorization: `Bearer ${settings.apiKey}` } : { "x-api-key": settings.apiKey }),
|
|
70
|
+
...(settings.engine === "claude" && settings.providerType !== "bedrock" ? { "anthropic-version": "2023-06-01" } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function deadline(options: CustomConnectionCaptureOptions): AbortSignal {
|
|
75
|
+
const timeout = AbortSignal.timeout(CUSTOM_CONNECTION_ATTEMPT_MS);
|
|
76
|
+
return options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function exchange(request: SafeHttpRequest, options: CustomConnectionCaptureOptions, signal: AbortSignal): Promise<SafeHttpResponse> {
|
|
80
|
+
try {
|
|
81
|
+
return await (options.request ?? safeRequest)({ ...request, signal }, {
|
|
82
|
+
connectMs: CUSTOM_CONNECTION_ATTEMPT_MS,
|
|
83
|
+
headersMs: CUSTOM_CONNECTION_ATTEMPT_MS,
|
|
84
|
+
idleMs: CUSTOM_CONNECTION_ATTEMPT_MS,
|
|
85
|
+
maxRequestBodyBytes: 16 * 1024,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw failure(error, signal);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function body(response: SafeHttpResponse, maxBytes: number, signal: AbortSignal): Promise<Buffer> {
|
|
93
|
+
try { return await readSafeBody(response, maxBytes); } catch (error) { throw failure(error, signal); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function decodeJson(raw: Buffer): unknown {
|
|
97
|
+
try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw)); } catch { throw new ConnectionFailure("wrong-protocol"); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function json(request: SafeHttpRequest, options: CustomConnectionCaptureOptions, signal: AbortSignal, maxBytes: number): Promise<{ value: Record<string, unknown>; bytes: number }> {
|
|
101
|
+
const response = await exchange(request, options, signal);
|
|
102
|
+
if (response.status !== 200) {
|
|
103
|
+
response.body.destroy();
|
|
104
|
+
throw statusFailure(response.status);
|
|
105
|
+
}
|
|
106
|
+
const raw = await body(response, maxBytes, signal);
|
|
107
|
+
const value = object(decodeJson(raw));
|
|
108
|
+
if (!value) throw new ConnectionFailure("wrong-protocol");
|
|
109
|
+
return { value, bytes: raw.byteLength };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validated(settings: CustomConnectionMaterial, enabled: EngineConnectCustomDiscoverFrame["enabled"], now: number): CustomConnectionMaterial {
|
|
113
|
+
const parsed = parseCustomConnectionMaterial(settings);
|
|
114
|
+
if (!parsed || !customConnectionEnabled(parsed, enabled)) throw new ConnectionFailure("unsupported");
|
|
115
|
+
if (parsed.expiresAt !== undefined && parsed.expiresAt !== null && parsed.expiresAt <= now) throw new ConnectionFailure("expired");
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function modelId(value: unknown, key: string): value is string {
|
|
120
|
+
return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\s\u0000-\u001f\u007f]/.test(value) && !value.includes(key);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Pagination never changes the destination or resource carrying the key. */
|
|
124
|
+
function nextPage(value: Record<string, unknown>, current: URL, initial: URL): URL | null {
|
|
125
|
+
if (value.has_more === true) {
|
|
126
|
+
if (typeof value.last_id !== "string" || !value.last_id || value.last_id.length > 512) throw new ConnectionFailure("wrong-protocol");
|
|
127
|
+
const next = new URL(current);
|
|
128
|
+
next.searchParams.set("after_id", value.last_id);
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
if (typeof value.nextToken === "string" && value.nextToken) {
|
|
132
|
+
if (value.nextToken.length > 4096) throw new ConnectionFailure("wrong-protocol");
|
|
133
|
+
const next = new URL(current);
|
|
134
|
+
next.searchParams.set("nextToken", value.nextToken);
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
const links = object(value.links);
|
|
138
|
+
if (links?.next !== undefined && links.next !== null) {
|
|
139
|
+
if (typeof links.next !== "string" || links.next.length > 4096) throw new ConnectionFailure("wrong-protocol");
|
|
140
|
+
const next = new URL(links.next, initial);
|
|
141
|
+
if (next.origin !== initial.origin || next.pathname !== initial.pathname || next.username || next.password || next.hash) throw new ConnectionFailure("wrong-protocol");
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Discover without writing a capture. A denied listing is a non-fatal UI
|
|
148
|
+
* outcome: the owner can enter model IDs and test them directly. */
|
|
149
|
+
export async function discoverCustomConnection(frame: EngineConnectCustomDiscoverFrame, options: CustomConnectionCaptureOptions = {}): Promise<CustomConnectionDiscoverResult> {
|
|
150
|
+
const signal = deadline(options);
|
|
151
|
+
try {
|
|
152
|
+
const parsedFrame = parseEngineConnectCustomDiscoverFrame(frame);
|
|
153
|
+
if (!parsedFrame || parsedFrame.settings.internal === true) throw new ConnectionFailure("unsupported");
|
|
154
|
+
const settings = validated(parsedFrame.settings, parsedFrame.enabled, (options.now ?? Date.now)());
|
|
155
|
+
if (settings.providerType === "bedrock" && settings.engine === "codex" && settings.transport === "runtime") return { ok: false, reason: "unsupported" };
|
|
156
|
+
const models = new Set<string>();
|
|
157
|
+
const streamingFoundationIds = new Set<string>();
|
|
158
|
+
let bytes = 0;
|
|
159
|
+
let pages = 0;
|
|
160
|
+
const bedrockClaude = settings.providerType === "bedrock" && settings.engine === "claude";
|
|
161
|
+
const roots = bedrockClaude
|
|
162
|
+
? [`https://bedrock.${settings.region}.amazonaws.com/foundation-models`, `https://bedrock.${settings.region}.amazonaws.com/inference-profiles`]
|
|
163
|
+
: [settings.providerType === "bedrock" ? `https://bedrock-mantle.${settings.region}.api.aws/v1/models` : `${settings.baseUrl}${settings.engine === "claude" ? "/v1" : ""}/models`];
|
|
164
|
+
let truncated = false;
|
|
165
|
+
discovery: for (const root of roots) {
|
|
166
|
+
const initial = new URL(root);
|
|
167
|
+
if (settings.engine === "claude" && settings.providerType !== "bedrock") initial.searchParams.set("limit", "1000");
|
|
168
|
+
let url: URL | null = initial;
|
|
169
|
+
const seen = new Set<string>();
|
|
170
|
+
while (url !== null) {
|
|
171
|
+
if (pages >= CUSTOM_CONNECTION_DISCOVERY_MAX_PAGES) { truncated = true; break discovery; }
|
|
172
|
+
if (seen.has(url.href)) throw new ConnectionFailure("wrong-protocol");
|
|
173
|
+
seen.add(url.href);
|
|
174
|
+
pages++;
|
|
175
|
+
const result = await json({ url: url.href, headers: headers(settings) }, options, signal, CUSTOM_CONNECTION_DISCOVERY_MAX_BYTES - bytes);
|
|
176
|
+
bytes += result.bytes;
|
|
177
|
+
const list = bedrockClaude ? result.value[root.endsWith("foundation-models") ? "modelSummaries" : "inferenceProfileSummaries"] : result.value.data;
|
|
178
|
+
if (!Array.isArray(list)) throw new ConnectionFailure("wrong-protocol");
|
|
179
|
+
for (const entry of list) {
|
|
180
|
+
const row = object(entry);
|
|
181
|
+
if (!row) throw new ConnectionFailure("wrong-protocol");
|
|
182
|
+
const id = bedrockClaude ? row.modelId ?? row.inferenceProfileId : row.id;
|
|
183
|
+
if (!modelId(id, settings.apiKey)) throw new ConnectionFailure("invalid-models");
|
|
184
|
+
if (bedrockClaude) {
|
|
185
|
+
if (root.endsWith("foundation-models")) {
|
|
186
|
+
if (row.providerName !== "Anthropic" || row.responseStreamingSupported !== true) continue;
|
|
187
|
+
streamingFoundationIds.add(id);
|
|
188
|
+
} else {
|
|
189
|
+
if (!Array.isArray(row.models) || !row.models.length || !row.models.every(value => {
|
|
190
|
+
const arn = object(value)?.modelArn;
|
|
191
|
+
return typeof arn === "string" && streamingFoundationIds.has(arn.slice(arn.lastIndexOf("/") + 1));
|
|
192
|
+
})) continue;
|
|
193
|
+
if (row.status !== undefined && row.status !== "ACTIVE") continue;
|
|
194
|
+
}
|
|
195
|
+
} else if (settings.providerType === "bedrock") {
|
|
196
|
+
// Mantle lists several incompatible route families. Names alone
|
|
197
|
+
// are not proof that a model serves the /openai/v1 transport.
|
|
198
|
+
const paths = row.supported_api_paths ?? row.supportedApiPaths;
|
|
199
|
+
if (row.api_path !== "/openai/v1" && row.apiPath !== "/openai/v1" && !(Array.isArray(paths) && paths.includes("/openai/v1"))) continue;
|
|
200
|
+
}
|
|
201
|
+
if (!models.has(id) && models.size >= CUSTOM_CONNECTION_DISCOVERY_MAX_MODELS) {
|
|
202
|
+
truncated = true;
|
|
203
|
+
break discovery;
|
|
204
|
+
}
|
|
205
|
+
models.add(id);
|
|
206
|
+
}
|
|
207
|
+
url = nextPage(result.value, url, initial);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (settings.providerType === "bedrock" && settings.transport === "mantle" && models.size === 0) return { ok: false, reason: "unsupported" };
|
|
211
|
+
return { ok: true, models: [...models], ...(truncated ? { truncated: true } : {}) };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return { ok: false, reason: failure(error, signal).reason };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validMessages(value: unknown): boolean {
|
|
218
|
+
const message = object(value);
|
|
219
|
+
const usage = object(message?.usage);
|
|
220
|
+
return Boolean(message && message.type === "message" && message.role === "assistant" && typeof message.id === "string" && message.id.length > 0 && typeof message.model === "string" && Array.isArray(message.content) && usage && [usage.input_tokens, usage.output_tokens].every(n => Number.isSafeInteger(n) && (n as number) >= 0));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Responses terminals are protocol outcomes, not merely successful HTTP. */
|
|
224
|
+
export function validateResponsesProbe(raw: Buffer): void {
|
|
225
|
+
let text: string;
|
|
226
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(raw).replace(/\r\n/g, "\n"); } catch { throw new ConnectionFailure("wrong-protocol"); }
|
|
227
|
+
const chunks = text.split("\n\n");
|
|
228
|
+
const tail = chunks.pop();
|
|
229
|
+
if (tail?.trim()) throw new ConnectionFailure("wrong-protocol");
|
|
230
|
+
let terminal = false;
|
|
231
|
+
for (const chunk of chunks) {
|
|
232
|
+
let event: string | undefined;
|
|
233
|
+
const data: string[] = [];
|
|
234
|
+
for (const line of chunk.split("\n")) {
|
|
235
|
+
if (line.startsWith(":")) continue;
|
|
236
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
237
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
|
|
238
|
+
else if (line && !line.startsWith("id:") && !line.startsWith("retry:")) throw new ConnectionFailure("wrong-protocol");
|
|
239
|
+
}
|
|
240
|
+
if (!data.length) continue;
|
|
241
|
+
if (data.join("\n") === "[DONE]") {
|
|
242
|
+
if (!terminal) throw new ConnectionFailure("wrong-protocol");
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (terminal) throw new ConnectionFailure("wrong-protocol");
|
|
246
|
+
const value = object(decodeJson(Buffer.from(data.join("\n"))));
|
|
247
|
+
if (!value || typeof value.type !== "string" || (event !== undefined && event !== value.type)) throw new ConnectionFailure("wrong-protocol");
|
|
248
|
+
if (value.type === "error" || value.type === "response.failed") throw new ConnectionFailure("upstream-error");
|
|
249
|
+
if (value.type === "response.completed") {
|
|
250
|
+
if (object(value.response)?.status !== "completed") throw new ConnectionFailure("wrong-protocol");
|
|
251
|
+
terminal = true;
|
|
252
|
+
} else if (value.type === "response.incomplete") {
|
|
253
|
+
const response = object(value.response);
|
|
254
|
+
if (response?.status !== "incomplete" || object(response.incomplete_details)?.reason !== "max_output_tokens") throw new ConnectionFailure("wrong-protocol");
|
|
255
|
+
terminal = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!terminal) throw new ConnectionFailure("wrong-protocol");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** AWS EventStream has two CRC32 checks: prelude and the complete message. */
|
|
262
|
+
export function eventStreamCrc32(bytes: Uint8Array): number {
|
|
263
|
+
let crc = 0xffffffff;
|
|
264
|
+
for (const byte of bytes) {
|
|
265
|
+
crc ^= byte;
|
|
266
|
+
for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
267
|
+
}
|
|
268
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function validateBedrockProbe(raw: Buffer): void {
|
|
272
|
+
let offset = 0;
|
|
273
|
+
let started = false;
|
|
274
|
+
let finishedMessage = false;
|
|
275
|
+
let stopped = false;
|
|
276
|
+
while (offset < raw.length) {
|
|
277
|
+
if (raw.length - offset < 16) throw new ConnectionFailure("wrong-protocol");
|
|
278
|
+
const length = raw.readUInt32BE(offset);
|
|
279
|
+
const headersLength = raw.readUInt32BE(offset + 4);
|
|
280
|
+
if (length < 16 || length > PROBE_MAX_BYTES || headersLength > length - 16 || offset + length > raw.length || raw.readUInt32BE(offset + 8) !== eventStreamCrc32(raw.subarray(offset, offset + 8)) || raw.readUInt32BE(offset + length - 4) !== eventStreamCrc32(raw.subarray(offset, offset + length - 4))) throw new ConnectionFailure("wrong-protocol");
|
|
281
|
+
const headerEnd = offset + 12 + headersLength;
|
|
282
|
+
let cursor = offset + 12;
|
|
283
|
+
const parsedHeaders = new Map<string, string>();
|
|
284
|
+
while (cursor < headerEnd) {
|
|
285
|
+
const nameLength = raw[cursor++]!;
|
|
286
|
+
if (!nameLength || cursor + nameLength + 3 > headerEnd) throw new ConnectionFailure("wrong-protocol");
|
|
287
|
+
const name = raw.subarray(cursor, cursor + nameLength).toString("utf8");
|
|
288
|
+
cursor += nameLength;
|
|
289
|
+
if (raw[cursor++] !== 7) throw new ConnectionFailure("wrong-protocol");
|
|
290
|
+
const valueLength = raw.readUInt16BE(cursor);
|
|
291
|
+
cursor += 2;
|
|
292
|
+
if (cursor + valueLength > headerEnd || parsedHeaders.has(name)) throw new ConnectionFailure("wrong-protocol");
|
|
293
|
+
parsedHeaders.set(name, raw.subarray(cursor, cursor + valueLength).toString("utf8"));
|
|
294
|
+
cursor += valueLength;
|
|
295
|
+
}
|
|
296
|
+
if (parsedHeaders.get(":message-type") !== "event" || parsedHeaders.get(":event-type") !== "chunk") {
|
|
297
|
+
const type = parsedHeaders.get(":event-type") ?? parsedHeaders.get(":exception-type") ?? "";
|
|
298
|
+
throw new ConnectionFailure(/throttl/i.test(type) ? "rate-limited" : /timeout/i.test(type) ? "timeout" : "upstream-error");
|
|
299
|
+
}
|
|
300
|
+
const payload = object(decodeJson(raw.subarray(headerEnd, offset + length - 4)));
|
|
301
|
+
if (!payload || typeof payload.bytes !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(payload.bytes)) throw new ConnectionFailure("wrong-protocol");
|
|
302
|
+
const event = object(decodeJson(Buffer.from(payload.bytes, "base64")));
|
|
303
|
+
if (!event || typeof event.type !== "string" || stopped) throw new ConnectionFailure("wrong-protocol");
|
|
304
|
+
if (event.type === "error") throw new ConnectionFailure("upstream-error");
|
|
305
|
+
if (event.type === "message_start") {
|
|
306
|
+
if (started || !validMessages(event.message)) throw new ConnectionFailure("wrong-protocol");
|
|
307
|
+
started = true;
|
|
308
|
+
} else if (event.type === "message_stop") {
|
|
309
|
+
if (!started || !finishedMessage) throw new ConnectionFailure("wrong-protocol");
|
|
310
|
+
stopped = true;
|
|
311
|
+
} else if (event.type === "message_delta") {
|
|
312
|
+
if (!started || typeof object(event.delta)?.stop_reason !== "string" || !Number.isSafeInteger(object(event.usage)?.output_tokens)) throw new ConnectionFailure("wrong-protocol");
|
|
313
|
+
finishedMessage = true;
|
|
314
|
+
} else if (!started || !["content_block_start", "content_block_delta", "content_block_stop", "ping"].includes(event.type)) throw new ConnectionFailure("wrong-protocol");
|
|
315
|
+
offset += length;
|
|
316
|
+
}
|
|
317
|
+
if (!started || !stopped) throw new ConnectionFailure("wrong-protocol");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function probe(settings: CustomConnectionMaterial, model: string, auth: CustomConnectionAuthMode, options: CustomConnectionCaptureOptions, allowFallback: boolean): Promise<CustomConnectionAuthMode> {
|
|
321
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
322
|
+
const signal = deadline(options);
|
|
323
|
+
try {
|
|
324
|
+
const bedrock = settings.providerType === "bedrock" && settings.engine === "claude";
|
|
325
|
+
const request: SafeHttpRequest = {
|
|
326
|
+
url: bedrock ? `${settings.baseUrl}/model/${encodeURIComponent(model)}/invoke-with-response-stream` : `${settings.baseUrl}${settings.engine === "claude" ? "/v1/messages" : "/responses"}`,
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { ...headers(settings, auth), "content-type": "application/json", accept: bedrock ? "application/vnd.amazon.eventstream" : settings.engine === "codex" ? "text/event-stream" : "application/json" },
|
|
329
|
+
body: JSON.stringify(settings.engine === "codex" ? { model, input: "Hi", store: false, stream: true, max_output_tokens: 16 } : { ...(bedrock ? { anthropic_version: "bedrock-2023-05-31" } : { model }), messages: [{ role: "user", content: "Hi" }], max_tokens: 1 }),
|
|
330
|
+
};
|
|
331
|
+
let response = await exchange(request, options, signal);
|
|
332
|
+
let usedAuth = auth;
|
|
333
|
+
if (response.status === 401 && allowFallback && settings.providerType === "custom" && settings.engine === "claude" && auth === "bearer") {
|
|
334
|
+
response.body.destroy();
|
|
335
|
+
response = await exchange({ ...request, headers: { ...headers(settings, "x-api-key"), "content-type": "application/json", accept: "application/json" } }, options, signal);
|
|
336
|
+
usedAuth = "x-api-key";
|
|
337
|
+
}
|
|
338
|
+
if (response.status !== 200) { response.body.destroy(); throw statusFailure(response.status); }
|
|
339
|
+
const raw = await body(response, PROBE_MAX_BYTES, signal);
|
|
340
|
+
if (bedrock) validateBedrockProbe(raw);
|
|
341
|
+
else if (settings.engine === "codex") validateResponsesProbe(raw);
|
|
342
|
+
else if (!validMessages(decodeJson(raw))) throw new ConnectionFailure("wrong-protocol");
|
|
343
|
+
return usedAuth;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const refused = failure(error, signal);
|
|
346
|
+
if (attempt === 0 && refused.retryable && !options.signal?.aborted) continue;
|
|
347
|
+
throw refused;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
throw new ConnectionFailure("upstream-error");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** All selected models are probed before the single immutable write. */
|
|
354
|
+
export async function saveCustomConnection(frame: EngineConnectCustomSaveFrame, store: CredentialStore | null, options: CustomConnectionCaptureOptions = {}): Promise<CustomConnectionSaveResult> {
|
|
355
|
+
try {
|
|
356
|
+
if (!parseCustomConnectionMaterial(frame?.settings) || frame.settings.internal === true) throw new ConnectionFailure("unsupported");
|
|
357
|
+
const parsed = parseEngineConnectCustomSaveFrame(frame);
|
|
358
|
+
if (!parsed || !validConnectionModels(parsed.models, parsed.defaultModel, parsed.backgroundModel, parsed.settings.engine, parsed.settings.apiKey)) throw new ConnectionFailure("invalid-models");
|
|
359
|
+
const settings = validated(parsed.settings, parsed.enabled, (options.now ?? Date.now)());
|
|
360
|
+
const flow = parseCaptureFlow(parsed.flow);
|
|
361
|
+
if (!flow || !store?.putImmutable) throw new ConnectionFailure("unsupported");
|
|
362
|
+
// Refuse an envelope that will never fit before spending any inference
|
|
363
|
+
// tokens. The successful auth mode is checked again before persistence.
|
|
364
|
+
try { buildEngineCapture(flow, { authKind: "custom-connection", connection: settings }); } catch { throw new ConnectionFailure("unsupported"); }
|
|
365
|
+
const authMode = await probe(settings, parsed.defaultModel, "bearer", options, true);
|
|
366
|
+
if (parsed.backgroundModel && parsed.backgroundModel !== parsed.defaultModel) await probe(settings, parsed.backgroundModel, authMode, options, false);
|
|
367
|
+
const connection = { ...settings, authMode };
|
|
368
|
+
const budget = connection.providerType === "openrouter" ? await readOpenRouterBudget(connection.apiKey, options) : undefined;
|
|
369
|
+
validated(connection, parsed.enabled, (options.now ?? Date.now)());
|
|
370
|
+
let built;
|
|
371
|
+
try { built = buildEngineCapture(flow, { authKind: "custom-connection", connection }); } catch { throw new ConnectionFailure("unsupported"); }
|
|
372
|
+
if ("error" in built) throw new ConnectionFailure("unsupported");
|
|
373
|
+
const capture = await writeCapture(store, flow, built, { signal: options.signal });
|
|
374
|
+
return { ok: true, capture, authMode, ...(budget ? { budget } : {}) };
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return { ok: false, reason: failure(error, options.signal).reason };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Internal saves deliberately have no transport or resolver path. The person
|
|
381
|
+
* selects Claude's header; the fenced write records exactly that choice. */
|
|
382
|
+
export async function saveCustomConnectionManual(frame: EngineConnectCustomManualSaveFrame, store: CredentialStore | null, options: Pick<CustomConnectionCaptureOptions, "signal" | "now"> = {}): Promise<CustomConnectionSaveResult> {
|
|
383
|
+
try {
|
|
384
|
+
const parsed = parseEngineConnectCustomManualSaveFrame(frame);
|
|
385
|
+
if (!parsed) throw new ConnectionFailure("unsupported");
|
|
386
|
+
if (!validConnectionModels(parsed.models, parsed.defaultModel, parsed.backgroundModel, parsed.settings.engine, parsed.settings.apiKey)) throw new ConnectionFailure("invalid-models");
|
|
387
|
+
const connection = validated(parsed.settings, parsed.enabled, (options.now ?? Date.now)());
|
|
388
|
+
const flow = parseCaptureFlow(parsed.flow);
|
|
389
|
+
if (!flow || !store?.putImmutable) throw new ConnectionFailure("unsupported");
|
|
390
|
+
const built = buildEngineCapture(flow, { authKind: "custom-connection", connection });
|
|
391
|
+
if ("error" in built) throw new ConnectionFailure("unsupported");
|
|
392
|
+
const capture = await writeCapture(store, flow, built, { signal: options.signal });
|
|
393
|
+
return { ok: true, capture, authMode: connection.authMode };
|
|
394
|
+
} catch (error) { return { ok: false, reason: failure(error, options.signal).reason }; }
|
|
395
|
+
}
|