synartesis 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +61 -4
- package/dist/{chunk-X4VQNEP5.js → chunk-7AUGXEWC.js} +21 -8
- package/dist/chunk-7AUGXEWC.js.map +1 -0
- package/dist/cli.js +183 -19
- package/dist/cli.js.map +1 -1
- package/dist/proxy.js +210 -3
- package/dist/proxy.js.map +1 -1
- package/manifests/filesystem.yaml +10 -0
- package/package.json +2 -1
- package/dist/chunk-X4VQNEP5.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
toPayload,
|
|
24
24
|
verifyAgainstServers,
|
|
25
25
|
wasRefused
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-7AUGXEWC.js";
|
|
27
27
|
import {
|
|
28
28
|
DriftConflict,
|
|
29
29
|
ManifestError,
|
|
@@ -34,11 +34,105 @@ import {
|
|
|
34
34
|
} from "./chunk-K3QIPVBY.js";
|
|
35
35
|
|
|
36
36
|
// src/cli.ts
|
|
37
|
-
import { existsSync as
|
|
37
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
38
38
|
import { dirname, resolve } from "path";
|
|
39
39
|
|
|
40
40
|
// src/init/draft.ts
|
|
41
41
|
import { z } from "zod";
|
|
42
|
+
|
|
43
|
+
// src/init/known.ts
|
|
44
|
+
import { existsSync, readFileSync } from "fs";
|
|
45
|
+
import { fileURLToPath } from "url";
|
|
46
|
+
var KNOWN = [
|
|
47
|
+
{ marker: "server-filesystem", manifest: "filesystem" },
|
|
48
|
+
{ marker: "server-github", manifest: "github" },
|
|
49
|
+
{ marker: "github-mcp-server", manifest: "github" },
|
|
50
|
+
{ marker: "server-memory", manifest: "memory" },
|
|
51
|
+
{ marker: "mcp-server-git", manifest: "git" },
|
|
52
|
+
{ marker: "server-git", manifest: "git" }
|
|
53
|
+
];
|
|
54
|
+
function manifestsDir() {
|
|
55
|
+
for (const up of ["../manifests/", "../../manifests/"]) {
|
|
56
|
+
const candidate = fileURLToPath(new URL(up, import.meta.url));
|
|
57
|
+
if (existsSync(candidate)) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
function knownPolicyFor(command, args) {
|
|
64
|
+
const line2 = [command, ...args].join(" ");
|
|
65
|
+
const hit = KNOWN.find((entry) => line2.includes(entry.marker));
|
|
66
|
+
const dir = manifestsDir();
|
|
67
|
+
if (hit === void 0 || dir === void 0) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
const path = `${dir}${hit.manifest}.yaml`;
|
|
71
|
+
if (!existsSync(path)) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const text = readFileSync(path, "utf8");
|
|
76
|
+
const source = toolsBlock(text);
|
|
77
|
+
const key = serverKey(text);
|
|
78
|
+
if (source === void 0 || key === void 0) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const rules = parseManifest(
|
|
82
|
+
`version: 1
|
|
83
|
+
servers:
|
|
84
|
+
${key}:
|
|
85
|
+
command: "true"
|
|
86
|
+
tools:
|
|
87
|
+
${source}
|
|
88
|
+
`,
|
|
89
|
+
path
|
|
90
|
+
).tools;
|
|
91
|
+
return { key, rules, name: hit.manifest, source };
|
|
92
|
+
} catch {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function toolsBlock(text) {
|
|
97
|
+
const at = text.search(/^tools:[ \t]*$/m);
|
|
98
|
+
if (at === -1) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
const body = text.slice(text.indexOf("\n", at) + 1);
|
|
102
|
+
const lines = [];
|
|
103
|
+
for (const line2 of body.split("\n")) {
|
|
104
|
+
if (/^[^\s#]/.test(line2)) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
lines.push(line2);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n").replace(/\s+$/, "");
|
|
110
|
+
}
|
|
111
|
+
function serverKey(text) {
|
|
112
|
+
const at = text.search(/^servers:[ \t]*$/m);
|
|
113
|
+
if (at === -1) {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
const body = text.slice(text.indexOf("\n", at) + 1);
|
|
117
|
+
for (const line2 of body.split("\n")) {
|
|
118
|
+
if (/^[^\s#]/.test(line2)) {
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
const named = /^ {2}([A-Za-z0-9_-]+):[ \t]*$/.exec(line2);
|
|
122
|
+
if (named?.[1] !== void 0) {
|
|
123
|
+
return named[1];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function toolsReferencedBy(rule2, key) {
|
|
129
|
+
const local = (qualified) => qualified === void 0 || !qualified.startsWith(`${key}.`) ? void 0 : qualified.slice(key.length + 1);
|
|
130
|
+
return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
|
|
131
|
+
(name) => name !== void 0
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/init/draft.ts
|
|
42
136
|
var toolSchema = z.looseObject({
|
|
43
137
|
name: z.string(),
|
|
44
138
|
description: z.string().optional(),
|
|
@@ -84,6 +178,38 @@ function draftTool(server, tool) {
|
|
|
84
178
|
lines.push(` gate: always`);
|
|
85
179
|
return lines.join("\n");
|
|
86
180
|
}
|
|
181
|
+
function patternFor(match) {
|
|
182
|
+
const source = match.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
|
|
183
|
+
return new RegExp(`^${source}$`);
|
|
184
|
+
}
|
|
185
|
+
function adopt(known, name, tools) {
|
|
186
|
+
const advertised = new Set(tools.map((tool) => tool.name));
|
|
187
|
+
const covered = /* @__PURE__ */ new Set();
|
|
188
|
+
for (const rule2 of known.rules) {
|
|
189
|
+
for (const needed of toolsReferencedBy(rule2, known.key)) {
|
|
190
|
+
if (!advertised.has(needed)) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const test = patternFor(rule2.match);
|
|
195
|
+
for (const tool of tools) {
|
|
196
|
+
if (test.test(`${known.key}.${tool.name}`)) {
|
|
197
|
+
covered.add(tool.name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (covered.size === 0) {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const renamed = known.source.replaceAll(`"${known.key}.`, `"${name}.`);
|
|
205
|
+
const missing = tools.filter((tool) => !covered.has(tool.name));
|
|
206
|
+
const extra = missing.length === 0 ? "" : [
|
|
207
|
+
"",
|
|
208
|
+
` # Not mentioned by the bundled ${known.name} policy, so gated until you say otherwise.`,
|
|
209
|
+
...missing.map((tool) => draftTool(name, tool))
|
|
210
|
+
].join("\n");
|
|
211
|
+
return { source: `${renamed}${extra}`, covered: covered.size };
|
|
212
|
+
}
|
|
87
213
|
async function draftManifest(options) {
|
|
88
214
|
const upstream = await connectStdioUpstream({
|
|
89
215
|
name: options.name,
|
|
@@ -126,12 +252,21 @@ async function draftManifest(options) {
|
|
|
126
252
|
` command: ${quote(options.command)}`,
|
|
127
253
|
` args: [${options.args.map(quote).join(", ")}]`
|
|
128
254
|
].join("\n");
|
|
129
|
-
const
|
|
255
|
+
const known = knownPolicyFor(options.command, options.args);
|
|
256
|
+
const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
|
|
257
|
+
const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
|
|
130
258
|
if (existing === void 0) {
|
|
131
|
-
|
|
259
|
+
const yaml = [
|
|
132
260
|
`# Generated by synartesis init from ${options.name}'s tools/list.`,
|
|
133
|
-
|
|
134
|
-
|
|
261
|
+
...adopted === void 0 ? [
|
|
262
|
+
`# Every tool starts gated. Working through the TODOs is the whole job:`,
|
|
263
|
+
`# a tool with no inverse is one an agent cannot use unsupervised.`
|
|
264
|
+
] : [
|
|
265
|
+
`# ${String(adopted.covered)} of its tools were recognised, so the policy that ships`,
|
|
266
|
+
`# with Synartesis for ${known?.name ?? "this server"} was used and checked against what this`,
|
|
267
|
+
`# server actually advertises. Read it before trusting it: it is a starting`,
|
|
268
|
+
`# point that happens to be finished, not a promise about your setup.`
|
|
269
|
+
],
|
|
135
270
|
``,
|
|
136
271
|
`version: 1`,
|
|
137
272
|
``,
|
|
@@ -142,8 +277,10 @@ async function draftManifest(options) {
|
|
|
142
277
|
policies,
|
|
143
278
|
``
|
|
144
279
|
].join("\n");
|
|
280
|
+
return adopted === void 0 || known === void 0 ? { yaml } : { yaml, adopted: { server: known.name, tools: adopted.covered } };
|
|
145
281
|
}
|
|
146
|
-
|
|
282
|
+
const merged = mergeInto(existing, server, policies, options.name);
|
|
283
|
+
return adopted === void 0 || known === void 0 ? { yaml: merged } : { yaml: merged, adopted: { server: known.name, tools: adopted.covered } };
|
|
147
284
|
}
|
|
148
285
|
function mergeInto(existing, server, policies, name) {
|
|
149
286
|
const serversAt = existing.indexOf("\nservers:");
|
|
@@ -234,8 +371,12 @@ async function rollback(options) {
|
|
|
234
371
|
}
|
|
235
372
|
};
|
|
236
373
|
const all = journal.getActions(runId);
|
|
237
|
-
const
|
|
374
|
+
const floor = options.toSeq;
|
|
375
|
+
const inScope = [...all].filter((action) => floor === void 0 || action.seq >= floor).sort((a, b) => b.seq - a.seq);
|
|
238
376
|
const steps = [];
|
|
377
|
+
const kept = (floor === void 0 ? [] : [...all].filter((action) => action.seq < floor)).sort(
|
|
378
|
+
(a, b) => b.seq - a.seq
|
|
379
|
+
);
|
|
239
380
|
let halted;
|
|
240
381
|
let leftInPlace = false;
|
|
241
382
|
for (const action of inScope) {
|
|
@@ -367,6 +508,14 @@ Resolve the conflict, then run undo --replan to check it against the world as it
|
|
|
367
508
|
};
|
|
368
509
|
break;
|
|
369
510
|
}
|
|
511
|
+
for (const action of kept) {
|
|
512
|
+
steps.push({
|
|
513
|
+
...describeStep(action),
|
|
514
|
+
kind: "kept",
|
|
515
|
+
reason: `below --to ${String(floor ?? 0)}, so it is left as it is`,
|
|
516
|
+
verified: true
|
|
517
|
+
});
|
|
518
|
+
}
|
|
370
519
|
const completedWholeRun = halted === void 0 && !leftInPlace && options.toSeq === void 0;
|
|
371
520
|
const status = completedWholeRun ? "rolled_back" : "partial";
|
|
372
521
|
if (!dryRun) {
|
|
@@ -423,7 +572,7 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
423
572
|
}
|
|
424
573
|
|
|
425
574
|
// src/watch.ts
|
|
426
|
-
import { existsSync } from "fs";
|
|
575
|
+
import { existsSync as existsSync2 } from "fs";
|
|
427
576
|
|
|
428
577
|
// src/keys.ts
|
|
429
578
|
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
@@ -552,7 +701,7 @@ async function* terminalKeys() {
|
|
|
552
701
|
async function watch(options) {
|
|
553
702
|
let journal;
|
|
554
703
|
const open = () => {
|
|
555
|
-
if (journal === void 0 &&
|
|
704
|
+
if (journal === void 0 && existsSync2(options.journalPath)) {
|
|
556
705
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
557
706
|
}
|
|
558
707
|
return journal;
|
|
@@ -661,7 +810,7 @@ async function watch(options) {
|
|
|
661
810
|
}
|
|
662
811
|
|
|
663
812
|
// src/console.ts
|
|
664
|
-
import { existsSync as
|
|
813
|
+
import { existsSync as existsSync3 } from "fs";
|
|
665
814
|
var FRAMES2 = [
|
|
666
815
|
"\u280B",
|
|
667
816
|
"\u2819",
|
|
@@ -867,7 +1016,7 @@ async function* terminalKeys2() {
|
|
|
867
1016
|
async function openConsole(options) {
|
|
868
1017
|
let journal;
|
|
869
1018
|
const open = () => {
|
|
870
|
-
if (journal === void 0 &&
|
|
1019
|
+
if (journal === void 0 && existsSync3(options.journalPath)) {
|
|
871
1020
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
872
1021
|
}
|
|
873
1022
|
return journal;
|
|
@@ -1108,6 +1257,8 @@ var COMMANDS = `
|
|
|
1108
1257
|
synartesis gates [--journal <path>]
|
|
1109
1258
|
synartesis close [runId] [--journal <path>]
|
|
1110
1259
|
synartesis proxy --manifest <path> [--journal <path>] what your agent runs
|
|
1260
|
+
[--http <port> --token <secret>] for a client that
|
|
1261
|
+
cannot start one
|
|
1111
1262
|
synartesis watch [--by <name>] [--journal <path>]
|
|
1112
1263
|
synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
|
|
1113
1264
|
synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
|
|
@@ -1223,27 +1374,34 @@ async function runInit(argv) {
|
|
|
1223
1374
|
}
|
|
1224
1375
|
const path = findManifest(flag(argv, "--manifest"));
|
|
1225
1376
|
const force = argv.includes("--force");
|
|
1226
|
-
const present =
|
|
1377
|
+
const present = existsSync4(path);
|
|
1227
1378
|
if (present && force) {
|
|
1228
1379
|
throw new UsageError(
|
|
1229
1380
|
`--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
|
|
1230
1381
|
);
|
|
1231
1382
|
}
|
|
1232
|
-
const
|
|
1383
|
+
const draft = await draftManifest({
|
|
1233
1384
|
name,
|
|
1234
1385
|
command,
|
|
1235
1386
|
args: argv.slice(separator + 2),
|
|
1236
|
-
...present ? { existing:
|
|
1387
|
+
...present ? { existing: readFileSync2(path, "utf8") } : {}
|
|
1237
1388
|
});
|
|
1238
|
-
parseManifest(yaml, path);
|
|
1389
|
+
parseManifest(draft.yaml, path);
|
|
1239
1390
|
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
1240
|
-
writeFileSync(path, yaml);
|
|
1391
|
+
writeFileSync(path, draft.yaml);
|
|
1241
1392
|
out("");
|
|
1242
1393
|
out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
|
|
1243
1394
|
out(` ${rule(54)}`);
|
|
1244
1395
|
out("");
|
|
1245
|
-
|
|
1246
|
-
|
|
1396
|
+
if (draft.adopted === void 0) {
|
|
1397
|
+
out(` ${style.quiet("Every tool is guarded until you say how to undo it.")}`);
|
|
1398
|
+
out(` ${style.quiet("Work through the TODOs, then point your MCP client at:")}`);
|
|
1399
|
+
} else {
|
|
1400
|
+
out(
|
|
1401
|
+
` ${style.quiet(`Recognised ${String(draft.adopted.tools)} tools, so the policy that ships for`)} ${style.strong(draft.adopted.server)} ${style.quiet("was used.")}`
|
|
1402
|
+
);
|
|
1403
|
+
out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
|
|
1404
|
+
}
|
|
1247
1405
|
out("");
|
|
1248
1406
|
out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
|
|
1249
1407
|
out("");
|
|
@@ -1518,7 +1676,13 @@ function report(result) {
|
|
|
1518
1676
|
out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
|
|
1519
1677
|
out(` ${rule(72)}`);
|
|
1520
1678
|
out("");
|
|
1679
|
+
let separated = false;
|
|
1521
1680
|
for (const step of result.steps) {
|
|
1681
|
+
if (step.kind === "kept" && !separated) {
|
|
1682
|
+
separated = true;
|
|
1683
|
+
out("");
|
|
1684
|
+
out(` ${style.quiet("left alone")}`);
|
|
1685
|
+
}
|
|
1522
1686
|
const unverified = step.kind === "revert" && !step.verified ? ` ${style.accent("[unverified]")}` : "";
|
|
1523
1687
|
const kind = step.kind === "halt" || step.kind === "permanent" ? style.accent(step.kind.padEnd(16)) : step.kind.padEnd(16);
|
|
1524
1688
|
out(
|