github-router 0.3.122 → 0.3.129

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.
@@ -13,6 +13,7 @@ import process$1 from "node:process";
13
13
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
14
14
  import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { Agent } from "undici";
16
17
  import { performance } from "node:perf_hooks";
17
18
  import { createInterface } from "node:readline";
18
19
  import Parser from "web-tree-sitter";
@@ -307,12 +308,12 @@ async function fetchWithTransientRetry(doFetch, opts = {}) {
307
308
  await res.body.cancel();
308
309
  } catch {}
309
310
  const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
310
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
311
+ const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
311
312
  if (label) {
312
313
  const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
313
- consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`);
314
+ consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay$1}ms`);
314
315
  }
315
- await abortableSleep(delay, signal);
316
+ await abortableSleep(delay$1, signal);
316
317
  }
317
318
  }
318
319
  /** Extract an HTTP status from a thrown error (HTTPError carries
@@ -353,9 +354,9 @@ async function withTransientRetry(fn, opts = {}) {
353
354
  if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
354
355
  const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
355
356
  const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
356
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
357
- if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay}ms`);
358
- await abortableSleep(delay, signal);
357
+ const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
358
+ if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay$1}ms`);
359
+ await abortableSleep(delay$1, signal);
359
360
  }
360
361
  }
361
362
  }
@@ -1199,7 +1200,7 @@ async function mapHttpError$1(response) {
1199
1200
  });
1200
1201
  }
1201
1202
  function mapNetworkError$1(err) {
1202
- if (isAbortLike$1(err)) return new ArtifactError({
1203
+ if (isAbortLike$2(err)) return new ArtifactError({
1203
1204
  code: "TIMEOUT",
1204
1205
  message: "artifact API request timed out or was aborted",
1205
1206
  retryable: true,
@@ -1234,7 +1235,7 @@ function detailToMessage$1(detail) {
1234
1235
  }
1235
1236
  if (typeof record.message === "string") return record.message;
1236
1237
  }
1237
- function isAbortLike$1(err) {
1238
+ function isAbortLike$2(err) {
1238
1239
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1239
1240
  }
1240
1241
 
@@ -1417,8 +1418,235 @@ function stringProp$1(description) {
1417
1418
  };
1418
1419
  }
1419
1420
 
1421
+ //#endregion
1422
+ //#region src/lib/fleet/tunnel-auth.ts
1423
+ var TunnelAuthError = class extends Error {
1424
+ code;
1425
+ constructor(code, message) {
1426
+ super(message);
1427
+ this.name = "TunnelAuthError";
1428
+ this.code = code;
1429
+ }
1430
+ };
1431
+ const REFRESH_MARGIN_MS = 5 * 6e4;
1432
+ const MIN_REMINT_INTERVAL_MS = 3e4;
1433
+ const DEVTUNNEL_TIMEOUT_MS = 1e4;
1434
+ const MINT_FAILURE_BACKOFF_MS = 3e4;
1435
+ const MAX_PLAUSIBLE_TTL_MS = 2880 * 6e4;
1436
+ const MAX_STDOUT_BYTES$2 = 256 * 1024;
1437
+ const TUNNEL_ID_RE$1 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1438
+ const JWT_REDACT_RE = /eyJ[A-Za-z0-9._-]{20,}/g;
1439
+ const SCHEME_TOKEN_RE = /(bearer|tunnel) +[!-~]+/gi;
1440
+ /** Strip credential-shaped substrings from any string before it is logged or surfaced. */
1441
+ function redactTunnelSecrets(s) {
1442
+ return s.replace(JWT_REDACT_RE, "<redacted-token>").replace(SCHEME_TOKEN_RE, "$1 <redacted-token>");
1443
+ }
1444
+ function safeRealpath(p) {
1445
+ try {
1446
+ return realpathSync(p);
1447
+ } catch {
1448
+ return nodePath.resolve(p);
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Guard the resolved `devtunnel` path: it must be a trusted ABSOLUTE path that
1453
+ * is not the current working directory's own binary. `resolveExecutable` already
1454
+ * excludes cwd; this is defense-in-depth against a cwd-local / relative
1455
+ * resolution ever reaching a child-process spawn. Both paths are canonicalized
1456
+ * (realpath, resolving `..` and symlinks) before the cwd-containment check, so a
1457
+ * non-canonical path like `/safe/../cwd/devtunnel` or a symlink cannot evade it.
1458
+ * Returns the (original) path to spawn, or throws.
1459
+ */
1460
+ function assertTrustedDevtunnelPath(resolved, cwd = typeof process.cwd === "function" ? nodePath.resolve(process.cwd()) : null) {
1461
+ if (!resolved) throw new TunnelAuthError("NOT_INSTALLED", "the devtunnel CLI was not found on PATH; install it and run `devtunnel user login` on this (control-plane) machine");
1462
+ if (!nodePath.isAbsolute(resolved)) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a non-absolute devtunnel binary");
1463
+ const ext = nodePath.extname(resolved).toLowerCase();
1464
+ if (ext === ".cmd" || ext === ".bat" || ext === ".ps1") throw new TunnelAuthError("NOT_INSTALLED", "resolved devtunnel is a script shim (.cmd/.bat/.ps1); github-router runs the native devtunnel(.exe) — ensure the native binary precedes any shim on PATH");
1465
+ const realResolved = safeRealpath(resolved);
1466
+ const realCwd = cwd ? safeRealpath(cwd) : null;
1467
+ if (realCwd && (realResolved === realCwd || realResolved.startsWith(realCwd + nodePath.sep))) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a cwd-local devtunnel binary");
1468
+ return resolved;
1469
+ }
1470
+ /**
1471
+ * The real runner: resolve `devtunnel` to a trusted absolute path (PATH-resolved,
1472
+ * cwd-excluded) and run it with `shell:false` (native binary).
1473
+ */
1474
+ function realDevtunnelRunner() {
1475
+ return async (args) => {
1476
+ const res = await runManagedExeCapture(assertTrustedDevtunnelPath(resolveExecutable("devtunnel")), args, {
1477
+ timeoutMs: DEVTUNNEL_TIMEOUT_MS,
1478
+ maxStdoutBytes: MAX_STDOUT_BYTES$2
1479
+ });
1480
+ return {
1481
+ stdout: res.stdout,
1482
+ stderr: res.stderr,
1483
+ code: res.code,
1484
+ timedOut: res.timedOut
1485
+ };
1486
+ };
1487
+ }
1488
+ function looksLikeJwt(s) {
1489
+ const parts = s.split(".");
1490
+ if (parts.length !== 3) return false;
1491
+ return parts.every((p) => p.length > 0 && /^[A-Za-z0-9_-]+$/.test(p));
1492
+ }
1493
+ /** Recursively collect JWT-shaped strings from arbitrary parsed JSON. */
1494
+ function collectJwts(value, out) {
1495
+ if (typeof value === "string") {
1496
+ if (value.startsWith("eyJ") && looksLikeJwt(value)) out.add(value);
1497
+ return;
1498
+ }
1499
+ if (Array.isArray(value)) {
1500
+ for (const v of value) collectJwts(v, out);
1501
+ return;
1502
+ }
1503
+ if (value && typeof value === "object") for (const v of Object.values(value)) collectJwts(v, out);
1504
+ }
1505
+ /**
1506
+ * Extract the single access token from `devtunnel token --json` output. Prefers
1507
+ * structured JSON; falls back to a token-shaped scan. Refuses to guess when zero
1508
+ * or more-than-one distinct tokens are present (so we never send a wrong JWT).
1509
+ */
1510
+ function extractToken(stdout) {
1511
+ const found = /* @__PURE__ */ new Set();
1512
+ try {
1513
+ collectJwts(JSON.parse(stdout), found);
1514
+ } catch {}
1515
+ if (found.size === 0) {
1516
+ for (const tok of stdout.split(/[^A-Za-z0-9._-]+/)) if (tok.startsWith("eyJ") && looksLikeJwt(tok)) found.add(tok);
1517
+ }
1518
+ if (found.size === 0) throw new TunnelAuthError("PARSE", "no tunnel access token found in devtunnel output");
1519
+ if (found.size > 1) throw new TunnelAuthError("PARSE", "devtunnel output contained more than one token; refusing to guess");
1520
+ return [...found][0];
1521
+ }
1522
+ /** Parse a JWT `exp` claim (seconds) into epoch milliseconds. Throws on a missing/non-numeric exp. */
1523
+ function parseJwtExpMs(jwt) {
1524
+ const parts = jwt.split(".");
1525
+ if (parts.length !== 3) throw new TunnelAuthError("PARSE", "tunnel token is not a JWT");
1526
+ let payload;
1527
+ try {
1528
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1529
+ } catch {
1530
+ throw new TunnelAuthError("PARSE", "tunnel token payload was not decodable");
1531
+ }
1532
+ const exp = payload?.exp;
1533
+ if (typeof exp !== "number" || !Number.isFinite(exp)) throw new TunnelAuthError("PARSE", "tunnel token has no numeric exp claim");
1534
+ return exp * 1e3;
1535
+ }
1536
+ function classifyMintFailure(res) {
1537
+ if (res.timedOut) return new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1538
+ const stderr = (res.stderr || "").toLowerCase();
1539
+ const tail = redactTunnelSecrets((res.stderr || "").trim()).slice(-300);
1540
+ const suffix = tail ? ` [${tail}]` : "";
1541
+ if (/log ?in|sign ?in|not authenticated|unauthor|401/.test(stderr)) return new TunnelAuthError("NOT_LOGGED_IN", `devtunnel is not logged in (or lacks access to this tunnel) on the control-plane machine; run \`devtunnel user login\`${suffix}`);
1542
+ if (/not found|404|does not exist|no such tunnel/.test(stderr)) return new TunnelAuthError("TUNNEL_NOT_FOUND", `devtunnel could not find the tunnel; verify tunnelId with \`devtunnel list\`${suffix}`);
1543
+ return new TunnelAuthError("MINT_FAILED", `devtunnel token failed (exit ${res.code})${suffix}`);
1544
+ }
1545
+ /**
1546
+ * Create a per-process token provider: lazy mint, per-tunnel in-memory cache,
1547
+ * single-flight, and short negative backoff on non-timeout failures.
1548
+ */
1549
+ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
1550
+ const cache = /* @__PURE__ */ new Map();
1551
+ const inflight = /* @__PURE__ */ new Map();
1552
+ const backoff = /* @__PURE__ */ new Map();
1553
+ async function mint(cfg) {
1554
+ if (!TUNNEL_ID_RE$1.test(cfg.tunnelId)) throw new TunnelAuthError("MINT_FAILED", "invalid tunnelId; must match a devtunnel tunnel name");
1555
+ const args = [
1556
+ "token",
1557
+ cfg.tunnelId,
1558
+ "--scopes",
1559
+ "connect",
1560
+ "--json"
1561
+ ];
1562
+ let res;
1563
+ try {
1564
+ res = await runner(args);
1565
+ } catch (err) {
1566
+ if (err instanceof TunnelAuthError) throw err;
1567
+ throw new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(err instanceof Error ? err.message : String(err)));
1568
+ }
1569
+ if (res.timedOut) throw new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1570
+ if (res.code !== 0) throw classifyMintFailure(res);
1571
+ const token = extractToken(res.stdout);
1572
+ const expMs = parseJwtExpMs(token);
1573
+ const now = Date.now();
1574
+ if (expMs <= now) throw new TunnelAuthError("PARSE", "devtunnel minted an already-expired token");
1575
+ if (expMs - now > MAX_PLAUSIBLE_TTL_MS) throw new TunnelAuthError("PARSE", "devtunnel token TTL is implausibly long; refusing");
1576
+ const existing = cache.get(cfg.tunnelId);
1577
+ if (!existing || expMs > existing.expMs) cache.set(cfg.tunnelId, {
1578
+ token,
1579
+ expMs,
1580
+ mintedAt: now
1581
+ });
1582
+ return cache.get(cfg.tunnelId).token;
1583
+ }
1584
+ function mintOnce(cfg) {
1585
+ const key = cfg.tunnelId;
1586
+ return (async () => {
1587
+ try {
1588
+ const token = await mint(cfg);
1589
+ backoff.delete(key);
1590
+ return token;
1591
+ } catch (err) {
1592
+ const e = err instanceof TunnelAuthError ? err : new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(String(err)));
1593
+ if (e.code !== "TIMEOUT") backoff.set(key, {
1594
+ until: Date.now() + MINT_FAILURE_BACKOFF_MS,
1595
+ err: e
1596
+ });
1597
+ const c = cache.get(key);
1598
+ if (c && c.expMs > Date.now()) return c.token;
1599
+ throw e;
1600
+ } finally {
1601
+ inflight.delete(key);
1602
+ }
1603
+ })();
1604
+ }
1605
+ return {
1606
+ async getToken(cfg) {
1607
+ const key = cfg.tunnelId;
1608
+ const now = Date.now();
1609
+ const cached$1 = cache.get(key);
1610
+ if (cached$1 && cached$1.expMs > now) {
1611
+ const comfortablyFresh = cached$1.expMs - now > REFRESH_MARGIN_MS;
1612
+ const recentlyMinted = now - cached$1.mintedAt < MIN_REMINT_INTERVAL_MS;
1613
+ if (comfortablyFresh || recentlyMinted) return cached$1.token;
1614
+ }
1615
+ const inf = inflight.get(key);
1616
+ if (inf) return inf;
1617
+ const bo = backoff.get(key);
1618
+ if (bo && now < bo.until) {
1619
+ if (cached$1 && cached$1.expMs > now) return cached$1.token;
1620
+ throw bo.err;
1621
+ }
1622
+ const p = mintOnce(cfg);
1623
+ inflight.set(key, p);
1624
+ return p;
1625
+ },
1626
+ invalidate(cfg) {
1627
+ cache.delete(cfg.tunnelId);
1628
+ backoff.delete(cfg.tunnelId);
1629
+ }
1630
+ };
1631
+ }
1632
+
1420
1633
  //#endregion
