synartesis 0.8.8 → 0.9.0
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/CHANGELOG.md +116 -0
- package/README.md +78 -4
- package/dist/{chunk-JE7MOCZO.js → chunk-AEEKBR5D.js} +1813 -52
- package/dist/cli.js +710 -1005
- package/dist/proxy.js +182 -49
- package/manifests/aws-docs.yaml +26 -0
- package/manifests/brave.yaml +33 -0
- package/manifests/chrome-devtools.yaml +115 -0
- package/manifests/exa.yaml +20 -0
- package/manifests/fetch.yaml +20 -0
- package/manifests/github.yaml +67 -85
- package/manifests/playwright.yaml +106 -0
- package/manifests/tavily.yaml +27 -0
- package/package.json +1 -1
|
@@ -19,6 +19,78 @@ var PROXY_FLAGS = [
|
|
|
19
19
|
"--token"
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
// src/notify.ts
|
|
23
|
+
import { spawn } from "child_process";
|
|
24
|
+
import { platform } from "os";
|
|
25
|
+
var SILENT = () => void 0;
|
|
26
|
+
var NAME_MAX = 60;
|
|
27
|
+
function shown(name) {
|
|
28
|
+
const clean = name.replace(/[\u0000-\u001f\u007f--]/g, "");
|
|
29
|
+
return clean.length > NAME_MAX ? `${clean.slice(0, NAME_MAX - 1)}\u2026` : clean;
|
|
30
|
+
}
|
|
31
|
+
function unmarked(text) {
|
|
32
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
33
|
+
}
|
|
34
|
+
function words(notice) {
|
|
35
|
+
return {
|
|
36
|
+
title: "Synartesis: a call is waiting for you",
|
|
37
|
+
body: `${shown(notice.server)}.${shown(notice.tool)} -- ${notice.approve}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var KILL_AFTER_MS = 5e3;
|
|
41
|
+
function launch(command, args) {
|
|
42
|
+
try {
|
|
43
|
+
const child = spawn(command, [...args], { stdio: "ignore" });
|
|
44
|
+
child.on("error", () => void 0);
|
|
45
|
+
const timer = setTimeout(() => child.kill(), KILL_AFTER_MS);
|
|
46
|
+
timer.unref();
|
|
47
|
+
child.on("exit", () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
});
|
|
50
|
+
child.unref();
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function desktopNotifier(env = process.env) {
|
|
55
|
+
if (env["SYNARTESIS_NOTIFY"] === "0") {
|
|
56
|
+
return SILENT;
|
|
57
|
+
}
|
|
58
|
+
const os = platform();
|
|
59
|
+
if (os === "darwin") {
|
|
60
|
+
return (notice) => {
|
|
61
|
+
const { title, body } = words(notice);
|
|
62
|
+
launch("osascript", [
|
|
63
|
+
"-e",
|
|
64
|
+
"on run argv",
|
|
65
|
+
"-e",
|
|
66
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
67
|
+
"-e",
|
|
68
|
+
"end run",
|
|
69
|
+
"--",
|
|
70
|
+
title,
|
|
71
|
+
body
|
|
72
|
+
]);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (os === "linux") {
|
|
76
|
+
return (notice) => {
|
|
77
|
+
const { title, body } = words(notice);
|
|
78
|
+
launch("notify-send", ["--app-name=Synartesis", "--", title, unmarked(body)]);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return SILENT;
|
|
82
|
+
}
|
|
83
|
+
function canNotify(env = process.env) {
|
|
84
|
+
if (env["SYNARTESIS_NOTIFY"] === "0") {
|
|
85
|
+
return "switched off by SYNARTESIS_NOTIFY=0";
|
|
86
|
+
}
|
|
87
|
+
const os = platform();
|
|
88
|
+
if (os === "darwin" || os === "linux") {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
return `there is no notifier for ${os} yet; use synartesis watch to see what is waiting`;
|
|
92
|
+
}
|
|
93
|
+
|
|
22
94
|
// src/invocation.ts
|
|
23
95
|
import { spawnSync } from "child_process";
|
|
24
96
|
import { accessSync, constants } from "fs";
|
|
@@ -202,6 +274,7 @@ var NOTHING_RECORDED_YET = [
|
|
|
202
274
|
];
|
|
203
275
|
|
|
204
276
|
// src/journal/journal.ts
|
|
277
|
+
import { randomBytes } from "crypto";
|
|
205
278
|
import { chmodSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
206
279
|
import { hostname } from "os";
|
|
207
280
|
import { dirname as dirname2 } from "path";
|
|
@@ -289,6 +362,84 @@ CREATE TABLE IF NOT EXISTS leases (
|
|
|
289
362
|
claimed_at TEXT NOT NULL
|
|
290
363
|
);
|
|
291
364
|
|
|
365
|
+
-- What each session's servers were started with, so an undo can tell whether
|
|
366
|
+
-- it is about to act on the same thing.
|
|
367
|
+
--
|
|
368
|
+
-- An undo starts the server again, reading its environment from the client
|
|
369
|
+
-- entry that wraps it. If that entry has changed since -- a memory server now
|
|
370
|
+
-- pointed at a different file -- the undo reaches a different store from the
|
|
371
|
+
-- one the session wrote to, sends its inverse there, and reports success. It
|
|
372
|
+
-- cannot tell, because nothing recorded what the session's server was given.
|
|
373
|
+
--
|
|
374
|
+
-- The working directory, and for every variable the client entry or the policy
|
|
375
|
+
-- declares, its name and a keyed HMAC of its value -- never the value. These
|
|
376
|
+
-- are mostly tokens, and a token is a different class of secret from the file
|
|
377
|
+
-- contents the rest of this journal holds: it must not land here in any form
|
|
378
|
+
-- that gives it back. A keyed hash does not, for anything with a token's
|
|
379
|
+
-- entropy, even to someone holding this file. The key is kept in the journal
|
|
380
|
+
-- itself (the secrets table below) rather than beside it: this file is already
|
|
381
|
+
-- the sensitive one and already owner-only, and a second file would be one
|
|
382
|
+
-- more thing to protect and to lose.
|
|
383
|
+
--
|
|
384
|
+
-- Not a schema version bump, for the reason the leases table above is not.
|
|
385
|
+
CREATE TABLE IF NOT EXISTS secrets (
|
|
386
|
+
name TEXT PRIMARY KEY,
|
|
387
|
+
value BLOB NOT NULL
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
CREATE TABLE IF NOT EXISTS run_servers (
|
|
391
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
392
|
+
server TEXT NOT NULL,
|
|
393
|
+
cwd TEXT,
|
|
394
|
+
fingerprints TEXT NOT NULL,
|
|
395
|
+
PRIMARY KEY (run_id, server)
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
-- A person's no, kept apart from every other way a row ends up denied.
|
|
399
|
+
--
|
|
400
|
+
-- The status alone cannot say it. A spent approval is stored as denied, and so
|
|
401
|
+
-- is an approval the client stopped waiting for, and a desktop timeout -- some
|
|
402
|
+
-- of them with the approver's name on the row. So "has anybody refused this
|
|
403
|
+
-- exact call?" asked of the status would tell an agent that arhaan said no to
|
|
404
|
+
-- a call arhaan had approved. Only a person's deny writes here.
|
|
405
|
+
--
|
|
406
|
+
-- lifted_at is a person changing their mind: approving the same row afterwards
|
|
407
|
+
-- lifts the denial rather than leaving two contradictory answers standing.
|
|
408
|
+
--
|
|
409
|
+
-- Not a schema version bump, for the reason the tables above are not.
|
|
410
|
+
CREATE TABLE IF NOT EXISTS denials (
|
|
411
|
+
action_id TEXT PRIMARY KEY REFERENCES actions(id),
|
|
412
|
+
server TEXT NOT NULL,
|
|
413
|
+
tool TEXT NOT NULL,
|
|
414
|
+
denied_by TEXT NOT NULL,
|
|
415
|
+
reason TEXT NOT NULL,
|
|
416
|
+
denied_at TEXT NOT NULL,
|
|
417
|
+
lifted_at TEXT,
|
|
418
|
+
lifted_by TEXT
|
|
419
|
+
);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS denials_recent ON denials(server, tool, denied_at);
|
|
421
|
+
|
|
422
|
+
-- A person saying "stop asking me about this tool" for a while.
|
|
423
|
+
--
|
|
424
|
+
-- Before this the only way to stop being asked was to edit the policy by hand
|
|
425
|
+
-- and restart the client, in the middle of whatever the agent was doing. This
|
|
426
|
+
-- takes effect on the next call, with no reload, and runs out by itself: a
|
|
427
|
+
-- yes that outlives the session it was given for is a yes nobody remembers
|
|
428
|
+
-- giving. stopped_at is the person taking it back before then.
|
|
429
|
+
--
|
|
430
|
+
-- Not a schema version bump, for the reason the tables above are not.
|
|
431
|
+
CREATE TABLE IF NOT EXISTS allows (
|
|
432
|
+
id INTEGER PRIMARY KEY,
|
|
433
|
+
server TEXT NOT NULL,
|
|
434
|
+
tool TEXT NOT NULL,
|
|
435
|
+
allowed_by TEXT NOT NULL,
|
|
436
|
+
allowed_at TEXT NOT NULL,
|
|
437
|
+
until TEXT NOT NULL,
|
|
438
|
+
stopped_at TEXT,
|
|
439
|
+
stopped_by TEXT
|
|
440
|
+
);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS allows_current ON allows(server, tool, until);
|
|
442
|
+
|
|
292
443
|
CREATE INDEX IF NOT EXISTS actions_by_run ON actions(run_id, seq);
|
|
293
444
|
|
|
294
445
|
-- Deliberately not a schema version bump. Adding an index changes no row and
|
|
@@ -424,6 +575,26 @@ var runSchema = z.object({
|
|
|
424
575
|
});
|
|
425
576
|
var seenSchema = z.object({ server: z.string(), ts: z.string() });
|
|
426
577
|
var countedSchema = z.object({ run_id: z.string(), n: z.number() });
|
|
578
|
+
var allowanceSchema = z.object({
|
|
579
|
+
server: z.string(),
|
|
580
|
+
tool: z.string(),
|
|
581
|
+
allowed_by: z.string(),
|
|
582
|
+
allowed_at: z.string(),
|
|
583
|
+
until: z.string()
|
|
584
|
+
});
|
|
585
|
+
function toAllowance(raw) {
|
|
586
|
+
const row = allowanceSchema.parse(raw);
|
|
587
|
+
return { server: row.server, tool: row.tool, by: row.allowed_by, at: row.allowed_at, until: row.until };
|
|
588
|
+
}
|
|
589
|
+
var denialSchema = z.object({
|
|
590
|
+
denied_by: z.string(),
|
|
591
|
+
reason: z.string(),
|
|
592
|
+
denied_at: z.string()
|
|
593
|
+
});
|
|
594
|
+
var runServerSchema = z.object({
|
|
595
|
+
cwd: z.string().nullable(),
|
|
596
|
+
fingerprints: z.string()
|
|
597
|
+
});
|
|
427
598
|
var leaseSchema = z.object({
|
|
428
599
|
action_id: z.string(),
|
|
429
600
|
host: z.string(),
|
|
@@ -1108,7 +1279,17 @@ var SqliteJournal = class {
|
|
|
1108
1279
|
let actions = 0;
|
|
1109
1280
|
const dropActions = this.#db.prepare("DELETE FROM actions WHERE run_id = ?");
|
|
1110
1281
|
const dropRun = this.#db.prepare("DELETE FROM runs WHERE id = ?");
|
|
1282
|
+
const dropServers = this.#db.prepare("DELETE FROM run_servers WHERE run_id = ?");
|
|
1283
|
+
const dropLeases = this.#db.prepare(
|
|
1284
|
+
"DELETE FROM leases WHERE action_id IN (SELECT id FROM actions WHERE run_id = ?)"
|
|
1285
|
+
);
|
|
1286
|
+
const dropDenials = this.#db.prepare(
|
|
1287
|
+
"DELETE FROM denials WHERE action_id IN (SELECT id FROM actions WHERE run_id = ?)"
|
|
1288
|
+
);
|
|
1111
1289
|
for (const id of ids) {
|
|
1290
|
+
dropServers.run(id);
|
|
1291
|
+
dropLeases.run(id);
|
|
1292
|
+
dropDenials.run(id);
|
|
1112
1293
|
actions += dropActions.run(id).changes;
|
|
1113
1294
|
runs += dropRun.run(id).changes;
|
|
1114
1295
|
}
|
|
@@ -1117,6 +1298,131 @@ var SqliteJournal = class {
|
|
|
1117
1298
|
return remove.immediate(runIds);
|
|
1118
1299
|
});
|
|
1119
1300
|
}
|
|
1301
|
+
denyByPerson(actionId, by, reason) {
|
|
1302
|
+
return this.#run(
|
|
1303
|
+
"denyByPerson",
|
|
1304
|
+
() => this.#db.transaction(() => {
|
|
1305
|
+
const changed = this.deny(actionId, by, reason);
|
|
1306
|
+
if (!changed) {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
const row = this.getAction(actionId);
|
|
1310
|
+
if (row === void 0) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
this.#db.prepare(
|
|
1314
|
+
`INSERT INTO denials (action_id, server, tool, denied_by, reason, denied_at)
|
|
1315
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1316
|
+
).run(actionId, row.server, row.tool, by, reason, (/* @__PURE__ */ new Date()).toISOString());
|
|
1317
|
+
return true;
|
|
1318
|
+
}).immediate()
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
findDenial(query) {
|
|
1322
|
+
return this.#run("findDenial", () => {
|
|
1323
|
+
const raws = this.#db.prepare(
|
|
1324
|
+
`SELECT a.*, d.denied_by, d.reason, d.denied_at
|
|
1325
|
+
FROM denials d INDEXED BY denials_recent
|
|
1326
|
+
JOIN actions a ON a.id = d.action_id
|
|
1327
|
+
WHERE d.server = ? AND d.tool = ? AND d.lifted_at IS NULL AND d.denied_at >= ?
|
|
1328
|
+
ORDER BY d.denied_at DESC`
|
|
1329
|
+
).all(query.server, query.tool, query.notBefore);
|
|
1330
|
+
const rows = raws.map((raw) => ({ action: toAction(raw), denial: denialSchema.parse(raw) }));
|
|
1331
|
+
const found = sameCall(
|
|
1332
|
+
rows.map((row) => row.action),
|
|
1333
|
+
query.args
|
|
1334
|
+
);
|
|
1335
|
+
const hit = found === void 0 ? void 0 : rows.find((row) => row.action.id === found.id);
|
|
1336
|
+
return hit === void 0 ? void 0 : { action: hit.action, by: hit.denial.denied_by, reason: hit.denial.reason, at: hit.denial.denied_at };
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
reverseDenial(actionId, by) {
|
|
1340
|
+
return this.#run(
|
|
1341
|
+
"reverseDenial",
|
|
1342
|
+
() => this.#db.transaction(() => {
|
|
1343
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1344
|
+
const standing2 = this.#db.prepare("SELECT 1 FROM denials WHERE action_id = ? AND lifted_at IS NULL").get(actionId);
|
|
1345
|
+
if (standing2 === void 0) {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
const flipped = this.#db.prepare(
|
|
1349
|
+
`UPDATE actions SET status = 'approved', approved_by = ?, approved_at = ?, error = NULL
|
|
1350
|
+
WHERE id = ? AND status = 'denied'`
|
|
1351
|
+
).run(by, now, actionId);
|
|
1352
|
+
if (flipped.changes !== 1) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
this.#db.prepare("UPDATE denials SET lifted_at = ?, lifted_by = ? WHERE action_id = ?").run(now, by, actionId);
|
|
1356
|
+
return true;
|
|
1357
|
+
}).immediate()
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
allow(server, tool, by, until) {
|
|
1361
|
+
this.#run("allow", () => {
|
|
1362
|
+
this.#db.prepare(
|
|
1363
|
+
"INSERT INTO allows (server, tool, allowed_by, allowed_at, until) VALUES (?, ?, ?, ?, ?)"
|
|
1364
|
+
).run(server, tool, by, (/* @__PURE__ */ new Date()).toISOString(), until);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
findAllowance(server, tool, now) {
|
|
1368
|
+
return this.#run("findAllowance", () => {
|
|
1369
|
+
const raw = this.#db.prepare(
|
|
1370
|
+
`SELECT * FROM allows INDEXED BY allows_current
|
|
1371
|
+
WHERE server = ? AND tool = ? AND until > ? AND stopped_at IS NULL
|
|
1372
|
+
ORDER BY until DESC LIMIT 1`
|
|
1373
|
+
).get(server, tool, now);
|
|
1374
|
+
return raw === void 0 ? void 0 : toAllowance(raw);
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
listAllowances(now) {
|
|
1378
|
+
return this.#run(
|
|
1379
|
+
"listAllowances",
|
|
1380
|
+
() => this.#db.prepare("SELECT * FROM allows WHERE until > ? AND stopped_at IS NULL ORDER BY until").all(now).map(toAllowance)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
stopAllowance(server, tool, by, now) {
|
|
1384
|
+
return this.#run("stopAllowance", () => {
|
|
1385
|
+
const result = this.#db.prepare(
|
|
1386
|
+
`UPDATE allows SET stopped_at = ?, stopped_by = ?
|
|
1387
|
+
WHERE server = ? AND tool = ? AND until > ? AND stopped_at IS NULL`
|
|
1388
|
+
).run(now, by, server, tool, now);
|
|
1389
|
+
return result.changes > 0;
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
markAllowed(actionId, by) {
|
|
1393
|
+
this.#run("markAllowed", () => {
|
|
1394
|
+
this.#db.prepare(
|
|
1395
|
+
"UPDATE actions SET approved_by = ?, approved_at = ? WHERE id = ? AND status = 'pending'"
|
|
1396
|
+
).run(by, (/* @__PURE__ */ new Date()).toISOString(), actionId);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
recordRunServer(runId, server, cwd, fingerprints) {
|
|
1400
|
+
this.#run("recordRunServer", () => {
|
|
1401
|
+
this.#db.prepare(
|
|
1402
|
+
`INSERT INTO run_servers (run_id, server, cwd, fingerprints) VALUES (?, ?, ?, ?)
|
|
1403
|
+
ON CONFLICT(run_id, server) DO UPDATE SET cwd = excluded.cwd,
|
|
1404
|
+
fingerprints = excluded.fingerprints`
|
|
1405
|
+
).run(runId, server, cwd ?? null, JSON.stringify(fingerprints));
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
runServer(runId, server) {
|
|
1409
|
+
return this.#run("runServer", () => {
|
|
1410
|
+
const raw = this.#db.prepare("SELECT cwd, fingerprints FROM run_servers WHERE run_id = ? AND server = ?").get(runId, server);
|
|
1411
|
+
if (raw === void 0) {
|
|
1412
|
+
return void 0;
|
|
1413
|
+
}
|
|
1414
|
+
const row = runServerSchema.parse(raw);
|
|
1415
|
+
const fingerprints = z.record(z.string(), z.string()).parse(JSON.parse(row.fingerprints));
|
|
1416
|
+
return { ...row.cwd === null ? {} : { cwd: row.cwd }, fingerprints };
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
fingerprintKey() {
|
|
1420
|
+
return this.#run("fingerprintKey", () => {
|
|
1421
|
+
this.#db.prepare("INSERT OR IGNORE INTO secrets (name, value) VALUES ('fingerprint', ?)").run(randomBytes(32));
|
|
1422
|
+
const row = z.object({ value: z.instanceof(Buffer) }).parse(this.#db.prepare("SELECT value FROM secrets WHERE name = 'fingerprint'").get());
|
|
1423
|
+
return row.value;
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1120
1426
|
vacuum() {
|
|
1121
1427
|
this.#run("vacuum", () => {
|
|
1122
1428
|
this.#db.exec("VACUUM");
|
|
@@ -1333,11 +1639,39 @@ var serverSpec = z2.strictObject({
|
|
|
1333
1639
|
command: z2.string().min(1),
|
|
1334
1640
|
args: z2.array(z2.string()).default([]),
|
|
1335
1641
|
env: z2.record(z2.string(), z2.string()).optional(),
|
|
1336
|
-
provenance: z2.enum(["live", "documented"]).optional()
|
|
1642
|
+
provenance: z2.enum(["live", "documented"]).optional(),
|
|
1643
|
+
trust_annotations: z2.boolean().optional()
|
|
1644
|
+
});
|
|
1645
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1646
|
+
var remoteSpec = z2.strictObject({
|
|
1647
|
+
url: z2.string().refine((value) => URL.canParse(value), "url must be a full address, like https://example.com/mcp").refine((value) => {
|
|
1648
|
+
const url = new URL(value);
|
|
1649
|
+
return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK.has(url.hostname);
|
|
1650
|
+
}, "url must be https, since it carries a token; plain http is accepted only for this machine"),
|
|
1651
|
+
transport: z2.enum(["auto", "http", "sse"]).default("auto"),
|
|
1652
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1653
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
1654
|
+
provenance: z2.enum(["live", "documented"]).optional(),
|
|
1655
|
+
trust_annotations: z2.boolean().optional()
|
|
1656
|
+
});
|
|
1657
|
+
var anyServer = z2.unknown().transform((value, context) => {
|
|
1658
|
+
const has = (key) => typeof value === "object" && value !== null && key in value;
|
|
1659
|
+
if (has("command") && has("url")) {
|
|
1660
|
+
context.addIssue({ code: "custom", message: "give command or url, not both" });
|
|
1661
|
+
return z2.NEVER;
|
|
1662
|
+
}
|
|
1663
|
+
const parsed = (has("url") ? remoteSpec : serverSpec).safeParse(value);
|
|
1664
|
+
if (!parsed.success) {
|
|
1665
|
+
for (const issue of parsed.error.issues) {
|
|
1666
|
+
context.addIssue({ ...issue, code: "custom", message: issue.message });
|
|
1667
|
+
}
|
|
1668
|
+
return z2.NEVER;
|
|
1669
|
+
}
|
|
1670
|
+
return parsed.data;
|
|
1337
1671
|
});
|
|
1338
1672
|
var manifestSchema = z2.strictObject({
|
|
1339
1673
|
version: z2.literal(1),
|
|
1340
|
-
servers: z2.record(z2.string(),
|
|
1674
|
+
servers: z2.record(z2.string(), anyServer),
|
|
1341
1675
|
tools: z2.array(toolPolicy).default([]),
|
|
1342
1676
|
pins: z2.record(z2.string(), z2.record(z2.string(), z2.string().min(1))).optional()
|
|
1343
1677
|
});
|
|
@@ -1366,22 +1700,17 @@ var Source = class {
|
|
|
1366
1700
|
throw new ManifestError(message, this.locate(path));
|
|
1367
1701
|
}
|
|
1368
1702
|
};
|
|
1369
|
-
var
|
|
1370
|
-
function
|
|
1371
|
-
const expanded = {};
|
|
1703
|
+
var MALFORMED = /\$\{(?![A-Za-z_][A-Za-z0-9_]*\})/;
|
|
1704
|
+
function checkReferences(source, path, env, field = "env") {
|
|
1372
1705
|
for (const [key, value] of Object.entries(env)) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
return found;
|
|
1382
|
-
});
|
|
1706
|
+
if (MALFORMED.test(value)) {
|
|
1707
|
+
source.fail(
|
|
1708
|
+
[...path, field, key],
|
|
1709
|
+
"a reference is written ${NAME}: letters, digits and underscores, not starting with a digit"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1383
1712
|
}
|
|
1384
|
-
return
|
|
1713
|
+
return env;
|
|
1385
1714
|
}
|
|
1386
1715
|
function serverSegment(pattern) {
|
|
1387
1716
|
const dot = pattern.indexOf(".");
|
|
@@ -1395,6 +1724,47 @@ function matchesAnyServer(segment, servers) {
|
|
|
1395
1724
|
const test = new RegExp(`^${source}$`);
|
|
1396
1725
|
return servers.some((name) => test.test(name));
|
|
1397
1726
|
}
|
|
1727
|
+
function stringsIn(value, path) {
|
|
1728
|
+
if (typeof value === "string") {
|
|
1729
|
+
return [[path, value]];
|
|
1730
|
+
}
|
|
1731
|
+
if (Array.isArray(value)) {
|
|
1732
|
+
return value.flatMap((item, index) => stringsIn(item, [...path, index]));
|
|
1733
|
+
}
|
|
1734
|
+
if (value !== null && typeof value === "object") {
|
|
1735
|
+
return Object.entries(value).flatMap(([key, item]) => stringsIn(item, [...path, key]));
|
|
1736
|
+
}
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
function spelled(inner) {
|
|
1740
|
+
const trimmed = inner.trim().replace(/^\$/, "");
|
|
1741
|
+
for (const [from, to] of [["args.", "$."], ["snapshot.", "$snapshot."], ["result.", "$result."]]) {
|
|
1742
|
+
if (trimmed.startsWith(from)) {
|
|
1743
|
+
return `"${to}${trimmed.slice(from.length)}"`;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return `"$.${trimmed}"`;
|
|
1747
|
+
}
|
|
1748
|
+
function checkSpelling(source, path, call) {
|
|
1749
|
+
if (call === void 0) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
for (const [at, text] of stringsIn(call.args, [...path, "args"])) {
|
|
1753
|
+
const braces = /\{\{\s*([^}]*?)\s*\}\}/.exec(text);
|
|
1754
|
+
if (braces !== null) {
|
|
1755
|
+
source.fail(at, `${braces[0]} is sent as written; write ${spelled(braces[1] ?? "")} instead`);
|
|
1756
|
+
}
|
|
1757
|
+
const dollar = /^\$\{([^}]*)\}$/.exec(text);
|
|
1758
|
+
if (dollar !== null) {
|
|
1759
|
+
source.fail(at, `${text} is not a reference here; write ${spelled(dollar[1] ?? "")} instead`);
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
referencesIn(text);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
source.fail(at, error instanceof Error ? error.message : String(error));
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1398
1768
|
function checkCall(source, path, call, servers, allowed) {
|
|
1399
1769
|
const segment = serverSegment(call.tool);
|
|
1400
1770
|
if (segment === "" || call.tool.endsWith(".")) {
|
|
@@ -1438,6 +1808,9 @@ function validate(source, manifest) {
|
|
|
1438
1808
|
);
|
|
1439
1809
|
}
|
|
1440
1810
|
seen.set(policy.match, index);
|
|
1811
|
+
checkSpelling(source, [...path, "snapshot"], policy.snapshot);
|
|
1812
|
+
checkSpelling(source, [...path, "inverse"], policy.inverse);
|
|
1813
|
+
checkSpelling(source, [...path, "verify"], policy.verify);
|
|
1441
1814
|
const segment = serverSegment(policy.match);
|
|
1442
1815
|
if (segment === "") {
|
|
1443
1816
|
source.fail([...path, "match"], `${policy.match} must be qualified as server.tool`);
|
|
@@ -1550,10 +1923,14 @@ function parseManifest(text, file) {
|
|
|
1550
1923
|
Object.entries(parsed.data.servers).map(([name, spec]) => [
|
|
1551
1924
|
name,
|
|
1552
1925
|
{
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1926
|
+
..."url" in spec ? {
|
|
1927
|
+
url: spec.url,
|
|
1928
|
+
transport: spec.transport,
|
|
1929
|
+
...spec.headers === void 0 ? {} : { headers: checkReferences(source, ["servers", name], spec.headers, "headers") }
|
|
1930
|
+
} : { command: spec.command, args: spec.args },
|
|
1931
|
+
...spec.env === void 0 ? {} : { env: checkReferences(source, ["servers", name], spec.env) },
|
|
1932
|
+
...spec.provenance === void 0 ? {} : { provenance: spec.provenance },
|
|
1933
|
+
...spec.trust_annotations === void 0 ? {} : { trustAnnotations: spec.trust_annotations }
|
|
1557
1934
|
}
|
|
1558
1935
|
])
|
|
1559
1936
|
),
|
|
@@ -1581,9 +1958,6 @@ function loadManifest(path) {
|
|
|
1581
1958
|
return parseManifest(text, path);
|
|
1582
1959
|
}
|
|
1583
1960
|
|
|
1584
|
-
// src/manifest/pin.ts
|
|
1585
|
-
import { createHash } from "crypto";
|
|
1586
|
-
|
|
1587
1961
|
// src/manifest/match.ts
|
|
1588
1962
|
function toRegExp(pattern) {
|
|
1589
1963
|
const source = pattern.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
|
|
@@ -1618,6 +1992,9 @@ function createPolicyResolver(manifest) {
|
|
|
1618
1992
|
}
|
|
1619
1993
|
|
|
1620
1994
|
// src/manifest/types.ts
|
|
1995
|
+
function isRemote(spec) {
|
|
1996
|
+
return spec.url !== void 0;
|
|
1997
|
+
}
|
|
1621
1998
|
function qualify(server, tool) {
|
|
1622
1999
|
return `${server}.${tool}`;
|
|
1623
2000
|
}
|
|
@@ -1630,6 +2007,7 @@ function splitQualified(qualified) {
|
|
|
1630
2007
|
}
|
|
1631
2008
|
|
|
1632
2009
|
// src/manifest/pin.ts
|
|
2010
|
+
import { createHash } from "crypto";
|
|
1633
2011
|
function fingerprint(inputSchema) {
|
|
1634
2012
|
return `sha256:${createHash("sha256").update(canonical(inputSchema)).digest("hex")}`;
|
|
1635
2013
|
}
|
|
@@ -1640,9 +2018,9 @@ function auditPins(server, advertised, manifest) {
|
|
|
1640
2018
|
}
|
|
1641
2019
|
const resolver = createPolicyResolver(manifest);
|
|
1642
2020
|
const faults = [];
|
|
1643
|
-
const
|
|
2021
|
+
const present2 = /* @__PURE__ */ new Set();
|
|
1644
2022
|
for (const tool of advertised) {
|
|
1645
|
-
|
|
2023
|
+
present2.add(tool.name);
|
|
1646
2024
|
if (!resolver.resolve(qualify(server, tool.name)).matched) {
|
|
1647
2025
|
continue;
|
|
1648
2026
|
}
|
|
@@ -1655,7 +2033,7 @@ function auditPins(server, advertised, manifest) {
|
|
|
1655
2033
|
}
|
|
1656
2034
|
}
|
|
1657
2035
|
for (const name of Object.keys(pins)) {
|
|
1658
|
-
if (!
|
|
2036
|
+
if (!present2.has(name)) {
|
|
1659
2037
|
faults.push({ kind: "gone", tool: name });
|
|
1660
2038
|
}
|
|
1661
2039
|
}
|
|
@@ -1695,7 +2073,13 @@ function explainPins(server, faults) {
|
|
|
1695
2073
|
// src/manifest/verify.ts
|
|
1696
2074
|
import { z as z3 } from "zod";
|
|
1697
2075
|
var listSchema = z3.looseObject({
|
|
1698
|
-
tools: z3.array(
|
|
2076
|
+
tools: z3.array(
|
|
2077
|
+
z3.looseObject({
|
|
2078
|
+
name: z3.string(),
|
|
2079
|
+
inputSchema: z3.unknown(),
|
|
2080
|
+
annotations: z3.looseObject({ readOnlyHint: z3.unknown().optional() }).optional().catch(void 0)
|
|
2081
|
+
})
|
|
2082
|
+
),
|
|
1699
2083
|
nextCursor: z3.string().optional()
|
|
1700
2084
|
});
|
|
1701
2085
|
async function toolShapes(upstream) {
|
|
@@ -1709,7 +2093,11 @@ async function toolShapes(upstream) {
|
|
|
1709
2093
|
)
|
|
1710
2094
|
);
|
|
1711
2095
|
for (const tool of page.tools) {
|
|
1712
|
-
shapes.push({
|
|
2096
|
+
shapes.push({
|
|
2097
|
+
name: tool.name,
|
|
2098
|
+
inputSchema: tool.inputSchema,
|
|
2099
|
+
...tool.annotations?.readOnlyHint === true ? { readOnly: true } : {}
|
|
2100
|
+
});
|
|
1713
2101
|
}
|
|
1714
2102
|
cursor = page.nextCursor;
|
|
1715
2103
|
} while (cursor !== void 0);
|
|
@@ -1766,6 +2154,36 @@ async function verifyAgainstServers(upstreams, manifest) {
|
|
|
1766
2154
|
);
|
|
1767
2155
|
}
|
|
1768
2156
|
}
|
|
2157
|
+
async function withoutMissingTools(upstreams, manifest) {
|
|
2158
|
+
const available = /* @__PURE__ */ new Map();
|
|
2159
|
+
for (const upstream of upstreams) {
|
|
2160
|
+
available.set(upstream.name, new Set((await toolShapes(upstream)).map((tool) => tool.name)));
|
|
2161
|
+
}
|
|
2162
|
+
const missing = (qualified) => {
|
|
2163
|
+
const target = splitQualified(qualified);
|
|
2164
|
+
if (target === void 0) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
const names = available.get(target.server);
|
|
2168
|
+
return names !== void 0 && !names.has(target.tool);
|
|
2169
|
+
};
|
|
2170
|
+
const disabled = [];
|
|
2171
|
+
const tools = manifest.tools.map((policy) => {
|
|
2172
|
+
const gone = [
|
|
2173
|
+
["snapshot", policy.snapshot?.tool],
|
|
2174
|
+
["inverse", policy.inverse?.tool],
|
|
2175
|
+
["verify", policy.verify?.tool]
|
|
2176
|
+
].filter((pair) => pair[1] !== void 0 && missing(pair[1]));
|
|
2177
|
+
if (gone.length === 0) {
|
|
2178
|
+
return policy;
|
|
2179
|
+
}
|
|
2180
|
+
disabled.push(
|
|
2181
|
+
`${policy.match}: its ${gone.map(([role, tool]) => `${role} ${tool}`).join(" and ")} ${gone.length === 1 ? "is" : "are"} gone from the server, so it is held until the policy is updated`
|
|
2182
|
+
);
|
|
2183
|
+
return { match: policy.match, class: "irreversible", gate: "always", refusal: "uncertain" };
|
|
2184
|
+
});
|
|
2185
|
+
return { manifest: { ...manifest, tools }, disabled };
|
|
2186
|
+
}
|
|
1769
2187
|
|
|
1770
2188
|
// src/manifest/standing.ts
|
|
1771
2189
|
function standing(manifest) {
|
|
@@ -1793,6 +2211,9 @@ function warnUntested(servers) {
|
|
|
1793
2211
|
const these = servers.length === 1 ? "this policy has" : "these policies have";
|
|
1794
2212
|
return `${names}: ${these} never been run against the real server. The classes and inverses here come from documentation, so undo may not work where it says it will. Run \`synartesis check\` against your own credentials, and expect to correct something.`;
|
|
1795
2213
|
}
|
|
2214
|
+
function trustsMarks(manifest, server) {
|
|
2215
|
+
return manifest.servers[server]?.trustAnnotations !== false && manifest.pins?.[server] === void 0;
|
|
2216
|
+
}
|
|
1796
2217
|
function ungoverned(manifest, advertised) {
|
|
1797
2218
|
const resolver = createPolicyResolver(manifest);
|
|
1798
2219
|
const found = [];
|
|
@@ -1861,9 +2282,73 @@ function createRouter(upstreams, manifest) {
|
|
|
1861
2282
|
};
|
|
1862
2283
|
}
|
|
1863
2284
|
|
|
2285
|
+
// src/proxy/environment.ts
|
|
2286
|
+
import { createHmac } from "crypto";
|
|
2287
|
+
var LAUNCHER = /^(SYNARTESIS_|npm_)/;
|
|
2288
|
+
var LAUNCHER_EXACT = /* @__PURE__ */ new Set(["INIT_CWD", "NODE"]);
|
|
2289
|
+
function ownedByTheLauncher(name) {
|
|
2290
|
+
return LAUNCHER.test(name) || LAUNCHER_EXACT.has(name);
|
|
2291
|
+
}
|
|
2292
|
+
var REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2293
|
+
function expandReferences(server, key, value, lookup) {
|
|
2294
|
+
return value.replace(REFERENCE, (whole, name) => {
|
|
2295
|
+
const found = lookup(name);
|
|
2296
|
+
if (found === void 0) {
|
|
2297
|
+
throw new ManifestError(
|
|
2298
|
+
`server ${server} needs ${whole} for ${key}, and it is not set; set ${name} where this server is started, or write the value in the policy`
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
return found;
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
function present(env) {
|
|
2305
|
+
const out = {};
|
|
2306
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2307
|
+
if (value !== void 0) {
|
|
2308
|
+
out[key] = value;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return out;
|
|
2312
|
+
}
|
|
2313
|
+
function upstreamEnv(server, spec, source) {
|
|
2314
|
+
const own = Object.fromEntries(
|
|
2315
|
+
Object.entries(present(process.env)).filter(([name]) => !ownedByTheLauncher(name))
|
|
2316
|
+
);
|
|
2317
|
+
const base = source.kind === "inherit" ? own : source.kind === "client" ? { ...source.own === false ? {} : own, ...source.env } : {};
|
|
2318
|
+
const lookup = (name) => base[name] ?? process.env[name];
|
|
2319
|
+
const declared = {};
|
|
2320
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
2321
|
+
declared[key] = expandReferences(server, key, value, lookup);
|
|
2322
|
+
}
|
|
2323
|
+
if (source.kind === "manifest") {
|
|
2324
|
+
return spec.env === void 0 ? void 0 : declared;
|
|
2325
|
+
}
|
|
2326
|
+
return { ...base, ...declared };
|
|
2327
|
+
}
|
|
2328
|
+
var ABSENT = "-";
|
|
2329
|
+
function fingerprint2(key, env, names) {
|
|
2330
|
+
const out = {};
|
|
2331
|
+
for (const name of [...new Set(names)].sort()) {
|
|
2332
|
+
const value = env?.[name];
|
|
2333
|
+
out[name] = value === void 0 ? ABSENT : createHmac("sha256", key).update(value).digest("hex");
|
|
2334
|
+
}
|
|
2335
|
+
return out;
|
|
2336
|
+
}
|
|
2337
|
+
function differing(recorded, now) {
|
|
2338
|
+
return Object.keys(recorded).filter((name) => recorded[name] !== now[name]).sort();
|
|
2339
|
+
}
|
|
2340
|
+
function declaredNames(spec, client) {
|
|
2341
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(spec.env ?? {}), ...Object.keys(client ?? {})])].sort();
|
|
2342
|
+
}
|
|
2343
|
+
|
|
1864
2344
|
// src/proxy/upstream.ts
|
|
1865
2345
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2346
|
+
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1866
2347
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2348
|
+
import {
|
|
2349
|
+
StreamableHTTPClientTransport,
|
|
2350
|
+
StreamableHTTPError
|
|
2351
|
+
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1867
2352
|
function describeError(error) {
|
|
1868
2353
|
return error instanceof Error ? error.message : String(error);
|
|
1869
2354
|
}
|
|
@@ -1874,10 +2359,10 @@ function settled(stream, ms) {
|
|
|
1874
2359
|
if (stream.readableEnded === true) {
|
|
1875
2360
|
return Promise.resolve();
|
|
1876
2361
|
}
|
|
1877
|
-
return new Promise((
|
|
2362
|
+
return new Promise((resolve5) => {
|
|
1878
2363
|
const done = () => {
|
|
1879
2364
|
clearTimeout(timer);
|
|
1880
|
-
|
|
2365
|
+
resolve5();
|
|
1881
2366
|
};
|
|
1882
2367
|
const timer = setTimeout(done, ms);
|
|
1883
2368
|
timer.unref?.();
|
|
@@ -1915,8 +2400,22 @@ function lastWords(text) {
|
|
|
1915
2400
|
if (named !== void 0) {
|
|
1916
2401
|
return named;
|
|
1917
2402
|
}
|
|
1918
|
-
const
|
|
1919
|
-
return
|
|
2403
|
+
const kept2 = lines.slice(-4).join("; ");
|
|
2404
|
+
return kept2 === "" ? void 0 : kept2;
|
|
2405
|
+
}
|
|
2406
|
+
async function connectUpstream(name, spec, options) {
|
|
2407
|
+
const env = upstreamEnv(name, spec, options.env);
|
|
2408
|
+
if (isRemote(spec)) {
|
|
2409
|
+
return await connectRemoteUpstream(name, spec, (variable) => env?.[variable] ?? process.env[variable]);
|
|
2410
|
+
}
|
|
2411
|
+
return await connectStdioUpstream({
|
|
2412
|
+
name,
|
|
2413
|
+
command: spec.command,
|
|
2414
|
+
args: spec.args,
|
|
2415
|
+
...env === void 0 ? {} : { env },
|
|
2416
|
+
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
2417
|
+
...options.stderr === void 0 ? {} : { stderr: options.stderr }
|
|
2418
|
+
});
|
|
1920
2419
|
}
|
|
1921
2420
|
async function connectStdioUpstream(spec) {
|
|
1922
2421
|
const started = await start(spec);
|
|
@@ -1941,6 +2440,7 @@ async function start(spec) {
|
|
|
1941
2440
|
command: spec.command,
|
|
1942
2441
|
args: [...spec.args ?? []],
|
|
1943
2442
|
...spec.env === void 0 ? {} : { env: { ...spec.env } },
|
|
2443
|
+
...spec.cwd === void 0 ? {} : { cwd: spec.cwd },
|
|
1944
2444
|
// "pipe" is what the sdk calls it; captured here so a failure can quote it.
|
|
1945
2445
|
stderr: wanted === "capture" ? "pipe" : wanted
|
|
1946
2446
|
});
|
|
@@ -1970,6 +2470,1243 @@ async function start(spec) {
|
|
|
1970
2470
|
}
|
|
1971
2471
|
return { client };
|
|
1972
2472
|
}
|
|
2473
|
+
async function connectRemoteUpstream(name, spec, lookup) {
|
|
2474
|
+
const headers = {};
|
|
2475
|
+
for (const [key, value] of Object.entries(spec.headers ?? {})) {
|
|
2476
|
+
headers[key] = expandReferences(name, key, value, lookup);
|
|
2477
|
+
}
|
|
2478
|
+
const url = new URL(spec.url);
|
|
2479
|
+
const requestInit = { headers, redirect: "error" };
|
|
2480
|
+
const open = async (kind2) => {
|
|
2481
|
+
const client = new Client({ ...PROXY_CLIENT_INFO });
|
|
2482
|
+
if (kind2 === "http") {
|
|
2483
|
+
await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
|
|
2484
|
+
} else {
|
|
2485
|
+
await client.connect(new SSEClientTransport(url, { requestInit }));
|
|
2486
|
+
}
|
|
2487
|
+
return client;
|
|
2488
|
+
};
|
|
2489
|
+
let kind = spec.transport === "sse" ? "sse" : "http";
|
|
2490
|
+
let current;
|
|
2491
|
+
try {
|
|
2492
|
+
current = await open(kind);
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
const code = statusOf(error);
|
|
2495
|
+
if (spec.transport === "auto" && (code === 400 || code === 404 || code === 405)) {
|
|
2496
|
+
kind = "sse";
|
|
2497
|
+
try {
|
|
2498
|
+
current = await open(kind);
|
|
2499
|
+
} catch (fallback) {
|
|
2500
|
+
throw new UpstreamError(name, "connect", fallback);
|
|
2501
|
+
}
|
|
2502
|
+
} else {
|
|
2503
|
+
throw new UpstreamError(name, "connect", refusal(error) ?? error);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return {
|
|
2507
|
+
name,
|
|
2508
|
+
get client() {
|
|
2509
|
+
return current;
|
|
2510
|
+
},
|
|
2511
|
+
async reconnect() {
|
|
2512
|
+
await current.close().catch(() => void 0);
|
|
2513
|
+
current = await open(kind);
|
|
2514
|
+
},
|
|
2515
|
+
classify(error) {
|
|
2516
|
+
const code = statusOf(error);
|
|
2517
|
+
if (code === void 0 || code < 400 || code >= 500) {
|
|
2518
|
+
return void 0;
|
|
2519
|
+
}
|
|
2520
|
+
return code === 404 ? "lost" : "not-sent";
|
|
2521
|
+
},
|
|
2522
|
+
close: async () => {
|
|
2523
|
+
await current.close();
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
function statusOf(error) {
|
|
2528
|
+
if (error instanceof StreamableHTTPError || error instanceof SseError) {
|
|
2529
|
+
return typeof error.code === "number" && error.code > 0 ? error.code : void 0;
|
|
2530
|
+
}
|
|
2531
|
+
return void 0;
|
|
2532
|
+
}
|
|
2533
|
+
function refusal(error) {
|
|
2534
|
+
const code = statusOf(error);
|
|
2535
|
+
if (code === 401 || code === 403) {
|
|
2536
|
+
return `the server refused the credentials it was given (HTTP ${String(code)}); check the token its headers name`;
|
|
2537
|
+
}
|
|
2538
|
+
return void 0;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/install/clients.ts
|
|
2542
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
2543
|
+
import { homedir as homedir2, platform as platform2 } from "os";
|
|
2544
|
+
import { basename as basename2, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
2545
|
+
|
|
2546
|
+
// src/install/toml.ts
|
|
2547
|
+
var HEADER = /^\s*\[(?!\[)([^[\]]+)\]\s*$/;
|
|
2548
|
+
function serverTables(lines) {
|
|
2549
|
+
const tables = [];
|
|
2550
|
+
let open;
|
|
2551
|
+
const close = (at) => {
|
|
2552
|
+
if (open !== void 0) {
|
|
2553
|
+
tables.push({ name: open.name, start: open.start, end: at });
|
|
2554
|
+
open = void 0;
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
lines.forEach((line, index) => {
|
|
2558
|
+
const header = HEADER.exec(line)?.[1];
|
|
2559
|
+
if (header === void 0) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
const parts = header.split(".");
|
|
2563
|
+
if (parts[0] === "mcp_servers" && parts.length === 2 && parts[1] !== void 0) {
|
|
2564
|
+
close(index);
|
|
2565
|
+
open = { name: unquote(parts[1]), start: index };
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
close(index);
|
|
2569
|
+
});
|
|
2570
|
+
close(lines.length);
|
|
2571
|
+
return tables;
|
|
2572
|
+
}
|
|
2573
|
+
function unquote(text) {
|
|
2574
|
+
const trimmed = text.trim();
|
|
2575
|
+
if (/^'.*'$/s.test(trimmed)) {
|
|
2576
|
+
return trimmed.slice(1, -1);
|
|
2577
|
+
}
|
|
2578
|
+
if (!/^".*"$/s.test(trimmed)) {
|
|
2579
|
+
return trimmed;
|
|
2580
|
+
}
|
|
2581
|
+
return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
2582
|
+
}
|
|
2583
|
+
function readKey(lines, table, key) {
|
|
2584
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`);
|
|
2585
|
+
for (let index = table.start + 1; index < table.end; index += 1) {
|
|
2586
|
+
const line = lines[index];
|
|
2587
|
+
if (line === void 0 || HEADER.test(line)) {
|
|
2588
|
+
break;
|
|
2589
|
+
}
|
|
2590
|
+
const value = pattern.exec(line)?.[1];
|
|
2591
|
+
if (value !== void 0) {
|
|
2592
|
+
return value.trim();
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return void 0;
|
|
2596
|
+
}
|
|
2597
|
+
function splitItems(inner) {
|
|
2598
|
+
const items = [];
|
|
2599
|
+
let current = "";
|
|
2600
|
+
let quote3;
|
|
2601
|
+
let escaped = false;
|
|
2602
|
+
for (const character of inner) {
|
|
2603
|
+
if (escaped) {
|
|
2604
|
+
current += character;
|
|
2605
|
+
escaped = false;
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
if (character === "\\" && quote3 === '"') {
|
|
2609
|
+
current += character;
|
|
2610
|
+
escaped = true;
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
if (quote3 === void 0 && (character === '"' || character === "'")) {
|
|
2614
|
+
quote3 = character;
|
|
2615
|
+
current += character;
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
if (character === quote3) {
|
|
2619
|
+
quote3 = void 0;
|
|
2620
|
+
current += character;
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
if (character === "," && quote3 === void 0) {
|
|
2624
|
+
items.push(current);
|
|
2625
|
+
current = "";
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
current += character;
|
|
2629
|
+
}
|
|
2630
|
+
items.push(current);
|
|
2631
|
+
return items;
|
|
2632
|
+
}
|
|
2633
|
+
function parseArray(value) {
|
|
2634
|
+
if (value === void 0 || !value.startsWith("[")) {
|
|
2635
|
+
return void 0;
|
|
2636
|
+
}
|
|
2637
|
+
if (!value.endsWith("]")) {
|
|
2638
|
+
return void 0;
|
|
2639
|
+
}
|
|
2640
|
+
const inner = value.slice(1, -1).trim();
|
|
2641
|
+
if (inner === "") {
|
|
2642
|
+
return [];
|
|
2643
|
+
}
|
|
2644
|
+
return splitItems(inner).map((item) => item.trim()).filter((item, index, all) => item !== "" || index !== all.length - 1).map(unquote);
|
|
2645
|
+
}
|
|
2646
|
+
function readServers(text) {
|
|
2647
|
+
const lines = text.split("\n");
|
|
2648
|
+
const servers = {};
|
|
2649
|
+
for (const table of serverTables(lines)) {
|
|
2650
|
+
const command = readKey(lines, table, "command");
|
|
2651
|
+
const entry = {};
|
|
2652
|
+
if (command !== void 0) {
|
|
2653
|
+
entry["command"] = unquote(command);
|
|
2654
|
+
}
|
|
2655
|
+
const rawArgs = readKey(lines, table, "args");
|
|
2656
|
+
const args = parseArray(rawArgs);
|
|
2657
|
+
if (args !== void 0) {
|
|
2658
|
+
entry["args"] = args;
|
|
2659
|
+
} else if (rawArgs !== void 0) {
|
|
2660
|
+
entry["unreadable"] = "its args array spans several lines, which this cannot read exactly";
|
|
2661
|
+
}
|
|
2662
|
+
const url = readKey(lines, table, "url");
|
|
2663
|
+
if (url !== void 0) {
|
|
2664
|
+
entry["url"] = unquote(url);
|
|
2665
|
+
}
|
|
2666
|
+
const enabled = readKey(lines, table, "enabled");
|
|
2667
|
+
if (enabled !== void 0) {
|
|
2668
|
+
entry["enabled"] = enabled.trim() === "true";
|
|
2669
|
+
}
|
|
2670
|
+
servers[table.name] = entry;
|
|
2671
|
+
}
|
|
2672
|
+
return servers;
|
|
2673
|
+
}
|
|
2674
|
+
var quote = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2675
|
+
function writeServers(text, servers) {
|
|
2676
|
+
const lines = text.split("\n");
|
|
2677
|
+
const current = readServers(text);
|
|
2678
|
+
for (const table of serverTables(lines).reverse()) {
|
|
2679
|
+
const wanted = servers[table.name];
|
|
2680
|
+
if (wanted === void 0 || wanted.command === void 0) {
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
const now = current[table.name];
|
|
2684
|
+
const unreadable = now?.["unreadable"];
|
|
2685
|
+
if (typeof unreadable === "string") {
|
|
2686
|
+
throw new Error(`cannot rewrite [mcp_servers.${table.name}]: ${unreadable}`);
|
|
2687
|
+
}
|
|
2688
|
+
if (now?.command === wanted.command && JSON.stringify(now.args ?? []) === JSON.stringify(wanted.args ?? [])) {
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
setKey(lines, table, "command", quote(wanted.command));
|
|
2692
|
+
setKey(lines, table, "args", `[${(wanted.args ?? []).map(quote).join(", ")}]`);
|
|
2693
|
+
}
|
|
2694
|
+
return lines.join("\n");
|
|
2695
|
+
}
|
|
2696
|
+
function setKey(lines, table, key, value) {
|
|
2697
|
+
const pattern = new RegExp(`^(\\s*)${key}\\s*=`);
|
|
2698
|
+
for (let index = table.start + 1; index < table.end; index += 1) {
|
|
2699
|
+
const line = lines[index];
|
|
2700
|
+
if (line === void 0 || HEADER.test(line)) {
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
const indent = pattern.exec(line)?.[1];
|
|
2704
|
+
if (indent !== void 0) {
|
|
2705
|
+
lines[index] = `${indent}${key} = ${value}`;
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
lines.splice(table.start + 1, 0, `${key} = ${value}`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/install/clients.ts
|
|
2713
|
+
var LABELS = {
|
|
2714
|
+
"claude-code": "Claude Code",
|
|
2715
|
+
"claude-desktop": "Claude Desktop",
|
|
2716
|
+
cursor: "Cursor",
|
|
2717
|
+
codex: "Codex",
|
|
2718
|
+
devin: "Devin Desktop",
|
|
2719
|
+
windsurf: "Windsurf",
|
|
2720
|
+
"gemini-cli": "Gemini CLI",
|
|
2721
|
+
"copilot-cli": "Copilot CLI",
|
|
2722
|
+
antigravity: "Antigravity"
|
|
2723
|
+
};
|
|
2724
|
+
var LOOKED_FOR = `Looked for ${Object.values(LABELS).slice(0, -1).join(", ")} and ${Object.values(LABELS).slice(-1).join("")}.`;
|
|
2725
|
+
var CLIENT_IDS = Object.keys(LABELS).filter(
|
|
2726
|
+
(name) => name in LABELS
|
|
2727
|
+
);
|
|
2728
|
+
function isClientId(value) {
|
|
2729
|
+
return CLIENT_IDS.some((known) => known === value);
|
|
2730
|
+
}
|
|
2731
|
+
function claudeDesktopPath() {
|
|
2732
|
+
const home2 = homedir2();
|
|
2733
|
+
switch (platform2()) {
|
|
2734
|
+
case "darwin":
|
|
2735
|
+
return join3(home2, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2736
|
+
case "win32":
|
|
2737
|
+
return join3(process.env["APPDATA"] ?? join3(home2, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
2738
|
+
default:
|
|
2739
|
+
return join3(process.env["XDG_CONFIG_HOME"] ?? join3(home2, ".config"), "Claude", "claude_desktop_config.json");
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function expandForClient(client, value) {
|
|
2743
|
+
if (client === "devin" || client === "windsurf") {
|
|
2744
|
+
return value.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_whole, name) => process.env[name] ?? "").replace(/\{\{env:([A-Za-z_][A-Za-z0-9_]*)\}\}/g, (_whole, name) => process.env[name] ?? "");
|
|
2745
|
+
}
|
|
2746
|
+
if (client === "gemini-cli") {
|
|
2747
|
+
return value.replace(
|
|
2748
|
+
/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g,
|
|
2749
|
+
(_whole, braced, bare) => process.env[braced ?? bare ?? ""] ?? ""
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
if (client === "claude-code") {
|
|
2753
|
+
return value.replace(
|
|
2754
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
|
|
2755
|
+
(whole, name, fallback) => process.env[name] ?? fallback ?? whole
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
if (client === "cursor") {
|
|
2759
|
+
return value.replace(
|
|
2760
|
+
/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
2761
|
+
(whole, name) => process.env[name] ?? whole
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
return value;
|
|
2765
|
+
}
|
|
2766
|
+
function discover(cwd) {
|
|
2767
|
+
const home2 = homedir2();
|
|
2768
|
+
const sites = [];
|
|
2769
|
+
const claudeCode = join3(home2, ".claude.json");
|
|
2770
|
+
if (existsSync3(claudeCode)) {
|
|
2771
|
+
const document = readJson(claudeCode);
|
|
2772
|
+
const projects = document?.["projects"];
|
|
2773
|
+
const here = resolve2(cwd);
|
|
2774
|
+
if (isRecord(projects)) {
|
|
2775
|
+
const others = Object.keys(projects).filter((path) => path !== here).sort();
|
|
2776
|
+
for (const project of Object.prototype.hasOwnProperty.call(projects, here) ? [here, ...others] : others) {
|
|
2777
|
+
const entry = projects[project];
|
|
2778
|
+
const servers = isRecord(entry) ? entry["mcpServers"] : void 0;
|
|
2779
|
+
if (project !== here && !(isRecord(servers) && Object.keys(servers).length > 0)) {
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
sites.push({
|
|
2783
|
+
client: "claude-code",
|
|
2784
|
+
label: LABELS["claude-code"],
|
|
2785
|
+
format: "json",
|
|
2786
|
+
path: claudeCode,
|
|
2787
|
+
scope: `project ${project}`,
|
|
2788
|
+
at: ["projects", project, "mcpServers"]
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
sites.push({
|
|
2793
|
+
client: "claude-code",
|
|
2794
|
+
label: LABELS["claude-code"],
|
|
2795
|
+
format: "json",
|
|
2796
|
+
path: claudeCode,
|
|
2797
|
+
scope: "global",
|
|
2798
|
+
at: ["mcpServers"]
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
const projectFile = join3(resolve2(cwd), ".mcp.json");
|
|
2802
|
+
if (existsSync3(projectFile)) {
|
|
2803
|
+
sites.push({
|
|
2804
|
+
client: "claude-code",
|
|
2805
|
+
label: LABELS["claude-code"],
|
|
2806
|
+
format: "json",
|
|
2807
|
+
path: projectFile,
|
|
2808
|
+
scope: "project file",
|
|
2809
|
+
at: ["mcpServers"]
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
const desktop = claudeDesktopPath();
|
|
2813
|
+
if (existsSync3(desktop)) {
|
|
2814
|
+
sites.push({
|
|
2815
|
+
client: "claude-desktop",
|
|
2816
|
+
label: LABELS["claude-desktop"],
|
|
2817
|
+
format: "json",
|
|
2818
|
+
path: desktop,
|
|
2819
|
+
scope: "global",
|
|
2820
|
+
at: ["mcpServers"]
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
const codex = join3(process.env["CODEX_HOME"] ?? join3(home2, ".codex"), "config.toml");
|
|
2824
|
+
if (existsSync3(codex)) {
|
|
2825
|
+
sites.push({
|
|
2826
|
+
client: "codex",
|
|
2827
|
+
label: LABELS.codex,
|
|
2828
|
+
format: "toml",
|
|
2829
|
+
path: codex,
|
|
2830
|
+
scope: "global",
|
|
2831
|
+
at: ["mcp_servers"]
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
const config = process.env["XDG_CONFIG_HOME"] ?? join3(home2, ".config");
|
|
2835
|
+
const plain = [
|
|
2836
|
+
["cursor", join3(resolve2(cwd), ".cursor", "mcp.json"), "project"],
|
|
2837
|
+
["cursor", join3(home2, ".cursor", "mcp.json"), "global"],
|
|
2838
|
+
// Windsurf is Devin Desktop now, which reads its own directory; an older
|
|
2839
|
+
// Windsurf keeps the one it always had.
|
|
2840
|
+
[
|
|
2841
|
+
"devin",
|
|
2842
|
+
platform2() === "win32" ? join3(process.env["APPDATA"] ?? join3(home2, "AppData", "Roaming"), "devin", "mcp_config.json") : join3(config, "devin", "mcp_config.json"),
|
|
2843
|
+
"global"
|
|
2844
|
+
],
|
|
2845
|
+
["windsurf", join3(home2, ".codeium", "windsurf", "mcp_config.json"), "global"],
|
|
2846
|
+
["gemini-cli", join3(resolve2(cwd), ".gemini", "settings.json"), "project"],
|
|
2847
|
+
["gemini-cli", join3(home2, ".gemini", "settings.json"), "global"],
|
|
2848
|
+
["copilot-cli", join3(process.env["COPILOT_HOME"] ?? join3(home2, ".copilot"), "mcp-config.json"), "global"],
|
|
2849
|
+
["antigravity", join3(resolve2(cwd), ".agents", "mcp_config.json"), "workspace"],
|
|
2850
|
+
["antigravity", join3(home2, ".gemini", "config", "mcp_config.json"), "global"]
|
|
2851
|
+
];
|
|
2852
|
+
for (const [client, path, scope] of plain) {
|
|
2853
|
+
if (existsSync3(path)) {
|
|
2854
|
+
sites.push({ client, label: LABELS[client], format: "json", path, scope, at: ["mcpServers"] });
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return sites;
|
|
2858
|
+
}
|
|
2859
|
+
function isRecord(value) {
|
|
2860
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2861
|
+
}
|
|
2862
|
+
function readJson(path) {
|
|
2863
|
+
try {
|
|
2864
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
2865
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2866
|
+
} catch {
|
|
2867
|
+
return void 0;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
var ConfigError = class extends Error {
|
|
2871
|
+
};
|
|
2872
|
+
function readDocument(site) {
|
|
2873
|
+
let text;
|
|
2874
|
+
try {
|
|
2875
|
+
text = readFileSync2(site.path, "utf8");
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
throw new ConfigError(`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2878
|
+
}
|
|
2879
|
+
let parsed;
|
|
2880
|
+
try {
|
|
2881
|
+
parsed = JSON.parse(text);
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
throw new ConfigError(
|
|
2884
|
+
`${site.path} is not valid JSON (${error instanceof Error ? error.message : String(error)}). Fix it or move it aside; synartesis will not rewrite a file it cannot read.`
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
if (!isRecord(parsed)) {
|
|
2888
|
+
throw new ConfigError(`${site.path} is not a JSON object, so it has no server list to change`);
|
|
2889
|
+
}
|
|
2890
|
+
return parsed;
|
|
2891
|
+
}
|
|
2892
|
+
function readServers2(document, at) {
|
|
2893
|
+
let node = document;
|
|
2894
|
+
for (const key of at) {
|
|
2895
|
+
if (!isRecord(node)) {
|
|
2896
|
+
return {};
|
|
2897
|
+
}
|
|
2898
|
+
node = node[key];
|
|
2899
|
+
}
|
|
2900
|
+
if (!isRecord(node)) {
|
|
2901
|
+
return {};
|
|
2902
|
+
}
|
|
2903
|
+
const servers = {};
|
|
2904
|
+
for (const [name, entry] of Object.entries(node)) {
|
|
2905
|
+
if (isRecord(entry)) {
|
|
2906
|
+
servers[name] = entry;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
return servers;
|
|
2910
|
+
}
|
|
2911
|
+
function withServers(document, at, servers) {
|
|
2912
|
+
const head = at[0];
|
|
2913
|
+
if (head === void 0) {
|
|
2914
|
+
throw new ConfigError("no path to the server list");
|
|
2915
|
+
}
|
|
2916
|
+
const rest = at.slice(1);
|
|
2917
|
+
const below = document[head];
|
|
2918
|
+
const child = rest.length === 0 ? servers : withServers(isRecord(below) ? below : {}, rest, servers);
|
|
2919
|
+
return { ...document, [head]: child };
|
|
2920
|
+
}
|
|
2921
|
+
function indentOf(path) {
|
|
2922
|
+
try {
|
|
2923
|
+
const line = /\n([ \t]+)"/.exec(readFileSync2(path, "utf8"));
|
|
2924
|
+
const found = line?.[1];
|
|
2925
|
+
if (found === void 0) {
|
|
2926
|
+
return 2;
|
|
2927
|
+
}
|
|
2928
|
+
return found.startsWith(" ") ? " " : found.length;
|
|
2929
|
+
} catch {
|
|
2930
|
+
return 2;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
function backupPathFor(path) {
|
|
2934
|
+
return `${path}.synartesis-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
2935
|
+
}
|
|
2936
|
+
var KEEP_BACKUPS = 5;
|
|
2937
|
+
function pruneBackups(path) {
|
|
2938
|
+
try {
|
|
2939
|
+
const dir = dirname3(path);
|
|
2940
|
+
const prefix = `${basename2(path)}.synartesis-backup-`;
|
|
2941
|
+
const ours = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
|
|
2942
|
+
for (const name of ours.slice(0, Math.max(0, ours.length - KEEP_BACKUPS))) {
|
|
2943
|
+
rmSync(join3(dir, name), { force: true });
|
|
2944
|
+
}
|
|
2945
|
+
} catch {
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
function writeDocument(site, document) {
|
|
2949
|
+
return writeText(site, `${JSON.stringify(document, void 0, indentOf(site.path))}
|
|
2950
|
+
`);
|
|
2951
|
+
}
|
|
2952
|
+
function writeText(site, text) {
|
|
2953
|
+
const backup = backupPathFor(site.path);
|
|
2954
|
+
const original = readFileSync2(site.path);
|
|
2955
|
+
writeFileSync(backup, original);
|
|
2956
|
+
pruneBackups(site.path);
|
|
2957
|
+
const temporary = join3(dirname3(site.path), `.synartesis-write-${String(process.pid)}.tmp`);
|
|
2958
|
+
try {
|
|
2959
|
+
writeFileSync(temporary, text);
|
|
2960
|
+
renameSync(temporary, site.path);
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
try {
|
|
2963
|
+
unlinkSync(temporary);
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2966
|
+
throw new ConfigError(
|
|
2967
|
+
`could not write ${site.path}: ${error instanceof Error ? error.message : String(error)}. The original is untouched, and a copy is at ${backup}.`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
return backup;
|
|
2971
|
+
}
|
|
2972
|
+
function serversAt(site) {
|
|
2973
|
+
if (site.format === "toml") {
|
|
2974
|
+
try {
|
|
2975
|
+
return readServers(readFileSync2(site.path, "utf8"));
|
|
2976
|
+
} catch (error) {
|
|
2977
|
+
throw new ConfigError(
|
|
2978
|
+
`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return readServers2(readDocument(site), site.at);
|
|
2983
|
+
}
|
|
2984
|
+
function saveServers(site, servers) {
|
|
2985
|
+
if (site.format === "toml") {
|
|
2986
|
+
const text = readFileSync2(site.path, "utf8");
|
|
2987
|
+
return writeText(site, writeServers(text, servers));
|
|
2988
|
+
}
|
|
2989
|
+
return writeDocument(site, withServers(readDocument(site), site.at, servers));
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// src/init/draft.ts
|
|
2993
|
+
import { z as z4 } from "zod";
|
|
2994
|
+
|
|
2995
|
+
// src/init/known.ts
|
|
2996
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
2997
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2998
|
+
var KNOWN = [
|
|
2999
|
+
{ marker: "server-filesystem", manifest: "filesystem" },
|
|
3000
|
+
{ marker: "server-github", manifest: "github" },
|
|
3001
|
+
{ marker: "github-mcp-server", manifest: "github" },
|
|
3002
|
+
{ marker: "server-memory", manifest: "memory" },
|
|
3003
|
+
{ marker: "mcp-server-git", manifest: "git" },
|
|
3004
|
+
{ marker: "server-git", manifest: "git" },
|
|
3005
|
+
{ marker: "@playwright/mcp", manifest: "playwright" },
|
|
3006
|
+
{ marker: "chrome-devtools-mcp", manifest: "chrome-devtools" },
|
|
3007
|
+
// Servers that only read, so their whole policy is "let it look".
|
|
3008
|
+
{ marker: "mcp-server-fetch", manifest: "fetch" },
|
|
3009
|
+
{ marker: "brave-search-mcp-server", manifest: "brave" },
|
|
3010
|
+
{ marker: "exa-mcp-server", manifest: "exa" },
|
|
3011
|
+
{ marker: "tavily-mcp", manifest: "tavily" },
|
|
3012
|
+
{ marker: "aws-documentation-mcp-server", manifest: "aws-docs" }
|
|
3013
|
+
];
|
|
3014
|
+
function manifestsDir() {
|
|
3015
|
+
for (const up of ["../manifests/", "../../manifests/"]) {
|
|
3016
|
+
const candidate = fileURLToPath2(new URL(up, import.meta.url));
|
|
3017
|
+
if (existsSync4(candidate)) {
|
|
3018
|
+
return candidate;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return void 0;
|
|
3022
|
+
}
|
|
3023
|
+
function knownPolicyFor(command, args) {
|
|
3024
|
+
const line = [command, ...args].join(" ");
|
|
3025
|
+
const hit = KNOWN.find((entry) => line.includes(entry.marker));
|
|
3026
|
+
const dir = manifestsDir();
|
|
3027
|
+
if (hit === void 0 || dir === void 0) {
|
|
3028
|
+
return void 0;
|
|
3029
|
+
}
|
|
3030
|
+
const path = `${dir}${hit.manifest}.yaml`;
|
|
3031
|
+
if (!existsSync4(path)) {
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
try {
|
|
3035
|
+
const text = readFileSync3(path, "utf8");
|
|
3036
|
+
const source = toolsBlock(text);
|
|
3037
|
+
const key = serverKey(text);
|
|
3038
|
+
if (source === void 0 || key === void 0) {
|
|
3039
|
+
return void 0;
|
|
3040
|
+
}
|
|
3041
|
+
const rules = parseManifest(
|
|
3042
|
+
`version: 1
|
|
3043
|
+
servers:
|
|
3044
|
+
${key}:
|
|
3045
|
+
command: "true"
|
|
3046
|
+
tools:
|
|
3047
|
+
${source}
|
|
3048
|
+
`,
|
|
3049
|
+
path
|
|
3050
|
+
).tools;
|
|
3051
|
+
const claimed = /^\s*provenance:\s*(live|documented)\s*$/m.exec(text)?.[1];
|
|
3052
|
+
return {
|
|
3053
|
+
key,
|
|
3054
|
+
rules,
|
|
3055
|
+
name: hit.manifest,
|
|
3056
|
+
source,
|
|
3057
|
+
...claimed === "live" || claimed === "documented" ? { provenance: claimed } : {}
|
|
3058
|
+
};
|
|
3059
|
+
} catch {
|
|
3060
|
+
return void 0;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
function toolsBlock(text) {
|
|
3064
|
+
const at = text.search(/^tools:[ \t]*$/m);
|
|
3065
|
+
if (at === -1) {
|
|
3066
|
+
return void 0;
|
|
3067
|
+
}
|
|
3068
|
+
const body = text.slice(text.indexOf("\n", at) + 1);
|
|
3069
|
+
const lines = [];
|
|
3070
|
+
for (const line of body.split("\n")) {
|
|
3071
|
+
if (/^[^\s#]/.test(line)) {
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
lines.push(line);
|
|
3075
|
+
}
|
|
3076
|
+
return lines.join("\n").replace(/\s+$/, "");
|
|
3077
|
+
}
|
|
3078
|
+
function serverKey(text) {
|
|
3079
|
+
const at = text.search(/^servers:[ \t]*$/m);
|
|
3080
|
+
if (at === -1) {
|
|
3081
|
+
return void 0;
|
|
3082
|
+
}
|
|
3083
|
+
const body = text.slice(text.indexOf("\n", at) + 1);
|
|
3084
|
+
for (const line of body.split("\n")) {
|
|
3085
|
+
if (/^[^\s#]/.test(line)) {
|
|
3086
|
+
return void 0;
|
|
3087
|
+
}
|
|
3088
|
+
const named = /^ {2}([A-Za-z0-9_-]+):[ \t]*$/.exec(line);
|
|
3089
|
+
if (named?.[1] !== void 0) {
|
|
3090
|
+
return named[1];
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
function toolsReferencedBy(rule2, key) {
|
|
3096
|
+
const local = (qualified) => qualified === void 0 || !qualified.startsWith(`${key}.`) ? void 0 : qualified.slice(key.length + 1);
|
|
3097
|
+
return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
|
|
3098
|
+
(name) => name !== void 0
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// src/init/draft.ts
|
|
3103
|
+
var toolSchema = z4.looseObject({
|
|
3104
|
+
name: z4.string(),
|
|
3105
|
+
description: z4.string().optional(),
|
|
3106
|
+
annotations: z4.looseObject({
|
|
3107
|
+
readOnlyHint: z4.boolean().optional(),
|
|
3108
|
+
destructiveHint: z4.boolean().optional(),
|
|
3109
|
+
idempotentHint: z4.boolean().optional()
|
|
3110
|
+
}).optional()
|
|
3111
|
+
});
|
|
3112
|
+
var listSchema2 = z4.looseObject({
|
|
3113
|
+
tools: z4.array(toolSchema),
|
|
3114
|
+
nextCursor: z4.string().optional()
|
|
3115
|
+
});
|
|
3116
|
+
function quote2(value) {
|
|
3117
|
+
return JSON.stringify(value);
|
|
3118
|
+
}
|
|
3119
|
+
function summarise(text) {
|
|
3120
|
+
if (text === void 0) {
|
|
3121
|
+
return "";
|
|
3122
|
+
}
|
|
3123
|
+
const single = text.replace(/\s+/g, " ").trim();
|
|
3124
|
+
return single.length > 96 ? `${single.slice(0, 93)}...` : single;
|
|
3125
|
+
}
|
|
3126
|
+
function draftTool(server, tool) {
|
|
3127
|
+
const match = `${server}.${tool.name}`;
|
|
3128
|
+
const lines = [];
|
|
3129
|
+
const description = summarise(tool.description);
|
|
3130
|
+
if (description !== "") {
|
|
3131
|
+
lines.push(` # ${description}`);
|
|
3132
|
+
}
|
|
3133
|
+
if (tool.annotations?.readOnlyHint === true) {
|
|
3134
|
+
lines.push(` # classified readonly from the server's readOnlyHint; verify it before relying on it.`);
|
|
3135
|
+
lines.push(` - match: ${quote2(match)}`);
|
|
3136
|
+
lines.push(` class: readonly`);
|
|
3137
|
+
return lines.join("\n");
|
|
3138
|
+
}
|
|
3139
|
+
lines.push(` # TODO: this is gated on every call until you describe how to undo it.`);
|
|
3140
|
+
lines.push(` # reversible needs a snapshot (a pre-read) and an inverse.`);
|
|
3141
|
+
lines.push(` # compensable needs an inverse only, usually built from $result.`);
|
|
3142
|
+
lines.push(` # irreversible is correct when neither exists; leave gate: always.`);
|
|
3143
|
+
lines.push(` - match: ${quote2(match)}`);
|
|
3144
|
+
lines.push(` class: irreversible`);
|
|
3145
|
+
lines.push(` gate: always`);
|
|
3146
|
+
return lines.join("\n");
|
|
3147
|
+
}
|
|
3148
|
+
function patternFor(match) {
|
|
3149
|
+
const source = match.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
|
|
3150
|
+
return new RegExp(`^${source}$`);
|
|
3151
|
+
}
|
|
3152
|
+
function adopt(known, name, tools) {
|
|
3153
|
+
const advertised = new Set(tools.map((tool) => tool.name));
|
|
3154
|
+
const covered = /* @__PURE__ */ new Set();
|
|
3155
|
+
for (const rule2 of known.rules) {
|
|
3156
|
+
for (const needed of toolsReferencedBy(rule2, known.key)) {
|
|
3157
|
+
if (!advertised.has(needed)) {
|
|
3158
|
+
return void 0;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
const test = patternFor(rule2.match);
|
|
3162
|
+
for (const tool of tools) {
|
|
3163
|
+
if (test.test(`${known.key}.${tool.name}`)) {
|
|
3164
|
+
covered.add(tool.name);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
if (covered.size === 0) {
|
|
3169
|
+
return void 0;
|
|
3170
|
+
}
|
|
3171
|
+
const renamed = known.source.replaceAll(`"${known.key}.`, `"${name}.`);
|
|
3172
|
+
const missing = tools.filter((tool) => !covered.has(tool.name));
|
|
3173
|
+
const extra = missing.length === 0 ? "" : [
|
|
3174
|
+
"",
|
|
3175
|
+
` # Not mentioned by the bundled ${known.name} policy, so gated until you say otherwise.`,
|
|
3176
|
+
...missing.map((tool) => draftTool(name, tool))
|
|
3177
|
+
].join("\n");
|
|
3178
|
+
return { source: `${renamed}${extra}`, covered: covered.size };
|
|
3179
|
+
}
|
|
3180
|
+
async function draftManifest(options) {
|
|
3181
|
+
const upstream = await connectUpstream(
|
|
3182
|
+
options.name,
|
|
3183
|
+
options.remote === void 0 ? { command: options.command, args: options.args } : { ...options.remote },
|
|
3184
|
+
{
|
|
3185
|
+
env: options.env === void 0 ? { kind: "inherit" } : { kind: "client", env: options.env },
|
|
3186
|
+
stderr: "capture",
|
|
3187
|
+
...options.cwd === void 0 ? {} : { cwd: options.cwd }
|
|
3188
|
+
}
|
|
3189
|
+
);
|
|
3190
|
+
let tools;
|
|
3191
|
+
try {
|
|
3192
|
+
const collected = [];
|
|
3193
|
+
let cursor;
|
|
3194
|
+
do {
|
|
3195
|
+
const page = listSchema2.parse(
|
|
3196
|
+
await upstream.client.request(
|
|
3197
|
+
{ method: "tools/list", params: cursor === void 0 ? {} : { cursor } },
|
|
3198
|
+
z4.looseObject({})
|
|
3199
|
+
)
|
|
3200
|
+
);
|
|
3201
|
+
collected.push(...page.tools);
|
|
3202
|
+
cursor = page.nextCursor;
|
|
3203
|
+
} while (cursor !== void 0);
|
|
3204
|
+
tools = collected;
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
throw new UpstreamError(options.name, "tools/list", error);
|
|
3207
|
+
} finally {
|
|
3208
|
+
await upstream.close();
|
|
3209
|
+
}
|
|
3210
|
+
if (tools.length === 0) {
|
|
3211
|
+
throw new ManifestError(`${options.name} exposes no tools, so there is no policy to write`);
|
|
3212
|
+
}
|
|
3213
|
+
const existing = options.existing?.trimEnd();
|
|
3214
|
+
if (existing !== void 0 && existing.includes(`
|
|
3215
|
+
${options.name}:`)) {
|
|
3216
|
+
throw new ManifestError(
|
|
3217
|
+
`${options.name} is already declared in the manifest; remove it first or choose another name`
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
const known = options.remote === void 0 ? knownPolicyFor(options.command, options.args) : void 0;
|
|
3221
|
+
const claim = known?.provenance === void 0 ? [] : [
|
|
3222
|
+
...known.provenance === "documented" ? [
|
|
3223
|
+
` # This policy has never been run against the real server. Check it`,
|
|
3224
|
+
` # against your own setup before trusting undo on it.`
|
|
3225
|
+
] : [],
|
|
3226
|
+
` provenance: ${known.provenance}`
|
|
3227
|
+
];
|
|
3228
|
+
const reach = options.remote === void 0 ? [` command: ${quote2(options.command)}`, ` args: [${options.args.map(quote2).join(", ")}]`] : [
|
|
3229
|
+
` url: ${quote2(options.remote.url)}`,
|
|
3230
|
+
` transport: ${options.remote.transport}`,
|
|
3231
|
+
...Object.keys(options.remote.headers).length === 0 ? [] : [
|
|
3232
|
+
` # Names, not values: each is filled in from the client entry's env`,
|
|
3233
|
+
` # when the server is reached, so the token never lands in this file.`,
|
|
3234
|
+
` headers:`,
|
|
3235
|
+
...Object.entries(options.remote.headers).map(
|
|
3236
|
+
([key, value]) => ` ${quote2(key)}: ${quote2(value)}`
|
|
3237
|
+
)
|
|
3238
|
+
]
|
|
3239
|
+
];
|
|
3240
|
+
const server = [` ${options.name}:`, ...reach, ...claim].join("\n");
|
|
3241
|
+
const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
|
|
3242
|
+
const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
|
|
3243
|
+
if (existing === void 0) {
|
|
3244
|
+
const yaml = [
|
|
3245
|
+
`# Generated by synartesis init from ${options.name}'s tools/list.`,
|
|
3246
|
+
...adopted === void 0 ? [
|
|
3247
|
+
`# Every tool starts gated. Working through the TODOs is the whole job:`,
|
|
3248
|
+
`# a tool with no inverse is one an agent cannot use unsupervised.`
|
|
3249
|
+
] : [
|
|
3250
|
+
`# ${String(adopted.covered)} of its tools were recognised, so the policy that ships`,
|
|
3251
|
+
`# with Synartesis for ${known?.name ?? "this server"} was used and checked against what this`,
|
|
3252
|
+
`# server actually advertises. Read it before trusting it: it is a starting`,
|
|
3253
|
+
`# point that happens to be finished, not a promise about your setup.`
|
|
3254
|
+
],
|
|
3255
|
+
``,
|
|
3256
|
+
`version: 1`,
|
|
3257
|
+
``,
|
|
3258
|
+
`servers:`,
|
|
3259
|
+
server,
|
|
3260
|
+
``,
|
|
3261
|
+
`tools:`,
|
|
3262
|
+
policies,
|
|
3263
|
+
``
|
|
3264
|
+
].join("\n");
|
|
3265
|
+
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
3266
|
+
yaml,
|
|
3267
|
+
adopted: {
|
|
3268
|
+
server: known.name,
|
|
3269
|
+
tools: adopted.covered,
|
|
3270
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
const merged = mergeInto(existing, server, policies, options.name);
|
|
3275
|
+
return adopted === void 0 || known === void 0 ? { yaml: merged } : {
|
|
3276
|
+
yaml: merged,
|
|
3277
|
+
adopted: {
|
|
3278
|
+
server: known.name,
|
|
3279
|
+
tools: adopted.covered,
|
|
3280
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function mergeInto(existing, server, policies, name) {
|
|
3285
|
+
const serversAt2 = existing.indexOf("\nservers:");
|
|
3286
|
+
const toolsAt = existing.indexOf("\ntools:");
|
|
3287
|
+
if (serversAt2 === -1 || toolsAt === -1 || toolsAt < serversAt2) {
|
|
3288
|
+
throw new ManifestError(
|
|
3289
|
+
"the existing manifest does not have a servers: block followed by a tools: block, so it cannot be extended automatically"
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
const head = existing.slice(0, toolsAt);
|
|
3293
|
+
const tail = existing.slice(toolsAt);
|
|
3294
|
+
return [
|
|
3295
|
+
head.trimEnd(),
|
|
3296
|
+
server,
|
|
3297
|
+
tail.trimEnd(),
|
|
3298
|
+
``,
|
|
3299
|
+
` # --- added by synartesis init for ${name} ---`,
|
|
3300
|
+
policies,
|
|
3301
|
+
``
|
|
3302
|
+
].join("\n");
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// src/install/install.ts
|
|
3306
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
3307
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
3308
|
+
function recordPathFor(manifestPath) {
|
|
3309
|
+
return resolve3(dirname4(manifestPath), "installed.json");
|
|
3310
|
+
}
|
|
3311
|
+
var EMPTY = { version: 1, wrapped: {} };
|
|
3312
|
+
function isRecord2(value) {
|
|
3313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3314
|
+
}
|
|
3315
|
+
function asRecord(value) {
|
|
3316
|
+
if (!isRecord2(value)) {
|
|
3317
|
+
return void 0;
|
|
3318
|
+
}
|
|
3319
|
+
const wrapped = value["wrapped"];
|
|
3320
|
+
if (!isRecord2(wrapped)) {
|
|
3321
|
+
return void 0;
|
|
3322
|
+
}
|
|
3323
|
+
const kept2 = {};
|
|
3324
|
+
for (const [key, entry] of Object.entries(wrapped)) {
|
|
3325
|
+
if (!isRecord2(entry)) {
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const original = entry["original"];
|
|
3329
|
+
const at = entry["at"];
|
|
3330
|
+
if (!isRecord2(original)) {
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
if (!Array.isArray(at) || !at.every((step) => typeof step === "string")) {
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
kept2[key] = { original, at };
|
|
3337
|
+
}
|
|
3338
|
+
return { version: 1, wrapped: kept2 };
|
|
3339
|
+
}
|
|
3340
|
+
function keyFor(site, server) {
|
|
3341
|
+
return [site.path, site.scope, server].join("");
|
|
3342
|
+
}
|
|
3343
|
+
function readRecord(manifestPath) {
|
|
3344
|
+
const path = recordPathFor(manifestPath);
|
|
3345
|
+
if (!existsSync5(path)) {
|
|
3346
|
+
return EMPTY;
|
|
3347
|
+
}
|
|
3348
|
+
try {
|
|
3349
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
3350
|
+
const record = asRecord(parsed);
|
|
3351
|
+
if (record !== void 0) {
|
|
3352
|
+
return record;
|
|
3353
|
+
}
|
|
3354
|
+
} catch {
|
|
3355
|
+
}
|
|
3356
|
+
return EMPTY;
|
|
3357
|
+
}
|
|
3358
|
+
function writeRecord(manifestPath, record) {
|
|
3359
|
+
mkdirSync2(dirname4(recordPathFor(manifestPath)), { recursive: true, mode: 448 });
|
|
3360
|
+
const path = recordPathFor(manifestPath);
|
|
3361
|
+
writeFileSync2(path, `${JSON.stringify(record, void 0, 2)}
|
|
3362
|
+
`, { mode: 384 });
|
|
3363
|
+
chmodSync2(path, 384);
|
|
3364
|
+
}
|
|
3365
|
+
function proxyEntry(manifestPath, server, original, invoker) {
|
|
3366
|
+
const command = { command: invoker.command, args: [...invoker.args] };
|
|
3367
|
+
return {
|
|
3368
|
+
// What the client keeps about a server besides how to start it: Copilot
|
|
3369
|
+
// CLI's `tools` list, Gemini CLI's `trust` and timeout, a stdio `type`.
|
|
3370
|
+
// Dropped, a client that requires one would stop offering the server.
|
|
3371
|
+
// Not the ways of reaching it, which are all replaced by the proxy.
|
|
3372
|
+
...Object.fromEntries(Object.entries(original).filter(([key, value]) => kept(key, value))),
|
|
3373
|
+
...command,
|
|
3374
|
+
args: [...command.args, "--manifest", resolve3(manifestPath), "--server", server],
|
|
3375
|
+
// The agent's environment, not ours: the upstream is started by the proxy
|
|
3376
|
+
// from the manifest, but a client that set `env` here meant it for the
|
|
3377
|
+
// server, and the manifest reads `${VAR}` out of exactly this environment.
|
|
3378
|
+
...original.env === void 0 ? {} : { env: original.env },
|
|
3379
|
+
...original.cwd === void 0 ? {} : { cwd: original.cwd }
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
var REPLACED = /* @__PURE__ */ new Set(["command", "args", "env", "cwd", "url", "serverUrl", "httpUrl", "headers"]);
|
|
3383
|
+
function kept(key, value) {
|
|
3384
|
+
if (key === "type") {
|
|
3385
|
+
return value === "stdio" || value === "local";
|
|
3386
|
+
}
|
|
3387
|
+
return !REPLACED.has(key);
|
|
3388
|
+
}
|
|
3389
|
+
function isWrapped(entry) {
|
|
3390
|
+
const args = entry.args ?? [];
|
|
3391
|
+
return args.includes("proxy") && (entry.command === "synartesis" || entry.command === "synartesis-proxy" || args.includes("synartesis") || args.some((arg) => arg.endsWith("dist/cli.js") || arg.endsWith("dist/proxy.js")));
|
|
3392
|
+
}
|
|
3393
|
+
function invokerFor(ourVersion, cliPath) {
|
|
3394
|
+
if (pathBinaryMatches(ourVersion)) {
|
|
3395
|
+
return { command: "synartesis", args: ["proxy"] };
|
|
3396
|
+
}
|
|
3397
|
+
return {
|
|
3398
|
+
command: process.execPath,
|
|
3399
|
+
args: [cliPath, "proxy"],
|
|
3400
|
+
note: "the synartesis on your PATH is a different build, so the entries name this one directly"
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
function headersOf(entry) {
|
|
3404
|
+
const headers = entry["headers"];
|
|
3405
|
+
if (typeof headers !== "object" || headers === null || Array.isArray(headers)) {
|
|
3406
|
+
return void 0;
|
|
3407
|
+
}
|
|
3408
|
+
const text = Object.entries(headers).filter(
|
|
3409
|
+
(pair) => typeof pair[1] === "string"
|
|
3410
|
+
);
|
|
3411
|
+
return text.length === 0 ? void 0 : Object.fromEntries(text);
|
|
3412
|
+
}
|
|
3413
|
+
function headerVariable(server, header) {
|
|
3414
|
+
const part = (text) => text.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3415
|
+
const name = `${part(server)}_MCP_${part(header)}`;
|
|
3416
|
+
return /^[A-Z_]/.test(name) ? name : `S_${name}`;
|
|
3417
|
+
}
|
|
3418
|
+
function transportOf(site, entry) {
|
|
3419
|
+
if (entry.type === "sse") {
|
|
3420
|
+
return "sse";
|
|
3421
|
+
}
|
|
3422
|
+
if (typeof entry["httpUrl"] === "string" || entry.type === "http" || entry.type === "streamable-http") {
|
|
3423
|
+
return "http";
|
|
3424
|
+
}
|
|
3425
|
+
if (site.client === "gemini-cli" && typeof entry.url === "string") {
|
|
3426
|
+
return "sse";
|
|
3427
|
+
}
|
|
3428
|
+
return "auto";
|
|
3429
|
+
}
|
|
3430
|
+
function bridgeFor(url) {
|
|
3431
|
+
return { command: "npx", args: ["-y", "mcp-remote", url] };
|
|
3432
|
+
}
|
|
3433
|
+
function unbridgeable(site, entry, remote) {
|
|
3434
|
+
if (site.format === "toml") {
|
|
3435
|
+
return "hosted; covering one in Codex's config is not supported yet";
|
|
3436
|
+
}
|
|
3437
|
+
if (!remote) {
|
|
3438
|
+
return "hosted; install --remote covers it through mcp-remote, which signs you in through your browser";
|
|
3439
|
+
}
|
|
3440
|
+
return void 0;
|
|
3441
|
+
}
|
|
3442
|
+
async function planInstall(sites, manifestPath, invoker, only, options = {}) {
|
|
3443
|
+
let yaml = existsSync5(manifestPath) ? readFileSync4(manifestPath, "utf8") : void 0;
|
|
3444
|
+
const plans = [];
|
|
3445
|
+
const existing = yaml === void 0 ? {} : parseManifest(yaml, manifestPath).servers;
|
|
3446
|
+
const claimed = new Set(Object.keys(existing));
|
|
3447
|
+
const pointedAt = /* @__PURE__ */ new Set();
|
|
3448
|
+
for (const site of sites) {
|
|
3449
|
+
let entries;
|
|
3450
|
+
try {
|
|
3451
|
+
entries = serversAt(site);
|
|
3452
|
+
} catch {
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
for (const entry of Object.values(entries)) {
|
|
3456
|
+
const args = entry.args ?? [];
|
|
3457
|
+
const manifest = args[args.indexOf("--manifest") + 1];
|
|
3458
|
+
const server = args[args.indexOf("--server") + 1];
|
|
3459
|
+
if (isWrapped(entry) && args.includes("--server") && manifest !== void 0 && server !== void 0 && resolve3(manifest) === resolve3(manifestPath)) {
|
|
3460
|
+
pointedAt.add(server);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
for (const site of sites) {
|
|
3465
|
+
const servers = serversAt(site);
|
|
3466
|
+
const planned = [];
|
|
3467
|
+
const skipped = [];
|
|
3468
|
+
for (const [name, original] of Object.entries(servers)) {
|
|
3469
|
+
if (only !== void 0 && !only(site, name)) {
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
if (isWrapped(original)) {
|
|
3473
|
+
skipped.push({ name, why: "already covered" });
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
const address = [original.url, original["serverUrl"], original["httpUrl"]].find(
|
|
3477
|
+
(one) => typeof one === "string"
|
|
3478
|
+
);
|
|
3479
|
+
const hosted = original.command === void 0 && address !== void 0;
|
|
3480
|
+
const headers = headersOf(original);
|
|
3481
|
+
const native = hosted && headers !== void 0 && site.format !== "toml" ? {
|
|
3482
|
+
url: address,
|
|
3483
|
+
transport: transportOf(site, original),
|
|
3484
|
+
headers: Object.fromEntries(
|
|
3485
|
+
Object.keys(headers).map((header) => [header, `\${${headerVariable(name, header)}}`])
|
|
3486
|
+
),
|
|
3487
|
+
env: Object.fromEntries(
|
|
3488
|
+
Object.entries(headers).map(([header, value]) => [headerVariable(name, header), value])
|
|
3489
|
+
)
|
|
3490
|
+
} : void 0;
|
|
3491
|
+
if (hosted && native === void 0) {
|
|
3492
|
+
const why = unbridgeable(site, original, options.remote === true);
|
|
3493
|
+
if (why !== void 0) {
|
|
3494
|
+
skipped.push({ name, why });
|
|
3495
|
+
continue;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
const entry = native !== void 0 ? { ...original, env: { ...original.env ?? {}, ...native.env } } : hosted ? (options.bridge ?? bridgeFor)(address) : original;
|
|
3499
|
+
if (entry.command === void 0 && native === void 0) {
|
|
3500
|
+
skipped.push({ name, why: "no command to start" });
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
if (original.enabled === false || original.disabled === true) {
|
|
3504
|
+
skipped.push({ name, why: "switched off in the config" });
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
if (typeof original["unreadable"] === "string") {
|
|
3508
|
+
skipped.push({ name, why: `left alone: ${original["unreadable"]}. Put args on one line, or wrap it by hand` });
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
const again = [name, `${name}-${site.client}`].find((candidate) => {
|
|
3512
|
+
const spec = existing[candidate];
|
|
3513
|
+
const args = entry.args ?? [];
|
|
3514
|
+
if (native !== void 0) {
|
|
3515
|
+
return spec?.url === native.url && !pointedAt.has(candidate);
|
|
3516
|
+
}
|
|
3517
|
+
return spec !== void 0 && spec.url === void 0 && !pointedAt.has(candidate) && spec.command === entry.command && spec.args.length === args.length && spec.args.every((arg, index) => arg === args[index]);
|
|
3518
|
+
});
|
|
3519
|
+
if (again !== void 0) {
|
|
3520
|
+
pointedAt.add(again);
|
|
3521
|
+
planned.push({
|
|
3522
|
+
name,
|
|
3523
|
+
original,
|
|
3524
|
+
wrapped: proxyEntry(manifestPath, again, entry, invoker),
|
|
3525
|
+
again: true
|
|
3526
|
+
});
|
|
3527
|
+
continue;
|
|
3528
|
+
}
|
|
3529
|
+
const key = claimed.has(name) ? `${name}-${site.client}` : name;
|
|
3530
|
+
if (claimed.has(key)) {
|
|
3531
|
+
skipped.push({ name, why: `already in the policy as ${key}` });
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
if (options.start === false) {
|
|
3535
|
+
claimed.add(key);
|
|
3536
|
+
const known = native === void 0 ? knownPolicyFor(entry.command ?? "", entry.args ?? []) : void 0;
|
|
3537
|
+
planned.push({
|
|
3538
|
+
name,
|
|
3539
|
+
original,
|
|
3540
|
+
wrapped: proxyEntry(manifestPath, key, entry, invoker),
|
|
3541
|
+
unstarted: true,
|
|
3542
|
+
...hosted && native === void 0 ? { bridged: address } : {},
|
|
3543
|
+
...native === void 0 ? {} : { direct: native.url },
|
|
3544
|
+
...known === void 0 ? {} : { adopted: known.name }
|
|
3545
|
+
});
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
options.starting?.(name);
|
|
3549
|
+
let draft;
|
|
3550
|
+
try {
|
|
3551
|
+
draft = await draftManifest({
|
|
3552
|
+
name: key,
|
|
3553
|
+
command: entry.command ?? "",
|
|
3554
|
+
args: [...entry.args ?? []],
|
|
3555
|
+
...native === void 0 ? {} : { remote: { url: native.url, transport: native.transport, headers: native.headers } },
|
|
3556
|
+
// As the client would start it, so a server that needs its token to
|
|
3557
|
+
// list its tools is drafted rather than reported as broken.
|
|
3558
|
+
env: Object.fromEntries(
|
|
3559
|
+
Object.entries(entry.env ?? {}).map(([k, v]) => [k, expandForClient(site.client, v)])
|
|
3560
|
+
),
|
|
3561
|
+
...entry.cwd === void 0 ? {} : { cwd: expandForClient(site.client, entry.cwd) },
|
|
3562
|
+
...yaml === void 0 ? {} : { existing: yaml }
|
|
3563
|
+
});
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
skipped.push({
|
|
3566
|
+
name,
|
|
3567
|
+
// Whole. The sentence worth reading comes last -- the server's own
|
|
3568
|
+
// "Please set SLACK_BOT_TOKEN" -- and cutting at sixty characters
|
|
3569
|
+
// kept only the SDK's preamble ahead of it.
|
|
3570
|
+
why: `will not start: ${error instanceof Error ? error.message : String(error)}`
|
|
3571
|
+
});
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
yaml = draft.yaml;
|
|
3575
|
+
claimed.add(key);
|
|
3576
|
+
planned.push({
|
|
3577
|
+
name,
|
|
3578
|
+
original,
|
|
3579
|
+
wrapped: proxyEntry(manifestPath, key, entry, invoker),
|
|
3580
|
+
...hosted && native === void 0 ? { bridged: address } : {},
|
|
3581
|
+
...native === void 0 ? {} : { direct: native.url },
|
|
3582
|
+
...draft.adopted === void 0 ? {} : {
|
|
3583
|
+
adopted: draft.adopted.server,
|
|
3584
|
+
tools: draft.adopted.tools,
|
|
3585
|
+
...draft.adopted.provenance === void 0 ? {} : { provenance: draft.adopted.provenance }
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
plans.push({ site, servers: planned, skipped });
|
|
3590
|
+
}
|
|
3591
|
+
return { plans, yaml: yaml ?? "" };
|
|
3592
|
+
}
|
|
3593
|
+
function applyInstall(plans, manifestPath, yaml) {
|
|
3594
|
+
if (!plans.some((plan) => plan.servers.length > 0)) {
|
|
3595
|
+
return [];
|
|
3596
|
+
}
|
|
3597
|
+
parseManifest(yaml, manifestPath);
|
|
3598
|
+
mkdirSync2(dirname4(resolve3(manifestPath)), { recursive: true, mode: 448 });
|
|
3599
|
+
writeFileSync2(manifestPath, yaml);
|
|
3600
|
+
const record = readRecord(manifestPath);
|
|
3601
|
+
const wrapped = { ...record.wrapped };
|
|
3602
|
+
const applied = [];
|
|
3603
|
+
for (const plan of plans) {
|
|
3604
|
+
if (plan.servers.length === 0) {
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
const servers = { ...serversAt(plan.site) };
|
|
3608
|
+
for (const server of plan.servers) {
|
|
3609
|
+
servers[server.name] = server.wrapped;
|
|
3610
|
+
wrapped[keyFor(plan.site, server.name)] = { original: server.original, at: plan.site.at };
|
|
3611
|
+
}
|
|
3612
|
+
writeRecord(manifestPath, { version: 1, wrapped });
|
|
3613
|
+
const backup = saveServers(plan.site, servers);
|
|
3614
|
+
applied.push({ site: plan.site, backup, servers: plan.servers.map((server) => server.name) });
|
|
3615
|
+
}
|
|
3616
|
+
return applied;
|
|
3617
|
+
}
|
|
3618
|
+
function applyUninstall(sites, manifestPath) {
|
|
3619
|
+
const record = readRecord(manifestPath);
|
|
3620
|
+
const restoredKeys = /* @__PURE__ */ new Set();
|
|
3621
|
+
const restored = [];
|
|
3622
|
+
for (const site of sites) {
|
|
3623
|
+
const servers = { ...serversAt(site) };
|
|
3624
|
+
const put = [];
|
|
3625
|
+
const unknown = [];
|
|
3626
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
3627
|
+
if (!isWrapped(entry)) {
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
const known = record.wrapped[keyFor(site, name)];
|
|
3631
|
+
if (known === void 0) {
|
|
3632
|
+
unknown.push(name);
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
servers[name] = known.original;
|
|
3636
|
+
put.push(name);
|
|
3637
|
+
restoredKeys.add(keyFor(site, name));
|
|
3638
|
+
}
|
|
3639
|
+
if (put.length === 0 && unknown.length === 0) {
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
const backup = put.length === 0 ? "" : saveServers(site, servers);
|
|
3643
|
+
restored.push({ site, backup, servers: put, unknown });
|
|
3644
|
+
}
|
|
3645
|
+
const remaining = Object.fromEntries(
|
|
3646
|
+
Object.entries(record.wrapped).filter(([key]) => !restoredKeys.has(key))
|
|
3647
|
+
);
|
|
3648
|
+
writeRecord(manifestPath, { version: 1, wrapped: remaining });
|
|
3649
|
+
return restored;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// src/install/entry-env.ts
|
|
3653
|
+
import { resolve as resolve4 } from "path";
|
|
3654
|
+
function argAfter(args, flag) {
|
|
3655
|
+
const at = args.indexOf(flag);
|
|
3656
|
+
return at === -1 ? void 0 : args[at + 1];
|
|
3657
|
+
}
|
|
3658
|
+
function expanded(client, env) {
|
|
3659
|
+
const out = {};
|
|
3660
|
+
for (const [key, value] of Object.entries(env ?? {})) {
|
|
3661
|
+
out[key] = expandForClient(client, value);
|
|
3662
|
+
}
|
|
3663
|
+
return out;
|
|
3664
|
+
}
|
|
3665
|
+
function sameEnv(a, b) {
|
|
3666
|
+
const keys = Object.keys(a);
|
|
3667
|
+
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
3668
|
+
}
|
|
3669
|
+
function clientEnvFor(manifestPath, server, cwd = process.cwd()) {
|
|
3670
|
+
const wanted = resolve4(manifestPath);
|
|
3671
|
+
const found = [];
|
|
3672
|
+
for (const site of discover(cwd)) {
|
|
3673
|
+
let entries;
|
|
3674
|
+
try {
|
|
3675
|
+
entries = serversAt(site);
|
|
3676
|
+
} catch {
|
|
3677
|
+
continue;
|
|
3678
|
+
}
|
|
3679
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
3680
|
+
if (!isWrapped(entry)) {
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
const args = entry.args ?? [];
|
|
3684
|
+
const manifest = argAfter(args, "--manifest");
|
|
3685
|
+
if (argAfter(args, "--server") !== server || manifest === void 0) {
|
|
3686
|
+
continue;
|
|
3687
|
+
}
|
|
3688
|
+
if (resolve4(manifest) !== wanted) {
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
found.push({
|
|
3692
|
+
env: expanded(site.client, entry.env),
|
|
3693
|
+
...entry.cwd === void 0 ? {} : { cwd: expandForClient(site.client, entry.cwd) },
|
|
3694
|
+
from: `${site.label} (${site.scope}) entry ${name}`
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
const [first, ...rest] = found;
|
|
3699
|
+
if (first === void 0) {
|
|
3700
|
+
return void 0;
|
|
3701
|
+
}
|
|
3702
|
+
const differing2 = rest.filter((other) => !sameEnv(other.env, first.env) || other.cwd !== first.cwd);
|
|
3703
|
+
if (differing2.length > 0) {
|
|
3704
|
+
throw new ConfigError(
|
|
3705
|
+
`server ${server} is wrapped by more than one client entry, with different settings: ` + [first, ...differing2].map((one) => one.from).join("; ") + `. Starting it with one of them could act on a different store from the one a session used, so it is not started.`
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
return first;
|
|
3709
|
+
}
|
|
1973
3710
|
|
|
1974
3711
|
// src/idempotency.ts
|
|
1975
3712
|
var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
|
|
@@ -1985,12 +3722,12 @@ function withIdempotencyKey(meta, key) {
|
|
|
1985
3722
|
}
|
|
1986
3723
|
|
|
1987
3724
|
// src/proxy/snapshot.ts
|
|
1988
|
-
import { z as
|
|
1989
|
-
var ToolResult =
|
|
1990
|
-
isError:
|
|
1991
|
-
content:
|
|
3725
|
+
import { z as z5 } from "zod";
|
|
3726
|
+
var ToolResult = z5.looseObject({
|
|
3727
|
+
isError: z5.boolean().default(false),
|
|
3728
|
+
content: z5.array(z5.looseObject({ type: z5.string() })).default([])
|
|
1992
3729
|
});
|
|
1993
|
-
function
|
|
3730
|
+
function refusal2(result) {
|
|
1994
3731
|
const parsed = ToolResult.safeParse(result);
|
|
1995
3732
|
if (!parsed.success || !parsed.data.isError) {
|
|
1996
3733
|
return void 0;
|
|
@@ -1998,11 +3735,11 @@ function refusal(result) {
|
|
|
1998
3735
|
const said = parsed.data.content.map((block) => typeof block["text"] === "string" ? block["text"] : "").filter((text) => text !== "").join(" ");
|
|
1999
3736
|
return said === "" ? JSON.stringify(result) : said;
|
|
2000
3737
|
}
|
|
2001
|
-
function
|
|
3738
|
+
function isRecord3(value) {
|
|
2002
3739
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2003
3740
|
}
|
|
2004
3741
|
function toPayload(result) {
|
|
2005
|
-
if (!
|
|
3742
|
+
if (!isRecord3(result)) {
|
|
2006
3743
|
return result;
|
|
2007
3744
|
}
|
|
2008
3745
|
const structured = result["structuredContent"];
|
|
@@ -2012,7 +3749,7 @@ function toPayload(result) {
|
|
|
2012
3749
|
const content = result["content"];
|
|
2013
3750
|
if (Array.isArray(content) && content.length === 1) {
|
|
2014
3751
|
const block = content[0];
|
|
2015
|
-
if (
|
|
3752
|
+
if (isRecord3(block) && block["type"] === "text" && typeof block["text"] === "string") {
|
|
2016
3753
|
const text = block["text"];
|
|
2017
3754
|
try {
|
|
2018
3755
|
return JSON.parse(text);
|
|
@@ -2025,7 +3762,7 @@ function toPayload(result) {
|
|
|
2025
3762
|
}
|
|
2026
3763
|
function resolveArgs(call, context) {
|
|
2027
3764
|
const resolved = resolveTemplate(call.args, context);
|
|
2028
|
-
if (!
|
|
3765
|
+
if (!isRecord3(resolved)) {
|
|
2029
3766
|
throw new ManifestError(`${call.tool} resolved to arguments that are not an object`);
|
|
2030
3767
|
}
|
|
2031
3768
|
return resolved;
|
|
@@ -2037,11 +3774,11 @@ function planInverse(call, context) {
|
|
|
2037
3774
|
}
|
|
2038
3775
|
return { server: target.server, tool: target.tool, args: resolveArgs(call, context) };
|
|
2039
3776
|
}
|
|
2040
|
-
var resolvedRead =
|
|
2041
|
-
server:
|
|
2042
|
-
tool:
|
|
2043
|
-
args:
|
|
2044
|
-
absentWhen:
|
|
3777
|
+
var resolvedRead = z5.object({
|
|
3778
|
+
server: z5.string(),
|
|
3779
|
+
tool: z5.string(),
|
|
3780
|
+
args: z5.record(z5.string(), z5.unknown()),
|
|
3781
|
+
absentWhen: z5.array(z5.string()).optional()
|
|
2045
3782
|
});
|
|
2046
3783
|
function toResolvedRead(parsed) {
|
|
2047
3784
|
return {
|
|
@@ -2080,14 +3817,15 @@ async function runRead(router, read2, signal) {
|
|
|
2080
3817
|
const { tool, args } = read2;
|
|
2081
3818
|
const ask = () => upstream.client.request(
|
|
2082
3819
|
{ method: "tools/call", params: { name: tool, arguments: args } },
|
|
2083
|
-
|
|
3820
|
+
z5.looseObject({}),
|
|
2084
3821
|
{ signal }
|
|
2085
3822
|
);
|
|
2086
3823
|
let raw;
|
|
2087
3824
|
try {
|
|
2088
3825
|
raw = await ask();
|
|
2089
3826
|
} catch (error) {
|
|
2090
|
-
|
|
3827
|
+
const lost = isDisconnected(error) || upstream.classify?.(error) === "lost";
|
|
3828
|
+
if (!lost || upstream.reconnect === void 0) {
|
|
2091
3829
|
throw new SnapshotError(label, describe(error), { cause: error });
|
|
2092
3830
|
}
|
|
2093
3831
|
try {
|
|
@@ -2126,10 +3864,12 @@ async function observeState(router, read2, signal) {
|
|
|
2126
3864
|
|
|
2127
3865
|
export {
|
|
2128
3866
|
PROXY_FLAGS,
|
|
3867
|
+
SILENT,
|
|
3868
|
+
desktopNotifier,
|
|
3869
|
+
canNotify,
|
|
2129
3870
|
cliCommand,
|
|
2130
3871
|
cliCommandFrom,
|
|
2131
3872
|
proxyCommand,
|
|
2132
|
-
pathBinaryMatches,
|
|
2133
3873
|
findManifest,
|
|
2134
3874
|
findJournal,
|
|
2135
3875
|
counted,
|
|
@@ -2148,19 +3888,23 @@ export {
|
|
|
2148
3888
|
loadManifest,
|
|
2149
3889
|
createPolicyResolver,
|
|
2150
3890
|
qualify,
|
|
3891
|
+
splitQualified,
|
|
3892
|
+
fingerprint,
|
|
2151
3893
|
pinBlock,
|
|
2152
3894
|
toolShapes,
|
|
2153
3895
|
verifyAgainstServers,
|
|
3896
|
+
withoutMissingTools,
|
|
2154
3897
|
standing,
|
|
2155
3898
|
untested,
|
|
2156
3899
|
describeStanding,
|
|
2157
3900
|
LIVE_IS_NOT_RECOVERY,
|
|
2158
3901
|
warnUntested,
|
|
3902
|
+
trustsMarks,
|
|
2159
3903
|
ungoverned,
|
|
2160
3904
|
IDEMPOTENCY_META_KEY,
|
|
2161
3905
|
withIdempotencyKey,
|
|
2162
3906
|
createRouter,
|
|
2163
|
-
refusal,
|
|
3907
|
+
refusal2 as refusal,
|
|
2164
3908
|
toPayload,
|
|
2165
3909
|
planInverse,
|
|
2166
3910
|
resolvedRead,
|
|
@@ -2169,6 +3913,23 @@ export {
|
|
|
2169
3913
|
isDisconnected,
|
|
2170
3914
|
runRead,
|
|
2171
3915
|
observeState,
|
|
2172
|
-
|
|
3916
|
+
upstreamEnv,
|
|
3917
|
+
fingerprint2,
|
|
3918
|
+
differing,
|
|
3919
|
+
declaredNames,
|
|
3920
|
+
connectUpstream,
|
|
3921
|
+
LOOKED_FOR,
|
|
3922
|
+
CLIENT_IDS,
|
|
3923
|
+
isClientId,
|
|
3924
|
+
discover,
|
|
3925
|
+
ConfigError,
|
|
3926
|
+
serversAt,
|
|
3927
|
+
draftManifest,
|
|
3928
|
+
isWrapped,
|
|
3929
|
+
invokerFor,
|
|
3930
|
+
planInstall,
|
|
3931
|
+
applyInstall,
|
|
3932
|
+
applyUninstall,
|
|
3933
|
+
clientEnvFor
|
|
2173
3934
|
};
|
|
2174
|
-
//# sourceMappingURL=chunk-
|
|
3935
|
+
//# sourceMappingURL=chunk-AEEKBR5D.js.map
|