github-router 0.3.122 → 0.3.126

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.
@@ -307,12 +307,12 @@ async function fetchWithTransientRetry(doFetch, opts = {}) {
307
307
  await res.body.cancel();
308
308
  } catch {}
309
309
  const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
310
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
310
+ const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
311
311
  if (label) {
312
312
  const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
313
- consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`);
313
+ consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay$1}ms`);
314
314
  }
315
- await abortableSleep(delay, signal);
315
+ await abortableSleep(delay$1, signal);
316
316
  }
317
317
  }
318
318
  /** Extract an HTTP status from a thrown error (HTTPError carries
@@ -353,9 +353,9 @@ async function withTransientRetry(fn, opts = {}) {
353
353
  if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
354
354
  const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
355
355
  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);
356
+ const delay$1 = 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$1}ms`);
358
+ await abortableSleep(delay$1, signal);
359
359
  }
360
360
  }
361
361
  }
@@ -1199,7 +1199,7 @@ async function mapHttpError$1(response) {
1199
1199
  });
1200
1200
  }
1201
1201
  function mapNetworkError$1(err) {
1202
- if (isAbortLike$1(err)) return new ArtifactError({
1202
+ if (isAbortLike$2(err)) return new ArtifactError({
1203
1203
  code: "TIMEOUT",
1204
1204
  message: "artifact API request timed out or was aborted",
1205
1205
  retryable: true,
@@ -1234,7 +1234,7 @@ function detailToMessage$1(detail) {
1234
1234
  }
1235
1235
  if (typeof record.message === "string") return record.message;
1236
1236
  }
1237
- function isAbortLike$1(err) {
1237
+ function isAbortLike$2(err) {
1238
1238
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1239
1239
  }
1240
1240
 
@@ -1417,6 +1417,218 @@ function stringProp$1(description) {
1417
1417
  };
1418
1418
  }
1419
1419
 
1420
+ //#endregion
1421
+ //#region src/lib/fleet/tunnel-auth.ts
1422
+ var TunnelAuthError = class extends Error {
1423
+ code;
1424
+ constructor(code, message) {
1425
+ super(message);
1426
+ this.name = "TunnelAuthError";
1427
+ this.code = code;
1428
+ }
1429
+ };
1430
+ const REFRESH_MARGIN_MS = 5 * 6e4;
1431
+ const MIN_REMINT_INTERVAL_MS = 3e4;
1432
+ const DEVTUNNEL_TIMEOUT_MS = 1e4;
1433
+ const MINT_FAILURE_BACKOFF_MS = 3e4;
1434
+ const MAX_PLAUSIBLE_TTL_MS = 2880 * 6e4;
1435
+ const MAX_STDOUT_BYTES$2 = 256 * 1024;
1436
+ const TUNNEL_ID_RE$1 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1437
+ const JWT_REDACT_RE = /eyJ[A-Za-z0-9._-]{20,}/g;
1438
+ const SCHEME_TOKEN_RE = /(bearer|tunnel) +[!-~]+/gi;
1439
+ /** Strip credential-shaped substrings from any string before it is logged or surfaced. */
1440
+ function redactTunnelSecrets(s) {
1441
+ return s.replace(JWT_REDACT_RE, "<redacted-token>").replace(SCHEME_TOKEN_RE, "$1 <redacted-token>");
1442
+ }
1443
+ function safeRealpath(p) {
1444
+ try {
1445
+ return realpathSync(p);
1446
+ } catch {
1447
+ return nodePath.resolve(p);
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Guard the resolved `devtunnel` path: it must be a trusted ABSOLUTE path that
1452
+ * is not the current working directory's own binary. `resolveExecutable` already
1453
+ * excludes cwd; this is defense-in-depth against a cwd-local / relative
1454
+ * resolution ever reaching a child-process spawn. Both paths are canonicalized
1455
+ * (realpath, resolving `..` and symlinks) before the cwd-containment check, so a
1456
+ * non-canonical path like `/safe/../cwd/devtunnel` or a symlink cannot evade it.
1457
+ * Returns the (original) path to spawn, or throws.
1458
+ */
1459
+ function assertTrustedDevtunnelPath(resolved, cwd = typeof process.cwd === "function" ? nodePath.resolve(process.cwd()) : null) {
1460
+ 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");
1461
+ if (!nodePath.isAbsolute(resolved)) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a non-absolute devtunnel binary");
1462
+ const ext = nodePath.extname(resolved).toLowerCase();
1463
+ 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");
1464
+ const realResolved = safeRealpath(resolved);
1465
+ const realCwd = cwd ? safeRealpath(cwd) : null;
1466
+ if (realCwd && (realResolved === realCwd || realResolved.startsWith(realCwd + nodePath.sep))) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a cwd-local devtunnel binary");
1467
+ return resolved;
1468
+ }
1469
+ /**
1470
+ * The real runner: resolve `devtunnel` to a trusted absolute path (PATH-resolved,
1471
+ * cwd-excluded) and run it with `shell:false` (native binary).
1472
+ */
1473
+ function realDevtunnelRunner() {
1474
+ return async (args) => {
1475
+ const res = await runManagedExeCapture(assertTrustedDevtunnelPath(resolveExecutable("devtunnel")), args, {
1476
+ timeoutMs: DEVTUNNEL_TIMEOUT_MS,
1477
+ maxStdoutBytes: MAX_STDOUT_BYTES$2
1478
+ });
1479
+ return {
1480
+ stdout: res.stdout,
1481
+ stderr: res.stderr,
1482
+ code: res.code,
1483
+ timedOut: res.timedOut
1484
+ };
1485
+ };
1486
+ }
1487
+ function looksLikeJwt(s) {
1488
+ const parts = s.split(".");
1489
+ if (parts.length !== 3) return false;
1490
+ return parts.every((p) => p.length > 0 && /^[A-Za-z0-9_-]+$/.test(p));
1491
+ }
1492
+ /** Recursively collect JWT-shaped strings from arbitrary parsed JSON. */
1493
+ function collectJwts(value, out) {
1494
+ if (typeof value === "string") {
1495
+ if (value.startsWith("eyJ") && looksLikeJwt(value)) out.add(value);
1496
+ return;
1497
+ }
1498
+ if (Array.isArray(value)) {
1499
+ for (const v of value) collectJwts(v, out);
1500
+ return;
1501
+ }
1502
+ if (value && typeof value === "object") for (const v of Object.values(value)) collectJwts(v, out);
1503
+ }
1504
+ /**
1505
+ * Extract the single access token from `devtunnel token --json` output. Prefers
1506
+ * structured JSON; falls back to a token-shaped scan. Refuses to guess when zero
1507
+ * or more-than-one distinct tokens are present (so we never send a wrong JWT).
1508
+ */
1509
+ function extractToken(stdout) {
1510
+ const found = /* @__PURE__ */ new Set();
1511
+ try {
1512
+ collectJwts(JSON.parse(stdout), found);
1513
+ } catch {}
1514
+ if (found.size === 0) {
1515
+ for (const tok of stdout.split(/[^A-Za-z0-9._-]+/)) if (tok.startsWith("eyJ") && looksLikeJwt(tok)) found.add(tok);
1516
+ }
1517
+ if (found.size === 0) throw new TunnelAuthError("PARSE", "no tunnel access token found in devtunnel output");
1518
+ if (found.size > 1) throw new TunnelAuthError("PARSE", "devtunnel output contained more than one token; refusing to guess");
1519
+ return [...found][0];
1520
+ }
1521
+ /** Parse a JWT `exp` claim (seconds) into epoch milliseconds. Throws on a missing/non-numeric exp. */
1522
+ function parseJwtExpMs(jwt) {
1523
+ const parts = jwt.split(".");
1524
+ if (parts.length !== 3) throw new TunnelAuthError("PARSE", "tunnel token is not a JWT");
1525
+ let payload;
1526
+ try {
1527
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1528
+ } catch {
1529
+ throw new TunnelAuthError("PARSE", "tunnel token payload was not decodable");
1530
+ }
1531
+ const exp = payload?.exp;
1532
+ if (typeof exp !== "number" || !Number.isFinite(exp)) throw new TunnelAuthError("PARSE", "tunnel token has no numeric exp claim");
1533
+ return exp * 1e3;
1534
+ }
1535
+ function classifyMintFailure(res) {
1536
+ if (res.timedOut) return new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1537
+ const stderr = (res.stderr || "").toLowerCase();
1538
+ const tail = redactTunnelSecrets((res.stderr || "").trim()).slice(-300);
1539
+ const suffix = tail ? ` [${tail}]` : "";
1540
+ 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}`);
1541
+ 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}`);
1542
+ return new TunnelAuthError("MINT_FAILED", `devtunnel token failed (exit ${res.code})${suffix}`);
1543
+ }
1544
+ /**
1545
+ * Create a per-process token provider: lazy mint, per-tunnel in-memory cache,
1546
+ * single-flight, and short negative backoff on non-timeout failures.
1547
+ */
1548
+ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
1549
+ const cache = /* @__PURE__ */ new Map();
1550
+ const inflight = /* @__PURE__ */ new Map();
1551
+ const backoff = /* @__PURE__ */ new Map();
1552
+ async function mint(cfg) {
1553
+ if (!TUNNEL_ID_RE$1.test(cfg.tunnelId)) throw new TunnelAuthError("MINT_FAILED", "invalid tunnelId; must match a devtunnel tunnel name");
1554
+ const args = [
1555
+ "token",
1556
+ cfg.tunnelId,
1557
+ "--scopes",
1558
+ "connect",
1559
+ "--json"
1560
+ ];
1561
+ let res;
1562
+ try {
1563
+ res = await runner(args);
1564
+ } catch (err) {
1565
+ if (err instanceof TunnelAuthError) throw err;
1566
+ throw new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(err instanceof Error ? err.message : String(err)));
1567
+ }
1568
+ if (res.timedOut) throw new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1569
+ if (res.code !== 0) throw classifyMintFailure(res);
1570
+ const token = extractToken(res.stdout);
1571
+ const expMs = parseJwtExpMs(token);
1572
+ const now = Date.now();
1573
+ if (expMs <= now) throw new TunnelAuthError("PARSE", "devtunnel minted an already-expired token");
1574
+ if (expMs - now > MAX_PLAUSIBLE_TTL_MS) throw new TunnelAuthError("PARSE", "devtunnel token TTL is implausibly long; refusing");
1575
+ const existing = cache.get(cfg.tunnelId);
1576
+ if (!existing || expMs > existing.expMs) cache.set(cfg.tunnelId, {
1577
+ token,
1578
+ expMs,
1579
+ mintedAt: now
1580
+ });
1581
+ return cache.get(cfg.tunnelId).token;
1582
+ }
1583
+ function mintOnce(cfg) {
1584
+ const key = cfg.tunnelId;
1585
+ return (async () => {
1586
+ try {
1587
+ const token = await mint(cfg);
1588
+ backoff.delete(key);
1589
+ return token;
1590
+ } catch (err) {
1591
+ const e = err instanceof TunnelAuthError ? err : new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(String(err)));
1592
+ if (e.code !== "TIMEOUT") backoff.set(key, {
1593
+ until: Date.now() + MINT_FAILURE_BACKOFF_MS,
1594
+ err: e
1595
+ });
1596
+ const c = cache.get(key);
1597
+ if (c && c.expMs > Date.now()) return c.token;
1598
+ throw e;
1599
+ } finally {
1600
+ inflight.delete(key);
1601
+ }
1602
+ })();
1603
+ }
1604
+ return {
1605
+ async getToken(cfg) {
1606
+ const key = cfg.tunnelId;
1607
+ const now = Date.now();
1608
+ const cached$1 = cache.get(key);
1609
+ if (cached$1 && cached$1.expMs > now) {
1610
+ const comfortablyFresh = cached$1.expMs - now > REFRESH_MARGIN_MS;
1611
+ const recentlyMinted = now - cached$1.mintedAt < MIN_REMINT_INTERVAL_MS;
1612
+ if (comfortablyFresh || recentlyMinted) return cached$1.token;
1613
+ }
1614
+ const inf = inflight.get(key);
1615
+ if (inf) return inf;
1616
+ const bo = backoff.get(key);
1617
+ if (bo && now < bo.until) {
1618
+ if (cached$1 && cached$1.expMs > now) return cached$1.token;
1619
+ throw bo.err;
1620
+ }
1621
+ const p = mintOnce(cfg);
1622
+ inflight.set(key, p);
1623
+ return p;
1624
+ },
1625
+ invalidate(cfg) {
1626
+ cache.delete(cfg.tunnelId);
1627
+ backoff.delete(cfg.tunnelId);
1628
+ }
1629
+ };
1630
+ }
1631
+
1420
1632
  //#endregion
