@hasna/switcher 0.1.3 → 0.1.5
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 +15 -6
- package/dist/cli/index.js +279 -253
- package/dist/credentials.d.ts +71 -8
- package/dist/domain.d.ts +1 -1
- package/dist/index.js +45 -6
- package/dist/mcp/index.js +47 -8
- package/dist/provider-stream.d.ts +17 -0
- package/dist/sdk.d.ts +10 -1
- package/dist/sdk.js +45 -6
- package/dist/serve/index.js +2 -2
- package/openapi.json +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -499,7 +499,7 @@ function canonicalPolicyJSON(value) {
|
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
// src/domain.ts
|
|
502
|
-
var VERSION = "0.1.
|
|
502
|
+
var VERSION = "0.1.5";
|
|
503
503
|
var harnessSchema = z3.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
504
504
|
var protocolSchema = z3.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
505
505
|
var idSchema = z3.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
@@ -1033,7 +1033,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
1035
|
// src/sdk.ts
|
|
1036
|
-
import {
|
|
1036
|
+
import { createClientTransport } from "@hasna/contracts/client";
|
|
1037
1037
|
|
|
1038
1038
|
// src/presets.ts
|
|
1039
1039
|
var route = (protocol, baseUrl, options = {}) => ({
|
|
@@ -1206,6 +1206,7 @@ function apiError(status, data, apiKey) {
|
|
|
1206
1206
|
|
|
1207
1207
|
class SwitcherClient {
|
|
1208
1208
|
options;
|
|
1209
|
+
transport;
|
|
1209
1210
|
baseUrl;
|
|
1210
1211
|
constructor(options) {
|
|
1211
1212
|
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
@@ -1213,9 +1214,50 @@ class SwitcherClient {
|
|
|
1213
1214
|
throw new Error("Switcher API key is required.");
|
|
1214
1215
|
this.options = { ...options };
|
|
1215
1216
|
}
|
|
1217
|
+
static fromEnvironment(env = process.env, options = {}) {
|
|
1218
|
+
const shared = createClientTransport("switcher", env, {
|
|
1219
|
+
credentials: options.credentials,
|
|
1220
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
1221
|
+
retry: false,
|
|
1222
|
+
fetchImpl: async (input, init) => {
|
|
1223
|
+
const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
|
|
1224
|
+
const key = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
1225
|
+
let response;
|
|
1226
|
+
try {
|
|
1227
|
+
response = await (options.fetch ?? fetch)(address, init);
|
|
1228
|
+
} catch {
|
|
1229
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
1230
|
+
}
|
|
1231
|
+
if (response.status === 401 || response.status === 403) {
|
|
1232
|
+
await response.body?.cancel().catch(() => {});
|
|
1233
|
+
throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
|
|
1234
|
+
}
|
|
1235
|
+
let data;
|
|
1236
|
+
try {
|
|
1237
|
+
data = await boundedJson(response);
|
|
1238
|
+
} catch {
|
|
1239
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
1240
|
+
}
|
|
1241
|
+
if (!response.ok)
|
|
1242
|
+
throw apiError(response.status, data, key);
|
|
1243
|
+
return Response.json(data);
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
|
|
1247
|
+
throw new Error("Shared credential transport was not invoked.");
|
|
1248
|
+
} });
|
|
1249
|
+
client.transport = shared.client;
|
|
1250
|
+
return client;
|
|
1251
|
+
}
|
|
1216
1252
|
async request(method, path, body, options = {}) {
|
|
1217
1253
|
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
1218
1254
|
throw new Error("Invalid API path.");
|
|
1255
|
+
if (this.transport)
|
|
1256
|
+
return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
|
|
1257
|
+
idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
|
|
1258
|
+
headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
|
|
1259
|
+
retry: false
|
|
1260
|
+
});
|
|
1219
1261
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
1220
1262
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
1221
1263
|
throw new Error("Switcher API key is required.");
|
|
@@ -1312,11 +1354,124 @@ class SwitcherClient {
|
|
|
1312
1354
|
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
1313
1355
|
}
|
|
1314
1356
|
}
|
|
1315
|
-
function clientFromEnv(env = process.env) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1357
|
+
function clientFromEnv(env = process.env, options = {}) {
|
|
1358
|
+
return SwitcherClient.fromEnvironment(env, options);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/provider-stream.ts
|
|
1362
|
+
function terminalEventObserver(protocol, contentType) {
|
|
1363
|
+
const enabled = contentType?.split(";", 1)[0].trim().toLowerCase() === "text/event-stream";
|
|
1364
|
+
const decoder = new TextDecoder;
|
|
1365
|
+
let line = "", data = [], size = 0, overflow = false, afterCR = false, complete = false;
|
|
1366
|
+
const dispatchLine = () => {
|
|
1367
|
+
if (!line) {
|
|
1368
|
+
if (!overflow && data.length) {
|
|
1369
|
+
const payload = data.join(`
|
|
1370
|
+
`);
|
|
1371
|
+
if (protocol === "openai-chat")
|
|
1372
|
+
complete ||= payload === "[DONE]";
|
|
1373
|
+
else
|
|
1374
|
+
try {
|
|
1375
|
+
const value = JSON.parse(payload);
|
|
1376
|
+
complete ||= protocol === "anthropic-messages" ? value?.type === "message_stop" : protocol === "openai-responses" && ["response.completed", "response.failed", "response.incomplete"].includes(value?.type);
|
|
1377
|
+
} catch {}
|
|
1378
|
+
}
|
|
1379
|
+
data = [];
|
|
1380
|
+
size = 0;
|
|
1381
|
+
overflow = false;
|
|
1382
|
+
} else if (!overflow && line.startsWith("data:"))
|
|
1383
|
+
data.push(line.slice(5).replace(/^ /, ""));
|
|
1384
|
+
line = "";
|
|
1385
|
+
};
|
|
1386
|
+
return (chunk) => {
|
|
1387
|
+
if (!enabled || protocol === "gemini-generate-content")
|
|
1388
|
+
return false;
|
|
1389
|
+
for (const char of decoder.decode(chunk, { stream: true })) {
|
|
1390
|
+
if (afterCR && char === `
|
|
1391
|
+
`) {
|
|
1392
|
+
afterCR = false;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
afterCR = char === "\r";
|
|
1396
|
+
if (char === "\r" || char === `
|
|
1397
|
+
`)
|
|
1398
|
+
dispatchLine();
|
|
1399
|
+
else if (++size <= 131072)
|
|
1400
|
+
line += char;
|
|
1401
|
+
else {
|
|
1402
|
+
overflow = true;
|
|
1403
|
+
line = "oversized";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return complete;
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function proxyProviderStream(input) {
|
|
1410
|
+
const reader = input.response.body.getReader();
|
|
1411
|
+
const terminal = terminalEventObserver(input.protocol, input.response.headers.get("content-type"));
|
|
1412
|
+
let ended = false, output;
|
|
1413
|
+
const end = (error) => {
|
|
1414
|
+
if (ended)
|
|
1415
|
+
return;
|
|
1416
|
+
ended = true;
|
|
1417
|
+
try {
|
|
1418
|
+
if (error)
|
|
1419
|
+
output.error(error);
|
|
1420
|
+
else
|
|
1421
|
+
output.close();
|
|
1422
|
+
} catch {}
|
|
1423
|
+
input.release();
|
|
1424
|
+
};
|
|
1425
|
+
const cancelReader = async () => {
|
|
1426
|
+
input.abort.abort();
|
|
1427
|
+
try {
|
|
1428
|
+
await reader.cancel();
|
|
1429
|
+
} catch {}
|
|
1430
|
+
};
|
|
1431
|
+
const stream = new ReadableStream({
|
|
1432
|
+
start(controller) {
|
|
1433
|
+
output = controller;
|
|
1434
|
+
},
|
|
1435
|
+
async pull(controller) {
|
|
1436
|
+
try {
|
|
1437
|
+
const chunk = await reader.read();
|
|
1438
|
+
if (ended)
|
|
1439
|
+
return;
|
|
1440
|
+
if (chunk.done) {
|
|
1441
|
+
input.inspect?.(new Uint8Array, true);
|
|
1442
|
+
end();
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
input.inspect?.(chunk.value);
|
|
1446
|
+
const complete = terminal(chunk.value);
|
|
1447
|
+
controller.enqueue(chunk.value);
|
|
1448
|
+
if (complete) {
|
|
1449
|
+
end();
|
|
1450
|
+
await cancelReader();
|
|
1451
|
+
}
|
|
1452
|
+
} catch {
|
|
1453
|
+
if (ended)
|
|
1454
|
+
return;
|
|
1455
|
+
if (input.requestSignal.aborted || input.abort.signal.aborted || input.closing())
|
|
1456
|
+
end();
|
|
1457
|
+
else {
|
|
1458
|
+
input.interrupted?.();
|
|
1459
|
+
end(new Error("Provider stream ended unexpectedly"));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
async cancel() {
|
|
1464
|
+
if (!ended) {
|
|
1465
|
+
ended = true;
|
|
1466
|
+
input.release();
|
|
1467
|
+
}
|
|
1468
|
+
await cancelReader();
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
return { stream, cancel: async () => {
|
|
1472
|
+
end();
|
|
1473
|
+
await cancelReader();
|
|
1474
|
+
} };
|
|
1320
1475
|
}
|
|
1321
1476
|
|
|
1322
1477
|
// src/opencode-model-policy.ts
|
|
@@ -1388,7 +1543,7 @@ function openCodeInvocationModel(args, policy, defaultAgent) {
|
|
|
1388
1543
|
|
|
1389
1544
|
// src/gemini-config.ts
|
|
1390
1545
|
import { readFile, writeFile as writeFile3, lstat as lstat2, readdir as readdir2, symlink, realpath as realpath2 } from "fs/promises";
|
|
1391
|
-
import { join as join4, dirname as dirname3, resolve as resolve4, isAbsolute as
|
|
1546
|
+
import { join as join4, dirname as dirname3, resolve as resolve4, isAbsolute as isAbsolute4, relative as relative2, sep } from "path";
|
|
1392
1547
|
import { homedir as homedir3 } from "os";
|
|
1393
1548
|
import { parseTree as parseTree2, getNodeValue } from "jsonc-parser";
|
|
1394
1549
|
|
|
@@ -1396,7 +1551,7 @@ import { parseTree as parseTree2, getNodeValue } from "jsonc-parser";
|
|
|
1396
1551
|
import { randomBytes } from "crypto";
|
|
1397
1552
|
import { mkdir as mkdir4, lstat } from "fs/promises";
|
|
1398
1553
|
import { homedir as homedir2 } from "os";
|
|
1399
|
-
import { join as join3, resolve as resolve3 } from "path";
|
|
1554
|
+
import { isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "path";
|
|
1400
1555
|
|
|
1401
1556
|
// src/store.ts
|
|
1402
1557
|
var {SQL } = globalThis.Bun;
|
|
@@ -1638,7 +1793,7 @@ async function fetchCatalogPage(url, headers, deadline) {
|
|
|
1638
1793
|
await waitForCatalogRetry(delay ?? 100 * 2 ** retry, deadline);
|
|
1639
1794
|
}
|
|
1640
1795
|
}
|
|
1641
|
-
async function discover(provider, env = process.env,
|
|
1796
|
+
async function discover(provider, env = process.env, resolveCredential) {
|
|
1642
1797
|
const refreshedAt = new Date().toISOString();
|
|
1643
1798
|
if (provider.manualModels.length)
|
|
1644
1799
|
return { models: provider.manualModels, source: "manual", refreshedAt };
|
|
@@ -1658,7 +1813,7 @@ async function discover(provider, env = process.env, resolveCredential2) {
|
|
|
1658
1813
|
if (authStyle !== "none" && credentialEnv) {
|
|
1659
1814
|
if (url.origin !== new URL(provider.baseUrl).origin && !provider.catalogCredentialEnv)
|
|
1660
1815
|
throw new Fault(422, "catalog_credential_authority", "A different catalog origin requires an explicit catalog credential reference or catalogAuthStyle: none.");
|
|
1661
|
-
const credential =
|
|
1816
|
+
const credential = resolveCredential ? await resolveCredential({ ...provider, baseUrl: provider.catalogBaseUrl ?? provider.baseUrl, credentialEnv }) : env[credentialEnv];
|
|
1662
1817
|
if (!credential)
|
|
1663
1818
|
throw new Fault(422, "credential_missing", "Provider credential environment variable is not available on the server.");
|
|
1664
1819
|
if (/[\r\n]/.test(credential))
|
|
@@ -1985,7 +2140,7 @@ var openapi_default = {
|
|
|
1985
2140
|
openapi: "3.0.3",
|
|
1986
2141
|
info: {
|
|
1987
2142
|
title: "Switcher API",
|
|
1988
|
-
version: "0.1.
|
|
2143
|
+
version: "0.1.5",
|
|
1989
2144
|
description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
|
|
1990
2145
|
},
|
|
1991
2146
|
security: [
|
|
@@ -5460,7 +5615,7 @@ var openapi_default = {
|
|
|
5460
5615
|
// src/service.ts
|
|
5461
5616
|
var snapshot = (profile, provider, catalog) => createHash3("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
|
|
5462
5617
|
var hash = (s) => createHash3("sha256").update(s).digest();
|
|
5463
|
-
function createHandler(store, apiKey, providerEnv = process.env,
|
|
5618
|
+
function createHandler(store, apiKey, providerEnv = process.env, resolveCredential) {
|
|
5464
5619
|
if (!apiKey || apiKey.length < 24)
|
|
5465
5620
|
throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
|
|
5466
5621
|
const expected = hash(`Bearer ${apiKey}`);
|
|
@@ -5542,7 +5697,7 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
|
|
|
5542
5697
|
if (resource === "providers" && id2 && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
|
|
5543
5698
|
parse2(z4.object({}).strict(), body);
|
|
5544
5699
|
const provider = await store.get("providers", id2);
|
|
5545
|
-
refreshed = { provider, catalog: await discover(provider, providerEnv,
|
|
5700
|
+
refreshed = { provider, catalog: await discover(provider, providerEnv, resolveCredential) };
|
|
5546
5701
|
}
|
|
5547
5702
|
const result = await store.mutate(key, fingerprint, async (db) => {
|
|
5548
5703
|
if ((resource === "providers" || resource === "profiles") && parts.length <= 3) {
|
|
@@ -5689,8 +5844,11 @@ async function startServer(options) {
|
|
|
5689
5844
|
}
|
|
5690
5845
|
|
|
5691
5846
|
// src/runtime.ts
|
|
5847
|
+
import { resolveCredential as resolveClientCredential, clientTransportEnvKeys, appConfigDiskValue, keychainConfigValue } from "@hasna/contracts/client";
|
|
5692
5848
|
function switcherHome(env = process.env) {
|
|
5693
|
-
|
|
5849
|
+
const override = env.HASNA_HOME?.trim();
|
|
5850
|
+
const root = override && isAbsolute3(override) ? override : join3(env.HOME?.trim() || homedir2(), ".hasna");
|
|
5851
|
+
return resolve3(env.HASNA_SWITCHER_HOME ?? join3(root, "switcher"));
|
|
5694
5852
|
}
|
|
5695
5853
|
async function privateDirectory(path) {
|
|
5696
5854
|
await mkdir4(path, { recursive: true, mode: 448 });
|
|
@@ -5700,9 +5858,12 @@ async function privateDirectory(path) {
|
|
|
5700
5858
|
if (process.platform !== "win32" && ((info.mode & 63) !== 0 || info.uid !== process.getuid?.()))
|
|
5701
5859
|
throw new Fault(500, "home_permissions", "Switcher data directory must be owned by this user and accessible only to its owner (mode 0700).");
|
|
5702
5860
|
}
|
|
5703
|
-
async function openCliRuntime(env = process.env,
|
|
5861
|
+
async function openCliRuntime(env = process.env, resolveCredential) {
|
|
5704
5862
|
const providerEnv = Object.fromEntries(Object.entries(env).filter(([name]) => name.startsWith("SWITCHER_PROVIDER_")));
|
|
5705
|
-
|
|
5863
|
+
const keys = clientTransportEnvKeys("switcher");
|
|
5864
|
+
const credential = resolveClientCredential("switcher", env);
|
|
5865
|
+
const configured = credential || keys.apiUrlKeys.some((name) => env[name] !== undefined) || keychainConfigValue("switcher", env) || appConfigDiskValue("switcher", env, keys.apiUrlKeys);
|
|
5866
|
+
if (configured) {
|
|
5706
5867
|
return { client: clientFromEnv(env), mode: "remote", providerEnv, close: async () => {} };
|
|
5707
5868
|
}
|
|
5708
5869
|
const home = switcherHome(env);
|
|
@@ -5714,7 +5875,7 @@ async function openCliRuntime(env = process.env, resolveCredential2) {
|
|
|
5714
5875
|
databaseUrl: env.HASNA_SWITCHER_DATABASE_URL,
|
|
5715
5876
|
sqlitePath: env.HASNA_SWITCHER_SQLITE_PATH ?? (env.HASNA_SWITCHER_DATABASE_URL ? undefined : join3(home, "switcher.db")),
|
|
5716
5877
|
providerEnv,
|
|
5717
|
-
resolveCredential
|
|
5878
|
+
resolveCredential
|
|
5718
5879
|
});
|
|
5719
5880
|
return { client: new SwitcherClient({ baseUrl: service.url, apiKey }), mode: "local", providerEnv, close: service.close };
|
|
5720
5881
|
}
|
|
@@ -5860,7 +6021,7 @@ function originalPaths(value, home) {
|
|
|
5860
6021
|
}
|
|
5861
6022
|
async function validateGeminiConfiguration(cwd, env = process.env) {
|
|
5862
6023
|
const home = env.GEMINI_CLI_HOME ?? env.HOME ?? homedir3();
|
|
5863
|
-
if (!
|
|
6024
|
+
if (!isAbsolute4(home))
|
|
5864
6025
|
throw new Error("Gemini requires an absolute native home.");
|
|
5865
6026
|
const systemPath = env.GEMINI_CLI_SYSTEM_SETTINGS_PATH ?? (process.platform === "darwin" ? "/Library/Application Support/GeminiCli/settings.json" : process.platform === "win32" ? "C:\\ProgramData\\gemini-cli\\settings.json" : "/etc/gemini-cli/settings.json");
|
|
5866
6027
|
const defaultsPath = env.GEMINI_CLI_SYSTEM_DEFAULTS_PATH ?? join4(dirname3(systemPath), "system-defaults.json");
|
|
@@ -5924,7 +6085,7 @@ async function snapshotContext(sourceDir, destinationDir, names) {
|
|
|
5924
6085
|
if (code.some(([a, b]) => start >= a && start < b))
|
|
5925
6086
|
continue;
|
|
5926
6087
|
const imported = resolve4(dirname3(source), match[2]), actual = await realpath2(imported).catch(() => imported), within = relative2(root, actual);
|
|
5927
|
-
if (within === ".." || within.startsWith(".." + sep) ||
|
|
6088
|
+
if (within === ".." || within.startsWith(".." + sep) || isAbsolute4(within))
|
|
5928
6089
|
throw new Error("Gemini global context imports outside the original context directory require a supported explicit context location.");
|
|
5929
6090
|
const target = files.get(imported) ?? join4(importsDir, `${files.size}.md`);
|
|
5930
6091
|
await privateDirectory(importsDir);
|
|
@@ -6062,55 +6223,8 @@ function geminiBridge(input) {
|
|
|
6062
6223
|
release();
|
|
6063
6224
|
return new Response(null, { status: response.status });
|
|
6064
6225
|
}
|
|
6065
|
-
const
|
|
6066
|
-
|
|
6067
|
-
const end = (error) => {
|
|
6068
|
-
if (ended)
|
|
6069
|
-
return;
|
|
6070
|
-
ended = true;
|
|
6071
|
-
try {
|
|
6072
|
-
if (error && !closing)
|
|
6073
|
-
output.error(error);
|
|
6074
|
-
else
|
|
6075
|
-
output.close();
|
|
6076
|
-
} catch {}
|
|
6077
|
-
release();
|
|
6078
|
-
};
|
|
6079
|
-
const stream = new ReadableStream({
|
|
6080
|
-
start(controller) {
|
|
6081
|
-
output = controller;
|
|
6082
|
-
},
|
|
6083
|
-
async pull(controller) {
|
|
6084
|
-
try {
|
|
6085
|
-
const chunk = await reader.read();
|
|
6086
|
-
if (ended)
|
|
6087
|
-
return;
|
|
6088
|
-
if (chunk.done)
|
|
6089
|
-
end();
|
|
6090
|
-
else
|
|
6091
|
-
controller.enqueue(chunk.value);
|
|
6092
|
-
} catch {
|
|
6093
|
-
end(new Error("Provider stream ended unexpectedly"));
|
|
6094
|
-
}
|
|
6095
|
-
},
|
|
6096
|
-
async cancel() {
|
|
6097
|
-
ended = true;
|
|
6098
|
-
record2.abort.abort();
|
|
6099
|
-
try {
|
|
6100
|
-
await reader.cancel();
|
|
6101
|
-
} finally {
|
|
6102
|
-
release();
|
|
6103
|
-
}
|
|
6104
|
-
}
|
|
6105
|
-
});
|
|
6106
|
-
record2.cancel = async () => {
|
|
6107
|
-
record2.abort.abort();
|
|
6108
|
-
try {
|
|
6109
|
-
await reader.cancel();
|
|
6110
|
-
} catch {} finally {
|
|
6111
|
-
end();
|
|
6112
|
-
}
|
|
6113
|
-
};
|
|
6226
|
+
const { stream, cancel } = proxyProviderStream({ response, protocol: input.protocol, requestSignal: request.signal, abort: record2.abort, closing: () => closing, release });
|
|
6227
|
+
record2.cancel = cancel;
|
|
6114
6228
|
return new Response(stream, { status: response.status, headers: { "content-type": response.headers.get("content-type") ?? "application/json", "cache-control": "no-store" } });
|
|
6115
6229
|
} catch {
|
|
6116
6230
|
release();
|
|
@@ -6360,9 +6474,9 @@ function createInferenceGateway(input) {
|
|
|
6360
6474
|
release();
|
|
6361
6475
|
return new Response(null, { status: response.status });
|
|
6362
6476
|
}
|
|
6363
|
-
const
|
|
6477
|
+
const decoder = new TextDecoder;
|
|
6364
6478
|
const sse = response.headers.get("content-type")?.includes("text/event-stream");
|
|
6365
|
-
let buffer = ""
|
|
6479
|
+
let buffer = "";
|
|
6366
6480
|
const observe = (text) => {
|
|
6367
6481
|
try {
|
|
6368
6482
|
const value = JSON.parse(text);
|
|
@@ -6377,14 +6491,14 @@ function createInferenceGateway(input) {
|
|
|
6377
6491
|
const inspect = (chunk, done = false) => {
|
|
6378
6492
|
buffer += decoder.decode(chunk, { stream: !done });
|
|
6379
6493
|
if (sse) {
|
|
6380
|
-
let
|
|
6494
|
+
let end = buffer.indexOf(`
|
|
6381
6495
|
`);
|
|
6382
|
-
while (
|
|
6383
|
-
const line = buffer.slice(0,
|
|
6384
|
-
buffer = buffer.slice(
|
|
6496
|
+
while (end >= 0) {
|
|
6497
|
+
const line = buffer.slice(0, end).trim();
|
|
6498
|
+
buffer = buffer.slice(end + 1);
|
|
6385
6499
|
if (line.startsWith("data:"))
|
|
6386
6500
|
observe(line.slice(5).trim());
|
|
6387
|
-
|
|
6501
|
+
end = buffer.indexOf(`
|
|
6388
6502
|
`);
|
|
6389
6503
|
}
|
|
6390
6504
|
if (buffer.length > 131072)
|
|
@@ -6394,53 +6508,10 @@ function createInferenceGateway(input) {
|
|
|
6394
6508
|
if (done && buffer)
|
|
6395
6509
|
observe(buffer);
|
|
6396
6510
|
};
|
|
6397
|
-
const
|
|
6398
|
-
|
|
6399
|
-
return;
|
|
6400
|
-
ended = true;
|
|
6401
|
-
try {
|
|
6402
|
-
if (error && !closing)
|
|
6403
|
-
output.error(error);
|
|
6404
|
-
else
|
|
6405
|
-
output.close();
|
|
6406
|
-
} catch {}
|
|
6407
|
-
release();
|
|
6408
|
-
};
|
|
6409
|
-
const stream = new ReadableStream({ start(controller) {
|
|
6410
|
-
output = controller;
|
|
6411
|
-
}, async pull(controller) {
|
|
6412
|
-
try {
|
|
6413
|
-
const chunk = await reader.read();
|
|
6414
|
-
if (ended)
|
|
6415
|
-
return;
|
|
6416
|
-
if (chunk.done) {
|
|
6417
|
-
inspect(new Uint8Array, true);
|
|
6418
|
-
end();
|
|
6419
|
-
} else {
|
|
6420
|
-
inspect(chunk.value);
|
|
6421
|
-
controller.enqueue(chunk.value);
|
|
6422
|
-
}
|
|
6423
|
-
} catch {
|
|
6424
|
-
current.reason = "stream_interrupted";
|
|
6425
|
-
end(new Error("Provider stream ended unexpectedly"));
|
|
6426
|
-
}
|
|
6427
|
-
}, async cancel() {
|
|
6428
|
-
ended = true;
|
|
6429
|
-
abort.abort();
|
|
6430
|
-
try {
|
|
6431
|
-
await reader.cancel();
|
|
6432
|
-
} finally {
|
|
6433
|
-
release();
|
|
6434
|
-
}
|
|
6511
|
+
const { stream, cancel } = proxyProviderStream({ response, protocol: input.protocol, requestSignal: request.signal, abort, closing: () => closing, release, inspect, interrupted: () => {
|
|
6512
|
+
current.reason = "stream_interrupted";
|
|
6435
6513
|
} });
|
|
6436
|
-
record2.cancel =
|
|
6437
|
-
abort.abort();
|
|
6438
|
-
try {
|
|
6439
|
-
await reader.cancel();
|
|
6440
|
-
} catch {} finally {
|
|
6441
|
-
end();
|
|
6442
|
-
}
|
|
6443
|
-
};
|
|
6514
|
+
record2.cancel = cancel;
|
|
6444
6515
|
return new Response(stream, { status: response.status, headers: { "content-type": response.headers.get("content-type") ?? "application/json", "cache-control": "no-store" } });
|
|
6445
6516
|
} catch (error) {
|
|
6446
6517
|
if (error instanceof Fault)
|
|
@@ -6464,7 +6535,7 @@ function createInferenceGateway(input) {
|
|
|
6464
6535
|
|
|
6465
6536
|
// src/codex-model-policy.ts
|
|
6466
6537
|
import { mkdir as mkdir5, readFile as readFile2, realpath as realpath3, stat as stat3, writeFile as writeFile4 } from "fs/promises";
|
|
6467
|
-
import { dirname as dirname4, isAbsolute as
|
|
6538
|
+
import { dirname as dirname4, isAbsolute as isAbsolute5, join as join5, resolve as resolve5 } from "path";
|
|
6468
6539
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6469
6540
|
import { createHash as createHash6 } from "crypto";
|
|
6470
6541
|
var dict = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -6484,7 +6555,7 @@ function codexAgentsFromEffectiveConfig(config, trustedProject = true, baseDir)
|
|
|
6484
6555
|
const rawConfigFile = value.config_file;
|
|
6485
6556
|
if (rawConfigFile !== undefined && typeof rawConfigFile !== "string")
|
|
6486
6557
|
throw new Error(`Codex agents.${name}.config_file must be a string.`);
|
|
6487
|
-
const configFile = rawConfigFile === undefined ? undefined :
|
|
6558
|
+
const configFile = rawConfigFile === undefined ? undefined : isAbsolute5(rawConfigFile) ? rawConfigFile : baseDir ? resolve5(baseDir, rawConfigFile) : undefined;
|
|
6488
6559
|
if (rawConfigFile !== undefined && !configFile)
|
|
6489
6560
|
throw new Error(`Codex agents.${name}.config_file must be absolute after precedence resolution.`);
|
|
6490
6561
|
const definition = { trusted: trustedProject, config_file: configFile };
|
|
@@ -6646,7 +6717,7 @@ async function prepareCodexModelPolicy(input) {
|
|
|
6646
6717
|
const global = await readConfig(globalPath) ?? {};
|
|
6647
6718
|
const projects = dict(global.projects) ? global.projects : {};
|
|
6648
6719
|
const markers = global.project_root_markers ?? [".git"];
|
|
6649
|
-
if (!Array.isArray(markers) || markers.some((m) => typeof m !== "string" ||
|
|
6720
|
+
if (!Array.isArray(markers) || markers.some((m) => typeof m !== "string" || isAbsolute5(m) || m.includes("..")))
|
|
6650
6721
|
throw new Error("Unsupported Codex project root markers.");
|
|
6651
6722
|
const ancestorDirs = [];
|
|
6652
6723
|
for (let dir = cwd;; dir = dirname4(dir)) {
|
|
@@ -6715,7 +6786,7 @@ async function prepareCodexModelPolicy(input) {
|
|
|
6715
6786
|
return { ...result, agentConfigPaths: await writeCodexAgentOverrides(result, input.stateDir), configPaths: loaded };
|
|
6716
6787
|
}
|
|
6717
6788
|
async function readCodexAgentConfig(path) {
|
|
6718
|
-
if (!
|
|
6789
|
+
if (!isAbsolute5(path))
|
|
6719
6790
|
throw new Error("Codex agent config_file must be absolute before launch.");
|
|
6720
6791
|
const canonical = await realpath3(path);
|
|
6721
6792
|
const info = await stat3(canonical);
|
|
@@ -6731,7 +6802,7 @@ async function readCodexAgentConfig(path) {
|
|
|
6731
6802
|
|
|
6732
6803
|
// src/harnesses.ts
|
|
6733
6804
|
import { access as access2, mkdir as mkdir10, readFile as readFile4, readdir as readdir4, writeFile as writeFile10, rm as rm2 } from "fs/promises";
|
|
6734
|
-
import { basename as basename2, dirname as dirname7, join as join11, isAbsolute as
|
|
6805
|
+
import { basename as basename2, dirname as dirname7, join as join11, isAbsolute as isAbsolute10, resolve as resolve8 } from "path";
|
|
6735
6806
|
import { homedir as homedir6, tmpdir } from "os";
|
|
6736
6807
|
import { createHash as createHash8, randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual5 } from "crypto";
|
|
6737
6808
|
import { execFile } from "child_process";
|
|
@@ -6741,7 +6812,7 @@ import { createConnection } from "net";
|
|
|
6741
6812
|
// src/aider-config.ts
|
|
6742
6813
|
import { readFile as readFile3, writeFile as writeFile5, lstat as lstat3, copyFile, rename, realpath as realpath4, access } from "fs/promises";
|
|
6743
6814
|
import { constants as constants2 } from "fs";
|
|
6744
|
-
import { dirname as dirname5, basename, join as join6, resolve as resolve6, isAbsolute as
|
|
6815
|
+
import { dirname as dirname5, basename, join as join6, resolve as resolve6, isAbsolute as isAbsolute6 } from "path";
|
|
6745
6816
|
import { homedir as homedir4 } from "os";
|
|
6746
6817
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
6747
6818
|
var KEY2 = "SWITCHER_HARNESS_API_KEY";
|
|
@@ -6800,7 +6871,7 @@ var startup = `yes-always gui browser upgrade install-main-branch just-check-upd
|
|
|
6800
6871
|
var safeParameters = new Set(`max_tokens temperature top_p top_k min_p stop seed reasoning_effort thinking`.split(" "));
|
|
6801
6872
|
var expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? join6(home, path.slice(2)) : path;
|
|
6802
6873
|
async function validateAiderConfiguration(cwd, args = [], home = process.env.HOME ?? homedir4()) {
|
|
6803
|
-
if (!
|
|
6874
|
+
if (!isAbsolute6(home))
|
|
6804
6875
|
throw new Error("Aider requires an absolute HOME to preserve native instruction paths.");
|
|
6805
6876
|
const parsed = aiderArguments(args), cwdRoot = await gitRoot(cwd);
|
|
6806
6877
|
const roots = new Set([resolve6(home), ...cwdRoot ? [cwdRoot] : [], resolve6(cwd)]);
|
|
@@ -7036,7 +7107,7 @@ function validateGrokResume(args) {
|
|
|
7036
7107
|
// src/cline-backend.ts
|
|
7037
7108
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
7038
7109
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7039
|
-
import { isAbsolute as
|
|
7110
|
+
import { isAbsolute as isAbsolute7, join as join7 } from "path";
|
|
7040
7111
|
var ENV_BY_PROTOCOL = {
|
|
7041
7112
|
"anthropic-messages": "ANTHROPIC_API_KEY",
|
|
7042
7113
|
"openai-responses": "OPENAI_API_KEY",
|
|
@@ -7082,7 +7153,7 @@ async function prepareClineLaunch(input) {
|
|
|
7082
7153
|
throw new Error(`Cline ${input.protocol} uses ${mapping.authStyle} authentication; use a provider with the native Cline auth contract.`);
|
|
7083
7154
|
if (input.credential && /[\r\n]/.test(input.credential))
|
|
7084
7155
|
throw new Error("Provider credential contains invalid header characters.");
|
|
7085
|
-
if (!
|
|
7156
|
+
if (!isAbsolute7(input.stateDir) || !isAbsolute7(input.cwd))
|
|
7086
7157
|
throw new Error("Launch state and working directories must be absolute.");
|
|
7087
7158
|
const dataDir = join7(input.stateDir, "cline-data");
|
|
7088
7159
|
const sessionDir = input.sessionDir ?? join7(dataDir, "sessions");
|
|
@@ -7164,7 +7235,7 @@ async function prepareClineLaunch(input) {
|
|
|
7164
7235
|
// src/opencode2-config.ts
|
|
7165
7236
|
import { mkdir as mkdir7, open as open2, realpath as realpath5, readdir as readdir3, stat as stat4, writeFile as writeFile7 } from "fs/promises";
|
|
7166
7237
|
import { constants as constants3 } from "fs";
|
|
7167
|
-
import { dirname as dirname6, isAbsolute as
|
|
7238
|
+
import { dirname as dirname6, isAbsolute as isAbsolute8, join as join8, relative as relative4, resolve as resolve7 } from "path";
|
|
7168
7239
|
import { homedir as homedir5 } from "os";
|
|
7169
7240
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7170
7241
|
import { Database as Database2 } from "bun:sqlite";
|
|
@@ -7180,7 +7251,7 @@ var maxTotalBytes2 = 8 * 1024 * 1024;
|
|
|
7180
7251
|
var maxFiles2 = 256;
|
|
7181
7252
|
var inside2 = (root, path) => {
|
|
7182
7253
|
const rel = relative4(root, path);
|
|
7183
|
-
return rel === "" || !
|
|
7254
|
+
return rel === "" || !isAbsolute8(rel) && rel !== ".." && !rel.startsWith("../");
|
|
7184
7255
|
};
|
|
7185
7256
|
var action = (value) => ({ bash: "shell", task: "subagent", write: "edit", patch: "edit" })[value] ?? value;
|
|
7186
7257
|
function invalid2(path, field) {
|
|
@@ -7301,7 +7372,7 @@ async function checkRemoteConfiguration(dataHome) {
|
|
|
7301
7372
|
}
|
|
7302
7373
|
async function isolateOpenCode2(cwd, stateDir, providerID, env = process.env) {
|
|
7303
7374
|
for (const name of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "OPENCODE_CONFIG_DIR"])
|
|
7304
|
-
if (env[name] && !
|
|
7375
|
+
if (env[name] && !isAbsolute8(env[name]))
|
|
7305
7376
|
throw new Error(`OpenCode 2 requires an absolute ${name} to preserve native configuration authority.`);
|
|
7306
7377
|
const home = resolve7(env.HOME || homedir5());
|
|
7307
7378
|
const configRoot = resolve7(env.OPENCODE_CONFIG_DIR || join8(env.XDG_CONFIG_HOME || join8(home, ".config"), "opencode"));
|
|
@@ -7723,7 +7794,7 @@ function compileHermesModelPolicy(currentModel, roles2, baseUrl, apiMode = "chat
|
|
|
7723
7794
|
// src/hermes-backend.ts
|
|
7724
7795
|
import { createHash as createHash7, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
7725
7796
|
import { mkdir as mkdir9, symlink as symlink2, writeFile as writeFile9 } from "fs/promises";
|
|
7726
|
-
import { isAbsolute as
|
|
7797
|
+
import { isAbsolute as isAbsolute9, join as join10 } from "path";
|
|
7727
7798
|
var hermesApiMode = {
|
|
7728
7799
|
"anthropic-messages": "anthropic_messages",
|
|
7729
7800
|
"openai-responses": "codex_responses",
|
|
@@ -7830,45 +7901,8 @@ function createHermesBridge(input) {
|
|
|
7830
7901
|
release();
|
|
7831
7902
|
return new Response(null, { status: upstream.status });
|
|
7832
7903
|
}
|
|
7833
|
-
const
|
|
7834
|
-
|
|
7835
|
-
const end = () => {
|
|
7836
|
-
if (ended)
|
|
7837
|
-
return;
|
|
7838
|
-
ended = true;
|
|
7839
|
-
release();
|
|
7840
|
-
};
|
|
7841
|
-
const stream = new ReadableStream({
|
|
7842
|
-
async pull(controller) {
|
|
7843
|
-
try {
|
|
7844
|
-
const chunk = await reader.read();
|
|
7845
|
-
if (chunk.done) {
|
|
7846
|
-
end();
|
|
7847
|
-
controller.close();
|
|
7848
|
-
} else
|
|
7849
|
-
controller.enqueue(chunk.value);
|
|
7850
|
-
} catch {
|
|
7851
|
-
end();
|
|
7852
|
-
controller.error(new Error("Provider stream ended unexpectedly"));
|
|
7853
|
-
}
|
|
7854
|
-
},
|
|
7855
|
-
async cancel() {
|
|
7856
|
-
ended = true;
|
|
7857
|
-
abortController.abort();
|
|
7858
|
-
try {
|
|
7859
|
-
await reader.cancel();
|
|
7860
|
-
} finally {
|
|
7861
|
-
release();
|
|
7862
|
-
}
|
|
7863
|
-
}
|
|
7864
|
-
});
|
|
7865
|
-
record3.cancel = async () => {
|
|
7866
|
-
abortController.abort();
|
|
7867
|
-
try {
|
|
7868
|
-
await reader.cancel();
|
|
7869
|
-
} catch {}
|
|
7870
|
-
end();
|
|
7871
|
-
};
|
|
7904
|
+
const { stream, cancel } = proxyProviderStream({ response: upstream, protocol: input.protocol, requestSignal: request.signal, abort: abortController, closing: () => closing, release });
|
|
7905
|
+
record3.cancel = cancel;
|
|
7872
7906
|
return new Response(stream, {
|
|
7873
7907
|
status: upstream.status,
|
|
7874
7908
|
headers: {
|
|
@@ -7899,7 +7933,7 @@ function createHermesBridge(input) {
|
|
|
7899
7933
|
};
|
|
7900
7934
|
}
|
|
7901
7935
|
async function prepareHermesLaunch(input) {
|
|
7902
|
-
if (!
|
|
7936
|
+
if (!isAbsolute9(input.stateDir) || !isAbsolute9(input.cwd))
|
|
7903
7937
|
throw new Error("Launch state and working directories must be absolute.");
|
|
7904
7938
|
if (input.protocol !== "anthropic-messages" && input.protocol !== "openai-responses" && input.protocol !== "openai-chat")
|
|
7905
7939
|
throw new Error("Hermes does not support this provider protocol.");
|
|
@@ -8544,56 +8578,8 @@ function grokBridge(input) {
|
|
|
8544
8578
|
release();
|
|
8545
8579
|
return new Response(null, { status: response.status });
|
|
8546
8580
|
}
|
|
8547
|
-
const
|
|
8548
|
-
|
|
8549
|
-
let output;
|
|
8550
|
-
const end = (error) => {
|
|
8551
|
-
if (ended)
|
|
8552
|
-
return;
|
|
8553
|
-
ended = true;
|
|
8554
|
-
try {
|
|
8555
|
-
if (error && !closing)
|
|
8556
|
-
output.error(error);
|
|
8557
|
-
else
|
|
8558
|
-
output.close();
|
|
8559
|
-
} catch {}
|
|
8560
|
-
release();
|
|
8561
|
-
};
|
|
8562
|
-
const stream = new ReadableStream({
|
|
8563
|
-
start(controller) {
|
|
8564
|
-
output = controller;
|
|
8565
|
-
},
|
|
8566
|
-
async pull(controller) {
|
|
8567
|
-
try {
|
|
8568
|
-
const chunk = await reader.read();
|
|
8569
|
-
if (ended)
|
|
8570
|
-
return;
|
|
8571
|
-
if (chunk.done)
|
|
8572
|
-
end();
|
|
8573
|
-
else
|
|
8574
|
-
controller.enqueue(chunk.value);
|
|
8575
|
-
} catch {
|
|
8576
|
-
end(new Error("Provider stream ended unexpectedly"));
|
|
8577
|
-
}
|
|
8578
|
-
},
|
|
8579
|
-
async cancel() {
|
|
8580
|
-
ended = true;
|
|
8581
|
-
record3.abort.abort();
|
|
8582
|
-
try {
|
|
8583
|
-
await reader.cancel();
|
|
8584
|
-
} finally {
|
|
8585
|
-
release();
|
|
8586
|
-
}
|
|
8587
|
-
}
|
|
8588
|
-
});
|
|
8589
|
-
record3.cancel = async () => {
|
|
8590
|
-
record3.abort.abort();
|
|
8591
|
-
try {
|
|
8592
|
-
await reader.cancel();
|
|
8593
|
-
} catch {} finally {
|
|
8594
|
-
end();
|
|
8595
|
-
}
|
|
8596
|
-
};
|
|
8581
|
+
const { stream, cancel } = proxyProviderStream({ response, protocol: input.protocol, requestSignal: request.signal, abort: record3.abort, closing: () => closing, release });
|
|
8582
|
+
record3.cancel = cancel;
|
|
8597
8583
|
return new Response(stream, { status: response.status, headers: { "content-type": response.headers.get("content-type") ?? "application/json", "cache-control": "no-store" } });
|
|
8598
8584
|
} catch {
|
|
8599
8585
|
release();
|
|
@@ -8849,7 +8835,7 @@ async function prepareNativeLaunch(input, providerBaseUrl = input.providerBaseUr
|
|
|
8849
8835
|
if (!compatible(input.harness, input.protocol))
|
|
8850
8836
|
throw new Error("Harness and provider protocol are incompatible.");
|
|
8851
8837
|
validateHarnessVersion(input.harness, input.version);
|
|
8852
|
-
if (!
|
|
8838
|
+
if (!isAbsolute10(input.stateDir) || !isAbsolute10(input.cwd))
|
|
8853
8839
|
throw new Error("Launch state and working directories must be absolute.");
|
|
8854
8840
|
if (!input.models.length || !input.models.some((m) => m.id === input.model))
|
|
8855
8841
|
throw new Error("Selected model is missing from the launch catalog.");
|
|
@@ -8957,7 +8943,7 @@ async function prepareNativeLaunch(input, providerBaseUrl = input.providerBaseUr
|
|
|
8957
8943
|
const api = { "anthropic-messages": "anthropic-messages", "openai-responses": "openai-responses", "openai-chat": "openai-completions" }[input.protocol];
|
|
8958
8944
|
const home = join11(input.stateDir, "dsh-home");
|
|
8959
8945
|
const sessionDir = input.sessionDir ?? join11(input.stateDir, "dsh-state");
|
|
8960
|
-
if (!
|
|
8946
|
+
if (!isAbsolute10(sessionDir))
|
|
8961
8947
|
throw new Error("DSH session directory must be absolute.");
|
|
8962
8948
|
await mkdir10(home, { recursive: true, mode: 448 });
|
|
8963
8949
|
await mkdir10(sessionDir, { recursive: true, mode: 448 });
|
|
@@ -9218,7 +9204,7 @@ async function prepareHarnessLaunch(input) {
|
|
|
9218
9204
|
validateHarnessVersion(input.harness, input.version);
|
|
9219
9205
|
if (!compatible(input.harness, input.protocol))
|
|
9220
9206
|
throw new Error("Harness and provider protocol are incompatible.");
|
|
9221
|
-
if (!
|
|
9207
|
+
if (!isAbsolute10(input.stateDir) || !isAbsolute10(input.cwd))
|
|
9222
9208
|
throw new Error("Launch state and working directories must be absolute.");
|
|
9223
9209
|
const compiledPolicy = compileModelPolicy(input.model, input.models, input.modelPolicy);
|
|
9224
9210
|
const nativePolicy = compileNativeModelPolicy({ harness: input.harness, mainModel: input.model, roles: compiledPolicy.roles, version: input.version });
|
|
@@ -9248,9 +9234,9 @@ async function prepareHarnessLaunch(input) {
|
|
|
9248
9234
|
}
|
|
9249
9235
|
|
|
9250
9236
|
// src/ori-model-policy.ts
|
|
9251
|
-
import { isAbsolute as
|
|
9237
|
+
import { isAbsolute as isAbsolute11 } from "path";
|
|
9252
9238
|
function prepareOriModelPolicy(target, prepared) {
|
|
9253
|
-
if (!
|
|
9239
|
+
if (!isAbsolute11(prepared.executable) || /[\0\r\n]/.test(prepared.executable))
|
|
9254
9240
|
throw new Error("Ori requires an absolute verified native executable.");
|
|
9255
9241
|
const entries = Object.entries(prepared.env);
|
|
9256
9242
|
if (entries.some(([key]) => !/^[A-Z_][A-Z0-9_]*$/.test(key)))
|
|
@@ -10222,19 +10208,20 @@ async function ensureLaunchProfile(client, provider, harness, model3, modelPolic
|
|
|
10222
10208
|
import { z as z6 } from "zod";
|
|
10223
10209
|
import { constants as constants5 } from "fs";
|
|
10224
10210
|
import { open as open3, readdir as readdir5, unlink, link, lstat as lstat4, access as access3, stat as stat5, realpath as realpath6, readlink } from "fs/promises";
|
|
10225
|
-
import { join as join13, isAbsolute as
|
|
10211
|
+
import { join as join13, isAbsolute as isAbsolute12, dirname as dirname8, parse as pathParts, resolve as resolvePath, sep as sep2 } from "path";
|
|
10226
10212
|
import { createHash as createHash10, randomUUID as randomUUID5 } from "crypto";
|
|
10227
10213
|
import { execFile as execFile3, spawn as spawn2 } from "child_process";
|
|
10228
10214
|
import { promisify as promisify3 } from "util";
|
|
10229
|
-
import { resolveCredential as
|
|
10215
|
+
import { resolveCredential, resolveClientTransport, toV1BaseUrl, clientTransportEnvKeys as clientTransportEnvKeys2, keychainConfigValue as keychainConfigValue2, appConfigDiskValue as appConfigDiskValue2 } from "@hasna/contracts/client";
|
|
10230
10216
|
var execute2 = promisify3(execFile3);
|
|
10231
10217
|
var reference = z6.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/).max(120);
|
|
10232
10218
|
var item = z6.string().min(1).max(500).regex(/^[^\x00-\x1f\x7f]+$/);
|
|
10233
10219
|
var vaultKey = z6.string().max(500).regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9][A-Za-z0-9_.-]*)*$/, "Use a vault key path, not an option or secret value");
|
|
10234
10220
|
var keychain = z6.object({ kind: z6.literal("keychain"), service: item, account: item }).strict();
|
|
10235
10221
|
var operator = z6.discriminatedUnion("kind", [
|
|
10222
|
+
z6.object({ kind: z6.literal("contracts") }).strict(),
|
|
10236
10223
|
z6.object({ kind: z6.literal("env") }).strict(),
|
|
10237
|
-
z6.object({ kind: z6.literal("keychain"), account: item }).strict()
|
|
10224
|
+
z6.object({ kind: z6.literal("keychain"), account: item.refine((value) => value.trim().length > 0 && value === value.trim(), "Vault account must be nonblank without surrounding whitespace") }).strict()
|
|
10238
10225
|
]);
|
|
10239
10226
|
var origin = z6.string().transform((value) => new URL(endpoint(value)).origin);
|
|
10240
10227
|
var credentialBindingSchema = z6.object({
|
|
@@ -10244,11 +10231,14 @@ var credentialBindingSchema = z6.object({
|
|
|
10244
10231
|
source: z6.discriminatedUnion("kind", [keychain, z6.object({
|
|
10245
10232
|
kind: z6.literal("vault"),
|
|
10246
10233
|
key: vaultKey,
|
|
10247
|
-
url: z6.string().max(2000).transform(endpoint),
|
|
10248
|
-
executable: z6.string().max(4096).regex(/^[^\x00-\x1f\x7f]+$/).refine(
|
|
10234
|
+
url: z6.string().max(2000).transform(endpoint).optional(),
|
|
10235
|
+
executable: z6.string().max(4096).regex(/^[^\x00-\x1f\x7f]+$/).refine(isAbsolute12, "Secrets executable must be an absolute path"),
|
|
10249
10236
|
operator
|
|
10250
10237
|
}).strict()])
|
|
10251
|
-
}).strict()
|
|
10238
|
+
}).strict().superRefine((binding, ctx) => {
|
|
10239
|
+
if (binding.source.kind === "vault" && binding.source.operator.kind !== "contracts" && !binding.source.url)
|
|
10240
|
+
ctx.addIssue({ code: "custom", path: ["source", "url"], message: "An explicit env or Keychain operator binding requires a vault URL." });
|
|
10241
|
+
});
|
|
10252
10242
|
var fingerprint = (binding) => createHash10("sha256").update(JSON.stringify(binding)).digest("hex");
|
|
10253
10243
|
|
|
10254
10244
|
class CredentialBindings {
|
|
@@ -10489,20 +10479,49 @@ class CredentialInterrupted extends CommandInterrupted {
|
|
|
10489
10479
|
super(exitCode, "Credential lookup was interrupted; no harness was started.");
|
|
10490
10480
|
}
|
|
10491
10481
|
}
|
|
10492
|
-
function vaultEnvironment(binding, env) {
|
|
10482
|
+
async function vaultEnvironment(binding, env, options = {}) {
|
|
10493
10483
|
if (binding.source.kind !== "vault")
|
|
10494
10484
|
throw new Fault(500, "credential_resolution", "Unexpected credential source.");
|
|
10495
|
-
|
|
10485
|
+
const source = binding.source;
|
|
10486
|
+
let credential, url = source.url;
|
|
10496
10487
|
try {
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10488
|
+
if (source.operator.kind === "contracts") {
|
|
10489
|
+
const pair = () => {
|
|
10490
|
+
const keys = clientTransportEnvKeys2("secrets");
|
|
10491
|
+
const configuredUrl = keys.apiUrlKeys.map((name) => env[name]).find((value) => value !== undefined) ?? keychainConfigValue2("secrets", env, options.keychain)?.value ?? appConfigDiskValue2("secrets", env, keys.apiUrlKeys)?.value;
|
|
10492
|
+
const resolution = resolveClientTransport("secrets", env, { credentials: options });
|
|
10493
|
+
const key = resolveCredential("secrets", env, options);
|
|
10494
|
+
if (key?.tier === "pointer")
|
|
10495
|
+
throw new Fault(422, "vault_operator_pointer", "A Secrets operator cannot bootstrap itself through HASNA_SECRETS_API_KEY_REF. Configure its operator in Keychain, canonical config/credentials, or an explicit key override.");
|
|
10496
|
+
if (!key?.apiKey || key.source !== resolution.apiKeySource || key.tier !== resolution.apiKeyTier)
|
|
10497
|
+
throw new Fault(422, "vault_operator_changed", "The vault operator changed during resolution; no credential was sent. Retry after configuration is stable.");
|
|
10498
|
+
const boundUrl = source.url ? toV1BaseUrl(source.url) : resolution.baseUrl;
|
|
10499
|
+
if (resolution.apiUrlSource !== "default" && boundUrl !== resolution.baseUrl)
|
|
10500
|
+
throw new Fault(422, "vault_operator_authority", "The binding vault URL conflicts with the canonical Secrets API URL; no credential was sent.");
|
|
10501
|
+
if (configuredUrl !== undefined && toV1BaseUrl(configuredUrl) !== resolution.baseUrl)
|
|
10502
|
+
throw new Fault(422, "vault_operator_changed", "The vault authority changed during resolution; no credential was sent.");
|
|
10503
|
+
return { key, url: configuredUrl?.trim() ?? source.url ?? boundUrl.replace(/\/v1$/, "") };
|
|
10504
|
+
};
|
|
10505
|
+
const first = pair(), second = pair();
|
|
10506
|
+
if (first.key.apiKey !== second.key.apiKey || first.key.source !== second.key.source || first.key.tier !== second.key.tier || first.url !== second.url)
|
|
10507
|
+
throw new Fault(422, "vault_operator_changed", "The vault operator or authority changed during resolution; no credential was sent. Retry after configuration is stable.");
|
|
10508
|
+
credential = second.key;
|
|
10509
|
+
url = second.url;
|
|
10510
|
+
} else {
|
|
10511
|
+
credential = source.operator.kind === "keychain" ? resolveCredential("secrets", { HASNA_STATION: source.operator.account }, { keychain: { ...options.keychain, enabled: true } }) : resolveCredential("secrets", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SECRETS_API_KEY")), { keychain: { enabled: false } });
|
|
10512
|
+
}
|
|
10513
|
+
} catch (error) {
|
|
10514
|
+
if (error instanceof Fault)
|
|
10515
|
+
throw error;
|
|
10516
|
+
const kind = error instanceof Error ? error.name : "";
|
|
10517
|
+
const reason = kind === "CredentialFileUnsafeError" ? "unsafe canonical config/credentials file" : kind === "ClientTransportConfigurationError" ? "invalid or missing canonical API URL/key" : source.operator.kind === "keychain" ? "unavailable pinned Keychain account" : "credential selection refused by Contracts (Keychain, canonical file, profile, or environment)";
|
|
10518
|
+
throw new Fault(422, "vault_operator_unavailable", `Cannot resolve the Secrets vault operator: ${reason}. Check the selected source in this terminal session; no alternate account was selected.`);
|
|
10500
10519
|
}
|
|
10501
10520
|
if (!credential?.apiKey)
|
|
10502
|
-
throw new Fault(422, "vault_operator_missing", "
|
|
10503
|
-
const allowed = /^(PATH|HOME|USER|LOGNAME|SHELL|LANG|LC_[A-Z_]+|TMPDIR|TEMP|TMP|HASNA_SECRETS_HOME|HASNA_STATION)$/;
|
|
10521
|
+
throw new Fault(422, "vault_operator_missing", "The selected vault operator has no credential. Use a canonical Contracts binding, restore its named Keychain account, or inject HASNA_SECRETS_API_KEY for an explicit env binding.");
|
|
10522
|
+
const allowed = /^(PATH|HOME|USER|LOGNAME|SHELL|LANG|LC_[A-Z_]+|TMPDIR|TEMP|TMP|HASNA_HOME|HASNA_CONFIG_HOME|HASNA_SECRETS_HOME|HASNA_STATION)$/;
|
|
10504
10523
|
const next = Object.fromEntries(Object.entries(env).filter(([name]) => allowed.test(name)));
|
|
10505
|
-
next.HASNA_SECRETS_API_URL =
|
|
10524
|
+
next.HASNA_SECRETS_API_URL = url;
|
|
10506
10525
|
next.HASNA_SECRETS_API_KEY = credential.apiKey;
|
|
10507
10526
|
next.HASNA_SECRETS_API_KEY_OVERRIDE = credential.apiKey;
|
|
10508
10527
|
if (binding.source.operator.kind === "keychain")
|
|
@@ -10515,7 +10534,7 @@ async function runVaultCommand(binding, args, env, delivery = {}, captureCheck =
|
|
|
10515
10534
|
if (process.platform === "win32")
|
|
10516
10535
|
throw new Fault(422, "vault_exec_unavailable", "Vault CLI bindings currently require POSIX process groups; use runtime environment injection on Windows.");
|
|
10517
10536
|
const executable = await validateVaultExecutable(binding.source.executable);
|
|
10518
|
-
const childEnv = { ...vaultEnvironment(binding, env), ...delivery };
|
|
10537
|
+
const childEnv = { ...await vaultEnvironment(binding, env), ...delivery };
|
|
10519
10538
|
return new Promise((resolveResult, reject2) => {
|
|
10520
10539
|
const child = spawn2(executable, args, { env: childEnv, stdio: ["ignore", captureCheck ? "pipe" : "ignore", "ignore"], detached: true, shell: false });
|
|
10521
10540
|
let failure;
|
|
@@ -10646,8 +10665,8 @@ var HELP = `switcher \u2014 launch a coding harness with a provider and its mode
|
|
|
10646
10665
|
[--ori-executable PATH] [--state-dir DIR]
|
|
10647
10666
|
[--timeout SECONDS] -- [native harness arguments]
|
|
10648
10667
|
switcher runs list|get [ID]
|
|
10649
|
-
switcher credentials bind PRESET --vault-key KEY --vault-url URL
|
|
10650
|
-
[--vault-cli PATH] [--vault-account ACCOUNT]
|
|
10668
|
+
switcher credentials bind PRESET --vault-key KEY [--vault-url URL]
|
|
10669
|
+
[--vault-cli PATH] [--vault-account ACCOUNT | --vault-operator env]
|
|
10651
10670
|
switcher credentials bind PRESET --keychain-service SERVICE --keychain-account ACCOUNT
|
|
10652
10671
|
switcher credentials list|check|remove [PRESET_OR_REFERENCE]
|
|
10653
10672
|
switcher doctor
|
|
@@ -10656,12 +10675,14 @@ HARNESS: claude, codex, grok, opencode, opencode2, pi, omp, dsh, cline, hermes,
|
|
|
10656
10675
|
PROTOCOL: anthropic-messages, openai-responses, openai-chat, gemini-generate-content
|
|
10657
10676
|
Without remote API configuration, the CLI owns a local authenticated API and
|
|
10658
10677
|
stores data in ~/.hasna/switcher (override HASNA_SWITCHER_HOME).
|
|
10659
|
-
|
|
10678
|
+
Remote API URL/key resolve through @hasna/contracts: overrides, Keychain,
|
|
10679
|
+
~/.hasna/switcher/config/credentials, then environment. A key alone uses the gateway.
|
|
10660
10680
|
A configured remote API never falls back to local data.
|
|
10661
10681
|
Provider credential references must start SWITCHER_PROVIDER_.
|
|
10662
10682
|
Credential bindings contain references only. Custom destinations require --origin URL.
|
|
10663
|
-
Vault bindings use the installed secrets CLI
|
|
10664
|
-
|
|
10683
|
+
Vault bindings use the installed secrets CLI and its canonical Contracts URL/key
|
|
10684
|
+
by default. --vault-account pins a Keychain account; --vault-operator env requires
|
|
10685
|
+
per-process HASNA_SECRETS_API_KEY. Explicit operators also require --vault-url.
|
|
10665
10686
|
--file accepts a JSON object including id; raw credentials are never accepted.
|
|
10666
10687
|
Fireworks discovery requires --catalog-account-id (or an explicit --catalog-url).
|
|
10667
10688
|
--json outputs machine-readable records (also the default for data commands).
|
|
@@ -10739,6 +10760,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
10739
10760
|
"vault-url": { type: "string" },
|
|
10740
10761
|
"vault-cli": { type: "string" },
|
|
10741
10762
|
"vault-account": { type: "string" },
|
|
10763
|
+
"vault-operator": { type: "string" },
|
|
10742
10764
|
"keychain-service": { type: "string" },
|
|
10743
10765
|
"keychain-account": { type: "string" },
|
|
10744
10766
|
origin: { type: "string", multiple: true }
|
|
@@ -10753,7 +10775,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
10753
10775
|
throw new Error("Unknown command. Run switcher --help.");
|
|
10754
10776
|
const providerFlags = ["url", "protocol", "preset", "credential-env", "auth-style", "catalog-url", "catalog-format", "catalog-auth-style", "catalog-credential-env", "catalog-account-id", "models-path"];
|
|
10755
10777
|
const provided = (names) => names.some((name) => values2[name] !== undefined);
|
|
10756
|
-
const credentialFlags = ["vault-key", "vault-url", "vault-cli", "vault-account", "keychain-service", "keychain-account", "origin"];
|
|
10778
|
+
const credentialFlags = ["vault-key", "vault-url", "vault-cli", "vault-account", "vault-operator", "keychain-service", "keychain-account", "origin"];
|
|
10757
10779
|
const credentials = new CredentialResolver;
|
|
10758
10780
|
if (command === "credentials") {
|
|
10759
10781
|
const bindingFlags = [...credentialFlags, "credential-env"];
|
|
@@ -10775,16 +10797,20 @@ async function main(args = process.argv.slice(2)) {
|
|
|
10775
10797
|
}
|
|
10776
10798
|
if (action2 !== "bind" || !id2)
|
|
10777
10799
|
throw new Fault(400, "invalid_request", "Use credentials bind PRESET, list, check PRESET_OR_REFERENCE, or remove PRESET_OR_REFERENCE.");
|
|
10778
|
-
const hasVault = provided(["vault-key", "vault-url", "vault-cli", "vault-account"]);
|
|
10800
|
+
const hasVault = provided(["vault-key", "vault-url", "vault-cli", "vault-account", "vault-operator"]);
|
|
10779
10801
|
const hasKeychain = provided(["keychain-service", "keychain-account"]);
|
|
10780
10802
|
if (hasVault === hasKeychain)
|
|
10781
10803
|
throw new Fault(400, "conflicting_options", "Choose one credential source: vault or Keychain.");
|
|
10804
|
+
if (values2["vault-operator"] !== undefined && !["contracts", "env"].includes(values2["vault-operator"]))
|
|
10805
|
+
throw new Fault(400, "invalid_request", "Use --vault-operator contracts or env.");
|
|
10806
|
+
if (values2["vault-account"] !== undefined && values2["vault-operator"] !== undefined)
|
|
10807
|
+
throw new Fault(400, "conflicting_options", "Use either --vault-account or --vault-operator.");
|
|
10782
10808
|
const source = hasVault ? {
|
|
10783
10809
|
kind: "vault",
|
|
10784
10810
|
key: values2["vault-key"],
|
|
10785
10811
|
url: values2["vault-url"],
|
|
10786
10812
|
executable: values2["vault-cli"] ?? Bun.which("secrets"),
|
|
10787
|
-
operator: values2["vault-account"] ? { kind: "keychain", account: values2["vault-account"] } : { kind: "
|
|
10813
|
+
operator: values2["vault-account"] !== undefined ? { kind: "keychain", account: values2["vault-account"] } : { kind: values2["vault-operator"] ?? "contracts" }
|
|
10788
10814
|
} : { kind: "keychain", service: values2["keychain-service"], account: values2["keychain-account"] };
|
|
10789
10815
|
output(await credentials.bindings.bind(parse2(credentialBindingSchema, { schema: 1, ...bindingTarget(id2, values2["credential-env"], values2.origin), source })));
|
|
10790
10816
|
return;
|