@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/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.3";
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 { resolveCredential } from "@hasna/contracts/client";
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
- const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
1317
- if (!env.HASNA_SWITCHER_API_URL || !credential())
1318
- throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
1319
- return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
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 isAbsolute3, relative as relative2, sep } from "path";
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, resolveCredential2) {
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 = resolveCredential2 ? await resolveCredential2({ ...provider, baseUrl: provider.catalogBaseUrl ?? provider.baseUrl, credentialEnv }) : env[credentialEnv];
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.3",
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, resolveCredential2) {
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, resolveCredential2) };
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
- return resolve3(env.HASNA_SWITCHER_HOME ?? join3(homedir2(), ".hasna", "switcher"));
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, resolveCredential2) {
5861
+ async function openCliRuntime(env = process.env, resolveCredential) {
5704
5862
  const providerEnv = Object.fromEntries(Object.entries(env).filter(([name]) => name.startsWith("SWITCHER_PROVIDER_")));
5705
- if (Object.hasOwn(env, "HASNA_SWITCHER_API_URL") || Object.hasOwn(env, "HASNA_SWITCHER_API_KEY")) {
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: resolveCredential2
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 (!isAbsolute3(home))
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) || isAbsolute3(within))
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 reader = response.body.getReader();
6066
- let ended = false, output;
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 reader = response.body.getReader(), decoder = new TextDecoder;
6477
+ const decoder = new TextDecoder;
6364
6478
  const sse = response.headers.get("content-type")?.includes("text/event-stream");
6365
- let buffer = "", ended = false, output;
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 end2 = buffer.indexOf(`
6494
+ let end = buffer.indexOf(`
6381
6495
  `);
6382
- while (end2 >= 0) {
6383
- const line = buffer.slice(0, end2).trim();
6384
- buffer = buffer.slice(end2 + 1);
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
- end2 = buffer.indexOf(`
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 end = (error) => {
6398
- if (ended)
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 = async () => {
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 isAbsolute4, join as join5, resolve as resolve5 } from "path";
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 : isAbsolute4(rawConfigFile) ? rawConfigFile : baseDir ? resolve5(baseDir, rawConfigFile) : 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" || isAbsolute4(m) || m.includes("..")))
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 (!isAbsolute4(path))
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 isAbsolute9, resolve as resolve8 } from "path";
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 isAbsolute5 } from "path";
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 (!isAbsolute5(home))
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 isAbsolute6, join as join7 } from "path";
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 (!isAbsolute6(input.stateDir) || !isAbsolute6(input.cwd))
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 isAbsolute7, join as join8, relative as relative4, resolve as resolve7 } from "path";
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 === "" || !isAbsolute7(rel) && rel !== ".." && !rel.startsWith("../");
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] && !isAbsolute7(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 isAbsolute8, join as join10 } from "path";
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 reader = upstream.body.getReader();
7834
- let ended = false;
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 (!isAbsolute8(input.stateDir) || !isAbsolute8(input.cwd))
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 reader = response.body.getReader();
8548
- let ended = false;
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 (!isAbsolute9(input.stateDir) || !isAbsolute9(input.cwd))
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 (!isAbsolute9(sessionDir))
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 (!isAbsolute9(input.stateDir) || !isAbsolute9(input.cwd))
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 isAbsolute10 } from "path";
9237
+ import { isAbsolute as isAbsolute11 } from "path";
9252
9238
  function prepareOriModelPolicy(target, prepared) {
9253
- if (!isAbsolute10(prepared.executable) || /[\0\r\n]/.test(prepared.executable))
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 isAbsolute11, dirname as dirname8, parse as pathParts, resolve as resolvePath, sep as sep2 } from "path";
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 resolveCredential2 } from "@hasna/contracts/client";
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(isAbsolute11, "Secrets executable must be an absolute path"),
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
- let credential;
10485
+ const source = binding.source;
10486
+ let credential, url = source.url;
10496
10487
  try {
10497
- credential = binding.source.operator.kind === "keychain" ? resolveCredential2("secrets", { HASNA_STATION: binding.source.operator.account }, { keychain: { enabled: true } }) : resolveCredential2("secrets", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SECRETS_API_KEY")), { keychain: { enabled: false } });
10498
- } catch {
10499
- throw new Fault(422, "vault_operator_unavailable", "The configured vault operator credential is inaccessible; no alternate account was selected.");
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", "Configure the vault operator Keychain account or inject HASNA_SECRETS_API_KEY for this process.");
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 = binding.source.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
- Set HASNA_SWITCHER_API_URL + HASNA_SWITCHER_API_KEY for a remote API.
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; --vault-account reads its operator
10664
- from macOS Keychain, otherwise HASNA_SECRETS_API_KEY must be injected per process.
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: "env" }
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;