github-router 0.3.121 → 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-CMdE-QiX.js → engine-DzgC_a0X.js} +1 -1
- package/dist/main.js +15 -5
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-CoeEliOe.js → peer-mcp-personas-Be4SAgm0.js} +1995 -80
- package/dist/peer-mcp-personas-Be4SAgm0.js.map +1 -0
- package/package.json +1 -1
- package/dist/peer-mcp-personas-CoeEliOe.js.map +0 -1
|
@@ -40,6 +40,7 @@ const state = {
|
|
|
40
40
|
showToken: false,
|
|
41
41
|
extendedBetas: false,
|
|
42
42
|
browseEnabled: false,
|
|
43
|
+
fleetEnabled: false,
|
|
43
44
|
powerBrowseEnabled: false,
|
|
44
45
|
humanlikeForce: "auto",
|
|
45
46
|
sessionId: randomUUID(),
|
|
@@ -306,12 +307,12 @@ async function fetchWithTransientRetry(doFetch, opts = {}) {
|
|
|
306
307
|
await res.body.cancel();
|
|
307
308
|
} catch {}
|
|
308
309
|
const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
309
|
-
const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
|
|
310
|
+
const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
|
|
310
311
|
if (label) {
|
|
311
312
|
const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
|
|
312
|
-
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`);
|
|
313
314
|
}
|
|
314
|
-
await abortableSleep(delay, signal);
|
|
315
|
+
await abortableSleep(delay$1, signal);
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
/** Extract an HTTP status from a thrown error (HTTPError carries
|
|
@@ -352,9 +353,9 @@ async function withTransientRetry(fn, opts = {}) {
|
|
|
352
353
|
if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
|
|
353
354
|
const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
|
|
354
355
|
const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
355
|
-
const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
|
|
356
|
-
if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay}ms`);
|
|
357
|
-
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);
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
}
|
|
@@ -1045,6 +1046,1886 @@ function collapsePathKeys(env) {
|
|
|
1045
1046
|
return env;
|
|
1046
1047
|
}
|
|
1047
1048
|
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/lib/artifact/client.ts
|
|
1051
|
+
var ArtifactError = class extends Error {
|
|
1052
|
+
code;
|
|
1053
|
+
retryable;
|
|
1054
|
+
status;
|
|
1055
|
+
detail;
|
|
1056
|
+
constructor(args) {
|
|
1057
|
+
super(args.message);
|
|
1058
|
+
this.name = "ArtifactError";
|
|
1059
|
+
this.code = args.code;
|
|
1060
|
+
this.retryable = args.retryable;
|
|
1061
|
+
this.status = args.status;
|
|
1062
|
+
this.detail = args.detail;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
var ArtifactClient = class {
|
|
1066
|
+
baseUrl;
|
|
1067
|
+
token;
|
|
1068
|
+
sessionId;
|
|
1069
|
+
fetchFn;
|
|
1070
|
+
constructor(options) {
|
|
1071
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1072
|
+
this.token = options.token;
|
|
1073
|
+
this.sessionId = options.sessionId;
|
|
1074
|
+
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
1075
|
+
}
|
|
1076
|
+
open(file, signal) {
|
|
1077
|
+
return this.request("POST", `/api/artifact/${encodeURIComponent(this.sessionId)}/open`, { file }, signal);
|
|
1078
|
+
}
|
|
1079
|
+
poll(timeoutMsHint, signal) {
|
|
1080
|
+
return this.request("GET", `/api/artifact/${encodeURIComponent(this.sessionId)}/poll`, void 0, signal, timeoutMsHint);
|
|
1081
|
+
}
|
|
1082
|
+
agentReply(text, signal) {
|
|
1083
|
+
return this.request("POST", `/api/artifact/${encodeURIComponent(this.sessionId)}/agent-reply`, { text }, signal, void 0, true);
|
|
1084
|
+
}
|
|
1085
|
+
async request(method, pathname, body, signal, timeoutMsHint, allowEmptyJson = false) {
|
|
1086
|
+
let url;
|
|
1087
|
+
try {
|
|
1088
|
+
url = new URL(pathname, `${this.baseUrl}/`);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
throw new ArtifactError({
|
|
1091
|
+
code: "UNREACHABLE",
|
|
1092
|
+
message: "artifact API base URL is invalid",
|
|
1093
|
+
retryable: false,
|
|
1094
|
+
detail: err
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const timeout = combineSignalAndTimeout(signal, timeoutMsHint);
|
|
1098
|
+
let response;
|
|
1099
|
+
try {
|
|
1100
|
+
response = await this.fetchFn(url.toString(), {
|
|
1101
|
+
method,
|
|
1102
|
+
headers: {
|
|
1103
|
+
Authorization: `Bearer ${this.token}`,
|
|
1104
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1105
|
+
},
|
|
1106
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
1107
|
+
redirect: "error",
|
|
1108
|
+
signal: timeout.signal
|
|
1109
|
+
});
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
throw mapNetworkError$1(err);
|
|
1112
|
+
} finally {
|
|
1113
|
+
timeout.cleanup();
|
|
1114
|
+
}
|
|
1115
|
+
if (!response.ok) throw await mapHttpError$1(response);
|
|
1116
|
+
const text = await response.text().catch((err) => {
|
|
1117
|
+
throw new ArtifactError({
|
|
1118
|
+
code: "INVALID_RESPONSE",
|
|
1119
|
+
message: "artifact API response body could not be read",
|
|
1120
|
+
retryable: false,
|
|
1121
|
+
detail: err
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
if (!text && allowEmptyJson) return {};
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(text);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
throw new ArtifactError({
|
|
1129
|
+
code: "INVALID_RESPONSE",
|
|
1130
|
+
message: "artifact API returned a non-JSON response",
|
|
1131
|
+
retryable: false,
|
|
1132
|
+
detail: err
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
function combineSignalAndTimeout(signal, timeoutMsHint) {
|
|
1138
|
+
const timeoutMs = typeof timeoutMsHint === "number" && Number.isFinite(timeoutMsHint) && timeoutMsHint > 0 ? timeoutMsHint : void 0;
|
|
1139
|
+
if (timeoutMs === void 0) return {
|
|
1140
|
+
signal,
|
|
1141
|
+
cleanup: () => {}
|
|
1142
|
+
};
|
|
1143
|
+
const controller = new AbortController();
|
|
1144
|
+
const abortFromCaller = () => {
|
|
1145
|
+
try {
|
|
1146
|
+
controller.abort(signal?.reason);
|
|
1147
|
+
} catch {
|
|
1148
|
+
controller.abort();
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
if (signal?.aborted) abortFromCaller();
|
|
1152
|
+
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
1153
|
+
const timer = setTimeout(() => {
|
|
1154
|
+
try {
|
|
1155
|
+
controller.abort(new DOMException("artifact API request timed out", "TimeoutError"));
|
|
1156
|
+
} catch {
|
|
1157
|
+
controller.abort();
|
|
1158
|
+
}
|
|
1159
|
+
}, timeoutMs);
|
|
1160
|
+
return {
|
|
1161
|
+
signal: controller.signal,
|
|
1162
|
+
cleanup: () => {
|
|
1163
|
+
clearTimeout(timer);
|
|
1164
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
async function mapHttpError$1(response) {
|
|
1169
|
+
const detail = await readErrorDetail$1(response);
|
|
1170
|
+
const upstreamMessage = detailToMessage$1(detail);
|
|
1171
|
+
const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
|
|
1172
|
+
if (response.status === 401 || response.status === 403) return new ArtifactError({
|
|
1173
|
+
code: "AUTH_FAILED",
|
|
1174
|
+
message: `artifact API authentication failed (${response.status})${suffix}`,
|
|
1175
|
+
retryable: false,
|
|
1176
|
+
status: response.status,
|
|
1177
|
+
detail
|
|
1178
|
+
});
|
|
1179
|
+
if (response.status === 404) return new ArtifactError({
|
|
1180
|
+
code: "NOT_FOUND",
|
|
1181
|
+
message: `artifact session or resource not found (404)${suffix}`,
|
|
1182
|
+
retryable: false,
|
|
1183
|
+
status: response.status,
|
|
1184
|
+
detail
|
|
1185
|
+
});
|
|
1186
|
+
if (response.status === 408 || response.status === 504) return new ArtifactError({
|
|
1187
|
+
code: "TIMEOUT",
|
|
1188
|
+
message: `artifact API request timed out (${response.status})${suffix}`,
|
|
1189
|
+
retryable: true,
|
|
1190
|
+
status: response.status,
|
|
1191
|
+
detail
|
|
1192
|
+
});
|
|
1193
|
+
return new ArtifactError({
|
|
1194
|
+
code: "UPSTREAM_ERROR",
|
|
1195
|
+
message: `artifact API returned HTTP ${response.status}${suffix}`,
|
|
1196
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
1197
|
+
status: response.status,
|
|
1198
|
+
detail
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function mapNetworkError$1(err) {
|
|
1202
|
+
if (isAbortLike$2(err)) return new ArtifactError({
|
|
1203
|
+
code: "TIMEOUT",
|
|
1204
|
+
message: "artifact API request timed out or was aborted",
|
|
1205
|
+
retryable: true,
|
|
1206
|
+
detail: err
|
|
1207
|
+
});
|
|
1208
|
+
return new ArtifactError({
|
|
1209
|
+
code: "UNREACHABLE",
|
|
1210
|
+
message: `artifact API unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1211
|
+
retryable: true,
|
|
1212
|
+
detail: err
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
async function readErrorDetail$1(response) {
|
|
1216
|
+
const text = await response.text().catch(() => "");
|
|
1217
|
+
if (!text) return void 0;
|
|
1218
|
+
try {
|
|
1219
|
+
return JSON.parse(text);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return text;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function detailToMessage$1(detail) {
|
|
1225
|
+
if (typeof detail === "string") return detail;
|
|
1226
|
+
if (typeof detail !== "object" || detail === null) return void 0;
|
|
1227
|
+
const record = detail;
|
|
1228
|
+
const error = record.error;
|
|
1229
|
+
if (typeof error === "string") return error;
|
|
1230
|
+
if (typeof error === "object" && error !== null) {
|
|
1231
|
+
const errorRecord = error;
|
|
1232
|
+
if (typeof errorRecord.message === "string") return errorRecord.message;
|
|
1233
|
+
if (typeof errorRecord.code === "string") return errorRecord.code;
|
|
1234
|
+
}
|
|
1235
|
+
if (typeof record.message === "string") return record.message;
|
|
1236
|
+
}
|
|
1237
|
+
function isAbortLike$2(err) {
|
|
1238
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/lib/artifact/tools.ts
|
|
1243
|
+
const ARTIFACT_GROUP = "peers";
|
|
1244
|
+
const ARTIFACT_POLL_TOOL_BUDGET_MS = 5e4;
|
|
1245
|
+
const ARTIFACT_SINGLE_POLL_TIMEOUT_MS = 25e3;
|
|
1246
|
+
const ARTIFACT_POLL_RETURN_MARGIN_MS = 1e3;
|
|
1247
|
+
const ARTIFACT_MAX_POLLS_PER_TOOL_CALL = 2;
|
|
1248
|
+
function tool(toolNameHttp, description, inputSchema, handler) {
|
|
1249
|
+
return {
|
|
1250
|
+
toolNameHttp,
|
|
1251
|
+
group: ARTIFACT_GROUP,
|
|
1252
|
+
capability: "artifact",
|
|
1253
|
+
description,
|
|
1254
|
+
inputSchema,
|
|
1255
|
+
async handler(args, signal) {
|
|
1256
|
+
try {
|
|
1257
|
+
return await handler(args, signal);
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
return errorResult$1(err);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const ARTIFACT_TOOLS = Object.freeze([
|
|
1265
|
+
tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ file: stringProp$1("Workspace-relative or absolute file path to show in the Artifact panel.") }, ["file"]), async (args, signal) => {
|
|
1266
|
+
const env = readArtifactEnv();
|
|
1267
|
+
if (!env) return missingEnvResult();
|
|
1268
|
+
const file = requiredString$1(args, "file");
|
|
1269
|
+
return ok$1({
|
|
1270
|
+
viewUrl: (await clientFromEnv(env).open(file, signal)).viewUrl,
|
|
1271
|
+
next_step: "Tell the user to review at the Artifact panel, then call artifact_poll."
|
|
1272
|
+
});
|
|
1273
|
+
}),
|
|
1274
|
+
tool("artifact_poll", "Wait for human Artifact review feedback from ai-or-die and return the prompts/layout warnings/DOM snapshot for the agent to act on. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({}, []), async (_args, signal) => {
|
|
1275
|
+
const env = readArtifactEnv();
|
|
1276
|
+
if (!env) return missingEnvResult();
|
|
1277
|
+
return ok$1(formatPollResponse(await pollUntilReady(clientFromEnv(env), signal)));
|
|
1278
|
+
}),
|
|
1279
|
+
tool("artifact_reply", "Send the agent's reply back to the ai-or-die Artifact review panel after applying or responding to human feedback. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ text: stringProp$1("Agent reply text to deliver to the human Artifact review panel.") }, ["text"]), async (args, signal) => {
|
|
1280
|
+
const env = readArtifactEnv();
|
|
1281
|
+
if (!env) return missingEnvResult();
|
|
1282
|
+
const text = requiredString$1(args, "text");
|
|
1283
|
+
return ok$1({
|
|
1284
|
+
ok: true,
|
|
1285
|
+
...await clientFromEnv(env).agentReply(text, signal),
|
|
1286
|
+
next_step: "Wait for further human review, or continue if the review loop is complete."
|
|
1287
|
+
});
|
|
1288
|
+
})
|
|
1289
|
+
]);
|
|
1290
|
+
function readArtifactEnv() {
|
|
1291
|
+
const baseUrl = process.env.AIORDIE_BASE_URL;
|
|
1292
|
+
const token = process.env.AIORDIE_TOKEN;
|
|
1293
|
+
const sessionId = process.env.AIORDIE_SESSION_ID;
|
|
1294
|
+
if (!baseUrl || !token || !sessionId) return void 0;
|
|
1295
|
+
return {
|
|
1296
|
+
baseUrl,
|
|
1297
|
+
token,
|
|
1298
|
+
sessionId
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function clientFromEnv(env) {
|
|
1302
|
+
return new ArtifactClient(env);
|
|
1303
|
+
}
|
|
1304
|
+
async function pollUntilReady(client, signal) {
|
|
1305
|
+
const deadline = Date.now() + ARTIFACT_POLL_TOOL_BUDGET_MS;
|
|
1306
|
+
let last;
|
|
1307
|
+
let attempts = 0;
|
|
1308
|
+
while (!signal?.aborted && attempts < ARTIFACT_MAX_POLLS_PER_TOOL_CALL) {
|
|
1309
|
+
attempts += 1;
|
|
1310
|
+
const remaining = deadline - Date.now();
|
|
1311
|
+
if (remaining <= ARTIFACT_POLL_RETURN_MARGIN_MS) break;
|
|
1312
|
+
const timeoutMsHint = Math.min(ARTIFACT_SINGLE_POLL_TIMEOUT_MS, Math.max(1, remaining - ARTIFACT_POLL_RETURN_MARGIN_MS));
|
|
1313
|
+
last = await client.poll(timeoutMsHint, signal);
|
|
1314
|
+
if (!isWaitingPoll(last)) return last;
|
|
1315
|
+
if (deadline - Date.now() <= ARTIFACT_POLL_RETURN_MARGIN_MS) break;
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
...last ?? { status: "waiting" },
|
|
1319
|
+
status: "waiting",
|
|
1320
|
+
next_step: "No human feedback is ready yet. Call artifact_poll again."
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function isWaitingPoll(response) {
|
|
1324
|
+
if (hasFeedback(response.prompts)) return false;
|
|
1325
|
+
const status = response.status.toLowerCase();
|
|
1326
|
+
return status === "waiting" || status === "pending" || status === "open" || status === "idle" || status === "timeout" || status === "no_feedback";
|
|
1327
|
+
}
|
|
1328
|
+
function hasFeedback(prompts) {
|
|
1329
|
+
if (Array.isArray(prompts)) return prompts.length > 0;
|
|
1330
|
+
if (typeof prompts === "string") return prompts.trim() !== "";
|
|
1331
|
+
if (typeof prompts === "object" && prompts !== null) return Object.keys(prompts).length > 0;
|
|
1332
|
+
return prompts !== void 0 && prompts !== null;
|
|
1333
|
+
}
|
|
1334
|
+
function formatPollResponse(response) {
|
|
1335
|
+
return definedObject$1({
|
|
1336
|
+
status: response.status,
|
|
1337
|
+
prompts: response.prompts,
|
|
1338
|
+
layout_warnings: response.layout_warnings,
|
|
1339
|
+
dom_snapshot: response.dom_snapshot,
|
|
1340
|
+
next_step: response.next_step ?? defaultPollNextStep(response.status)
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
function defaultPollNextStep(status) {
|
|
1344
|
+
return isWaitingStatus(status) ? "No human feedback is ready yet. Call artifact_poll again." : "Apply the human Artifact review feedback, then call artifact_reply with a concise summary.";
|
|
1345
|
+
}
|
|
1346
|
+
function isWaitingStatus(status) {
|
|
1347
|
+
const normalized = status.toLowerCase();
|
|
1348
|
+
return normalized === "waiting" || normalized === "pending" || normalized === "open" || normalized === "idle" || normalized === "timeout" || normalized === "no_feedback";
|
|
1349
|
+
}
|
|
1350
|
+
function requiredString$1(args, key) {
|
|
1351
|
+
const value = args[key];
|
|
1352
|
+
if (typeof value !== "string" || value.trim() === "") throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
|
|
1353
|
+
return value;
|
|
1354
|
+
}
|
|
1355
|
+
var ArtifactToolInputError = class extends Error {
|
|
1356
|
+
code;
|
|
1357
|
+
constructor(code, message) {
|
|
1358
|
+
super(message);
|
|
1359
|
+
this.name = "ArtifactToolInputError";
|
|
1360
|
+
this.code = code;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
function missingEnvResult() {
|
|
1364
|
+
return jsonResult$1({ error: {
|
|
1365
|
+
code: "NOT_IN_AIORDIE_TAB",
|
|
1366
|
+
message: "artifact tools only work inside an ai-or-die tab-backed Claude session. Missing AIORDIE_BASE_URL, AIORDIE_TOKEN, or AIORDIE_SESSION_ID."
|
|
1367
|
+
} }, true);
|
|
1368
|
+
}
|
|
1369
|
+
function ok$1(value) {
|
|
1370
|
+
return jsonResult$1(value, false);
|
|
1371
|
+
}
|
|
1372
|
+
function jsonResult$1(value, isError) {
|
|
1373
|
+
return {
|
|
1374
|
+
content: [{
|
|
1375
|
+
type: "text",
|
|
1376
|
+
text: JSON.stringify(value)
|
|
1377
|
+
}],
|
|
1378
|
+
...isError ? { isError: true } : {}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function errorResult$1(err) {
|
|
1382
|
+
if (err instanceof ArtifactError) return jsonResult$1({ error: definedObject$1({
|
|
1383
|
+
code: err.code,
|
|
1384
|
+
message: err.message,
|
|
1385
|
+
retryable: err.retryable,
|
|
1386
|
+
status: err.status
|
|
1387
|
+
}) }, true);
|
|
1388
|
+
return jsonResult$1({ error: {
|
|
1389
|
+
code: errorCode$1(err),
|
|
1390
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1391
|
+
} }, true);
|
|
1392
|
+
}
|
|
1393
|
+
function errorCode$1(err) {
|
|
1394
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
1395
|
+
const code = err.code;
|
|
1396
|
+
if (typeof code === "string") return code;
|
|
1397
|
+
}
|
|
1398
|
+
return "ARTIFACT_ERROR";
|
|
1399
|
+
}
|
|
1400
|
+
function definedObject$1(input) {
|
|
1401
|
+
const result = {};
|
|
1402
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
|
|
1403
|
+
return result;
|
|
1404
|
+
}
|
|
1405
|
+
function objectSchema$1(properties, required) {
|
|
1406
|
+
return {
|
|
1407
|
+
type: "object",
|
|
1408
|
+
required,
|
|
1409
|
+
additionalProperties: false,
|
|
1410
|
+
properties
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function stringProp$1(description) {
|
|
1414
|
+
return {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
description
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
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
|
+
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/lib/fleet/client.ts
|
|
1634
|
+
var FleetError = class extends Error {
|
|
1635
|
+
code;
|
|
1636
|
+
retryable;
|
|
1637
|
+
status;
|
|
1638
|
+
detail;
|
|
1639
|
+
constructor(args) {
|
|
1640
|
+
super(args.message);
|
|
1641
|
+
this.name = "FleetError";
|
|
1642
|
+
this.code = args.code;
|
|
1643
|
+
this.retryable = args.retryable;
|
|
1644
|
+
this.status = args.status;
|
|
1645
|
+
this.detail = args.detail;
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
function encodeSessionId(instanceId, localId) {
|
|
1649
|
+
return `${instanceId}:${localId}`;
|
|
1650
|
+
}
|
|
1651
|
+
function decodeSessionId(globalId) {
|
|
1652
|
+
const idx = globalId.indexOf(":");
|
|
1653
|
+
if (idx <= 0 || idx === globalId.length - 1) throw new FleetError({
|
|
1654
|
+
code: "SESSION_NOT_FOUND",
|
|
1655
|
+
message: `invalid fleet sessionId ${JSON.stringify(globalId)}; expected "instanceId:localSessionId"`,
|
|
1656
|
+
retryable: false
|
|
1657
|
+
});
|
|
1658
|
+
return {
|
|
1659
|
+
instanceId: globalId.slice(0, idx),
|
|
1660
|
+
localId: globalId.slice(idx + 1)
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
var FleetClient = class {
|
|
1664
|
+
baseUrl;
|
|
1665
|
+
origin;
|
|
1666
|
+
token;
|
|
1667
|
+
fetchFn;
|
|
1668
|
+
getTunnelToken;
|
|
1669
|
+
onTunnelAuthInvalidate;
|
|
1670
|
+
constructor(options) {
|
|
1671
|
+
this.baseUrl = options.url.replace(/\/+$/, "");
|
|
1672
|
+
this.origin = new URL(this.baseUrl).origin;
|
|
1673
|
+
this.token = options.token;
|
|
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);
|
|
1680
|
+
}
|
|
1681
|
+
listSessions(signal) {
|
|
1682
|
+
return this.request("GET", "/api/control/sessions", void 0, void 0, signal);
|
|
1683
|
+
}
|
|
1684
|
+
status(sessionId, signal) {
|
|
1685
|
+
return this.request("GET", `/api/control/sessions/${encodeURIComponent(sessionId)}/status`, void 0, void 0, signal);
|
|
1686
|
+
}
|
|
1687
|
+
readSession(sessionId, lines, signal) {
|
|
1688
|
+
return this.request("GET", `/api/control/sessions/${encodeURIComponent(sessionId)}/read`, lines === void 0 ? void 0 : { lines: String(lines) }, void 0, signal);
|
|
1689
|
+
}
|
|
1690
|
+
createSession(input, signal) {
|
|
1691
|
+
return this.request("POST", "/api/control/sessions/create", void 0, input, signal);
|
|
1692
|
+
}
|
|
1693
|
+
stopSession(sessionId, modeOrInput, idempotencyKeyOrSignal, signal) {
|
|
1694
|
+
const body = {};
|
|
1695
|
+
let requestSignal;
|
|
1696
|
+
if (typeof modeOrInput === "object" && modeOrInput !== null) {
|
|
1697
|
+
if (modeOrInput.mode !== void 0) body.mode = modeOrInput.mode;
|
|
1698
|
+
if (modeOrInput.idempotencyKey !== void 0) body.idempotencyKey = modeOrInput.idempotencyKey;
|
|
1699
|
+
requestSignal = typeof idempotencyKeyOrSignal === "string" ? signal : idempotencyKeyOrSignal;
|
|
1700
|
+
} else {
|
|
1701
|
+
if (modeOrInput !== void 0) body.mode = modeOrInput;
|
|
1702
|
+
if (typeof idempotencyKeyOrSignal === "string") {
|
|
1703
|
+
body.idempotencyKey = idempotencyKeyOrSignal;
|
|
1704
|
+
requestSignal = signal;
|
|
1705
|
+
} else requestSignal = idempotencyKeyOrSignal;
|
|
1706
|
+
}
|
|
1707
|
+
return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/stop`, void 0, body, requestSignal);
|
|
1708
|
+
}
|
|
1709
|
+
sendMessage(sessionId, input, signal) {
|
|
1710
|
+
return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/message`, void 0, input, signal);
|
|
1711
|
+
}
|
|
1712
|
+
sendKeys(sessionId, input, signal) {
|
|
1713
|
+
return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/keys`, void 0, input, signal);
|
|
1714
|
+
}
|
|
1715
|
+
respond(sessionId, input, signal) {
|
|
1716
|
+
return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/respond`, void 0, input, signal);
|
|
1717
|
+
}
|
|
1718
|
+
waitEvents(input, signal) {
|
|
1719
|
+
const query = {};
|
|
1720
|
+
if (input.cursor !== void 0) query.cursor = input.cursor;
|
|
1721
|
+
if (input.timeoutMs !== void 0) query.timeoutMs = String(input.timeoutMs);
|
|
1722
|
+
if (input.sessionIds !== void 0) query.sessionIds = input.sessionIds.join(",");
|
|
1723
|
+
if (input.kinds !== void 0) query.kinds = input.kinds.join(",");
|
|
1724
|
+
return this.request("GET", "/api/control/events", query, void 0, signal);
|
|
1725
|
+
}
|
|
1726
|
+
readFile(pathValue, signal) {
|
|
1727
|
+
return this.request("GET", "/api/files/content", { path: pathValue }, void 0, signal);
|
|
1728
|
+
}
|
|
1729
|
+
listDir(pathValue, signal) {
|
|
1730
|
+
return this.request("GET", "/api/files", { path: pathValue }, void 0, signal);
|
|
1731
|
+
}
|
|
1732
|
+
search(queryValue, pathValue, signal) {
|
|
1733
|
+
const query = { q: queryValue };
|
|
1734
|
+
if (pathValue !== void 0) query.path = pathValue;
|
|
1735
|
+
return this.request("GET", "/api/search", query, void 0, signal);
|
|
1736
|
+
}
|
|
1737
|
+
gitShow(input, signal) {
|
|
1738
|
+
const query = {};
|
|
1739
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1740
|
+
if (value === void 0 || value === null) continue;
|
|
1741
|
+
if (key === "instance") continue;
|
|
1742
|
+
query[key] = String(value);
|
|
1743
|
+
}
|
|
1744
|
+
return this.request("GET", "/api/files/git-show", query, void 0, signal);
|
|
1745
|
+
}
|
|
1746
|
+
async request(method, pathname, query, body, signal) {
|
|
1747
|
+
const url = new URL(pathname, `${this.baseUrl}/`);
|
|
1748
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
|
|
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();
|
|
1795
|
+
}
|
|
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
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
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) {
|
|
1817
|
+
const detail = await readErrorDetail(response);
|
|
1818
|
+
const upstreamMessage = detailToMessage(detail);
|
|
1819
|
+
const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
|
|
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({
|
|
1829
|
+
code: "AUTH_FAILED",
|
|
1830
|
+
message: `fleet instance authentication failed (${status})${suffix}`,
|
|
1831
|
+
retryable: false,
|
|
1832
|
+
status,
|
|
1833
|
+
detail
|
|
1834
|
+
});
|
|
1835
|
+
if (status === 404) return new FleetError({
|
|
1836
|
+
code: "SESSION_NOT_FOUND",
|
|
1837
|
+
message: `fleet session or resource not found (404)${suffix}`,
|
|
1838
|
+
retryable: false,
|
|
1839
|
+
status,
|
|
1840
|
+
detail
|
|
1841
|
+
});
|
|
1842
|
+
if (status === 409 || status === 412) return new FleetError({
|
|
1843
|
+
code: "PRECONDITION_FAILED",
|
|
1844
|
+
message: `fleet instance precondition failed (${status})${suffix}`,
|
|
1845
|
+
retryable: false,
|
|
1846
|
+
status,
|
|
1847
|
+
detail
|
|
1848
|
+
});
|
|
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({
|
|
1857
|
+
code: "TIMEOUT",
|
|
1858
|
+
message: `fleet instance request timed out (${status})${suffix}`,
|
|
1859
|
+
retryable: true,
|
|
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,
|
|
1875
|
+
detail
|
|
1876
|
+
});
|
|
1877
|
+
return new FleetError({
|
|
1878
|
+
code: "UPSTREAM_ERROR",
|
|
1879
|
+
message: `fleet instance returned HTTP ${status}${suffix}`,
|
|
1880
|
+
retryable: status >= 500,
|
|
1881
|
+
status,
|
|
1882
|
+
detail
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
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({
|
|
1919
|
+
code: "TIMEOUT",
|
|
1920
|
+
message: "fleet instance request timed out or was aborted",
|
|
1921
|
+
retryable: true,
|
|
1922
|
+
detail: err
|
|
1923
|
+
});
|
|
1924
|
+
return new FleetError({
|
|
1925
|
+
code: "UNREACHABLE",
|
|
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" : ""}`,
|
|
1927
|
+
retryable: true,
|
|
1928
|
+
detail: err
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
async function readErrorDetail(response) {
|
|
1932
|
+
const text = await response.text().catch(() => "");
|
|
1933
|
+
if (!text) return void 0;
|
|
1934
|
+
try {
|
|
1935
|
+
return JSON.parse(text);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return text;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function detailToMessage(detail) {
|
|
1941
|
+
if (typeof detail === "string") return detail;
|
|
1942
|
+
if (typeof detail !== "object" || detail === null) return void 0;
|
|
1943
|
+
const record = detail;
|
|
1944
|
+
const error = record.error;
|
|
1945
|
+
if (typeof error === "string") return error;
|
|
1946
|
+
if (typeof error === "object" && error !== null) {
|
|
1947
|
+
const errorRecord = error;
|
|
1948
|
+
if (typeof errorRecord.message === "string") return errorRecord.message;
|
|
1949
|
+
if (typeof errorRecord.code === "string") return errorRecord.code;
|
|
1950
|
+
}
|
|
1951
|
+
if (typeof record.message === "string") return record.message;
|
|
1952
|
+
}
|
|
1953
|
+
function isAbortLike$1(err) {
|
|
1954
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
//#endregion
|
|
1958
|
+
//#region src/lib/fleet/registry.ts
|
|
1959
|
+
var FleetRegistryError = class extends Error {
|
|
1960
|
+
code;
|
|
1961
|
+
constructor(code, message) {
|
|
1962
|
+
super(message);
|
|
1963
|
+
this.name = "FleetRegistryError";
|
|
1964
|
+
this.code = code;
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
function defaultFleetConfigPath() {
|
|
1968
|
+
return process.env.GH_ROUTER_FLEET_CONFIG || nodePath.join(os.homedir(), ".local", "share", "github-router", "fleet.json");
|
|
1969
|
+
}
|
|
1970
|
+
async function loadFleetRegistryConfig(configPath = defaultFleetConfigPath()) {
|
|
1971
|
+
let stat$1;
|
|
1972
|
+
try {
|
|
1973
|
+
stat$1 = await fs.stat(configPath);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
if (isNodeErrorCode(err, "ENOENT")) return { instances: [] };
|
|
1976
|
+
throw err;
|
|
1977
|
+
}
|
|
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.`);
|
|
1979
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
1980
|
+
if (raw.trim() === "") return { instances: [] };
|
|
1981
|
+
const parsed = JSON.parse(raw);
|
|
1982
|
+
if (!isObject(parsed)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry must be a JSON object");
|
|
1983
|
+
const instances = parsed.instances;
|
|
1984
|
+
if (instances === void 0) return { instances: [] };
|
|
1985
|
+
if (!Array.isArray(instances)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instances must be an array");
|
|
1986
|
+
return { instances: instances.map(parseInstance) };
|
|
1987
|
+
}
|
|
1988
|
+
var FleetRegistry = class {
|
|
1989
|
+
loader;
|
|
1990
|
+
loaded;
|
|
1991
|
+
constructor(options = {}) {
|
|
1992
|
+
if (options.config !== void 0) {
|
|
1993
|
+
const config = options.config;
|
|
1994
|
+
this.loader = () => config;
|
|
1995
|
+
} else if (options.loadConfig !== void 0) this.loader = options.loadConfig;
|
|
1996
|
+
else {
|
|
1997
|
+
const configPath = options.configPath;
|
|
1998
|
+
this.loader = () => loadFleetRegistryConfig(configPath);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
async resolveInstance(arg) {
|
|
2002
|
+
const instances = await this.instancesWithTokens();
|
|
2003
|
+
const wanted = typeof arg === "string" ? arg.trim() : "";
|
|
2004
|
+
if (wanted) {
|
|
2005
|
+
const byId = instances.find((instance) => instance.id === wanted);
|
|
2006
|
+
if (byId) return resolvedInstance(byId);
|
|
2007
|
+
const labelMatches = instances.filter((instance) => instance.label.toLocaleLowerCase() === wanted.toLocaleLowerCase());
|
|
2008
|
+
if (labelMatches.length > 1) throw new FleetRegistryError("AMBIGUOUS_LABEL", `fleet instance label ${JSON.stringify(wanted)} matches ${labelMatches.length} instances; use an id`);
|
|
2009
|
+
if (labelMatches.length === 1) return resolvedInstance(labelMatches[0]);
|
|
2010
|
+
throw new FleetRegistryError("INSTANCE_NOT_FOUND", `fleet instance ${JSON.stringify(wanted)} was not found`);
|
|
2011
|
+
}
|
|
2012
|
+
const defaultInstance = instances.find((instance) => instance.default === true);
|
|
2013
|
+
if (defaultInstance) return resolvedInstance(defaultInstance);
|
|
2014
|
+
if (instances.length === 1) return resolvedInstance(instances[0]);
|
|
2015
|
+
throw new FleetRegistryError("INSTANCE_REQUIRED", instances.length === 0 ? "fleet instance is required; registry is empty" : "fleet instance is required; specify an instance id or label");
|
|
2016
|
+
}
|
|
2017
|
+
async listInstances() {
|
|
2018
|
+
return (await this.instancesWithTokens()).map((instance) => ({
|
|
2019
|
+
id: instance.id,
|
|
2020
|
+
label: instance.label,
|
|
2021
|
+
url: instance.url,
|
|
2022
|
+
default: instance.default,
|
|
2023
|
+
allowExec: instance.allowExec
|
|
2024
|
+
}));
|
|
2025
|
+
}
|
|
2026
|
+
instancesWithTokens() {
|
|
2027
|
+
if (!this.loaded) this.loaded = Promise.resolve(this.loader()).then((config) => normalizeConfig(config));
|
|
2028
|
+
return this.loaded;
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
function normalizeConfig(config) {
|
|
2032
|
+
if (!isObject(config)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry config must be an object");
|
|
2033
|
+
const instances = config.instances ?? [];
|
|
2034
|
+
if (!Array.isArray(instances)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instances must be an array");
|
|
2035
|
+
return instances.map(parseInstance);
|
|
2036
|
+
}
|
|
2037
|
+
function parseInstance(raw) {
|
|
2038
|
+
if (!isObject(raw)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instance must be an object");
|
|
2039
|
+
const instance = raw;
|
|
2040
|
+
const id = instance.id;
|
|
2041
|
+
const label = instance.label;
|
|
2042
|
+
const url = instance.url;
|
|
2043
|
+
const token = instance.token;
|
|
2044
|
+
if (typeof id !== "string" || id.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instance id must be a non-empty string");
|
|
2045
|
+
if (typeof label !== "string" || label.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} label must be a non-empty string`);
|
|
2046
|
+
if (typeof url !== "string" || url.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} url must be a non-empty string`);
|
|
2047
|
+
const trimmedUrl = url.trim();
|
|
2048
|
+
let parsedUrl;
|
|
2049
|
+
try {
|
|
2050
|
+
parsedUrl = new URL(trimmedUrl);
|
|
2051
|
+
} catch {
|
|
2052
|
+
throw invalidInstanceUrlError(id);
|
|
2053
|
+
}
|
|
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)`);
|
|
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);
|
|
2060
|
+
return {
|
|
2061
|
+
id: id.trim(),
|
|
2062
|
+
label: label.trim(),
|
|
2063
|
+
url: trimmedUrl,
|
|
2064
|
+
token,
|
|
2065
|
+
default: instance.default === true ? true : void 0,
|
|
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;
|
|
2085
|
+
}
|
|
2086
|
+
function invalidInstanceUrlError(id) {
|
|
2087
|
+
return new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url must be https (or http://localhost for local testing)`);
|
|
2088
|
+
}
|
|
2089
|
+
function isAllowedInstanceUrl(url) {
|
|
2090
|
+
if (url.protocol === "https:") return true;
|
|
2091
|
+
if (url.protocol !== "http:") return false;
|
|
2092
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
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
|
+
}
|
|
2104
|
+
function resolvedInstance(instance) {
|
|
2105
|
+
return {
|
|
2106
|
+
id: instance.id,
|
|
2107
|
+
label: instance.label,
|
|
2108
|
+
url: instance.url,
|
|
2109
|
+
token: instance.token,
|
|
2110
|
+
allowExec: instance.allowExec,
|
|
2111
|
+
tunnelId: instance.tunnelId,
|
|
2112
|
+
tunnelToken: instance.tunnelToken
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function isObject(value) {
|
|
2116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2117
|
+
}
|
|
2118
|
+
function isNodeErrorCode(err, code) {
|
|
2119
|
+
return isObject(err) && err.code === code;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
//#endregion
|
|
2123
|
+
//#region src/lib/fleet/tools.ts
|
|
2124
|
+
const FLEET_GROUP = "fleet";
|
|
2125
|
+
const INSTANCE_PROBE_TIMEOUT_MS = 2e3;
|
|
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";
|
|
2136
|
+
var FleetToolInputError = class extends Error {
|
|
2137
|
+
code;
|
|
2138
|
+
constructor(code, message) {
|
|
2139
|
+
super(message);
|
|
2140
|
+
this.name = "FleetToolInputError";
|
|
2141
|
+
this.code = code;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
let defaultRegistry;
|
|
2145
|
+
let defaultTunnelProvider;
|
|
2146
|
+
const awaitTurnCursors = /* @__PURE__ */ new Map();
|
|
2147
|
+
const instanceProbeCache = /* @__PURE__ */ new Map();
|
|
2148
|
+
function createFleetTools(options = {}) {
|
|
2149
|
+
const registry = options.registry;
|
|
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);
|
|
2155
|
+
function getRegistry() {
|
|
2156
|
+
if (registry) return registry;
|
|
2157
|
+
defaultRegistry ??= new FleetRegistry();
|
|
2158
|
+
return defaultRegistry;
|
|
2159
|
+
}
|
|
2160
|
+
function clientFor(instance) {
|
|
2161
|
+
const key = `${instance.id}\0${instance.url}\0${instance.token}\0${instance.tunnelId ?? ""}\0${instance.tunnelToken ?? ""}`;
|
|
2162
|
+
const existing = clients.get(key);
|
|
2163
|
+
if (existing) return existing;
|
|
2164
|
+
const created = options.createClient ? options.createClient(instance) : new FleetClient({
|
|
2165
|
+
url: instance.url,
|
|
2166
|
+
token: instance.token,
|
|
2167
|
+
fetchFn: options.fetchFn,
|
|
2168
|
+
...tunnelClientOptions(instance, tunnelProvider)
|
|
2169
|
+
});
|
|
2170
|
+
clients.set(key, created);
|
|
2171
|
+
return created;
|
|
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
|
+
}
|
|
2197
|
+
async function resolve(arg) {
|
|
2198
|
+
return getRegistry().resolveInstance(arg);
|
|
2199
|
+
}
|
|
2200
|
+
async function resolveSession(args) {
|
|
2201
|
+
const globalId = requiredString(args, "sessionId");
|
|
2202
|
+
const decoded = decodeSessionId(globalId);
|
|
2203
|
+
const instance = await resolve(decoded.instanceId);
|
|
2204
|
+
const explicitInstance = optionalString(args, "instance");
|
|
2205
|
+
if (explicitInstance !== void 0) {
|
|
2206
|
+
const explicit = await resolve(explicitInstance);
|
|
2207
|
+
if (explicit.id !== decoded.instanceId) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionId is for instance ${JSON.stringify(decoded.instanceId)} but arguments.instance resolved to ${JSON.stringify(explicit.id)}`);
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
instance,
|
|
2211
|
+
localId: decoded.localId,
|
|
2212
|
+
globalId
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
async function probeInstance(info) {
|
|
2216
|
+
const cacheKey = `${info.id}\0${info.url}`;
|
|
2217
|
+
const now = Date.now();
|
|
2218
|
+
const cached$1 = instanceProbeCache.get(cacheKey);
|
|
2219
|
+
if (cached$1 && now - cached$1.at < INSTANCE_PROBE_CACHE_TTL_MS) return cached$1.result;
|
|
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
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
const result = failedProbeResult(info, "UNREACHABLE");
|
|
2255
|
+
instanceProbeCache.set(cacheKey, {
|
|
2256
|
+
result,
|
|
2257
|
+
at: Date.now()
|
|
2258
|
+
});
|
|
2259
|
+
return result;
|
|
2260
|
+
}
|
|
2261
|
+
function tool$1(toolNameHttp, description, inputSchema, handler) {
|
|
2262
|
+
return {
|
|
2263
|
+
toolNameHttp,
|
|
2264
|
+
group: FLEET_GROUP,
|
|
2265
|
+
description,
|
|
2266
|
+
inputSchema,
|
|
2267
|
+
capability: "fleet",
|
|
2268
|
+
async handler(args, signal) {
|
|
2269
|
+
try {
|
|
2270
|
+
return await handler(args, signal);
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
return errorResult(err);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
return Object.freeze([
|
|
2278
|
+
tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
|
|
2279
|
+
return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
|
|
2280
|
+
}),
|
|
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) => {
|
|
2282
|
+
const instance = await resolve(optionalString(args, "instance"));
|
|
2283
|
+
const response = await clientFor(instance).listSessions(signal);
|
|
2284
|
+
return ok({
|
|
2285
|
+
resolvedInstance: publicInstance(instance),
|
|
2286
|
+
sessions: response.sessions.map((session) => globalizeSession(instance.id, session))
|
|
2287
|
+
});
|
|
2288
|
+
}),
|
|
2289
|
+
tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema({
|
|
2290
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2291
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2292
|
+
lines: numberProp("Number of recent lines to read."),
|
|
2293
|
+
format: stringProp("Reserved for future formatting; results are JSON text today.")
|
|
2294
|
+
}, ["sessionId"]), async (args, signal) => {
|
|
2295
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2296
|
+
const lines = optionalNumber(args, "lines");
|
|
2297
|
+
const response = await clientFor(instance).readSession(localId, lines, signal);
|
|
2298
|
+
return ok({
|
|
2299
|
+
resolvedInstance: publicInstance(instance),
|
|
2300
|
+
...response,
|
|
2301
|
+
sessionId: globalId
|
|
2302
|
+
});
|
|
2303
|
+
}),
|
|
2304
|
+
tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema({
|
|
2305
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2306
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId.")
|
|
2307
|
+
}, ["sessionId"]), async (args, signal) => {
|
|
2308
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2309
|
+
const response = await clientFor(instance).status(localId, signal);
|
|
2310
|
+
return ok({
|
|
2311
|
+
resolvedInstance: publicInstance(instance),
|
|
2312
|
+
...response,
|
|
2313
|
+
sessionId: globalId
|
|
2314
|
+
});
|
|
2315
|
+
}),
|
|
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({
|
|
2317
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2318
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2319
|
+
message: stringProp("Message text to deliver to the session."),
|
|
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.")
|
|
2322
|
+
}, [
|
|
2323
|
+
"sessionId",
|
|
2324
|
+
"message",
|
|
2325
|
+
"idempotencyKey"
|
|
2326
|
+
]), async (args, signal) => {
|
|
2327
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2328
|
+
const awaitMs = optionalNumber(args, "awaitMs");
|
|
2329
|
+
const response = await clientFor(instance).sendMessage(localId, {
|
|
2330
|
+
message: requiredString(args, "message"),
|
|
2331
|
+
idempotencyKey: requiredString(args, "idempotencyKey"),
|
|
2332
|
+
...awaitMs === void 0 ? {} : { awaitMs }
|
|
2333
|
+
}, signal);
|
|
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;
|
|
2338
|
+
return jsonResult({
|
|
2339
|
+
resolvedInstance: publicInstance(instance),
|
|
2340
|
+
sessionId: globalId,
|
|
2341
|
+
...response,
|
|
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)." } : {}
|
|
2349
|
+
}, isError);
|
|
2350
|
+
}),
|
|
2351
|
+
tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
|
|
2352
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2353
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2354
|
+
keys: stringProp("Key sequence to send."),
|
|
2355
|
+
idempotencyKey: stringProp("Caller-generated idempotency key."),
|
|
2356
|
+
raw: booleanProp("Pass keys through as raw input when the instance supports it.")
|
|
2357
|
+
}, [
|
|
2358
|
+
"sessionId",
|
|
2359
|
+
"keys",
|
|
2360
|
+
"idempotencyKey"
|
|
2361
|
+
]), async (args, signal) => {
|
|
2362
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2363
|
+
const raw = optionalBoolean(args, "raw");
|
|
2364
|
+
const response = await clientFor(instance).sendKeys(localId, {
|
|
2365
|
+
keys: requiredString(args, "keys"),
|
|
2366
|
+
idempotencyKey: requiredString(args, "idempotencyKey"),
|
|
2367
|
+
...raw === void 0 ? {} : { raw }
|
|
2368
|
+
}, signal);
|
|
2369
|
+
return ok({
|
|
2370
|
+
resolvedInstance: publicInstance(instance),
|
|
2371
|
+
sessionId: globalId,
|
|
2372
|
+
...response
|
|
2373
|
+
});
|
|
2374
|
+
}),
|
|
2375
|
+
tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema({
|
|
2376
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2377
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2378
|
+
choice: stringProp("Named or numbered choice to select."),
|
|
2379
|
+
optionValue: stringProp("Exact option value to select."),
|
|
2380
|
+
keys: stringProp("Explicit key override to send instead of a mapped choice."),
|
|
2381
|
+
idempotencyKey: stringProp("Caller-generated idempotency key.")
|
|
2382
|
+
}, ["sessionId", "idempotencyKey"]), async (args, signal) => {
|
|
2383
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2384
|
+
const input = definedObject({
|
|
2385
|
+
choice: optionalString(args, "choice"),
|
|
2386
|
+
optionValue: optionalString(args, "optionValue"),
|
|
2387
|
+
keys: optionalString(args, "keys"),
|
|
2388
|
+
idempotencyKey: requiredString(args, "idempotencyKey")
|
|
2389
|
+
});
|
|
2390
|
+
const response = await clientFor(instance).respond(localId, input, signal);
|
|
2391
|
+
return ok({
|
|
2392
|
+
resolvedInstance: publicInstance(instance),
|
|
2393
|
+
sessionId: globalId,
|
|
2394
|
+
...response
|
|
2395
|
+
});
|
|
2396
|
+
}),
|
|
2397
|
+
tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema({
|
|
2398
|
+
instance: stringProp("Required instance id or label. Create never uses the registry default."),
|
|
2399
|
+
agent: stringProp("Agent/runtime to create on the instance."),
|
|
2400
|
+
name: stringProp("Optional display name for the session."),
|
|
2401
|
+
workingDir: stringProp("Optional working directory on the remote instance."),
|
|
2402
|
+
idempotencyKey: stringProp("Caller-generated idempotency key."),
|
|
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.")
|
|
2407
|
+
}, [
|
|
2408
|
+
"instance",
|
|
2409
|
+
"agent",
|
|
2410
|
+
"idempotencyKey"
|
|
2411
|
+
]), async (args, signal) => {
|
|
2412
|
+
const instance = await resolve(requiredString(args, "instance"));
|
|
2413
|
+
const agent = requiredString(args, "agent");
|
|
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);
|
|
2419
|
+
const response = await clientFor(instance).createSession(definedObject({
|
|
2420
|
+
agent,
|
|
2421
|
+
name: optionalString(args, "name"),
|
|
2422
|
+
workingDir: optionalString(args, "workingDir"),
|
|
2423
|
+
start: optionalBoolean(args, "start"),
|
|
2424
|
+
readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
|
|
2425
|
+
permissionMode,
|
|
2426
|
+
agentArgs,
|
|
2427
|
+
idempotencyKey
|
|
2428
|
+
}), signal);
|
|
2429
|
+
const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
|
|
2430
|
+
return ok({
|
|
2431
|
+
resolvedInstance: publicInstance(instance),
|
|
2432
|
+
...response,
|
|
2433
|
+
sessionId: localSessionId ? encodeSessionId(instance.id, localSessionId) : response.sessionId
|
|
2434
|
+
});
|
|
2435
|
+
}),
|
|
2436
|
+
tool$1("stop_session", "Stop a fleet session.", objectSchema({
|
|
2437
|
+
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2438
|
+
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2439
|
+
idempotencyKey: stringProp("Caller-generated idempotency key."),
|
|
2440
|
+
mode: stringProp("Optional stop mode understood by the remote instance.")
|
|
2441
|
+
}, ["sessionId", "idempotencyKey"]), async (args, signal) => {
|
|
2442
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
2443
|
+
const idempotencyKey = requiredString(args, "idempotencyKey");
|
|
2444
|
+
const response = await clientFor(instance).stopSession(localId, definedObject({
|
|
2445
|
+
mode: optionalString(args, "mode"),
|
|
2446
|
+
idempotencyKey
|
|
2447
|
+
}), signal);
|
|
2448
|
+
return ok({
|
|
2449
|
+
resolvedInstance: publicInstance(instance),
|
|
2450
|
+
sessionId: globalId,
|
|
2451
|
+
...response
|
|
2452
|
+
});
|
|
2453
|
+
}),
|
|
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({
|
|
2455
|
+
instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
|
|
2456
|
+
sessionIds: arrayProp("Global session ids to filter to."),
|
|
2457
|
+
timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
|
|
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.")
|
|
2460
|
+
}, []), async (args, signal) => {
|
|
2461
|
+
const target = await resolveAwaitTarget(args, getRegistry());
|
|
2462
|
+
const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
|
|
2463
|
+
const timeoutMs = optionalNumber(args, "timeoutMs");
|
|
2464
|
+
const kinds = optionalStringArray(args, "kinds");
|
|
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 } : {}
|
|
2500
|
+
}));
|
|
2501
|
+
const events$1 = responses.flatMap(({ instance, response }) => response.events.map((event) => stampEvent(instance, event))).sort(compareStampedEvents);
|
|
2502
|
+
const gaps = responses.flatMap(({ instance, response }) => response.gaps.map((gap) => ({
|
|
2503
|
+
instance: publicInstance(instance),
|
|
2504
|
+
...gap
|
|
2505
|
+
})));
|
|
2506
|
+
return ok({
|
|
2507
|
+
resolvedInstances: target.instances.map(publicInstance),
|
|
2508
|
+
events: events$1,
|
|
2509
|
+
gaps,
|
|
2510
|
+
cursors: responses.map(({ instance, response }) => ({
|
|
2511
|
+
instance: publicInstance(instance),
|
|
2512
|
+
cursor: response.cursor
|
|
2513
|
+
})),
|
|
2514
|
+
more: responses.some(({ response }) => response.more),
|
|
2515
|
+
...errors.length > 0 ? { errors } : {}
|
|
2516
|
+
});
|
|
2517
|
+
}),
|
|
2518
|
+
tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
|
|
2519
|
+
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2520
|
+
path: stringProp("Remote file path to read.")
|
|
2521
|
+
}, ["path"]), async (args, signal) => {
|
|
2522
|
+
const instance = await resolve(optionalString(args, "instance"));
|
|
2523
|
+
const response = await clientFor(instance).readFile(requiredString(args, "path"), signal);
|
|
2524
|
+
return ok({
|
|
2525
|
+
resolvedInstance: publicInstance(instance),
|
|
2526
|
+
...response
|
|
2527
|
+
});
|
|
2528
|
+
}),
|
|
2529
|
+
tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema({
|
|
2530
|
+
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2531
|
+
path: stringProp("Remote directory path to list.")
|
|
2532
|
+
}, ["path"]), async (args, signal) => {
|
|
2533
|
+
const instance = await resolve(optionalString(args, "instance"));
|
|
2534
|
+
const response = await clientFor(instance).listDir(requiredString(args, "path"), signal);
|
|
2535
|
+
return ok({
|
|
2536
|
+
resolvedInstance: publicInstance(instance),
|
|
2537
|
+
...response
|
|
2538
|
+
});
|
|
2539
|
+
}),
|
|
2540
|
+
tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema({
|
|
2541
|
+
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2542
|
+
query: stringProp("Search query."),
|
|
2543
|
+
path: stringProp("Optional path scope.")
|
|
2544
|
+
}, ["query"]), async (args, signal) => {
|
|
2545
|
+
const instance = await resolve(optionalString(args, "instance"));
|
|
2546
|
+
const response = await clientFor(instance).search(requiredString(args, "query"), optionalString(args, "path"), signal);
|
|
2547
|
+
return ok({
|
|
2548
|
+
resolvedInstance: publicInstance(instance),
|
|
2549
|
+
...response
|
|
2550
|
+
});
|
|
2551
|
+
}),
|
|
2552
|
+
tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema({
|
|
2553
|
+
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2554
|
+
path: stringProp("Remote repository path or file path for git-show."),
|
|
2555
|
+
ref: stringProp("Optional git ref/revision."),
|
|
2556
|
+
rev: stringProp("Optional git revision alias."),
|
|
2557
|
+
commit: stringProp("Optional commit id.")
|
|
2558
|
+
}, ["path"]), async (args, signal) => {
|
|
2559
|
+
const instance = await resolve(optionalString(args, "instance"));
|
|
2560
|
+
const response = await clientFor(instance).gitShow({
|
|
2561
|
+
...args,
|
|
2562
|
+
instance: void 0
|
|
2563
|
+
}, signal);
|
|
2564
|
+
return ok({
|
|
2565
|
+
resolvedInstance: publicInstance(instance),
|
|
2566
|
+
...response
|
|
2567
|
+
});
|
|
2568
|
+
})
|
|
2569
|
+
]);
|
|
2570
|
+
}
|
|
2571
|
+
const FLEET_TOOLS = createFleetTools();
|
|
2572
|
+
function createProbeTimeout() {
|
|
2573
|
+
const timeout = AbortSignal.timeout;
|
|
2574
|
+
if (typeof timeout === "function") return {
|
|
2575
|
+
signal: timeout(INSTANCE_PROBE_TIMEOUT_MS),
|
|
2576
|
+
cleanup: () => {}
|
|
2577
|
+
};
|
|
2578
|
+
const controller = new AbortController();
|
|
2579
|
+
const timer = setTimeout(() => controller.abort(), INSTANCE_PROBE_TIMEOUT_MS);
|
|
2580
|
+
return {
|
|
2581
|
+
signal: controller.signal,
|
|
2582
|
+
cleanup: () => clearTimeout(timer)
|
|
2583
|
+
};
|
|
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
|
+
}
|
|
2643
|
+
function fleetProbeErrorCode(err) {
|
|
2644
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
2645
|
+
const code = err.code;
|
|
2646
|
+
if (typeof code === "string" && isFleetErrorCode(code)) return code;
|
|
2647
|
+
}
|
|
2648
|
+
if (isAbortLike(err)) return "TIMEOUT";
|
|
2649
|
+
return "UNREACHABLE";
|
|
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
|
+
}
|
|
2660
|
+
function isFleetErrorCode(code) {
|
|
2661
|
+
switch (code) {
|
|
2662
|
+
case "UNREACHABLE":
|
|
2663
|
+
case "AUTH_FAILED":
|
|
2664
|
+
case "SESSION_NOT_FOUND":
|
|
2665
|
+
case "PRECONDITION_FAILED":
|
|
2666
|
+
case "TIMEOUT":
|
|
2667
|
+
case "UPSTREAM_ERROR":
|
|
2668
|
+
case "NO_HOST":
|
|
2669
|
+
case "RELAY_ERROR":
|
|
2670
|
+
case "BAD_REQUEST":
|
|
2671
|
+
case "RATE_LIMITED": return true;
|
|
2672
|
+
default: return false;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
async function resolveAwaitTarget(args, registry) {
|
|
2676
|
+
const instanceArgs = optionalStringArray(args, "instances");
|
|
2677
|
+
const sessionIdArgs = optionalStringArray(args, "sessionIds");
|
|
2678
|
+
const localSessionIdsByInstance = /* @__PURE__ */ new Map();
|
|
2679
|
+
for (const sessionId of sessionIdArgs ?? []) {
|
|
2680
|
+
const decoded = decodeSessionId(sessionId);
|
|
2681
|
+
const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
|
|
2682
|
+
existing.push(decoded.localId);
|
|
2683
|
+
localSessionIdsByInstance.set(decoded.instanceId, existing);
|
|
2684
|
+
}
|
|
2685
|
+
let instances;
|
|
2686
|
+
if (instanceArgs !== void 0 && instanceArgs.length > 0) {
|
|
2687
|
+
instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
|
|
2688
|
+
const ids = new Set(instances.map((instance) => instance.id));
|
|
2689
|
+
for (const instanceId of localSessionIdsByInstance.keys()) if (!ids.has(instanceId)) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionIds include instance ${JSON.stringify(instanceId)} which is not in arguments.instances`);
|
|
2690
|
+
} else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
|
|
2691
|
+
else {
|
|
2692
|
+
const infos = await registry.listInstances();
|
|
2693
|
+
if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
|
|
2694
|
+
instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
|
|
2695
|
+
}
|
|
2696
|
+
return {
|
|
2697
|
+
instances,
|
|
2698
|
+
localSessionIdsByInstance
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function globalizeSession(instanceId, session) {
|
|
2702
|
+
return {
|
|
2703
|
+
...session,
|
|
2704
|
+
sessionId: encodeSessionId(instanceId, session.sessionId)
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function stampEvent(instance, event) {
|
|
2708
|
+
return {
|
|
2709
|
+
...event,
|
|
2710
|
+
instance: publicInstance(instance),
|
|
2711
|
+
...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
|
|
2712
|
+
};
|
|
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
|
+
}
|
|
2722
|
+
function compareStampedEvents(a, b) {
|
|
2723
|
+
const atA = eventAtMs(a.at);
|
|
2724
|
+
const atB = eventAtMs(b.at);
|
|
2725
|
+
if (atA !== atB) return atA - atB;
|
|
2726
|
+
return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
|
|
2727
|
+
}
|
|
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);
|
|
2758
|
+
return {
|
|
2759
|
+
id: info.id,
|
|
2760
|
+
label: info.label,
|
|
2761
|
+
reachable: false,
|
|
2762
|
+
error: code,
|
|
2763
|
+
...hint ? { hint } : {}
|
|
2764
|
+
};
|
|
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
|
+
}
|
|
2800
|
+
function uniqueInstances(instances) {
|
|
2801
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2802
|
+
const result = [];
|
|
2803
|
+
for (const instance of instances) {
|
|
2804
|
+
if (seen.has(instance.id)) continue;
|
|
2805
|
+
seen.add(instance.id);
|
|
2806
|
+
result.push(instance);
|
|
2807
|
+
}
|
|
2808
|
+
return result;
|
|
2809
|
+
}
|
|
2810
|
+
function publicInstance(instance) {
|
|
2811
|
+
return {
|
|
2812
|
+
id: instance.id,
|
|
2813
|
+
label: instance.label
|
|
2814
|
+
};
|
|
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
|
+
}
|
|
2836
|
+
function ok(value) {
|
|
2837
|
+
return jsonResult(value, false);
|
|
2838
|
+
}
|
|
2839
|
+
function jsonResult(value, isError) {
|
|
2840
|
+
return {
|
|
2841
|
+
content: [{
|
|
2842
|
+
type: "text",
|
|
2843
|
+
text: JSON.stringify(value)
|
|
2844
|
+
}],
|
|
2845
|
+
...isError ? { isError: true } : {}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
function errorResult(err) {
|
|
2849
|
+
return jsonResult({ error: {
|
|
2850
|
+
code: errorCode(err),
|
|
2851
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2852
|
+
} }, true);
|
|
2853
|
+
}
|
|
2854
|
+
function errorCode(err) {
|
|
2855
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
2856
|
+
const code = err.code;
|
|
2857
|
+
if (typeof code === "string") return code;
|
|
2858
|
+
}
|
|
2859
|
+
return "FLEET_ERROR";
|
|
2860
|
+
}
|
|
2861
|
+
function definedObject(input) {
|
|
2862
|
+
const result = {};
|
|
2863
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
|
|
2864
|
+
return result;
|
|
2865
|
+
}
|
|
2866
|
+
function requiredString(args, key) {
|
|
2867
|
+
const value = args[key];
|
|
2868
|
+
if (typeof value !== "string" || value.trim() === "") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
|
|
2869
|
+
return value;
|
|
2870
|
+
}
|
|
2871
|
+
function optionalString(args, key) {
|
|
2872
|
+
const value = args[key];
|
|
2873
|
+
if (value === void 0) return void 0;
|
|
2874
|
+
if (typeof value !== "string") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
|
|
2875
|
+
return value.trim() === "" ? void 0 : value;
|
|
2876
|
+
}
|
|
2877
|
+
function optionalNumber(args, key) {
|
|
2878
|
+
const value = args[key];
|
|
2879
|
+
if (value === void 0) return void 0;
|
|
2880
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
|
|
2881
|
+
return value;
|
|
2882
|
+
}
|
|
2883
|
+
function optionalBoolean(args, key) {
|
|
2884
|
+
const value = args[key];
|
|
2885
|
+
if (value === void 0) return void 0;
|
|
2886
|
+
if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
|
|
2887
|
+
return value;
|
|
2888
|
+
}
|
|
2889
|
+
function optionalStringArray(args, key) {
|
|
2890
|
+
const value = args[key];
|
|
2891
|
+
if (value === void 0) return void 0;
|
|
2892
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
|
|
2893
|
+
return value;
|
|
2894
|
+
}
|
|
2895
|
+
function objectSchema(properties, required) {
|
|
2896
|
+
return {
|
|
2897
|
+
type: "object",
|
|
2898
|
+
required,
|
|
2899
|
+
additionalProperties: false,
|
|
2900
|
+
properties
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
function stringProp(description) {
|
|
2904
|
+
return {
|
|
2905
|
+
type: "string",
|
|
2906
|
+
description
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
function numberProp(description) {
|
|
2910
|
+
return {
|
|
2911
|
+
type: "number",
|
|
2912
|
+
description
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function booleanProp(description) {
|
|
2916
|
+
return {
|
|
2917
|
+
type: "boolean",
|
|
2918
|
+
description
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
function arrayProp(description) {
|
|
2922
|
+
return {
|
|
2923
|
+
type: "array",
|
|
2924
|
+
items: { type: "string" },
|
|
2925
|
+
description
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
|
|
1048
2929
|
//#endregion
|
|
1049
2930
|
//#region src/lib/tree-sitter-grammars.ts
|
|
1050
2931
|
/**
|
|
@@ -5046,7 +6927,7 @@ async function runInit(workspace) {
|
|
|
5046
6927
|
];
|
|
5047
6928
|
const onInactivityCheck = makeIndexProgressProbe(workspace);
|
|
5048
6929
|
const startMs = Date.now();
|
|
5049
|
-
let ok = false;
|
|
6930
|
+
let ok$2 = false;
|
|
5050
6931
|
let failureClass;
|
|
5051
6932
|
try {
|
|
5052
6933
|
const res = await runManagedExeCapture(binary, args, {
|
|
@@ -5063,10 +6944,10 @@ async function runInit(workspace) {
|
|
|
5063
6944
|
}).catch(() => {});
|
|
5064
6945
|
}
|
|
5065
6946
|
});
|
|
5066
|
-
ok = !res.stalled && !res.timedOut && res.code === 0;
|
|
5067
|
-
if (!ok) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
|
|
6947
|
+
ok$2 = !res.stalled && !res.timedOut && res.code === 0;
|
|
6948
|
+
if (!ok$2) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
|
|
5068
6949
|
} catch {
|
|
5069
|
-
ok = false;
|
|
6950
|
+
ok$2 = false;
|
|
5070
6951
|
failureClass = "launch";
|
|
5071
6952
|
} finally {
|
|
5072
6953
|
releaseInit(workspace);
|
|
@@ -5083,9 +6964,9 @@ async function runInit(workspace) {
|
|
|
5083
6964
|
finalMeta.lastIndexedDirty = g.dirty;
|
|
5084
6965
|
}
|
|
5085
6966
|
} catch {}
|
|
5086
|
-
finalMeta.status = ok ? "ready" : "failed";
|
|
6967
|
+
finalMeta.status = ok$2 ? "ready" : "failed";
|
|
5087
6968
|
finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5088
|
-
if (ok) {
|
|
6969
|
+
if (ok$2) {
|
|
5089
6970
|
finalMeta.failedAttempts = 0;
|
|
5090
6971
|
finalMeta.failureClass = void 0;
|
|
5091
6972
|
} else {
|
|
@@ -6164,8 +8045,8 @@ async function isHumanlikeAutoOn(tabId, signal) {
|
|
|
6164
8045
|
} catch {}
|
|
6165
8046
|
return humanlikeAutoCache.tabs.has(tabId);
|
|
6166
8047
|
}
|
|
6167
|
-
async function maybeInjectHumanlikeDelay(tool, signal, tabId) {
|
|
6168
|
-
if (!PACED_TOOLS.has(tool)) return;
|
|
8048
|
+
async function maybeInjectHumanlikeDelay(tool$1, signal, tabId) {
|
|
8049
|
+
if (!PACED_TOOLS.has(tool$1)) return;
|
|
6169
8050
|
let on = state.humanlikeForce === "on";
|
|
6170
8051
|
if (!on && state.humanlikeForce === "auto") on = await isHumanlikeAutoOn(tabId, signal);
|
|
6171
8052
|
if (!on) return;
|
|
@@ -6270,8 +8151,8 @@ const PER_TOOL_TIMEOUTS = {
|
|
|
6270
8151
|
maxMs: 1e4
|
|
6271
8152
|
}
|
|
6272
8153
|
};
|
|
6273
|
-
function pickTimeout(tool) {
|
|
6274
|
-
if (tool in PER_TOOL_TIMEOUTS) return PER_TOOL_TIMEOUTS[tool];
|
|
8154
|
+
function pickTimeout(tool$1) {
|
|
8155
|
+
if (tool$1 in PER_TOOL_TIMEOUTS) return PER_TOOL_TIMEOUTS[tool$1];
|
|
6275
8156
|
return {
|
|
6276
8157
|
defaultMs: 1e4,
|
|
6277
8158
|
maxMs: 3e4
|
|
@@ -6284,7 +8165,7 @@ function pickTimeout(tool) {
|
|
|
6284
8165
|
* client sends notifications/cancelled, the WS is force-closed and
|
|
6285
8166
|
* the promise rejects so the slot releases cleanly.
|
|
6286
8167
|
*/
|
|
6287
|
-
async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
|
|
8168
|
+
async function bridgeCall(endpoint, tool$1, args, timeoutMs, signal) {
|
|
6288
8169
|
return new Promise((resolve, reject) => {
|
|
6289
8170
|
const id = randomUUID();
|
|
6290
8171
|
const ws = new WebSocket(`ws://127.0.0.1:${endpoint.port}`, { headers: { authorization: `Bearer ${endpoint.token}` } });
|
|
@@ -6318,7 +8199,7 @@ async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
|
|
|
6318
8199
|
}
|
|
6319
8200
|
ws.send(JSON.stringify({
|
|
6320
8201
|
id,
|
|
6321
|
-
tool,
|
|
8202
|
+
tool: tool$1,
|
|
6322
8203
|
args
|
|
6323
8204
|
}));
|
|
6324
8205
|
});
|
|
@@ -6377,8 +8258,8 @@ function blockedUrlEnvelope(reason) {
|
|
|
6377
8258
|
* happy-path `ensureBridgeReady()` (and its NMH install) is the accepted
|
|
6378
8259
|
* cost of keeping the credentials fresh.
|
|
6379
8260
|
*/
|
|
6380
|
-
async function browserPreflight(tool, args) {
|
|
6381
|
-
const policy = preflightUrlPolicy(tool.startsWith("browser_") ? tool : `browser_${tool}`, args);
|
|
8261
|
+
async function browserPreflight(tool$1, args) {
|
|
8262
|
+
const policy = preflightUrlPolicy(tool$1.startsWith("browser_") ? tool$1 : `browser_${tool$1}`, args);
|
|
6382
8263
|
if (policy.blocked) return { envelope: blockedUrlEnvelope(policy.reason) };
|
|
6383
8264
|
const ready = await ensureBridgeReady();
|
|
6384
8265
|
if (ready.install_required) return { envelope: installRequiredToolResult(ready) };
|
|
@@ -6389,23 +8270,23 @@ async function browserPreflight(tool, args) {
|
|
|
6389
8270
|
* src/lib/browser-mcp/index.ts. Returns the standard MCP tool-result
|
|
6390
8271
|
* envelope.
|
|
6391
8272
|
*/
|
|
6392
|
-
async function dispatchBrowserTool(tool, args, signal, opts = {}) {
|
|
6393
|
-
const policy = preflightUrlPolicy(tool, args);
|
|
8273
|
+
async function dispatchBrowserTool(tool$1, args, signal, opts = {}) {
|
|
8274
|
+
const policy = preflightUrlPolicy(tool$1, args);
|
|
6394
8275
|
if (policy.blocked) return blockedUrlEnvelope(policy.reason);
|
|
6395
8276
|
const ready = await ensureBridgeReady();
|
|
6396
8277
|
if (ready.install_required) return installRequiredToolResult(ready);
|
|
6397
|
-
await maybeInjectHumanlikeDelay(tool, signal, typeof args.tabId === "number" ? args.tabId : void 0);
|
|
6398
|
-
const { defaultMs, maxMs } = pickTimeout(tool);
|
|
8278
|
+
await maybeInjectHumanlikeDelay(tool$1, signal, typeof args.tabId === "number" ? args.tabId : void 0);
|
|
8279
|
+
const { defaultMs, maxMs } = pickTimeout(tool$1);
|
|
6399
8280
|
const callerTimeout = typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? Math.min(opts.timeoutMs, maxMs) : defaultMs;
|
|
6400
8281
|
try {
|
|
6401
8282
|
const resp = await bridgeCall({
|
|
6402
8283
|
port: ready.port,
|
|
6403
8284
|
token: ready.token
|
|
6404
|
-
}, tool, args, callerTimeout, signal);
|
|
8285
|
+
}, tool$1, args, callerTimeout, signal);
|
|
6405
8286
|
if (resp.ok) {
|
|
6406
8287
|
const text = typeof resp.data === "string" ? resp.data : JSON.stringify(resp.data, null, 2);
|
|
6407
8288
|
logAudit$1({
|
|
6408
|
-
tool,
|
|
8289
|
+
tool: tool$1,
|
|
6409
8290
|
argsBytes: argsByteSize(args),
|
|
6410
8291
|
durationMs: 0,
|
|
6411
8292
|
profile: typeof args.profile === "string" ? args.profile : "isolated",
|
|
@@ -6417,7 +8298,7 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
|
|
|
6417
8298
|
}] };
|
|
6418
8299
|
}
|
|
6419
8300
|
logAudit$1({
|
|
6420
|
-
tool,
|
|
8301
|
+
tool: tool$1,
|
|
6421
8302
|
argsBytes: argsByteSize(args),
|
|
6422
8303
|
durationMs: 0,
|
|
6423
8304
|
profile: typeof args.profile === "string" ? args.profile : "isolated",
|
|
@@ -6427,14 +8308,14 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
|
|
|
6427
8308
|
return {
|
|
6428
8309
|
content: [{
|
|
6429
8310
|
type: "text",
|
|
6430
|
-
text: `${tool} failed: ${resp.error}${resp.code ? ` (${resp.code})` : ""}`
|
|
8311
|
+
text: `${tool$1} failed: ${resp.error}${resp.code ? ` (${resp.code})` : ""}`
|
|
6431
8312
|
}],
|
|
6432
8313
|
isError: true
|
|
6433
8314
|
};
|
|
6434
8315
|
} catch (err) {
|
|
6435
8316
|
const message = err instanceof Error ? err.message : String(err);
|
|
6436
8317
|
logAudit$1({
|
|
6437
|
-
tool,
|
|
8318
|
+
tool: tool$1,
|
|
6438
8319
|
argsBytes: argsByteSize(args),
|
|
6439
8320
|
durationMs: 0,
|
|
6440
8321
|
profile: typeof args.profile === "string" ? args.profile : "isolated",
|
|
@@ -6444,7 +8325,7 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
|
|
|
6444
8325
|
return {
|
|
6445
8326
|
content: [{
|
|
6446
8327
|
type: "text",
|
|
6447
|
-
text: `${tool} failed: ${message}`
|
|
8328
|
+
text: `${tool$1} failed: ${message}`
|
|
6448
8329
|
}],
|
|
6449
8330
|
isError: true
|
|
6450
8331
|
};
|
|
@@ -7405,20 +9286,20 @@ function compressorAvailable() {
|
|
|
7405
9286
|
* free-form `message.content` and strip a leading / trailing ```` ``` ````
|
|
7406
9287
|
* code fence before parsing.
|
|
7407
9288
|
*/
|
|
7408
|
-
async function callCompressor(systemPrompt, userMessage, tool, signal) {
|
|
9289
|
+
async function callCompressor(systemPrompt, userMessage, tool$1, signal) {
|
|
7409
9290
|
const backend = pickBackend();
|
|
7410
9291
|
if (!backend) throw new Error(`browser-mcp compressor: no backend available in catalog. Checked: ${COMPRESSOR_FALLBACK_CHAIN.join(", ")}`);
|
|
7411
9292
|
const release = acquireInFlightSlot();
|
|
7412
9293
|
if (!release) throw new Error("browser-mcp compressor: inflight slot saturated (cap 8); try again shortly");
|
|
7413
9294
|
try {
|
|
7414
|
-
return backend.endpoint === "responses" ? await callViaResponses(backend.id, systemPrompt, userMessage, tool, signal) : await callViaChat(backend.id, systemPrompt, userMessage, tool, signal);
|
|
9295
|
+
return backend.endpoint === "responses" ? await callViaResponses(backend.id, systemPrompt, userMessage, tool$1, signal) : await callViaChat(backend.id, systemPrompt, userMessage, tool$1, signal);
|
|
7415
9296
|
} finally {
|
|
7416
9297
|
release();
|
|
7417
9298
|
}
|
|
7418
9299
|
}
|
|
7419
9300
|
/** Forced-tool-call over `/chat/completions`. Parses the function-call
|
|
7420
9301
|
* arguments, falling back to fenced free-form content. */
|
|
7421
|
-
async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
|
|
9302
|
+
async function callViaChat(model, systemPrompt, userMessage, tool$1, signal) {
|
|
7422
9303
|
const msg = (await createChatCompletions({
|
|
7423
9304
|
model,
|
|
7424
9305
|
stream: false,
|
|
@@ -7432,14 +9313,14 @@ async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
|
|
|
7432
9313
|
tools: [{
|
|
7433
9314
|
type: "function",
|
|
7434
9315
|
function: {
|
|
7435
|
-
name: tool.name,
|
|
7436
|
-
description: tool.description,
|
|
7437
|
-
parameters: tool.parameters
|
|
9316
|
+
name: tool$1.name,
|
|
9317
|
+
description: tool$1.description,
|
|
9318
|
+
parameters: tool$1.parameters
|
|
7438
9319
|
}
|
|
7439
9320
|
}],
|
|
7440
9321
|
tool_choice: {
|
|
7441
9322
|
type: "function",
|
|
7442
|
-
function: { name: tool.name }
|
|
9323
|
+
function: { name: tool$1.name }
|
|
7443
9324
|
}
|
|
7444
9325
|
}, void 0, signal)).choices?.[0]?.message;
|
|
7445
9326
|
const toolArgs = msg?.tool_calls?.[0]?.function?.arguments;
|
|
@@ -7453,7 +9334,7 @@ async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
|
|
|
7453
9334
|
* items of `type: "function_call"` carrying the `arguments` JSON string.
|
|
7454
9335
|
* Image parts use `input_image` (vs chat's `image_url`) — see
|
|
7455
9336
|
* `toResponsesContent`. */
|
|
7456
|
-
async function callViaResponses(model, systemPrompt, userMessage, tool, signal) {
|
|
9337
|
+
async function callViaResponses(model, systemPrompt, userMessage, tool$1, signal) {
|
|
7457
9338
|
const resp = await createResponses({
|
|
7458
9339
|
model,
|
|
7459
9340
|
stream: false,
|
|
@@ -7466,13 +9347,13 @@ async function callViaResponses(model, systemPrompt, userMessage, tool, signal)
|
|
|
7466
9347
|
}],
|
|
7467
9348
|
tools: [{
|
|
7468
9349
|
type: "function",
|
|
7469
|
-
name: tool.name,
|
|
7470
|
-
description: tool.description,
|
|
7471
|
-
parameters: tool.parameters
|
|
9350
|
+
name: tool$1.name,
|
|
9351
|
+
description: tool$1.description,
|
|
9352
|
+
parameters: tool$1.parameters
|
|
7472
9353
|
}],
|
|
7473
9354
|
tool_choice: {
|
|
7474
9355
|
type: "function",
|
|
7475
|
-
name: tool.name
|
|
9356
|
+
name: tool$1.name
|
|
7476
9357
|
}
|
|
7477
9358
|
}, void 0, signal);
|
|
7478
9359
|
const output = Array.isArray(resp.output) ? resp.output : [];
|
|
@@ -7528,8 +9409,8 @@ function extractResponsesText$1(output) {
|
|
|
7528
9409
|
* directly so the underlying function can change signature without
|
|
7529
9410
|
* breaking the public surface.
|
|
7530
9411
|
*/
|
|
7531
|
-
async function callCompressorPublic(systemPrompt, userMessage, tool, signal) {
|
|
7532
|
-
return callCompressor(systemPrompt, userMessage, tool, signal);
|
|
9412
|
+
async function callCompressorPublic(systemPrompt, userMessage, tool$1, signal) {
|
|
9413
|
+
return callCompressor(systemPrompt, userMessage, tool$1, signal);
|
|
7533
9414
|
}
|
|
7534
9415
|
/**
|
|
7535
9416
|
* Strip a single leading / trailing ``` (or ```json) code fence from a
|
|
@@ -8643,12 +10524,12 @@ const BROWSER_TOOLS = Object.freeze([
|
|
|
8643
10524
|
capability: "browser_power",
|
|
8644
10525
|
async handler(args, signal) {
|
|
8645
10526
|
const kind = args.kind === "network" ? "network" : "console";
|
|
8646
|
-
const tool = kind === "network" ? "browser_network_log" : "browser_console_logs";
|
|
10527
|
+
const tool$1 = kind === "network" ? "browser_network_log" : "browser_console_logs";
|
|
8647
10528
|
const tabId = typeof args.tabId === "number" ? args.tabId : void 0;
|
|
8648
10529
|
const level = typeof args.level === "string" ? args.level : "all";
|
|
8649
10530
|
const regexStr = typeof args.regex === "string" ? args.regex : void 0;
|
|
8650
10531
|
const limit = typeof args.limit === "number" ? Math.min(1e3, Math.max(1, args.limit)) : 100;
|
|
8651
|
-
const env = await dispatchBrowserTool(tool, {
|
|
10532
|
+
const env = await dispatchBrowserTool(tool$1, {
|
|
8652
10533
|
tabId,
|
|
8653
10534
|
level
|
|
8654
10535
|
}, signal);
|
|
@@ -9536,12 +11417,12 @@ function formatValidationPath(error) {
|
|
|
9536
11417
|
* @returns The validated (and potentially coerced) arguments
|
|
9537
11418
|
* @throws Error with formatted message if validation fails
|
|
9538
11419
|
*/
|
|
9539
|
-
function validateToolArguments(tool, toolCall) {
|
|
11420
|
+
function validateToolArguments(tool$1, toolCall) {
|
|
9540
11421
|
const args = structuredClone(toolCall.arguments);
|
|
9541
|
-
Value.Convert(tool.parameters, args);
|
|
9542
|
-
const validator = getValidator(tool.parameters);
|
|
9543
|
-
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
|
|
9544
|
-
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
|
11422
|
+
Value.Convert(tool$1.parameters, args);
|
|
11423
|
+
const validator = getValidator(tool$1.parameters);
|
|
11424
|
+
if (!hasTypeBoxMetadata(tool$1.parameters) && isJsonSchemaObject(tool$1.parameters)) {
|
|
11425
|
+
const coerced = coerceWithJsonSchema(args, tool$1.parameters);
|
|
9545
11426
|
if (coerced !== args) if (isRecord(args) && isRecord(coerced)) {
|
|
9546
11427
|
for (const key of Object.keys(args)) delete args[key];
|
|
9547
11428
|
Object.assign(args, coerced);
|
|
@@ -9850,9 +11731,9 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
|
|
|
9850
11731
|
function shouldTerminateToolBatch(finalizedCalls) {
|
|
9851
11732
|
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
|
9852
11733
|
}
|
|
9853
|
-
function prepareToolCallArguments(tool, toolCall) {
|
|
9854
|
-
if (!tool.prepareArguments) return toolCall;
|
|
9855
|
-
const preparedArguments = tool.prepareArguments(toolCall.arguments);
|
|
11734
|
+
function prepareToolCallArguments(tool$1, toolCall) {
|
|
11735
|
+
if (!tool$1.prepareArguments) return toolCall;
|
|
11736
|
+
const preparedArguments = tool$1.prepareArguments(toolCall.arguments);
|
|
9856
11737
|
if (preparedArguments === toolCall.arguments) return toolCall;
|
|
9857
11738
|
return {
|
|
9858
11739
|
...toolCall,
|
|
@@ -9860,14 +11741,14 @@ function prepareToolCallArguments(tool, toolCall) {
|
|
|
9860
11741
|
};
|
|
9861
11742
|
}
|
|
9862
11743
|
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
9863
|
-
const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
|
|
9864
|
-
if (!tool) return {
|
|
11744
|
+
const tool$1 = currentContext.tools?.find((t) => t.name === toolCall.name);
|
|
11745
|
+
if (!tool$1) return {
|
|
9865
11746
|
kind: "immediate",
|
|
9866
11747
|
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
|
|
9867
11748
|
isError: true
|
|
9868
11749
|
};
|
|
9869
11750
|
try {
|
|
9870
|
-
const validatedArgs = validateToolArguments(tool, prepareToolCallArguments(tool, toolCall));
|
|
11751
|
+
const validatedArgs = validateToolArguments(tool$1, prepareToolCallArguments(tool$1, toolCall));
|
|
9871
11752
|
if (config.beforeToolCall) {
|
|
9872
11753
|
const beforeResult = await config.beforeToolCall({
|
|
9873
11754
|
assistantMessage,
|
|
@@ -9894,7 +11775,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
9894
11775
|
return {
|
|
9895
11776
|
kind: "prepared",
|
|
9896
11777
|
toolCall,
|
|
9897
|
-
tool,
|
|
11778
|
+
tool: tool$1,
|
|
9898
11779
|
args: validatedArgs
|
|
9899
11780
|
};
|
|
9900
11781
|
} catch (error) {
|
|
@@ -12192,7 +14073,7 @@ const REPORT_INSUFFICIENT_DESCRIPTION = "Finish by declaring the requested value
|
|
|
12192
14073
|
function makeBrowserTool(meta, parameters, dispatch, sessionId) {
|
|
12193
14074
|
const wireName = `browser_${meta.name}`;
|
|
12194
14075
|
const policy = tabPolicyFor(meta.name);
|
|
12195
|
-
const tool = {
|
|
14076
|
+
const tool$1 = {
|
|
12196
14077
|
name: meta.name,
|
|
12197
14078
|
label: meta.label,
|
|
12198
14079
|
description: meta.description,
|
|
@@ -12217,8 +14098,8 @@ function makeBrowserTool(meta, parameters, dispatch, sessionId) {
|
|
|
12217
14098
|
return textResult$1(text);
|
|
12218
14099
|
}
|
|
12219
14100
|
};
|
|
12220
|
-
if (meta.executionMode) tool.executionMode = meta.executionMode;
|
|
12221
|
-
return tool;
|
|
14101
|
+
if (meta.executionMode) tool$1.executionMode = meta.executionMode;
|
|
14102
|
+
return tool$1;
|
|
12222
14103
|
}
|
|
12223
14104
|
/**
|
|
12224
14105
|
* Build a synthetic terminal tool. `execute` never touches the browser — it
|
|
@@ -12699,9 +14580,9 @@ const calculateParametersTokens = (parameters, encoder, constants) => {
|
|
|
12699
14580
|
/**
|
|
12700
14581
|
* Calculate tokens for a single tool
|
|
12701
14582
|
*/
|
|
12702
|
-
const calculateToolTokens = (tool, encoder, constants) => {
|
|
14583
|
+
const calculateToolTokens = (tool$1, encoder, constants) => {
|
|
12703
14584
|
let tokens = constants.funcInit;
|
|
12704
|
-
const func = tool.function;
|
|
14585
|
+
const func = tool$1.function;
|
|
12705
14586
|
const fName = func.name;
|
|
12706
14587
|
let fDesc = func.description || "";
|
|
12707
14588
|
if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
|
|
@@ -12715,7 +14596,7 @@ const calculateToolTokens = (tool, encoder, constants) => {
|
|
|
12715
14596
|
*/
|
|
12716
14597
|
const numTokensForTools = (tools, encoder, constants) => {
|
|
12717
14598
|
let funcTokenCount = 0;
|
|
12718
|
-
for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
|
|
14599
|
+
for (const tool$1 of tools) funcTokenCount += calculateToolTokens(tool$1, encoder, constants);
|
|
12719
14600
|
funcTokenCount += constants.funcEnd;
|
|
12720
14601
|
return funcTokenCount;
|
|
12721
14602
|
};
|
|
@@ -13001,6 +14882,28 @@ function browserToolsEnabled() {
|
|
|
13001
14882
|
return hasSupportedBrowserInstalled();
|
|
13002
14883
|
}
|
|
13003
14884
|
/**
|
|
14885
|
+
* Gate for the fleet session-control MCP tools (`mcp__fleet__*`).
|
|
14886
|
+
*
|
|
14887
|
+
* Returns true iff the operator opted in (`state.fleetEnabled`, set by
|
|
14888
|
+
* `--fleet`, OR `GH_ROUTER_ENABLE_FLEET=1` read directly so non-
|
|
14889
|
+
* `setupAndServe` startup paths — tests, embedded use — can still flip
|
|
14890
|
+
* the gate). Fleet needs no local installed dependency check.
|
|
14891
|
+
*/
|
|
14892
|
+
function fleetToolsEnabled() {
|
|
14893
|
+
return state.fleetEnabled || process.env.GH_ROUTER_ENABLE_FLEET === "1";
|
|
14894
|
+
}
|
|
14895
|
+
/**
|
|
14896
|
+
* Gate for ai-or-die Artifact review tools.
|
|
14897
|
+
*
|
|
14898
|
+
* Returns true iff this github-router process was launched inside an
|
|
14899
|
+
* ai-or-die tab and received the tab-scoped API trio. The tools are
|
|
14900
|
+
* otherwise invisible at `tools/list` and rejected at `tools/call`; direct
|
|
14901
|
+
* handler calls still return a friendly isError envelope.
|
|
14902
|
+
*/
|
|
14903
|
+
function artifactToolsEnabled() {
|
|
14904
|
+
return !!(process.env.AIORDIE_BASE_URL && process.env.AIORDIE_TOKEN && process.env.AIORDIE_SESSION_ID);
|
|
14905
|
+
}
|
|
14906
|
+
/**
|
|
13004
14907
|
* Gate for the `browse` worker tool (the Pi-driven autonomous browser
|
|
13005
14908
|
* agent that delegates a browsing task to its own context).
|
|
13006
14909
|
*
|
|
@@ -13196,6 +15099,8 @@ function toolEntries(scope) {
|
|
|
13196
15099
|
if (t.capability === "browse_agent") return browseAgentEnabled();
|
|
13197
15100
|
if (t.capability === "stand_in") return standInToolEnabled();
|
|
13198
15101
|
if (t.capability === "browser") return browserToolsEnabled();
|
|
15102
|
+
if (t.capability === "fleet") return fleetToolsEnabled();
|
|
15103
|
+
if (t.capability === "artifact") return artifactToolsEnabled();
|
|
13199
15104
|
if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
|
|
13200
15105
|
if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
|
|
13201
15106
|
return true;
|
|
@@ -13522,6 +15427,8 @@ async function handleToolsCall(body, scope) {
|
|
|
13522
15427
|
if (nonPersonaTool && nonPersonaTool.capability === "browse_agent" && !browseAgentEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
13523
15428
|
if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
13524
15429
|
if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15430
|
+
if (nonPersonaTool && nonPersonaTool.capability === "fleet" && !fleetToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15431
|
+
if (nonPersonaTool && nonPersonaTool.capability === "artifact" && !artifactToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
13525
15432
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
13526
15433
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
13527
15434
|
let personaPrompt;
|
|
@@ -16197,32 +18104,32 @@ function toolbeltTool(workspace) {
|
|
|
16197
18104
|
description: "Run a read-only code-analysis CLI in the workspace with NO shell (args are literal — no pipes / redirects / chaining / globbing). Tools: rg, fd, sg (ast-grep), jq, yq, gron, scc, tokei, difft (difftastic), and git (read-only subcommands). Write/exec flags (fd -x, rg --pre, ast-grep --rewrite, yq -i) and mutating git subcommands are rejected. Returns combined stdout (stderr appended on non-zero exit).",
|
|
16198
18105
|
parameters: TOOLBELT_PARAMS,
|
|
16199
18106
|
async execute(_toolCallId, params, signal) {
|
|
16200
|
-
const tool = params.tool;
|
|
18107
|
+
const tool$1 = params.tool;
|
|
16201
18108
|
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
16202
|
-
if (!TOOLBELT_TOOL_SET.has(tool)) throw new Error(`toolbelt: unknown tool '${tool}'`);
|
|
16203
|
-
if (tool === "git") {
|
|
18109
|
+
if (!TOOLBELT_TOOL_SET.has(tool$1)) throw new Error(`toolbelt: unknown tool '${tool$1}'`);
|
|
18110
|
+
if (tool$1 === "git") {
|
|
16204
18111
|
const sub = args[0];
|
|
16205
18112
|
if (!sub || !GIT_READONLY_SUBCOMMANDS.has(sub)) throw new Error(`git: only read-only subcommands are allowed and the subcommand must be args[0] (no leading -C/-c). Allowed: ${[...GIT_READONLY_SUBCOMMANDS].join(", ")}. Got: ${sub ? `'${sub}'` : "<none>"}`);
|
|
16206
18113
|
for (const arg of args) if (gitArgDenied(arg)) throw new Error(`git: flag '${arg}' is not allowed (toolbelt is read-only)`);
|
|
16207
18114
|
} else {
|
|
16208
|
-
if (tool === "sg" && args[0] && SG_DENIED_SUBCOMMANDS.has(args[0])) throw new Error(`sg: subcommand '${args[0]}' is not allowed (toolbelt is read-only)`);
|
|
16209
|
-
const denied = TOOLBELT_DENIED_FLAGS[tool];
|
|
18115
|
+
if (tool$1 === "sg" && args[0] && SG_DENIED_SUBCOMMANDS.has(args[0])) throw new Error(`sg: subcommand '${args[0]}' is not allowed (toolbelt is read-only)`);
|
|
18116
|
+
const denied = TOOLBELT_DENIED_FLAGS[tool$1];
|
|
16210
18117
|
if (denied) {
|
|
16211
|
-
for (const arg of args) if (argViolatesDenylist(denied, arg)) throw new Error(`${tool}: arg '${arg}' carries a write/exec flag (toolbelt is read-only)`);
|
|
18118
|
+
for (const arg of args) if (argViolatesDenylist(denied, arg)) throw new Error(`${tool$1}: arg '${arg}' carries a write/exec flag (toolbelt is read-only)`);
|
|
16212
18119
|
}
|
|
16213
18120
|
}
|
|
16214
18121
|
const env = buildEnv();
|
|
16215
|
-
if (tool === "git") {
|
|
18122
|
+
if (tool$1 === "git") {
|
|
16216
18123
|
env.GIT_PAGER = "cat";
|
|
16217
18124
|
env.PAGER = "cat";
|
|
16218
18125
|
env.GIT_TERMINAL_PROMPT = "0";
|
|
16219
18126
|
env.GIT_OPTIONAL_LOCKS = "0";
|
|
16220
18127
|
}
|
|
16221
|
-
const binPath = resolveExecutable(tool, { env });
|
|
16222
|
-
if (!binPath) return textResult(`${tool}: not available on this host (not on PATH / toolbelt). rg/fd/jq/yq/sg/gron/scc/difft ship with the toolbelt; git and tokei may require a system install.`);
|
|
18128
|
+
const binPath = resolveExecutable(tool$1, { env });
|
|
18129
|
+
if (!binPath) return textResult(`${tool$1}: not available on this host (not on PATH / toolbelt). rg/fd/jq/yq/sg/gron/scc/difft ship with the toolbelt; git and tokei may require a system install.`);
|
|
16223
18130
|
const TOOLBELT_TIMEOUT_MS = 6e4;
|
|
16224
18131
|
const TOOLBELT_STDOUT_CAP = 1024 * 1024;
|
|
16225
|
-
const res = await runManagedExeCapture(binPath, tool === "git" ? buildGitExecArgs(args) : args, {
|
|
18132
|
+
const res = await runManagedExeCapture(binPath, tool$1 === "git" ? buildGitExecArgs(args) : args, {
|
|
16226
18133
|
cwd: workspace,
|
|
16227
18134
|
env,
|
|
16228
18135
|
timeoutMs: TOOLBELT_TIMEOUT_MS,
|
|
@@ -16232,13 +18139,13 @@ function toolbeltTool(workspace) {
|
|
|
16232
18139
|
else signal?.addEventListener("abort", () => killChildTree(child), { once: true });
|
|
16233
18140
|
}
|
|
16234
18141
|
});
|
|
16235
|
-
if (signal?.aborted) throw new Error(`${tool} aborted`);
|
|
16236
|
-
if (res.timedOut) throw new Error(`${tool} timed out after ${TOOLBELT_TIMEOUT_MS}ms`);
|
|
18142
|
+
if (signal?.aborted) throw new Error(`${tool$1} aborted`);
|
|
18143
|
+
if (res.timedOut) throw new Error(`${tool$1} timed out after ${TOOLBELT_TIMEOUT_MS}ms`);
|
|
16237
18144
|
const parts = [];
|
|
16238
18145
|
if (res.stdout) parts.push(res.stdout);
|
|
16239
18146
|
if ((res.code !== 0 || !res.stdout) && res.stderr.trim()) parts.push(`[stderr] ${res.stderr.trim()}`);
|
|
16240
18147
|
if (res.stdoutTruncated) parts.push(`[truncated at ${TOOLBELT_STDOUT_CAP} bytes — narrow the query]`);
|
|
16241
|
-
if (parts.length === 0) parts.push(`(${tool} exited ${res.code} with no output)`);
|
|
18148
|
+
if (parts.length === 0) parts.push(`(${tool$1} exited ${res.code} with no output)`);
|
|
16242
18149
|
return textResult(parts.join("\n"));
|
|
16243
18150
|
}
|
|
16244
18151
|
};
|
|
@@ -19233,7 +21140,8 @@ const MCP_GROUPS = Object.freeze([
|
|
|
19233
21140
|
"workers",
|
|
19234
21141
|
"orchestrate",
|
|
19235
21142
|
"browser",
|
|
19236
|
-
"decide"
|
|
21143
|
+
"decide",
|
|
21144
|
+
"fleet"
|
|
19237
21145
|
]);
|
|
19238
21146
|
const GROUP_META = Object.freeze({
|
|
19239
21147
|
peers: {
|
|
@@ -19265,6 +21173,11 @@ const GROUP_META = Object.freeze({
|
|
|
19265
21173
|
preferredKey: "decide",
|
|
19266
21174
|
urlSuffix: "decide",
|
|
19267
21175
|
serverInfoName: "github-router-decide"
|
|
21176
|
+
},
|
|
21177
|
+
fleet: {
|
|
21178
|
+
preferredKey: "fleet",
|
|
21179
|
+
urlSuffix: "fleet",
|
|
21180
|
+
serverInfoName: "github-router-fleet"
|
|
19268
21181
|
}
|
|
19269
21182
|
});
|
|
19270
21183
|
/** True iff `s` is a registered group name (route `:group` param validation). */
|
|
@@ -20376,6 +22289,8 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
20376
22289
|
return runStandInToolCall(args, signal);
|
|
20377
22290
|
}
|
|
20378
22291
|
},
|
|
22292
|
+
...ARTIFACT_TOOLS,
|
|
22293
|
+
...FLEET_TOOLS,
|
|
20379
22294
|
...BROWSER_TOOLS.map((t) => ({
|
|
20380
22295
|
...t,
|
|
20381
22296
|
group: "browser",
|
|
@@ -20704,5 +22619,5 @@ async function runStandInToolCall(args, signal) {
|
|
|
20704
22619
|
}
|
|
20705
22620
|
|
|
20706
22621
|
//#endregion
|
|
20707
|
-
export { handleMcpDelete as $, IMPLEMENT_DEFAULT_MODEL as A,
|
|
20708
|
-
//# sourceMappingURL=peer-mcp-personas-
|
|
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 };
|
|
22623
|
+
//# sourceMappingURL=peer-mcp-personas-Be4SAgm0.js.map
|