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.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{engine-sii7Kmk6.js → engine-DzgC_a0X.js} +1 -1
- package/dist/main.js +5 -4
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-B6z15bmc.js → peer-mcp-personas-Be4SAgm0.js} +734 -121
- package/dist/peer-mcp-personas-Be4SAgm0.js.map +1 -0
- package/package.json +1 -1
- package/dist/peer-mcp-personas-B6z15bmc.js.map +0 -1
|
@@ -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$
|
|
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$
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
})
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (${
|
|
1830
|
+
message: `fleet instance authentication failed (${status})${suffix}`,
|
|
1554
1831
|
retryable: false,
|
|
1555
|
-
status
|
|
1832
|
+
status,
|
|
1556
1833
|
detail
|
|
1557
1834
|
});
|
|
1558
|
-
if (
|
|
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
|
|
1839
|
+
status,
|
|
1563
1840
|
detail
|
|
1564
1841
|
});
|
|
1565
|
-
if (
|
|
1842
|
+
if (status === 409 || status === 412) return new FleetError({
|
|
1566
1843
|
code: "PRECONDITION_FAILED",
|
|
1567
|
-
message: `fleet instance precondition failed (${
|
|
1844
|
+
message: `fleet instance precondition failed (${status})${suffix}`,
|
|
1568
1845
|
retryable: false,
|
|
1569
|
-
status
|
|
1846
|
+
status,
|
|
1570
1847
|
detail
|
|
1571
1848
|
});
|
|
1572
|
-
if (
|
|
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 (${
|
|
1858
|
+
message: `fleet instance request timed out (${status})${suffix}`,
|
|
1575
1859
|
retryable: true,
|
|
1576
|
-
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 ${
|
|
1582
|
-
retryable:
|
|
1583
|
-
status
|
|
1879
|
+
message: `fleet instance returned HTTP ${status}${suffix}`,
|
|
1880
|
+
retryable: status >= 500,
|
|
1881
|
+
status,
|
|
1584
1882
|
detail
|
|
1585
1883
|
});
|
|
1586
1884
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
result,
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
1925
|
-
const confirmed = response.confirmed
|
|
1926
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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
|
-
|
|
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":
|
|
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 =
|
|
2193
|
-
const atB =
|
|
2194
|
-
if (atA !== atB) return atA
|
|
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
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
const
|
|
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
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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-
|
|
22623
|
+
//# sourceMappingURL=peer-mcp-personas-Be4SAgm0.js.map
|