1421
1633
  //#region src/lib/fleet/client.ts
1422
1634
  var FleetError = class extends Error {
@@ -1450,12 +1662,21 @@ function decodeSessionId(globalId) {
1450
1662
  }
1451
1663
  var FleetClient = class {
1452
1664
  baseUrl;
1665
+ origin;
1453
1666
  token;
1454
1667
  fetchFn;
1668
+ getTunnelToken;
1669
+ onTunnelAuthInvalidate;
1455
1670
  constructor(options) {
1456
1671
  this.baseUrl = options.url.replace(/\/+$/, "");
1672
+ this.origin = new URL(this.baseUrl).origin;
1457
1673
  this.token = options.token;
1458
1674
  this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
1675
+ this.getTunnelToken = options.getTunnelToken;
1676
+ this.onTunnelAuthInvalidate = options.onTunnelAuthInvalidate;
1677
+ }
1678
+ capabilities(signal) {
1679
+ return this.request("GET", "/api/control/capabilities", void 0, void 0, signal);
1459
1680
  }
1460
1681
  listSessions(signal) {
1461
1682
  return this.request("GET", "/api/control/sessions", void 0, void 0, signal);
@@ -1525,67 +1746,176 @@ var FleetClient = class {
1525
1746
  async request(method, pathname, query, body, signal) {
1526
1747
  const url = new URL(pathname, `${this.baseUrl}/`);
1527
1748
  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);
1749
+ if (url.origin !== this.origin) throw new FleetError({
1750
+ code: "UNREACHABLE",
1751
+ message: "fleet request URL origin did not match the registered instance origin",
1752
+ retryable: false
1753
+ });
1754
+ const devtunnelHost = isDevtunnelHost(url.hostname);
1755
+ const tunnelEligible = this.getTunnelToken !== void 0 && devtunnelHost && url.protocol === "https:";
1756
+ for (let attempt = 0; attempt < 2; attempt++) {
1757
+ let tunnelToken;
1758
+ if (tunnelEligible) try {
1759
+ tunnelToken = await this.getTunnelToken();
1760
+ } catch (err) {
1761
+ throw mapTunnelAuthError(err);
1762
+ }
1763
+ const attachTunnel = tunnelToken !== void 0 && tunnelToken !== "";
1764
+ const canRetry = attachTunnel && !!this.onTunnelAuthInvalidate && attempt === 0;
1765
+ const headers = {
1766
+ Authorization: `Bearer ${this.token}`,
1767
+ ...devtunnelHost ? { "X-Tunnel-Skip-Anti-Phishing-Page": "true" } : {},
1768
+ ...attachTunnel ? { "X-Tunnel-Authorization": `tunnel ${tunnelToken}` } : {},
1769
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
1770
+ };
1771
+ let response;
1772
+ try {
1773
+ response = await this.fetchFn(url.toString(), {
1774
+ method,
1775
+ headers,
1776
+ body: body === void 0 ? void 0 : JSON.stringify(body),
1777
+ redirect: "error",
1778
+ signal
1779
+ });
1780
+ } catch (err) {
1781
+ if (canRetry && method === "GET") {
1782
+ this.onTunnelAuthInvalidate();
1783
+ continue;
1784
+ }
1785
+ throw mapNetworkError(err, devtunnelHost);
1786
+ }
1787
+ if (!response.ok) {
1788
+ if ((response.status === 401 || response.status === 403) && canRetry) {
1789
+ this.onTunnelAuthInvalidate();
1790
+ continue;
1791
+ }
1792
+ throw await mapHttpError(response, url.toString());
1793
+ }
1794
+ return await response.json();
1542
1795
  }
1543
- if (!response.ok) throw await mapHttpError(response);
1544
- return await response.json();
1796
+ throw new FleetError({
1797
+ code: "AUTH_FAILED",
1798
+ message: "fleet instance tunnel authentication failed after re-mint; verify the tunnel and `devtunnel user login`",
1799
+ retryable: false
1800
+ });
1545
1801
  }
1546
1802
  };
1547
- async function mapHttpError(response) {
1803
+ /** Dev Tunnel access tokens are only ever scoped to the `*.devtunnels.ms` service. */
1804
+ function isDevtunnelHost(hostname) {
1805
+ return hostname === "devtunnels.ms" || hostname.endsWith(".devtunnels.ms");
1806
+ }
1807
+ function mapTunnelAuthError(err) {
1808
+ if (err instanceof TunnelAuthError) return new FleetError({
1809
+ code: "AUTH_FAILED",
1810
+ message: err.message,
1811
+ retryable: err.code === "TIMEOUT",
1812
+ detail: { tunnelAuth: err.code }
1813
+ });
1814
+ return mapNetworkError(err);
1815
+ }
1816
+ async function mapHttpError(response, requestUrl) {
1548
1817
  const detail = await readErrorDetail(response);
1549
1818
  const upstreamMessage = detailToMessage(detail);
1550
1819
  const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
1551
- if (response.status === 401 || response.status === 403) return new FleetError({
1820
+ const status = response.status;
1821
+ if (isDevTunnelHost(requestUrl) && detectDevTunnelNoHost(status, detail)) return new FleetError({
1822
+ code: "NO_HOST",
1823
+ message: `dev tunnel relay reports no host connected (${status})${suffix}`,
1824
+ retryable: true,
1825
+ status,
1826
+ detail
1827
+ });
1828
+ if (status === 401 || status === 403) return new FleetError({
1552
1829
  code: "AUTH_FAILED",
1553
- message: `fleet instance authentication failed (${response.status})${suffix}`,
1830
+ message: `fleet instance authentication failed (${status})${suffix}`,
1554
1831
  retryable: false,
1555
- status: response.status,
1832
+ status,
1556
1833
  detail
1557
1834
  });
1558
- if (response.status === 404) return new FleetError({
1835
+ if (status === 404) return new FleetError({
1559
1836
  code: "SESSION_NOT_FOUND",
1560
1837
  message: `fleet session or resource not found (404)${suffix}`,
1561
1838
  retryable: false,
1562
- status: response.status,
1839
+ status,
1563
1840
  detail
1564
1841
  });
1565
- if (response.status === 409 || response.status === 412) return new FleetError({
1842
+ if (status === 409 || status === 412) return new FleetError({
1566
1843
  code: "PRECONDITION_FAILED",
1567
- message: `fleet instance precondition failed (${response.status})${suffix}`,
1844
+ message: `fleet instance precondition failed (${status})${suffix}`,
1568
1845
  retryable: false,
1569
- status: response.status,
1846
+ status,
1570
1847
  detail
1571
1848
  });
1572
- if (response.status === 408 || response.status === 504) return new FleetError({
1849
+ if (status === 400) return new FleetError({
1850
+ code: "BAD_REQUEST",
1851
+ message: `fleet instance rejected the request (400)${suffix}`,
1852
+ retryable: false,
1853
+ status,
1854
+ detail
1855
+ });
1856
+ if (status === 408 || status === 504) return new FleetError({
1573
1857
  code: "TIMEOUT",
1574
- message: `fleet instance request timed out (${response.status})${suffix}`,
1858
+ message: `fleet instance request timed out (${status})${suffix}`,
1575
1859
  retryable: true,
1576
- status: response.status,
1860
+ status,
1861
+ detail
1862
+ });
1863
+ if ((status === 502 || status === 503) && isDevTunnelHost(requestUrl)) return new FleetError({
1864
+ code: "RELAY_ERROR",
1865
+ message: `dev tunnel relay returned HTTP ${status} (host may be down, restarting, or under load)${suffix}`,
1866
+ retryable: true,
1867
+ status,
1868
+ detail
1869
+ });
1870
+ if (status === 429) return new FleetError({
1871
+ code: "RATE_LIMITED",
1872
+ message: `fleet instance rate-limited the request (429)${suffix}`,
1873
+ retryable: true,
1874
+ status,
1577
1875
  detail
1578
1876
  });
1579
1877
  return new FleetError({
1580
1878
  code: "UPSTREAM_ERROR",
1581
- message: `fleet instance returned HTTP ${response.status}${suffix}`,
1582
- retryable: response.status === 429 || response.status >= 500,
1583
- status: response.status,
1879
+ message: `fleet instance returned HTTP ${status}${suffix}`,
1880
+ retryable: status >= 500,
1881
+ status,
1584
1882
  detail
1585
1883
  });
1586
1884
  }
1587
- function mapNetworkError(err) {
1588
- if (isAbortLike(err)) return new FleetError({
1885
+ const DEVTUNNEL_HOST_RE$1 = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
1886
+ /** F4: only Dev Tunnel relay hosts may be classified NO_HOST / RELAY_ERROR. */
1887
+ function isDevTunnelHost(requestUrl) {
1888
+ try {
1889
+ return DEVTUNNEL_HOST_RE$1.test(new URL(requestUrl).hostname);
1890
+ } catch {
1891
+ return false;
1892
+ }
1893
+ }
1894
+ const DEVTUNNEL_NO_HOST_SIGNALS = [
1895
+ "no host is currently connected",
1896
+ "tunnel is not currently hosted",
1897
+ "host is not accepting connections",
1898
+ "tunnel host is not connected",
1899
+ "no connection to the host",
1900
+ "tunnelporthostnotconnected"
1901
+ ];
1902
+ function detectDevTunnelNoHost(status, detail) {
1903
+ if (status !== 502 && status !== 503 && status !== 404) return false;
1904
+ const haystack = detailToSearchString(detail).toLowerCase();
1905
+ if (haystack === "") return false;
1906
+ return DEVTUNNEL_NO_HOST_SIGNALS.some((signal) => haystack.includes(signal));
1907
+ }
1908
+ function detailToSearchString(detail) {
1909
+ if (detail === void 0 || detail === null) return "";
1910
+ if (typeof detail === "string") return detail;
1911
+ try {
1912
+ return JSON.stringify(detail);
1913
+ } catch {
1914
+ return String(detail);
1915
+ }
1916
+ }
1917
+ function mapNetworkError(err, devtunnelHost = false) {
1918
+ if (isAbortLike$1(err)) return new FleetError({
1589
1919
  code: "TIMEOUT",
1590
1920
  message: "fleet instance request timed out or was aborted",
1591
1921
  retryable: true,
@@ -1593,7 +1923,7 @@ function mapNetworkError(err) {
1593
1923
  });
1594
1924
  return new FleetError({
1595
1925
  code: "UNREACHABLE",
1596
- message: `fleet instance unreachable: ${err instanceof Error ? err.message : String(err)}`,
1926
+ 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
1927
  retryable: true,
1598
1928
  detail: err
1599
1929
  });
@@ -1620,7 +1950,7 @@ function detailToMessage(detail) {
1620
1950
  }
1621
1951
  if (typeof record.message === "string") return record.message;
1622
1952
  }
1623
- function isAbortLike(err) {
1953
+ function isAbortLike$1(err) {
1624
1954
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1625
1955
  }
1626
1956
 
@@ -1645,7 +1975,7 @@ async function loadFleetRegistryConfig(configPath = defaultFleetConfigPath()) {
1645
1975
  if (isNodeErrorCode(err, "ENOENT")) return { instances: [] };
1646
1976
  throw err;
1647
1977
  }
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.`);
1978
+ 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
1979
  const raw = await fs.readFile(configPath, "utf8");
1650
1980
  if (raw.trim() === "") return { instances: [] };
1651
1981
  const parsed = JSON.parse(raw);
@@ -1722,15 +2052,36 @@ function parseInstance(raw) {
1722
2052
  throw invalidInstanceUrlError(id);
1723
2053
  }
1724
2054
  if (!isAllowedInstanceUrl(parsedUrl)) throw invalidInstanceUrlError(id);
2055
+ assertDevTunnelUrlShape(id, parsedUrl);
2056
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} url must not contain embedded credentials (userinfo)`);
1725
2057
  if (typeof token !== "string" || token === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} token must be a non-empty string`);
2058
+ const tunnelId = parseTunnelId(id, instance.tunnelId);
2059
+ const tunnelToken = parseTunnelToken(id, instance.tunnelToken);
1726
2060
  return {
1727
2061
  id: id.trim(),
1728
2062
  label: label.trim(),
1729
2063
  url: trimmedUrl,
1730
2064
  token,
1731
2065
  default: instance.default === true ? true : void 0,
1732
- allowExec: instance.allowExec === true ? true : void 0
1733
- };
2066
+ allowExec: instance.allowExec === true ? true : void 0,
2067
+ tunnelId,
2068
+ tunnelToken
2069
+ };
2070
+ }
2071
+ const TUNNEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2072
+ function parseTunnelId(id, raw) {
2073
+ if (raw === void 0) return void 0;
2074
+ 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\`)`);
2075
+ return raw.trim();
2076
+ }
2077
+ function parseTunnelToken(id, raw) {
2078
+ if (raw === void 0) return void 0;
2079
+ if (typeof raw !== "string") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a string`);
2080
+ let t = raw.trim();
2081
+ if (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'")) t = t.slice(1, -1).trim();
2082
+ t = t.replace(/^X-Tunnel-Authorization:\s*/i, "").replace(/^tunnel\s+/i, "").trim();
2083
+ if (t === "" || /\s/.test(t)) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a non-empty single-line token`);
2084
+ return t;
1734
2085
  }
1735
2086
  function invalidInstanceUrlError(id) {
1736
2087
  return new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url must be https (or http://localhost for local testing)`);
@@ -1740,13 +2091,25 @@ function isAllowedInstanceUrl(url) {
1740
2091
  if (url.protocol !== "http:") return false;
1741
2092
  return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
1742
2093
  }
2094
+ const DEVTUNNEL_HOST_RE = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
2095
+ function assertDevTunnelUrlShape(id, url) {
2096
+ if (!DEVTUNNEL_HOST_RE.test(url.hostname)) return;
2097
+ if (url.port === "") return;
2098
+ const firstDot = url.hostname.indexOf(".");
2099
+ const firstLabel = firstDot < 0 ? url.hostname : url.hostname.slice(0, firstDot);
2100
+ const rest = firstDot < 0 ? "" : url.hostname.slice(firstDot + 1);
2101
+ const corrected = rest === "" ? `https://${firstLabel}-${url.port}.devtunnels.ms` : `https://${firstLabel}-${url.port}.${rest}`;
2102
+ 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).`);
2103
+ }
1743
2104
  function resolvedInstance(instance) {
1744
2105
  return {
1745
2106
  id: instance.id,
1746
2107
  label: instance.label,
1747
2108
  url: instance.url,
1748
2109
  token: instance.token,
1749
- allowExec: instance.allowExec
2110
+ allowExec: instance.allowExec,
2111
+ tunnelId: instance.tunnelId,
2112
+ tunnelToken: instance.tunnelToken
1750
2113
  };
1751
2114
  }
1752
2115
  function isObject(value) {
@@ -1761,6 +2124,15 @@ function isNodeErrorCode(err, code) {
1761
2124
  const FLEET_GROUP = "fleet";
1762
2125
  const INSTANCE_PROBE_TIMEOUT_MS = 2e3;
1763
2126
  const INSTANCE_PROBE_CACHE_TTL_MS = 5e3;
2127
+ const CAPABILITIES_CACHE_TTL_MS = 6e4;
2128
+ const AWAIT_TURN_DEFAULT_TIMEOUT_MS = 3e4;
2129
+ const AWAIT_TURN_TIMEOUT_SLACK_MS = 5e3;
2130
+ const LIST_INSTANCES_FANOUT_CONCURRENCY = 16;
2131
+ const AWAIT_TURN_FANOUT_CONCURRENCY = 256;
2132
+ const INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES = 1;
2133
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS = 250;
2134
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS = 1e3;
2135
+ const FLEET_FANOUT_CONCURRENCY_ENV = "GH_ROUTER_FLEET_FANOUT_CONCURRENCY";
1764
2136
  var FleetToolInputError = class extends Error {
1765
2137
  code;
1766
2138
  constructor(code, message) {
@@ -1770,28 +2142,58 @@ var FleetToolInputError = class extends Error {
1770
2142
  }
1771
2143
  };
1772
2144
  let defaultRegistry;
2145
+ let defaultTunnelProvider;
1773
2146
  const awaitTurnCursors = /* @__PURE__ */ new Map();
1774
2147
  const instanceProbeCache = /* @__PURE__ */ new Map();
1775
2148
  function createFleetTools(options = {}) {
1776
2149
  const registry = options.registry;
1777
2150
  const clients = /* @__PURE__ */ new Map();
2151
+ const capabilitiesCache = /* @__PURE__ */ new Map();
2152
+ const tunnelProvider = options.tunnelTokenProvider ?? (defaultTunnelProvider ??= createTunnelTokenProvider());
2153
+ const probeRetryDelay = options.probeRetryDelay ?? delay;
2154
+ const awaitTurnDeadlineSlackMs = nonNegativeNumberOrDefault(options.awaitTurnDeadlineSlackMs, AWAIT_TURN_TIMEOUT_SLACK_MS);
1778
2155
  function getRegistry() {
1779
2156
  if (registry) return registry;
1780
2157
  defaultRegistry ??= new FleetRegistry();
1781
2158
  return defaultRegistry;
1782
2159
  }
1783
2160
  function clientFor(instance) {
1784
- const key = `${instance.id}\0${instance.url}\0${instance.token}`;
2161
+ const key = `${instance.id}\0${instance.url}\0${instance.token}\0${instance.tunnelId ?? ""}\0${instance.tunnelToken ?? ""}`;
1785
2162
  const existing = clients.get(key);
1786
2163
  if (existing) return existing;
1787
2164
  const created = options.createClient ? options.createClient(instance) : new FleetClient({
1788
2165
  url: instance.url,
1789
2166
  token: instance.token,
1790
- fetchFn: options.fetchFn
2167
+ fetchFn: options.fetchFn,
2168
+ ...tunnelClientOptions(instance, tunnelProvider)
1791
2169
  });
1792
2170
  clients.set(key, created);
1793
2171
  return created;
1794
2172
  }
2173
+ async function getInstanceCapabilities(instance, signal) {
2174
+ const now = Date.now();
2175
+ const cached$1 = capabilitiesCache.get(instance.id);
2176
+ if (cached$1 && now - cached$1.at < CAPABILITIES_CACHE_TTL_MS) return cached$1.caps;
2177
+ try {
2178
+ const response = await clientFor(instance).capabilities(signal);
2179
+ const caps = new Set(response.capabilities);
2180
+ capabilitiesCache.set(instance.id, {
2181
+ caps,
2182
+ at: Date.now()
2183
+ });
2184
+ return caps;
2185
+ } catch {
2186
+ capabilitiesCache.set(instance.id, {
2187
+ caps: null,
2188
+ at: Date.now()
2189
+ });
2190
+ return null;
2191
+ }
2192
+ }
2193
+ async function assertCapability(instance, cap, featureName, signal) {
2194
+ const caps = await getInstanceCapabilities(instance, signal);
2195
+ 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`);
2196
+ }
1795
2197
  async function resolve(arg) {
1796
2198
  return getRegistry().resolveInstance(arg);
1797
2199
  }
@@ -1815,37 +2217,46 @@ function createFleetTools(options = {}) {
1815
2217
  const now = Date.now();
1816
2218
  const cached$1 = instanceProbeCache.get(cacheKey);
1817
2219
  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();
2220
+ for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
2221
+ const timeout = createProbeTimeout();
2222
+ try {
2223
+ const response = await clientFor(await resolve(info.id)).listSessions(timeout.signal);
2224
+ const lastSeen = Date.now();
2225
+ const result$1 = {
2226
+ id: info.id,
2227
+ label: info.label,
2228
+ reachable: true,
2229
+ sessionCount: response.sessions.length,
2230
+ lastSeen
2231
+ };
2232
+ instanceProbeCache.set(cacheKey, {
2233
+ result: result$1,
2234
+ at: lastSeen
2235
+ });
2236
+ return result$1;
2237
+ } catch (err) {
2238
+ const code = fleetProbeErrorCode(err);
2239
+ if (code === "RATE_LIMITED" && attempt < INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES) {
2240
+ timeout.cleanup();
2241
+ await probeRetryDelay(probeRateLimitBackoffMs(attempt));
2242
+ continue;
2243
+ }
2244
+ const result$1 = failedProbeResult(info, code);
2245
+ instanceProbeCache.set(cacheKey, {
2246
+ result: result$1,
2247
+ at: Date.now()
2248
+ });
2249
+ return result$1;
2250
+ } finally {
2251
+ timeout.cleanup();
2252
+ }
1848
2253
  }
2254
+ const result = failedProbeResult(info, "UNREACHABLE");
2255
+ instanceProbeCache.set(cacheKey, {
2256
+ result,
2257
+ at: Date.now()
2258
+ });
2259
+ return result;
1849
2260
  }
1850
2261
  function tool$1(toolNameHttp, description, inputSchema, handler) {
1851
2262
  return {
@@ -1865,8 +2276,7 @@ function createFleetTools(options = {}) {
1865
2276
  }
1866
2277
  return Object.freeze([
1867
2278
  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))) });
2279
+ return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
1870
2280
  }),
1871
2281
  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
2282
  const instance = await resolve(optionalString(args, "instance"));
@@ -1903,12 +2313,12 @@ function createFleetTools(options = {}) {
1903
2313
  sessionId: globalId
1904
2314
  });
1905
2315
  }),
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({
2316
+ 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
2317
  sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
1908
2318
  instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
1909
2319
  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.")
2320
+ idempotencyKey: stringProp("Caller-generated idempotency key. Reuse the same key on retry; the upstream dedupes so a retry never re-types."),
2321
+ 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
2322
  }, [
1913
2323
  "sessionId",
1914
2324
  "message",
@@ -1921,14 +2331,21 @@ function createFleetTools(options = {}) {
1921
2331
  idempotencyKey: requiredString(args, "idempotencyKey"),
1922
2332
  ...awaitMs === void 0 ? {} : { awaitMs }
1923
2333
  }, signal);
1924
- const delivered = response.delivered !== false;
1925
- const confirmed = response.confirmed !== false;
1926
- const isError = !delivered || awaitMs !== void 0 && awaitMs > 0 && !confirmed;
2334
+ const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2335
+ const confirmed = delivered && response.confirmed === true;
2336
+ const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2337
+ const isError = !delivered;
1927
2338
  return jsonResult({
1928
2339
  resolvedInstance: publicInstance(instance),
1929
2340
  sessionId: globalId,
1930
2341
  ...response,
1931
- ...isError ? { message: !delivered ? "message was not delivered by the upstream instance" : `message delivery was not confirmed within awaitMs=${awaitMs}` } : {}
2342
+ delivered,
2343
+ confirmed,
2344
+ ...confirmationTimedOut ? {
2345
+ confirmationPending: true,
2346
+ confirmationTimedOut: true
2347
+ } : {},
2348
+ ...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
2349
  }, isError);
1933
2350
  }),
1934
2351
  tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
@@ -1983,19 +2400,30 @@ function createFleetTools(options = {}) {
1983
2400
  name: stringProp("Optional display name for the session."),
1984
2401
  workingDir: stringProp("Optional working directory on the remote instance."),
1985
2402
  idempotencyKey: stringProp("Caller-generated idempotency key."),
1986
- start: booleanProp("Whether the remote instance should start the session immediately.")
2403
+ start: booleanProp("Whether the remote instance should start the session immediately."),
2404
+ readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2405
+ 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."),
2406
+ 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
2407
  }, [
1988
2408
  "instance",
1989
2409
  "agent",
1990
2410
  "idempotencyKey"
1991
2411
  ]), async (args, signal) => {
1992
2412
  const instance = await resolve(requiredString(args, "instance"));
2413
+ const agent = requiredString(args, "agent");
1993
2414
  const idempotencyKey = requiredString(args, "idempotencyKey");
2415
+ const permissionMode = optionalString(args, "permissionMode");
2416
+ const agentArgs = optionalStringArray(args, "agentArgs");
2417
+ if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2418
+ if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
1994
2419
  const response = await clientFor(instance).createSession(definedObject({
1995
- agent: requiredString(args, "agent"),
2420
+ agent,
1996
2421
  name: optionalString(args, "name"),
1997
2422
  workingDir: optionalString(args, "workingDir"),
1998
2423
  start: optionalBoolean(args, "start"),
2424
+ readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
2425
+ permissionMode,
2426
+ agentArgs,
1999
2427
  idempotencyKey
2000
2428
  }), signal);
2001
2429
  const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
@@ -2023,30 +2451,52 @@ function createFleetTools(options = {}) {
2023
2451
  ...response
2024
2452
  });
2025
2453
  }),
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({
2454
+ 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
2455
  instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
2028
2456
  sessionIds: arrayProp("Global session ids to filter to."),
2029
2457
  timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
2030
- kinds: arrayProp("Optional event kinds to filter to.")
2458
+ kinds: arrayProp("Optional event kinds to filter to."),
2459
+ 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
2460
  }, []), async (args, signal) => {
2032
2461
  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);
2462
+ const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
2036
2463
  const timeoutMs = optionalNumber(args, "timeoutMs");
2037
2464
  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
- };
2465
+ const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
2466
+ const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
2467
+ const combined = combineAbortSignals([signal, deadline.signal]);
2468
+ try {
2469
+ const response = await clientFor(instance).waitEvents(definedObject({
2470
+ cursor: cursorByInstance.get(instance.id),
2471
+ timeoutMs,
2472
+ sessionIds: target.localSessionIdsByInstance.get(instance.id),
2473
+ kinds
2474
+ }), combined.signal);
2475
+ cursorByInstance.set(instance.id, response.cursor);
2476
+ return {
2477
+ ok: true,
2478
+ instance,
2479
+ response
2480
+ };
2481
+ } catch (err) {
2482
+ const error = fleetProbeErrorCode(err);
2483
+ const hint = fleetProbeHint(error);
2484
+ return {
2485
+ ok: false,
2486
+ instance,
2487
+ error,
2488
+ ...hint ? { hint } : {}
2489
+ };
2490
+ } finally {
2491
+ combined.cleanup();
2492
+ deadline.cleanup();
2493
+ }
2494
+ });
2495
+ const responses = results.filter(isAwaitTurnSuccess);
2496
+ const errors = results.filter(isAwaitTurnFailure).map(({ instance, error, hint }) => ({
2497
+ instance: publicInstance(instance),
2498
+ error,
2499
+ ...hint ? { hint } : {}
2050
2500
  }));
