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