1421
1634
  //#region src/lib/fleet/client.ts
1635
+ const IS_BUN = typeof globalThis.Bun !== "undefined";
1636
+ let sharedInsecureDispatcher;
1637
+ function insecureDispatcher() {
1638
+ return sharedInsecureDispatcher ??= new Agent({ connect: { rejectUnauthorized: false } });
1639
+ }
1640
+ /**
1641
+ * Attach the runtime-correct TLS-verification-off mechanism to a fetch init for a
1642
+ * single self-signed direct-HTTPS instance: Bun → `tls`, Node → an undici
1643
+ * `dispatcher`. Exported so BOTH runtime branches are unit-testable under one
1644
+ * interpreter (the untested Node branch is exactly what shipped broken).
1645
+ */
1646
+ function applyInsecureTls(init, isBun = IS_BUN) {
1647
+ if (isBun) init.tls = { rejectUnauthorized: false };
1648
+ else init.dispatcher = insecureDispatcher();
1649
+ }
1422
1650
  var FleetError = class extends Error {
1423
1651
  code;
1424
1652
  retryable;
@@ -1450,12 +1678,23 @@ function decodeSessionId(globalId) {
1450
1678
  }
1451
1679
  var FleetClient = class {
1452
1680
  baseUrl;
1681
+ origin;
1453
1682
  token;
1454
1683
  fetchFn;
1684
+ getTunnelToken;
1685
+ onTunnelAuthInvalidate;
1686
+ insecureTLS;
1455
1687
  constructor(options) {
1456
1688
  this.baseUrl = options.url.replace(/\/+$/, "");
1689
+ this.origin = new URL(this.baseUrl).origin;
1457
1690
  this.token = options.token;
1458
1691
  this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
1692
+ this.getTunnelToken = options.getTunnelToken;
1693
+ this.onTunnelAuthInvalidate = options.onTunnelAuthInvalidate;
1694
+ this.insecureTLS = options.insecureTLS === true;
1695
+ }
1696
+ capabilities(signal) {
1697
+ return this.request("GET", "/api/control/capabilities", void 0, void 0, signal);
1459
1698
  }
1460
1699
  listSessions(signal) {
1461
1700
  return this.request("GET", "/api/control/sessions", void 0, void 0, signal);
@@ -1525,67 +1764,178 @@ var FleetClient = class {
1525
1764
  async request(method, pathname, query, body, signal) {
1526
1765
  const url = new URL(pathname, `${this.baseUrl}/`);
1527
1766
  for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
1528
- let response;
1529
- try {
1530
- response = await this.fetchFn(url.toString(), {
1531
- method,
1532
- headers: {
1533
- Authorization: `Bearer ${this.token}`,
1534
- ...body === void 0 ? {} : { "Content-Type": "application/json" }
1535
- },
1536
- body: body === void 0 ? void 0 : JSON.stringify(body),
1537
- redirect: "error",
1538
- signal
1539
- });
1540
- } catch (err) {
1541
- throw mapNetworkError(err);
1767
+ if (url.origin !== this.origin) throw new FleetError({
1768
+ code: "UNREACHABLE",
1769
+ message: "fleet request URL origin did not match the registered instance origin",
1770
+ retryable: false
1771
+ });
1772
+ const devtunnelHost = isDevtunnelHost(url.hostname);
1773
+ const tunnelEligible = this.getTunnelToken !== void 0 && devtunnelHost && url.protocol === "https:";
1774
+ for (let attempt = 0; attempt < 2; attempt++) {
1775
+ let tunnelToken;
1776
+ if (tunnelEligible) try {
1777
+ tunnelToken = await this.getTunnelToken();
1778
+ } catch (err) {
1779
+ throw mapTunnelAuthError(err);
1780
+ }
1781
+ const attachTunnel = tunnelToken !== void 0 && tunnelToken !== "";
1782
+ const canRetry = attachTunnel && !!this.onTunnelAuthInvalidate && attempt === 0;
1783
+ const headers = {
1784
+ Authorization: `Bearer ${this.token}`,
1785
+ ...devtunnelHost ? { "X-Tunnel-Skip-Anti-Phishing-Page": "true" } : {},
1786
+ ...attachTunnel ? { "X-Tunnel-Authorization": `tunnel ${tunnelToken}` } : {},
1787
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
1788
+ };
1789
+ let response;
1790
+ try {
1791
+ const init = {
1792
+ method,
1793
+ headers,
1794
+ body: body === void 0 ? void 0 : JSON.stringify(body),
1795
+ redirect: "error",
1796
+ signal
1797
+ };
1798
+ if (this.insecureTLS) applyInsecureTls(init);
1799
+ response = await this.fetchFn(url.toString(), init);
1800
+ } catch (err) {
1801
+ if (canRetry && method === "GET") {
1802
+ this.onTunnelAuthInvalidate();
1803
+ continue;
1804
+ }
1805
+ throw mapNetworkError(err, devtunnelHost);
1806
+ }
1807
+ if (!response.ok) {
1808
+ if ((response.status === 401 || response.status === 403) && canRetry) {
1809
+ this.onTunnelAuthInvalidate();
1810
+ continue;
1811
+ }
1812
+ throw await mapHttpError(response, url.toString());
1813
+ }
1814
+ return await response.json();
1542
1815
  }
1543
- if (!response.ok) throw await mapHttpError(response);
1544
- return await response.json();
1816
+ throw new FleetError({
1817
+ code: "AUTH_FAILED",
1818
+ message: "fleet instance tunnel authentication failed after re-mint; verify the tunnel and `devtunnel user login`",
1819
+ retryable: false
1820
+ });
1545
1821
  }
1546
1822
  };
1547
- async function mapHttpError(response) {
1823
+ /** Dev Tunnel access tokens are only ever scoped to the `*.devtunnels.ms` service. */
1824
+ function isDevtunnelHost(hostname) {
1825
+ return hostname === "devtunnels.ms" || hostname.endsWith(".devtunnels.ms");
1826
+ }
1827
+ function mapTunnelAuthError(err) {
1828
+ if (err instanceof TunnelAuthError) return new FleetError({
1829
+ code: "AUTH_FAILED",
1830
+ message: err.message,
1831
+ retryable: err.code === "TIMEOUT",
1832
+ detail: { tunnelAuth: err.code }
1833
+ });
1834
+ return mapNetworkError(err);
1835
+ }
1836
+ async function mapHttpError(response, requestUrl) {
1548
1837
  const detail = await readErrorDetail(response);
1549
1838
  const upstreamMessage = detailToMessage(detail);
1550
1839
  const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
1551
- if (response.status === 401 || response.status === 403) return new FleetError({
1840
+ const status = response.status;
1841
+ if (isDevTunnelHost(requestUrl) && detectDevTunnelNoHost(status, detail)) return new FleetError({
1842
+ code: "NO_HOST",
1843
+ message: `dev tunnel relay reports no host connected (${status})${suffix}`,
1844
+ retryable: true,
1845
+ status,
1846
+ detail
1847
+ });
1848
+ if (status === 401 || status === 403) return new FleetError({
1552
1849
  code: "AUTH_FAILED",
1553
- message: `fleet instance authentication failed (${response.status})${suffix}`,
1850
+ message: `fleet instance authentication failed (${status})${suffix}`,
1554
1851
  retryable: false,
1555
- status: response.status,
1852
+ status,
1556
1853
  detail
1557
1854
  });
1558
- if (response.status === 404) return new FleetError({
1855
+ if (status === 404) return new FleetError({
1559
1856
  code: "SESSION_NOT_FOUND",
1560
1857
  message: `fleet session or resource not found (404)${suffix}`,
1561
1858
  retryable: false,
1562
- status: response.status,
1859
+ status,
1563
1860
  detail
1564
1861
  });
1565
- if (response.status === 409 || response.status === 412) return new FleetError({
1862
+ if (status === 409 || status === 412) return new FleetError({
1566
1863
  code: "PRECONDITION_FAILED",
1567
- message: `fleet instance precondition failed (${response.status})${suffix}`,
1864
+ message: `fleet instance precondition failed (${status})${suffix}`,
1568
1865
  retryable: false,
1569
- status: response.status,
1866
+ status,
1570
1867
  detail
1571
1868
  });
1572
- if (response.status === 408 || response.status === 504) return new FleetError({
1869
+ if (status === 400) return new FleetError({
1870
+ code: "BAD_REQUEST",
1871
+ message: `fleet instance rejected the request (400)${suffix}`,
1872
+ retryable: false,
1873
+ status,
1874
+ detail
1875
+ });
1876
+ if (status === 408 || status === 504) return new FleetError({
1573
1877
  code: "TIMEOUT",
1574
- message: `fleet instance request timed out (${response.status})${suffix}`,
1878
+ message: `fleet instance request timed out (${status})${suffix}`,
1575
1879
  retryable: true,
1576
- status: response.status,
1880
+ status,
1881
+ detail
1882
+ });
1883
+ if ((status === 502 || status === 503) && isDevTunnelHost(requestUrl)) return new FleetError({
1884
+ code: "RELAY_ERROR",
1885
+ message: `dev tunnel relay returned HTTP ${status} (host may be down, restarting, or under load)${suffix}`,
1886
+ retryable: true,
1887
+ status,
1888
+ detail
1889
+ });
1890
+ if (status === 429) return new FleetError({
1891
+ code: "RATE_LIMITED",
1892
+ message: `fleet instance rate-limited the request (429)${suffix}`,
1893
+ retryable: true,
1894
+ status,
1577
1895
  detail
1578
1896
  });
1579
1897
  return new FleetError({
1580
1898
  code: "UPSTREAM_ERROR",
1581
- message: `fleet instance returned HTTP ${response.status}${suffix}`,
1582
- retryable: response.status === 429 || response.status >= 500,
1583
- status: response.status,
1899
+ message: `fleet instance returned HTTP ${status}${suffix}`,
1900
+ retryable: status >= 500,
1901
+ status,
1584
1902
  detail
1585
1903
  });
1586
1904
  }
1587
- function mapNetworkError(err) {
1588
- if (isAbortLike(err)) return new FleetError({
1905
+ const DEVTUNNEL_HOST_RE$1 = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
1906
+ /** F4: only Dev Tunnel relay hosts may be classified NO_HOST / RELAY_ERROR. */
1907
+ function isDevTunnelHost(requestUrl) {
1908
+ try {
1909
+ return DEVTUNNEL_HOST_RE$1.test(new URL(requestUrl).hostname);
1910
+ } catch {
1911
+ return false;
1912
+ }
1913
+ }
1914
+ const DEVTUNNEL_NO_HOST_SIGNALS = [
1915
+ "no host is currently connected",
1916
+ "tunnel is not currently hosted",
1917
+ "host is not accepting connections",
1918
+ "tunnel host is not connected",
1919
+ "no connection to the host",
1920
+ "tunnelporthostnotconnected"
1921
+ ];
1922
+ function detectDevTunnelNoHost(status, detail) {
1923
+ if (status !== 502 && status !== 503 && status !== 404) return false;
1924
+ const haystack = detailToSearchString(detail).toLowerCase();
1925
+ if (haystack === "") return false;
1926
+ return DEVTUNNEL_NO_HOST_SIGNALS.some((signal) => haystack.includes(signal));
1927
+ }
1928
+ function detailToSearchString(detail) {
1929
+ if (detail === void 0 || detail === null) return "";
1930
+ if (typeof detail === "string") return detail;
1931
+ try {
1932
+ return JSON.stringify(detail);
1933
+ } catch {
1934
+ return String(detail);
1935
+ }
1936
+ }
1937
+ function mapNetworkError(err, devtunnelHost = false) {
1938
+ if (isAbortLike$1(err)) return new FleetError({
1589
1939
  code: "TIMEOUT",
1590
1940
  message: "fleet instance request timed out or was aborted",
1591
1941
  retryable: true,
@@ -1593,7 +1943,7 @@ function mapNetworkError(err) {
1593
1943
  });
1594
1944
  return new FleetError({
1595
1945
  code: "UNREACHABLE",
1596
- message: `fleet instance unreachable: ${err instanceof Error ? err.message : String(err)}`,
1946
+ message: `fleet instance unreachable: ${err instanceof Error ? err.message : String(err)}${devtunnelHost ? " — if this is a private VS Code Dev Tunnel, an unauthenticated request is redirected to GitHub auth (which we refuse to follow): set a `tunnelId` (auto-mint) / `tunnelToken` in the registry, or make the tunnel anonymous" : ""}`,
1597
1947
  retryable: true,
1598
1948
  detail: err
1599
1949
  });
@@ -1620,7 +1970,7 @@ function detailToMessage(detail) {
1620
1970
  }
1621
1971
  if (typeof record.message === "string") return record.message;
1622
1972
  }
1623
- function isAbortLike(err) {
1973
+ function isAbortLike$1(err) {
1624
1974
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1625
1975
  }
1626
1976
 
@@ -1645,7 +1995,7 @@ async function loadFleetRegistryConfig(configPath = defaultFleetConfigPath()) {
1645
1995
  if (isNodeErrorCode(err, "ENOENT")) return { instances: [] };
1646
1996
  throw err;
1647
1997
  }
1648
- if (process.platform !== "win32" && (stat$1.mode & 63) !== 0) console.warn(`[fleet] Registry file ${configPath} is group/other-readable; it contains bearer tokens. Consider chmod 600.`);
1998
+ if (process.platform !== "win32" && (stat$1.mode & 63) !== 0) console.warn(`[fleet] Registry file ${configPath} is group/other-readable; it contains bearer / tunnel credentials. Consider chmod 600.`);
1649
1999
  const raw = await fs.readFile(configPath, "utf8");
1650
2000
  if (raw.trim() === "") return { instances: [] };
1651
2001
  const parsed = JSON.parse(raw);
@@ -1722,15 +2072,48 @@ function parseInstance(raw) {
1722
2072
  throw invalidInstanceUrlError(id);
1723
2073
  }
1724
2074
  if (!isAllowedInstanceUrl(parsedUrl)) throw invalidInstanceUrlError(id);
2075
+ assertDevTunnelUrlShape(id, parsedUrl);
2076
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} url must not contain embedded credentials (userinfo)`);
1725
2077
  if (typeof token !== "string" || token === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} token must be a non-empty string`);
2078
+ const tunnelId = parseTunnelId(id, instance.tunnelId);
2079
+ const tunnelToken = parseTunnelToken(id, instance.tunnelToken);
2080
+ const insecureTLS = parseInsecureTLS(id, instance.insecureTLS);
2081
+ if (insecureTLS) {
2082
+ if (parsedUrl.protocol !== "https:") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} insecureTLS only applies to an https url (an http url has no TLS to relax)`);
2083
+ if (tunnelId !== void 0 || tunnelToken !== void 0) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} insecureTLS must not be combined with a Dev Tunnel (tunnelId/tunnelToken); the relay presents a valid public cert, so disabling verification only exposes the bearer/tunnel token to MITM`);
2084
+ if (!isLocalNetworkHost(parsedUrl.hostname)) throw new FleetRegistryError("INVALID_CONFIG", DEVTUNNEL_HOST_RE.test(parsedUrl.hostname) ? `fleet registry instance ${id} insecureTLS must not be set on a Dev Tunnel host; *.devtunnels.ms presents a valid public cert` : `fleet registry instance ${id} insecureTLS is only allowed for a local-network host (loopback, a private/LAN IP, or a .local name); refusing to disable TLS verification for public host ${parsedUrl.hostname}`);
2085
+ }
1726
2086
  return {
1727
2087
  id: id.trim(),
1728
2088
  label: label.trim(),
1729
2089
  url: trimmedUrl,
1730
2090
  token,
1731
2091
  default: instance.default === true ? true : void 0,
1732
- allowExec: instance.allowExec === true ? true : void 0
1733
- };
2092
+ allowExec: instance.allowExec === true ? true : void 0,
2093
+ tunnelId,
2094
+ tunnelToken,
2095
+ insecureTLS
2096
+ };
2097
+ }
2098
+ const TUNNEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2099
+ function parseTunnelId(id, raw) {
2100
+ if (raw === void 0) return void 0;
2101
+ if (typeof raw !== "string" || !TUNNEL_ID_RE.test(raw.trim())) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelId must match ${TUNNEL_ID_RE.source} (a devtunnel tunnel name from \`devtunnel list\`)`);
2102
+ return raw.trim();
2103
+ }
2104
+ function parseTunnelToken(id, raw) {
2105
+ if (raw === void 0) return void 0;
2106
+ if (typeof raw !== "string") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a string`);
2107
+ let t = raw.trim();
2108
+ if (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'")) t = t.slice(1, -1).trim();
2109
+ t = t.replace(/^X-Tunnel-Authorization:\s*/i, "").replace(/^tunnel\s+/i, "").trim();
2110
+ if (t === "" || /\s/.test(t)) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a non-empty single-line token`);
2111
+ return t;
2112
+ }
2113
+ function parseInsecureTLS(id, raw) {
2114
+ if (raw === void 0) return void 0;
2115
+ if (typeof raw !== "boolean") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} insecureTLS must be a boolean`);
2116
+ return raw === true ? true : void 0;
1734
2117
  }
1735
2118
  function invalidInstanceUrlError(id) {
1736
2119
  return new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url must be https (or http://localhost for local testing)`);
@@ -1740,13 +2123,56 @@ function isAllowedInstanceUrl(url) {
1740
2123
  if (url.protocol !== "http:") return false;
1741
2124
  return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
1742
2125
  }
2126
+ function isLocalNetworkHost(hostnameRaw) {
2127
+ const host = hostnameRaw.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
2128
+ if (host === "localhost") return true;
2129
+ if (host.endsWith(".local")) return true;
2130
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
2131
+ if (v4) {
2132
+ const octets = [
2133
+ Number(v4[1]),
2134
+ Number(v4[2]),
2135
+ Number(v4[3]),
2136
+ Number(v4[4])
2137
+ ];
2138
+ if (octets.some((o) => o > 255)) return false;
2139
+ const a = octets[0];
2140
+ const b = octets[1];
2141
+ if (a === 127) return true;
2142
+ if (a === 10) return true;
2143
+ if (a === 172 && b >= 16 && b <= 31) return true;
2144
+ if (a === 192 && b === 168) return true;
2145
+ if (a === 169 && b === 254) return true;
2146
+ return false;
2147
+ }
2148
+ if (host.includes(":")) {
2149
+ if (host === "::1") return true;
2150
+ if (/^fe[89ab]/.test(host)) return true;
2151
+ if (/^f[cd]/.test(host)) return true;
2152
+ return false;
2153
+ }
2154
+ return false;
2155
+ }
2156
+ const DEVTUNNEL_HOST_RE = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
2157
+ function assertDevTunnelUrlShape(id, url) {
2158
+ if (!DEVTUNNEL_HOST_RE.test(url.hostname)) return;
2159
+ if (url.port === "") return;
2160
+ const firstDot = url.hostname.indexOf(".");
2161
+ const firstLabel = firstDot < 0 ? url.hostname : url.hostname.slice(0, firstDot);
2162
+ const rest = firstDot < 0 ? "" : url.hostname.slice(firstDot + 1);
2163
+ const corrected = rest === "" ? `https://${firstLabel}-${url.port}.devtunnels.ms` : `https://${firstLabel}-${url.port}.${rest}`;
2164
+ throw new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url ${url.href} uses the wrong Dev Tunnel form: the forwarded port must be fused into the hostname, not given as a :port suffix. Use ${corrected} instead (the bare \`<id>.<cluster>.devtunnels.ms:<port>\` host addresses the tunnel-management endpoint, not the relayed service).`);
2165
+ }
1743
2166
  function resolvedInstance(instance) {
1744
2167
  return {
1745
2168
  id: instance.id,
1746
2169
  label: instance.label,
1747
2170
  url: instance.url,
1748
2171
  token: instance.token,
1749
- allowExec: instance.allowExec
2172
+ allowExec: instance.allowExec,
2173
+ tunnelId: instance.tunnelId,
2174
+ tunnelToken: instance.tunnelToken,
2175
+ insecureTLS: instance.insecureTLS
1750
2176
  };
1751
2177
  }
1752
2178
  function isObject(value) {
@@ -1761,6 +2187,15 @@ function isNodeErrorCode(err, code) {
1761
2187
  const FLEET_GROUP = "fleet";
1762
2188
  const INSTANCE_PROBE_TIMEOUT_MS = 2e3;
1763
2189
  const INSTANCE_PROBE_CACHE_TTL_MS = 5e3;
2190
+ const CAPABILITIES_CACHE_TTL_MS = 6e4;
2191
+ const AWAIT_TURN_DEFAULT_TIMEOUT_MS = 3e4;
2192
+ const AWAIT_TURN_TIMEOUT_SLACK_MS = 5e3;
2193
+ const LIST_INSTANCES_FANOUT_CONCURRENCY = 16;
2194
+ const AWAIT_TURN_FANOUT_CONCURRENCY = 256;
2195
+ const INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES = 1;
2196
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS = 250;
2197
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS = 1e3;
2198
+ const FLEET_FANOUT_CONCURRENCY_ENV = "GH_ROUTER_FLEET_FANOUT_CONCURRENCY";
1764
2199
  var FleetToolInputError = class extends Error {
1765
2200
  code;
1766
2201
  constructor(code, message) {
@@ -1770,28 +2205,59 @@ var FleetToolInputError = class extends Error {
1770
2205
  }
1771
2206
  };
1772
2207
  let defaultRegistry;
2208
+ let defaultTunnelProvider;
1773
2209
  const awaitTurnCursors = /* @__PURE__ */ new Map();
1774
2210
  const instanceProbeCache = /* @__PURE__ */ new Map();
1775
2211
  function createFleetTools(options = {}) {
1776
2212
  const registry = options.registry;
1777
2213
  const clients = /* @__PURE__ */ new Map();
2214
+ const capabilitiesCache = /* @__PURE__ */ new Map();
2215
+ const tunnelProvider = options.tunnelTokenProvider ?? (defaultTunnelProvider ??= createTunnelTokenProvider());
2216
+ const probeRetryDelay = options.probeRetryDelay ?? delay;
2217
+ const awaitTurnDeadlineSlackMs = nonNegativeNumberOrDefault(options.awaitTurnDeadlineSlackMs, AWAIT_TURN_TIMEOUT_SLACK_MS);
1778
2218
  function getRegistry() {
1779
2219
  if (registry) return registry;
1780
2220
  defaultRegistry ??= new FleetRegistry();
1781
2221
  return defaultRegistry;
1782
2222
  }
1783
2223
  function clientFor(instance) {
1784
- const key = `${instance.id}\0${instance.url}\0${instance.token}`;
2224
+ const key = `${instance.id}\0${instance.url}\0${instance.token}\0${instance.tunnelId ?? ""}\0${instance.tunnelToken ?? ""}\0${instance.insecureTLS === true ? "1" : "0"}`;
1785
2225
  const existing = clients.get(key);
1786
2226
  if (existing) return existing;
1787
2227
  const created = options.createClient ? options.createClient(instance) : new FleetClient({
1788
2228
  url: instance.url,
1789
2229
  token: instance.token,
1790
- fetchFn: options.fetchFn
2230
+ fetchFn: options.fetchFn,
2231
+ insecureTLS: instance.insecureTLS,
2232
+ ...tunnelClientOptions(instance, tunnelProvider)
1791
2233
  });
1792
2234
  clients.set(key, created);
1793
2235
  return created;
1794
2236
  }
2237
+ async function getInstanceCapabilities(instance, signal) {
2238
+ const now = Date.now();
2239
+ const cached$1 = capabilitiesCache.get(instance.id);
2240
+ if (cached$1 && now - cached$1.at < CAPABILITIES_CACHE_TTL_MS) return cached$1.caps;
2241
+ try {
2242
+ const response = await clientFor(instance).capabilities(signal);
2243
+ const caps = new Set(response.capabilities);
2244
+ capabilitiesCache.set(instance.id, {
2245
+ caps,
2246
+ at: Date.now()
2247
+ });
2248
+ return caps;
2249
+ } catch {
2250
+ capabilitiesCache.set(instance.id, {
2251
+ caps: null,
2252
+ at: Date.now()
2253
+ });
2254
+ return null;
2255
+ }
2256
+ }
2257
+ async function assertCapability(instance, cap, featureName, signal) {
2258
+ const caps = await getInstanceCapabilities(instance, signal);
2259
+ if (caps !== null && !caps.has(cap)) throw new FleetToolInputError("UNSUPPORTED_CAPABILITY", `fleet instance ${instance.id} does not advertise the '${cap}' capability required for ${featureName}; omit it or upgrade the ai-or-die control plane`);
2260
+ }
1795
2261
  async function resolve(arg) {
1796
2262
  return getRegistry().resolveInstance(arg);
1797
2263
  }
@@ -1815,37 +2281,46 @@ function createFleetTools(options = {}) {
1815
2281
  const now = Date.now();
1816
2282
  const cached$1 = instanceProbeCache.get(cacheKey);
1817
2283
  if (cached$1 && now - cached$1.at < INSTANCE_PROBE_CACHE_TTL_MS) return cached$1.result;
1818
- const timeout = createProbeTimeout();
1819
- try {
1820
- const response = await clientFor(await resolve(info.id)).listSessions(timeout.signal);
1821
- const lastSeen = Date.now();
1822
- const result = {
1823
- id: info.id,
1824
- label: info.label,
1825
- reachable: true,
1826
- sessionCount: response.sessions.length,
1827
- lastSeen
1828
- };
1829
- instanceProbeCache.set(cacheKey, {
1830
- result,
1831
- at: lastSeen
1832
- });
1833
- return result;
1834
- } catch (err) {
1835
- const result = {
1836
- id: info.id,
1837
- label: info.label,
1838
- reachable: false,
1839
- error: fleetProbeErrorCode(err)
1840
- };
1841
- instanceProbeCache.set(cacheKey, {
1842
- result,
1843
- at: Date.now()
1844
- });
1845
- return result;
1846
- } finally {
1847
- timeout.cleanup();
2284
+ for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
2285
+ const timeout = createProbeTimeout();
2286
+ try {
2287
+ const response = await clientFor(await resolve(info.id)).listSessions(timeout.signal);
2288
+ const lastSeen = Date.now();
2289
+ const result$1 = {
2290
+ id: info.id,
2291
+ label: info.label,
2292
+ reachable: true,
2293
+ sessionCount: response.sessions.length,
2294
+ lastSeen
2295
+ };
2296
+ instanceProbeCache.set(cacheKey, {
2297
+ result: result$1,
2298
+ at: lastSeen
2299
+ });
2300
+ return result$1;
2301
+ } catch (err) {
2302
+ const code = fleetProbeErrorCode(err);
2303
+ if (code === "RATE_LIMITED" && attempt < INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES) {
2304
+ timeout.cleanup();
2305
+ await probeRetryDelay(probeRateLimitBackoffMs(attempt));
2306
+ continue;
2307
+ }
2308
+ const result$1 = failedProbeResult(info, code);
2309
+ instanceProbeCache.set(cacheKey, {
2310
+ result: result$1,
2311
+ at: Date.now()
2312
+ });
2313
+ return result$1;
2314
+ } finally {
2315
+ timeout.cleanup();
2316
+ }
1848
2317
  }
2318
+ const result = failedProbeResult(info, "UNREACHABLE");
2319
+ instanceProbeCache.set(cacheKey, {
2320
+ result,
2321
+ at: Date.now()
2322
+ });
2323
+ return result;
1849
2324
  }
1850
2325
  function tool$1(toolNameHttp, description, inputSchema, handler) {
1851
2326
  return {
@@ -1865,8 +2340,7 @@ function createFleetTools(options = {}) {
1865
2340
  }
1866
2341
  return Object.freeze([
1867
2342
  tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
1868
- const instances = await getRegistry().listInstances();
1869
- return ok({ instances: await Promise.all(instances.map((instance) => probeInstance(instance))) });
2343
+ return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
1870
2344
  }),
1871
2345
  tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema({ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
1872
2346
  const instance = await resolve(optionalString(args, "instance"));
@@ -1903,12 +2377,12 @@ function createFleetTools(options = {}) {
1903
2377
  sessionId: globalId
1904
2378
  });
1905
2379
  }),
1906
- tool$1("send_message", "Send a message to a fleet session. Returns isError if delivery failed or an awaited confirmation did not arrive.", objectSchema({
2380
+ tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema({
1907
2381
  sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
1908
2382
  instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
1909
2383
  message: stringProp("Message text to deliver to the session."),
1910
- idempotencyKey: stringProp("Caller-generated idempotency key."),
1911
- awaitMs: numberProp("Optional confirmation wait time in milliseconds.")
2384
+ idempotencyKey: stringProp("Caller-generated idempotency key. Reuse the same key on retry; the upstream dedupes so a retry never re-types."),
2385
+ awaitMs: numberProp("Optional best-effort confirmation wait (ms) NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
1912
2386
  }, [
1913
2387
  "sessionId",
1914
2388
  "message",
@@ -1921,14 +2395,21 @@ function createFleetTools(options = {}) {
1921
2395
  idempotencyKey: requiredString(args, "idempotencyKey"),
1922
2396
  ...awaitMs === void 0 ? {} : { awaitMs }
1923
2397
  }, signal);
1924
- const delivered = response.delivered !== false;
1925
- const confirmed = response.confirmed !== false;
1926
- const isError = !delivered || awaitMs !== void 0 && awaitMs > 0 && !confirmed;
2398
+ const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2399
+ const confirmed = delivered && response.confirmed === true;
2400
+ const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2401
+ const isError = !delivered;
1927
2402
  return jsonResult({
1928
2403
  resolvedInstance: publicInstance(instance),
1929
2404
  sessionId: globalId,
1930
2405
  ...response,
1931
- ...isError ? { message: !delivered ? "message was not delivered by the upstream instance" : `message delivery was not confirmed within awaitMs=${awaitMs}` } : {}
2406
+ delivered,
2407
+ confirmed,
2408
+ ...confirmationTimedOut ? {
2409
+ confirmationPending: true,
2410
+ confirmationTimedOut: true
2411
+ } : {},
2412
+ ...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
1932
2413
  }, isError);
1933
2414
  }),
1934
2415
  tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
@@ -1983,19 +2464,30 @@ function createFleetTools(options = {}) {
1983
2464
  name: stringProp("Optional display name for the session."),
1984
2465
  workingDir: stringProp("Optional working directory on the remote instance."),
1985
2466
  idempotencyKey: stringProp("Caller-generated idempotency key."),
1986
- start: booleanProp("Whether the remote instance should start the session immediately.")
2467
+ start: booleanProp("Whether the remote instance should start the session immediately."),
2468
+ readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2469
+ permissionMode: stringProp("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
2470
+ agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
1987
2471
  }, [
1988
2472
  "instance",
1989
2473
  "agent",
1990
2474
  "idempotencyKey"
1991
2475
  ]), async (args, signal) => {
1992
2476
  const instance = await resolve(requiredString(args, "instance"));
2477
+ const agent = requiredString(args, "agent");
1993
2478
  const idempotencyKey = requiredString(args, "idempotencyKey");
2479
+ const permissionMode = optionalString(args, "permissionMode");
2480
+ const agentArgs = optionalStringArray(args, "agentArgs");
2481
+ if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2482
+ if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
1994
2483
  const response = await clientFor(instance).createSession(definedObject({
1995
- agent: requiredString(args, "agent"),
2484
+ agent,
1996
2485
  name: optionalString(args, "name"),
1997
2486
  workingDir: optionalString(args, "workingDir"),
1998
2487
  start: optionalBoolean(args, "start"),
2488
+ readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
2489
+ permissionMode,
2490
+ agentArgs,
1999
2491
  idempotencyKey
2000
2492
  }), signal);
2001
2493
  const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
@@ -2023,30 +2515,52 @@ function createFleetTools(options = {}) {
2023
2515
  ...response
2024
2516
  });
2025
2517
  }),
2026
- tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target cursors, so callers do not pass cursor tokens.", objectSchema({
2518
+ tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema({
2027
2519
  instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
2028
2520
  sessionIds: arrayProp("Global session ids to filter to."),
2029
2521
  timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
2030
- kinds: arrayProp("Optional event kinds to filter to.")
2522
+ kinds: arrayProp("Optional event kinds to filter to."),
2523
+ watcherId: stringProp("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
2031
2524
  }, []), async (args, signal) => {
2032
2525
  const target = await resolveAwaitTarget(args, getRegistry());
2033
- const clientKey = target.instances.map((instance) => instance.id).sort().join(",");
2034
- const cursorByInstance = awaitTurnCursors.get(clientKey) ?? /* @__PURE__ */ new Map();
2035
- awaitTurnCursors.set(clientKey, cursorByInstance);
2526
+ const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
2036
2527
  const timeoutMs = optionalNumber(args, "timeoutMs");
2037
2528
  const kinds = optionalStringArray(args, "kinds");
2038
- const responses = await Promise.all(target.instances.map(async (instance) => {
2039
- const response = await clientFor(instance).waitEvents(definedObject({
2040
- cursor: cursorByInstance.get(instance.id),
2041
- timeoutMs,
2042
- sessionIds: target.localSessionIdsByInstance.get(instance.id),
2043
- kinds
2044
- }), signal);
2045
- cursorByInstance.set(instance.id, response.cursor);
2046
- return {
2047
- instance,
2048
- response
2049
- };
2529
+ const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
2530
+ const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
2531
+ const combined = combineAbortSignals([signal, deadline.signal]);
2532
+ try {
2533
+ const response = await clientFor(instance).waitEvents(definedObject({
2534
+ cursor: cursorByInstance.get(instance.id),
2535
+ timeoutMs,
2536
+ sessionIds: target.localSessionIdsByInstance.get(instance.id),
2537
+ kinds
2538
+ }), combined.signal);
2539
+ cursorByInstance.set(instance.id, response.cursor);
2540
+ return {
2541
+ ok: true,
2542
+ instance,
2543
+ response
2544
+ };
2545
+ } catch (err) {
2546
+ const error = fleetProbeErrorCode(err);
2547
+ const hint = fleetProbeHint(error);
2548
+ return {
2549
+ ok: false,
2550
+ instance,
2551
+ error,
2552
+ ...hint ? { hint } : {}
2553
+ };
2554
+ } finally {
2555
+ combined.cleanup();
2556
+ deadline.cleanup();
2557
+ }
2558
+ });
2559
+ const responses = results.filter(isAwaitTurnSuccess);
2560
+ const errors = results.filter(isAwaitTurnFailure).map(({ instance, error, hint }) => ({
2561
+ instance: publicInstance(instance),
2562
+ error,
2563
+ ...hint ? { hint } : {}
2050
2564
  }));
2051
2565
  const events$1 = responses.flatMap(({ instance, response }) => response.events.map((event) => stampEvent(instance, event))).sort(compareStampedEvents);
2052
2566
  const gaps = responses.flatMap(({ instance, response }) => response.gaps.map((gap) => ({
@@ -2059,9 +2573,10 @@ function createFleetTools(options = {}) {
2059
2573
  gaps,
2060
2574
  cursors: responses.map(({ instance, response }) => ({
2061
2575
  instance: publicInstance(instance),
2062
- ...parseCursor(response.cursor)
2576
+ cursor: response.cursor
2063
2577
  })),
2064
- more: responses.some(({ response }) => response.more)
2578
+ more: responses.some(({ response }) => response.more),
2579
+ ...errors.length > 0 ? { errors } : {}
2065
2580
  });
2066
2581
  }),
2067
2582
  tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
@@ -2131,13 +2646,81 @@ function createProbeTimeout() {
2131
2646
  cleanup: () => clearTimeout(timer)
2132
2647
  };
2133
2648
  }
2649
+ function createAwaitTurnDeadline(timeoutMs, slackMs) {
2650
+ const deadlineMs = Math.max(0, timeoutMs ?? AWAIT_TURN_DEFAULT_TIMEOUT_MS) + slackMs;
2651
+ const controller = new AbortController();
2652
+ const timer = setTimeout(() => {
2653
+ const err = /* @__PURE__ */ new Error("await_turn per-instance deadline exceeded");
2654
+ err.name = "TimeoutError";
2655
+ controller.abort(err);
2656
+ }, deadlineMs);
2657
+ return {
2658
+ signal: controller.signal,
2659
+ cleanup: () => clearTimeout(timer)
2660
+ };
2661
+ }
2662
+ function combineAbortSignals(signals) {
2663
+ const noop = () => {};
2664
+ const present = signals.filter((signal) => signal !== void 0);
2665
+ if (present.length === 0) return {
2666
+ signal: void 0,
2667
+ cleanup: noop
2668
+ };
2669
+ if (present.length === 1) return {
2670
+ signal: present[0],
2671
+ cleanup: noop
2672
+ };
2673
+ const any = AbortSignal.any;
2674
+ if (typeof any === "function") return {
2675
+ signal: any(present),
2676
+ cleanup: noop
2677
+ };
2678
+ const controller = new AbortController();
2679
+ const listeners = [];
2680
+ const cleanup = () => {
2681
+ for (const { signal, handler } of listeners) signal.removeEventListener("abort", handler);
2682
+ listeners.length = 0;
2683
+ };
2684
+ for (const signal of present) {
2685
+ if (signal.aborted) {
2686
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2687
+ cleanup();
2688
+ return {
2689
+ signal: controller.signal,
2690
+ cleanup: noop
2691
+ };
2692
+ }
2693
+ const handler = () => {
2694
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2695
+ };
2696
+ signal.addEventListener("abort", handler, { once: true });
2697
+ listeners.push({
2698
+ signal,
2699
+ handler
2700
+ });
2701
+ }
2702
+ return {
2703
+ signal: controller.signal,
2704
+ cleanup
2705
+ };
2706
+ }
2134
2707
  function fleetProbeErrorCode(err) {
2135
2708
  if (typeof err === "object" && err !== null && "code" in err) {
2136
2709
  const code = err.code;
2137
2710
  if (typeof code === "string" && isFleetErrorCode(code)) return code;
2138
2711
  }
2712
+ if (isAbortLike(err)) return "TIMEOUT";
2139
2713
  return "UNREACHABLE";
2140
2714
  }
2715
+ function fleetProbeHint(code) {
2716
+ switch (code) {
2717
+ case "NO_HOST": return "tunnel relay up, no ai-or-die host connected (start the host on that machine)";
2718
+ case "RELAY_ERROR": return "tunnel relay returned an error; the host may be down, restarting, or under load";
2719
+ case "TIMEOUT": return "no response before the probe deadline; the host may be slow or the tunnel may have no host";
2720
+ case "UNREACHABLE": return "could not connect (DNS or connection failure); check the instance url";
2721
+ default: return;
2722
+ }
2723
+ }
2141
2724
  function isFleetErrorCode(code) {
2142
2725
  switch (code) {
2143
2726
  case "UNREACHABLE":
@@ -2145,7 +2728,11 @@ function isFleetErrorCode(code) {
2145
2728
  case "SESSION_NOT_FOUND":
2146
2729
  case "PRECONDITION_FAILED":
2147
2730
  case "TIMEOUT":
2148
- case "UPSTREAM_ERROR": return true;
2731
+ case "UPSTREAM_ERROR":
2732
+ case "NO_HOST":
2733
+ case "RELAY_ERROR":
2734
+ case "BAD_REQUEST":
2735
+ case "RATE_LIMITED": return true;
2149
2736
  default: return false;
2150
2737
  }
2151
2738
  }
@@ -2188,22 +2775,92 @@ function stampEvent(instance, event) {
2188
2775
  ...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
2189
2776
  };
2190
2777
  }
2778
+ function eventAtMs(value) {
2779
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2780
+ if (typeof value === "string") {
2781
+ const parsed = Date.parse(value);
2782
+ if (!Number.isNaN(parsed)) return parsed;
2783
+ }
2784
+ return 0;
2785
+ }
2191
2786
  function compareStampedEvents(a, b) {
2192
- const atA = typeof a.at === "string" ? a.at : "";
2193
- const atB = typeof b.at === "string" ? b.at : "";
2194
- if (atA !== atB) return atA < atB ? -1 : 1;
2787
+ const atA = eventAtMs(a.at);
2788
+ const atB = eventAtMs(b.at);
2789
+ if (atA !== atB) return atA - atB;
2195
2790
  return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
2196
2791
  }
2197
- function parseCursor(cursor) {
2198
- const idx = cursor.indexOf(":");
2199
- if (idx < 0) return { cursor };
2200
- const seq = Number(cursor.slice(idx + 1));
2792
+ const MAX_WATCHER_ID_LEN = 200;
2793
+ const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
2794
+ function awaitTurnCursorKey(watcherId) {
2795
+ const id = watcherId ?? "default";
2796
+ return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
2797
+ }
2798
+ function takeAwaitTurnCursorMap(clientKey) {
2799
+ const existing = awaitTurnCursors.get(clientKey);
2800
+ if (existing) {
2801
+ awaitTurnCursors.delete(clientKey);
2802
+ awaitTurnCursors.set(clientKey, existing);
2803
+ return existing;
2804
+ }
2805
+ const created = /* @__PURE__ */ new Map();
2806
+ awaitTurnCursors.set(clientKey, created);
2807
+ while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
2808
+ const oldest = awaitTurnCursors.keys().next().value;
2809
+ if (oldest === void 0) break;
2810
+ awaitTurnCursors.delete(oldest);
2811
+ }
2812
+ return created;
2813
+ }
2814
+ function isAwaitTurnSuccess(result) {
2815
+ return result.ok;
2816
+ }
2817
+ function isAwaitTurnFailure(result) {
2818
+ return !result.ok;
2819
+ }
2820
+ function failedProbeResult(info, code) {
2821
+ const hint = fleetProbeHint(code);
2201
2822
  return {
2202
- cursor,
2203
- epoch: cursor.slice(0, idx),
2204
- ...Number.isFinite(seq) ? { seq } : {}
2823
+ id: info.id,
2824
+ label: info.label,
2825
+ reachable: false,
2826
+ error: code,
2827
+ ...hint ? { hint } : {}
2205
2828
  };
2206
2829
  }
2830
+ async function mapWithConcurrency(items, limit, fn) {
2831
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
2832
+ const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
2833
+ const results = new Array(items.length);
2834
+ let nextIndex = 0;
2835
+ async function worker() {
2836
+ while (nextIndex < items.length) {
2837
+ const index = nextIndex++;
2838
+ results[index] = await fn(items[index], index);
2839
+ }
2840
+ }
2841
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
2842
+ return results;
2843
+ }
2844
+ function fleetFanoutConcurrency(defaultLimit) {
2845
+ const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
2846
+ const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
2847
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
2848
+ return defaultLimit;
2849
+ }
2850
+ function probeRateLimitBackoffMs(attempt) {
2851
+ return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
2852
+ }
2853
+ function nonNegativeNumberOrDefault(value, fallback) {
2854
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
2855
+ }
2856
+ async function delay(ms) {
2857
+ if (ms <= 0) return;
2858
+ await new Promise((resolve) => setTimeout(resolve, ms));
2859
+ }
2860
+ function isAbortLike(err) {
2861
+ if (!(err instanceof Error)) return false;
2862
+ return err.name === "AbortError" || err.name === "TimeoutError";
2863
+ }
2207
2864
  function uniqueInstances(instances) {
2208
2865
  const seen = /* @__PURE__ */ new Set();
2209
2866
  const result = [];
@@ -2220,6 +2877,26 @@ function publicInstance(instance) {
2220
2877
  label: instance.label
2221
2878
  };
2222
2879
  }
2880
+ /**
2881
+ * Build the FleetClient tunnel-auth options for a resolved instance.
2882
+ * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
2883
+ * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
2884
+ * retry, since it cannot be re-minted); else no tunnel auth.
2885
+ */
2886
+ function tunnelClientOptions(instance, provider) {
2887
+ if (instance.tunnelId) {
2888
+ const cfg = { tunnelId: instance.tunnelId };
2889
+ return {
2890
+ getTunnelToken: () => provider.getToken(cfg),
2891
+ onTunnelAuthInvalidate: () => provider.invalidate(cfg)
2892
+ };
2893
+ }
2894
+ if (instance.tunnelToken) {
2895
+ const token = instance.tunnelToken;
2896
+ return { getTunnelToken: async () => token };
2897
+ }
2898
+ return {};
2899
+ }
2223
2900
  function ok(value) {
2224
2901
  return jsonResult(value, false);
2225
2902
  }
@@ -11362,7 +12039,7 @@ var PendingMessageQueue = class {
11362
12039
  * `Agent` owns the current transcript, emits lifecycle events, executes tools,
11363
12040
  * and exposes queueing APIs for steering and follow-up messages.
11364
12041
  */
11365
- var Agent = class {
12042
+ var Agent$1 = class {
11366
12043
  _state;
11367
12044
  listeners = /* @__PURE__ */ new Set();
11368
12045
  steeringQueue;
@@ -18325,7 +19002,7 @@ async function runWorkerAgentOnce(opts) {
18325
19002
  getMessages,
18326
19003
  planState
18327
19004
  });
18328
- const agent = new Agent({
19005
+ const agent = new Agent$1({
18329
19006
  initialState: {
18330
19007
  systemPrompt: systemPromptFor(opts.mode),
18331
19008
  model: makeModelShim(resolved.modelId),
@@ -22007,4 +22684,4 @@ async function runStandInToolCall(args, signal) {
22007
22684
 
22008
22685
  //#endregion
22009
22686
  export { handleMcpDelete as $, IMPLEMENT_DEFAULT_MODEL as A, setupCopilotToken as At, TOOLBELT_TOOLS$1 as B, sleep as Bt, stopGateEnabledForRepo as C, DEFAULT_PORT as Ct, liveExec as D, pickClaudeDefault as Dt, resolveSealedGate as E, generateRandomPort as Et, availableToolCommands as F, cacheVSCodeVersion as Ft, buildAdvisorStream as G, GITHUB_API_BASE_URL as Gt, searchWeb as H, fetchWithTransientRetry as Ht, buildToolbeltAwareness as I, filterBetaHeader as It, buildOpenAIErrorEvent as J, githubHeaders as Jt, injectAdvisorTool as K, copilotBaseUrl as Kt, toolbeltEnabled as L, isNullish as Lt, appendPlanReminder as M, tryRefreshAndRetry as Mt, runWorkerAgent as N, cacheCopilotVersion as Nt, BROWSE_DEFAULT_MODEL as O, getPackageVersion as Ot, withNoOutputRetry as P, cacheModels as Pt, relayAnthropicStream as Q, toolbeltSkipSet as R, resolveCodexModel as Rt, repoRoot as S, DEFAULT_CODEX_MODEL_FALLBACKS as St, trustRepo as T, UPSTREAM_INACTIVITY_TIMEOUT_MS as Tt, ADVISOR_INTERNAL_TOOL_NAME as U, HTTPError as Ut, assetFor as V, getModels as Vt, ADVISOR_TOOL_INSTRUCTIONS as W, forwardError as Wt, logStreamError as X, isControllerClosedError as Y, state as Yt, readIteratorWithTimeout as Z, fileFindingsStore as _, extractZipMember as _t, buildPeerAwarenessSnippet as a, countTokens as at, isSubagentContext as b, DEFAULT_CLAUDE_MODEL_FALLBACKS as bt, buildStopHookCommand as c, createResponses as ct, fileBlockBudget as d, readResponseBodyCapped as dt, handleMcpPost as et, injectStopHookIntoSettingsFile as f, parseJsonOrDiagnose as ft, fileBaselineStore as g, extractTarGzMember as gt, stopReviewEnabled as h, provisionAndIndexColbert as ht, buildAgentPrompt as i, workerToolsEnabled as it, PLAN_DEFAULT_MODEL as j, setupGitHubToken as jt, DEFAULT_MODEL as k, withInstallLock as kt, captureLaunchBaseline as l, createChatCompletions as lt, stopGateId as m, hasSupportedBrowserInstalled as mt, MCP_GROUPS as n, fleetToolsEnabled as nt, personasFor as o, createMessages as ot, launchBaselineKey as p, provisionBrowserAssets as pt, isAdvisorRequested as q, copilotHeaders as qt, assertMcpToolSurfaceConsistent as r, standInToolEnabled as rt, buildSessionBindHookCommand as s, getTokenCount as st, GROUP_META as t, browserToolsEnabled as tt, decideStopHook as u, MAX_RESPONSE_BODY_BYTES as ut, fileLastPromptStore as v, collapsePathKeys as vt, stopReviewStateDir as w, UPSTREAM_FETCH_TIMEOUT_MS as wt, repoFingerprint as x, DEFAULT_CODEX_MODEL as xt, fileReviewDebounce as y, toolbeltPathOverride as yt, vscodeRipgrepPath as z, resolveModel as zt };
22010
- //# sourceMappingURL=peer-mcp-personas-B6z15bmc.js.map
22687
+ //# sourceMappingURL=peer-mcp-personas-B1Oqydxt.js.map