2051
2501
  const events$1 = responses.flatMap(({ instance, response }) => response.events.map((event) => stampEvent(instance, event))).sort(compareStampedEvents);
2052
2502
  const gaps = responses.flatMap(({ instance, response }) => response.gaps.map((gap) => ({
@@ -2059,9 +2509,10 @@ function createFleetTools(options = {}) {
2059
2509
  gaps,
2060
2510
  cursors: responses.map(({ instance, response }) => ({
2061
2511
  instance: publicInstance(instance),
2062
- ...parseCursor(response.cursor)
2512
+ cursor: response.cursor
2063
2513
  })),
2064
- more: responses.some(({ response }) => response.more)
2514
+ more: responses.some(({ response }) => response.more),
2515
+ ...errors.length > 0 ? { errors } : {}
2065
2516
  });
2066
2517
  }),
2067
2518
  tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
@@ -2131,13 +2582,81 @@ function createProbeTimeout() {
2131
2582
  cleanup: () => clearTimeout(timer)
2132
2583
  };
2133
2584
  }
2585
+ function createAwaitTurnDeadline(timeoutMs, slackMs) {
2586
+ const deadlineMs = Math.max(0, timeoutMs ?? AWAIT_TURN_DEFAULT_TIMEOUT_MS) + slackMs;
2587
+ const controller = new AbortController();
2588
+ const timer = setTimeout(() => {
2589
+ const err = /* @__PURE__ */ new Error("await_turn per-instance deadline exceeded");
2590
+ err.name = "TimeoutError";
2591
+ controller.abort(err);
2592
+ }, deadlineMs);
2593
+ return {
2594
+ signal: controller.signal,
2595
+ cleanup: () => clearTimeout(timer)
2596
+ };
2597
+ }
2598
+ function combineAbortSignals(signals) {
2599
+ const noop = () => {};
2600
+ const present = signals.filter((signal) => signal !== void 0);
2601
+ if (present.length === 0) return {
2602
+ signal: void 0,
2603
+ cleanup: noop
2604
+ };
2605
+ if (present.length === 1) return {
2606
+ signal: present[0],
2607
+ cleanup: noop
2608
+ };
2609
+ const any = AbortSignal.any;
2610
+ if (typeof any === "function") return {
2611
+ signal: any(present),
2612
+ cleanup: noop
2613
+ };
2614
+ const controller = new AbortController();
2615
+ const listeners = [];
2616
+ const cleanup = () => {
2617
+ for (const { signal, handler } of listeners) signal.removeEventListener("abort", handler);
2618
+ listeners.length = 0;
2619
+ };
2620
+ for (const signal of present) {
2621
+ if (signal.aborted) {
2622
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2623
+ cleanup();
2624
+ return {
2625
+ signal: controller.signal,
2626
+ cleanup: noop
2627
+ };
2628
+ }
2629
+ const handler = () => {
2630
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2631
+ };
2632
+ signal.addEventListener("abort", handler, { once: true });
2633
+ listeners.push({
2634
+ signal,
2635
+ handler
2636
+ });
2637
+ }
2638
+ return {
2639
+ signal: controller.signal,
2640
+ cleanup
2641
+ };
2642
+ }
2134
2643
  function fleetProbeErrorCode(err) {
2135
2644
  if (typeof err === "object" && err !== null && "code" in err) {
2136
2645
  const code = err.code;
2137
2646
  if (typeof code === "string" && isFleetErrorCode(code)) return code;
2138
2647
  }
2648
+ if (isAbortLike(err)) return "TIMEOUT";
2139
2649
  return "UNREACHABLE";
2140
2650
  }
