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