2651
+ function fleetProbeHint(code) {
2652
+ switch (code) {
2653
+ case "NO_HOST": return "tunnel relay up, no ai-or-die host connected (start the host on that machine)";
2654
+ case "RELAY_ERROR": return "tunnel relay returned an error; the host may be down, restarting, or under load";
2655
+ case "TIMEOUT": return "no response before the probe deadline; the host may be slow or the tunnel may have no host";
2656
+ case "UNREACHABLE": return "could not connect (DNS or connection failure); check the instance url";
2657
+ default: return;
2658
+ }
2659
+ }
2141
2660
  function isFleetErrorCode(code) {
2142
2661
  switch (code) {
2143
2662
  case "UNREACHABLE":
@@ -2145,7 +2664,11 @@ function isFleetErrorCode(code) {
2145
2664
  case "SESSION_NOT_FOUND":
2146
2665
  case "PRECONDITION_FAILED":
2147
2666
  case "TIMEOUT":
2148
- case "UPSTREAM_ERROR": return true;
2667
+ case "UPSTREAM_ERROR":
2668
+ case "NO_HOST":
2669
+ case "RELAY_ERROR":
2670
+ case "BAD_REQUEST":
2671
+ case "RATE_LIMITED": return true;
2149
2672
  default: return false;
2150
2673
  }
2151
2674
  }
@@ -2188,22 +2711,92 @@ function stampEvent(instance, event) {
2188
2711
  ...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
2189
2712
  };
2190
2713
  }
2714
+ function eventAtMs(value) {
2715
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2716
+ if (typeof value === "string") {
2717
+ const parsed = Date.parse(value);
2718
+ if (!Number.isNaN(parsed)) return parsed;
2719
+ }
2720
+ return 0;
2721
+ }
2191
2722
  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;
2723
+ const atA = eventAtMs(a.at);
2724
+ const atB = eventAtMs(b.at);
2725
+ if (atA !== atB) return atA - atB;
2195
2726
  return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
2196
2727
  }
2197
- function parseCursor(cursor) {
2198
- const idx = cursor.indexOf(":");
2199
- if (idx < 0) return { cursor };
2200
- const seq = Number(cursor.slice(idx + 1));
2728
+ const MAX_WATCHER_ID_LEN = 200;
2729
+ const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
2730
+ function awaitTurnCursorKey(watcherId) {
2731
+ const id = watcherId ?? "default";
2732
+ return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
2733
+ }
2734
+ function takeAwaitTurnCursorMap(clientKey) {
2735
+ const existing = awaitTurnCursors.get(clientKey);
2736
+ if (existing) {
2737
+ awaitTurnCursors.delete(clientKey);
2738
+ awaitTurnCursors.set(clientKey, existing);
2739
+ return existing;
2740
+ }
2741
+ const created = /* @__PURE__ */ new Map();
2742
+ awaitTurnCursors.set(clientKey, created);
2743
+ while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
2744
+ const oldest = awaitTurnCursors.keys().next().value;
2745
+ if (oldest === void 0) break;
2746
+ awaitTurnCursors.delete(oldest);
2747
+ }
2748
+ return created;
2749
+ }
2750
+ function isAwaitTurnSuccess(result) {
2751
+ return result.ok;
2752
+ }
2753
+ function isAwaitTurnFailure(result) {
2754
+ return !result.ok;
2755
+ }
2756
+ function failedProbeResult(info, code) {
2757
+ const hint = fleetProbeHint(code);
2201
2758
  return {
2202
- cursor,
2203
- epoch: cursor.slice(0, idx),
2204
- ...Number.isFinite(seq) ? { seq } : {}
2759
+ id: info.id,
2760
+ label: info.label,
2761
+ reachable: false,
2762
+ error: code,
2763
+ ...hint ? { hint } : {}
2205
2764
  };
2206
2765
  }
2766
+ async function mapWithConcurrency(items, limit, fn) {
2767
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
2768
+ const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
2769
+ const results = new Array(items.length);
2770
+ let nextIndex = 0;
2771
+ async function worker() {
2772
+ while (nextIndex < items.length) {
2773
+ const index = nextIndex++;
2774
+ results[index] = await fn(items[index], index);
2775
+ }
2776
+ }
2777
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
2778
+ return results;
2779
+ }
2780
+ function fleetFanoutConcurrency(defaultLimit) {
2781
+ const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
2782
+ const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
2783
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
2784
+ return defaultLimit;
2785
+ }
2786
+ function probeRateLimitBackoffMs(attempt) {
2787
+ return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
2788
+ }
2789
+ function nonNegativeNumberOrDefault(value, fallback) {
2790
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
2791
+ }
2792
+ async function delay(ms) {
2793
+ if (ms <= 0) return;
2794
+ await new Promise((resolve) => setTimeout(resolve, ms));
2795
+ }
2796
+ function isAbortLike(err) {
2797
+ if (!(err instanceof Error)) return false;
2798
+ return err.name === "AbortError" || err.name === "TimeoutError";
2799
+ }
2207
2800
  function uniqueInstances(instances) {
2208
2801
  const seen = /* @__PURE__ */ new Set();
2209
2802
  const result = [];
@@ -2220,6 +2813,26 @@ function publicInstance(instance) {
2220
2813
  label: instance.label
2221
2814
  };
2222
2815
  }
2816
+ /**
2817
+ * Build the FleetClient tunnel-auth options for a resolved instance.
2818
+ * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
2819
+ * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
2820
+ * retry, since it cannot be re-minted); else no tunnel auth.
2821
+ */
2822
+ function tunnelClientOptions(instance, provider) {
2823
+ if (instance.tunnelId) {
2824
+ const cfg = { tunnelId: instance.tunnelId };
2825
+ return {
2826
+ getTunnelToken: () => provider.getToken(cfg),
2827
+ onTunnelAuthInvalidate: () => provider.invalidate(cfg)
2828
+ };
2829
+ }
2830
+ if (instance.tunnelToken) {
2831
+ const token = instance.tunnelToken;
2832
+ return { getTunnelToken: async () => token };
2833
+ }
2834
+ return {};
2835
+ }
2223
2836
  function ok(value) {
2224
2837
  return jsonResult(value, false);
2225
2838
  }
@@ -22007,4 +22620,4 @@ async function runStandInToolCall(args, signal) {
22007
22620
 
22008
22621
  //#endregion
22009
22622
  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
22623
+ //# sourceMappingURL=peer-mcp-personas-Be4SAgm0.js.map