synartesis 0.6.12 → 0.6.13
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 +168 -0
- package/README.md +103 -2
- package/dist/{chunk-FUVEHRJP.js → chunk-6X7GPUGJ.js} +231 -46
- package/dist/cli.js +192 -43
- package/dist/proxy.js +21 -3
- package/manifests/filesystem.yaml +8 -0
- package/manifests/git.yaml +8 -0
- package/manifests/github.yaml +10 -0
- package/manifests/memory.yaml +18 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
IDEMPOTENCY_META_KEY,
|
|
3
4
|
NOTHING_RECORDED_YET,
|
|
4
5
|
WORDMARK,
|
|
5
6
|
banner,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
connectStdioUpstream,
|
|
9
10
|
createPolicyResolver,
|
|
10
11
|
createRouter,
|
|
12
|
+
describeStanding,
|
|
11
13
|
findJournal,
|
|
12
14
|
findManifest,
|
|
13
15
|
labelFor,
|
|
@@ -16,18 +18,23 @@ import {
|
|
|
16
18
|
openJournal,
|
|
17
19
|
parseManifest,
|
|
18
20
|
pathBinaryMatches,
|
|
21
|
+
pinBlock,
|
|
19
22
|
planInverse,
|
|
20
23
|
planRead,
|
|
21
24
|
proxyCommand,
|
|
22
25
|
qualify,
|
|
23
26
|
resolvedRead,
|
|
24
27
|
rule,
|
|
28
|
+
standing,
|
|
25
29
|
style,
|
|
26
30
|
toPayload,
|
|
27
31
|
toResolvedRead,
|
|
32
|
+
toolShapes,
|
|
33
|
+
untested,
|
|
28
34
|
verifyAgainstServers,
|
|
35
|
+
warnUntested,
|
|
29
36
|
wasRefused
|
|
30
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-6X7GPUGJ.js";
|
|
31
38
|
import {
|
|
32
39
|
DriftConflict,
|
|
33
40
|
ManifestError,
|
|
@@ -95,7 +102,14 @@ ${source}
|
|
|
95
102
|
`,
|
|
96
103
|
path
|
|
97
104
|
).tools;
|
|
98
|
-
|
|
105
|
+
const claimed = /^\s*provenance:\s*(live|documented)\s*$/m.exec(text)?.[1];
|
|
106
|
+
return {
|
|
107
|
+
key,
|
|
108
|
+
rules,
|
|
109
|
+
name: hit.manifest,
|
|
110
|
+
source,
|
|
111
|
+
...claimed === "live" || claimed === "documented" ? { provenance: claimed } : {}
|
|
112
|
+
};
|
|
99
113
|
} catch {
|
|
100
114
|
return void 0;
|
|
101
115
|
}
|
|
@@ -254,12 +268,20 @@ async function draftManifest(options) {
|
|
|
254
268
|
`${options.name} is already declared in the manifest; remove it first or choose another name`
|
|
255
269
|
);
|
|
256
270
|
}
|
|
271
|
+
const known = knownPolicyFor(options.command, options.args);
|
|
272
|
+
const claim = known?.provenance === void 0 ? [] : [
|
|
273
|
+
...known.provenance === "documented" ? [
|
|
274
|
+
` # This policy has never been run against the real server. Check it`,
|
|
275
|
+
` # against your own setup before trusting undo on it.`
|
|
276
|
+
] : [],
|
|
277
|
+
` provenance: ${known.provenance}`
|
|
278
|
+
];
|
|
257
279
|
const server = [
|
|
258
280
|
` ${options.name}:`,
|
|
259
281
|
` command: ${quote(options.command)}`,
|
|
260
|
-
` args: [${options.args.map(quote).join(", ")}]
|
|
282
|
+
` args: [${options.args.map(quote).join(", ")}]`,
|
|
283
|
+
...claim
|
|
261
284
|
].join("\n");
|
|
262
|
-
const known = knownPolicyFor(options.command, options.args);
|
|
263
285
|
const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
|
|
264
286
|
const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
|
|
265
287
|
if (existing === void 0) {
|
|
@@ -284,10 +306,24 @@ async function draftManifest(options) {
|
|
|
284
306
|
policies,
|
|
285
307
|
``
|
|
286
308
|
].join("\n");
|
|
287
|
-
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
309
|
+
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
310
|
+
yaml,
|
|
311
|
+
adopted: {
|
|
312
|
+
server: known.name,
|
|
313
|
+
tools: adopted.covered,
|
|
314
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
315
|
+
}
|
|
316
|
+
};
|
|
288
317
|
}
|
|
289
318
|
const merged = mergeInto(existing, server, policies, options.name);
|
|
290
|
-
return adopted === void 0 || known === void 0 ? { yaml: merged } : {
|
|
319
|
+
return adopted === void 0 || known === void 0 ? { yaml: merged } : {
|
|
320
|
+
yaml: merged,
|
|
321
|
+
adopted: {
|
|
322
|
+
server: known.name,
|
|
323
|
+
tools: adopted.covered,
|
|
324
|
+
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
325
|
+
}
|
|
326
|
+
};
|
|
291
327
|
}
|
|
292
328
|
function mergeInto(existing, server, policies, name) {
|
|
293
329
|
const serversAt2 = existing.indexOf("\nservers:");
|
|
@@ -312,7 +348,6 @@ function mergeInto(existing, server, policies, name) {
|
|
|
312
348
|
|
|
313
349
|
// src/rollback/rollback.ts
|
|
314
350
|
import { z as z2 } from "zod";
|
|
315
|
-
var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
|
|
316
351
|
var inversePlan = z2.object({
|
|
317
352
|
server: z2.string(),
|
|
318
353
|
tool: z2.string(),
|
|
@@ -331,8 +366,10 @@ function classify(action, replanning, goAhead) {
|
|
|
331
366
|
case "rolled_back":
|
|
332
367
|
return { kind: "already-reverted", reason: "already rolled back", verified: true };
|
|
333
368
|
case "failed":
|
|
334
|
-
case "denied":
|
|
335
|
-
|
|
369
|
+
case "denied": {
|
|
370
|
+
const why = labelFor(action) === "used" ? "never applied: its approval moved to the call that ran" : `never applied (${action.status})`;
|
|
371
|
+
return { kind: "skip", reason: why, verified: true };
|
|
372
|
+
}
|
|
336
373
|
case "pending":
|
|
337
374
|
return {
|
|
338
375
|
kind: "halt",
|
|
@@ -572,7 +609,7 @@ ${seen}` : seen;
|
|
|
572
609
|
};
|
|
573
610
|
}
|
|
574
611
|
function unverifiedBecause(action) {
|
|
575
|
-
return action.verify === void 0 ? "no
|
|
612
|
+
return action.verify === void 0 ? "no read declared for this tool, so drift could not be ruled out -- a `verify` read in its policy would give it one" : "the post-state was never captured, so drift could not be ruled out";
|
|
576
613
|
}
|
|
577
614
|
function describeStep(action) {
|
|
578
615
|
return { seq: action.seq, server: action.server, tool: action.tool };
|
|
@@ -652,7 +689,11 @@ async function inspect(options) {
|
|
|
652
689
|
continue;
|
|
653
690
|
}
|
|
654
691
|
if (SETTLED.has(action.status)) {
|
|
655
|
-
resources.push({
|
|
692
|
+
resources.push({
|
|
693
|
+
...at,
|
|
694
|
+
condition: "not-applied",
|
|
695
|
+
note: labelFor(action) === "used" ? "its approval moved to the call that ran" : action.status
|
|
696
|
+
});
|
|
656
697
|
continue;
|
|
657
698
|
}
|
|
658
699
|
if (action.status === "rolled_back") {
|
|
@@ -881,6 +922,19 @@ function size(bytes) {
|
|
|
881
922
|
}
|
|
882
923
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
883
924
|
}
|
|
925
|
+
function summariseValue(value) {
|
|
926
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
927
|
+
if (flat.length <= 48) {
|
|
928
|
+
return flat;
|
|
929
|
+
}
|
|
930
|
+
const looksLikeALocation = !value.includes("\n") && /[/\\]/.test(value);
|
|
931
|
+
if (looksLikeALocation) {
|
|
932
|
+
const tail = value.slice(-44);
|
|
933
|
+
const from = tail.search(/[/\\]/);
|
|
934
|
+
return `\u2026${from === -1 ? tail : tail.slice(from)}`;
|
|
935
|
+
}
|
|
936
|
+
return size(Buffer.byteLength(value));
|
|
937
|
+
}
|
|
884
938
|
function summariseArgs(args, limit = 60) {
|
|
885
939
|
if (!isRecord(args)) {
|
|
886
940
|
return args === void 0 ? "" : JSON.stringify(args);
|
|
@@ -888,7 +942,7 @@ function summariseArgs(args, limit = 60) {
|
|
|
888
942
|
const parts = [];
|
|
889
943
|
for (const [key, value] of Object.entries(args)) {
|
|
890
944
|
if (typeof value === "string") {
|
|
891
|
-
parts.push(
|
|
945
|
+
parts.push(`${key} ${summariseValue(value)}`);
|
|
892
946
|
continue;
|
|
893
947
|
}
|
|
894
948
|
if (Array.isArray(value)) {
|
|
@@ -1643,7 +1697,11 @@ async function planInstall(sites, manifestPath, invoker) {
|
|
|
1643
1697
|
name,
|
|
1644
1698
|
original: entry,
|
|
1645
1699
|
wrapped: proxyEntry(manifestPath, key, entry, invoker),
|
|
1646
|
-
...draft.adopted === void 0 ? {} : {
|
|
1700
|
+
...draft.adopted === void 0 ? {} : {
|
|
1701
|
+
adopted: draft.adopted.server,
|
|
1702
|
+
tools: draft.adopted.tools,
|
|
1703
|
+
...draft.adopted.provenance === void 0 ? {} : { provenance: draft.adopted.provenance }
|
|
1704
|
+
}
|
|
1647
1705
|
});
|
|
1648
1706
|
}
|
|
1649
1707
|
plans.push({ site, servers: planned, skipped });
|
|
@@ -1877,7 +1935,7 @@ function runsView(journal, screen, options) {
|
|
|
1877
1935
|
return ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(name) : style.strong(name)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${style.quiet(run.status.padEnd(11))} ${style.quiet(`${String(actions.length)} actions`)}${note}`;
|
|
1878
1936
|
});
|
|
1879
1937
|
}
|
|
1880
|
-
function
|
|
1938
|
+
function standing2(actions) {
|
|
1881
1939
|
let undoable = 0;
|
|
1882
1940
|
let conflicted = 0;
|
|
1883
1941
|
for (const action of actions) {
|
|
@@ -1897,7 +1955,7 @@ function elsewhere(journal, exceptId) {
|
|
|
1897
1955
|
if (run.id === exceptId) {
|
|
1898
1956
|
continue;
|
|
1899
1957
|
}
|
|
1900
|
-
const found =
|
|
1958
|
+
const found = standing2(journal.getActions(run.id));
|
|
1901
1959
|
if (found.undoable > 0 || found.conflicted > 0) {
|
|
1902
1960
|
return run;
|
|
1903
1961
|
}
|
|
@@ -2138,7 +2196,7 @@ async function openConsole(options) {
|
|
|
2138
2196
|
if (run === void 0) {
|
|
2139
2197
|
return [];
|
|
2140
2198
|
}
|
|
2141
|
-
const here =
|
|
2199
|
+
const here = standing2(ready.getActions(run.id));
|
|
2142
2200
|
const id = run.id.slice(0, 8);
|
|
2143
2201
|
if (here.undoable > 0) {
|
|
2144
2202
|
return [
|
|
@@ -2359,7 +2417,7 @@ async function openConsole(options) {
|
|
|
2359
2417
|
if (ready === void 0 || run === void 0) {
|
|
2360
2418
|
return;
|
|
2361
2419
|
}
|
|
2362
|
-
const here =
|
|
2420
|
+
const here = standing2(ready.getActions(run.id));
|
|
2363
2421
|
if (here.undoable === 0 && here.conflicted === 0) {
|
|
2364
2422
|
say(nothingToUndo(ready, run));
|
|
2365
2423
|
return;
|
|
@@ -2381,7 +2439,7 @@ async function openConsole(options) {
|
|
|
2381
2439
|
if (ready === void 0 || run === void 0) {
|
|
2382
2440
|
return;
|
|
2383
2441
|
}
|
|
2384
|
-
const here =
|
|
2442
|
+
const here = standing2(ready.getActions(run.id));
|
|
2385
2443
|
if (here.undoable === 0 && here.conflicted === 0) {
|
|
2386
2444
|
say(nothingToUndo(ready, run));
|
|
2387
2445
|
return;
|
|
@@ -2541,6 +2599,7 @@ var COMMANDS = `
|
|
|
2541
2599
|
synartesis status
|
|
2542
2600
|
synartesis init <server> -- <command> [args...] [--manifest <path>]
|
|
2543
2601
|
synartesis check [--manifest <path>]
|
|
2602
|
+
synartesis pin [--manifest <path>]
|
|
2544
2603
|
synartesis list [--journal <path>]
|
|
2545
2604
|
synartesis show <runId> [--full] [--live] [--journal <path>]
|
|
2546
2605
|
synartesis gates [--journal <path>]
|
|
@@ -2612,6 +2671,19 @@ journal is too. Set SYNARTESIS_HOME to put that somewhere else.
|
|
|
2612
2671
|
Exit codes: 0 complete, 1 halted or partial, 2 bad usage or configuration.
|
|
2613
2672
|
`;
|
|
2614
2673
|
var UsageError = class extends Error {
|
|
2674
|
+
/**
|
|
2675
|
+
* Whether the command list helps.
|
|
2676
|
+
*
|
|
2677
|
+
* It does for a mistyped flag or command. It does not for "there is nothing
|
|
2678
|
+
* here to act on", which is a fact about the journal rather than about what
|
|
2679
|
+
* was typed -- and answering that with forty lines of unrelated commands
|
|
2680
|
+
* buries the one sentence that matters.
|
|
2681
|
+
*/
|
|
2682
|
+
listCommands;
|
|
2683
|
+
constructor(message, listCommands = true) {
|
|
2684
|
+
super(message);
|
|
2685
|
+
this.listCommands = listCommands;
|
|
2686
|
+
}
|
|
2615
2687
|
};
|
|
2616
2688
|
function flag(argv, name) {
|
|
2617
2689
|
const at = argv.indexOf(name);
|
|
@@ -2641,9 +2713,51 @@ function positional(argv) {
|
|
|
2641
2713
|
}
|
|
2642
2714
|
return values;
|
|
2643
2715
|
}
|
|
2716
|
+
async function runPin(argv) {
|
|
2717
|
+
const path = findManifest(flag(argv, "--manifest"));
|
|
2718
|
+
const manifest = loadManifest(path);
|
|
2719
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
2720
|
+
const upstreams = [];
|
|
2721
|
+
try {
|
|
2722
|
+
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2723
|
+
const upstream = await connectStdioUpstream({
|
|
2724
|
+
name,
|
|
2725
|
+
command: spec.command,
|
|
2726
|
+
args: spec.args,
|
|
2727
|
+
stderr: "capture",
|
|
2728
|
+
...spec.env === void 0 ? {} : { env: spec.env }
|
|
2729
|
+
});
|
|
2730
|
+
upstreams.push(upstream);
|
|
2731
|
+
shapes.set(name, await toolShapes(upstream));
|
|
2732
|
+
}
|
|
2733
|
+
} finally {
|
|
2734
|
+
for (const upstream of upstreams) {
|
|
2735
|
+
await upstream.close();
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
const block2 = pinBlock(shapes, manifest);
|
|
2739
|
+
out("");
|
|
2740
|
+
out(` ${style.label("pins for")} ${style.strong(path)}`);
|
|
2741
|
+
out("");
|
|
2742
|
+
for (const line2 of block2.split("\n")) {
|
|
2743
|
+
out(` ${line2}`);
|
|
2744
|
+
}
|
|
2745
|
+
out("");
|
|
2746
|
+
out(` ${style.quiet("Paste this into the manifest. From then on a tool whose shape")}`);
|
|
2747
|
+
out(` ${style.quiet("changes stops the proxy instead of quietly keeping its old policy.")}`);
|
|
2748
|
+
out("");
|
|
2749
|
+
return 0;
|
|
2750
|
+
}
|
|
2644
2751
|
async function runCheck(argv) {
|
|
2645
2752
|
const path = findManifest(flag(argv, "--manifest"));
|
|
2646
2753
|
const manifest = loadManifest(path);
|
|
2754
|
+
const unproven = untested(manifest);
|
|
2755
|
+
if (unproven.length > 0) {
|
|
2756
|
+
out("");
|
|
2757
|
+
for (const line2 of wrapped(warnUntested(unproven), 74)) {
|
|
2758
|
+
out(` ${style.accent(line2)}`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2647
2761
|
const upstreams = [];
|
|
2648
2762
|
try {
|
|
2649
2763
|
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
@@ -2673,8 +2787,20 @@ async function runCheck(argv) {
|
|
|
2673
2787
|
out(` ${rule(54)}`);
|
|
2674
2788
|
out("");
|
|
2675
2789
|
out(` ${style.quiet("servers ")} ${Object.keys(manifest.servers).join(", ")}`);
|
|
2790
|
+
for (const entry of standing(manifest)) {
|
|
2791
|
+
const said = describeStanding(entry);
|
|
2792
|
+
out(
|
|
2793
|
+
` ${style.quiet(" ")} ${style.strong(entry.server)} ${entry.provenance === "documented" ? style.accent(said) : style.quiet(said)}`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2676
2796
|
out(` ${style.quiet("policies")} ${[...counts].map(([k, v]) => `${String(v)} ${k}`).join(", ")}`);
|
|
2677
2797
|
out(` ${style.quiet("guarded ")} ${style.accent(String(gated))}`);
|
|
2798
|
+
const pinned = Object.entries(manifest.pins ?? {});
|
|
2799
|
+
const servers = Object.keys(manifest.servers);
|
|
2800
|
+
const unpinned = servers.filter((name) => !pinned.some(([held]) => held === name));
|
|
2801
|
+
out(
|
|
2802
|
+
` ${style.quiet("pinned ")} ${pinned.length === 0 ? style.quiet("nothing -- run `synartesis pin`") : pinned.map(([name, tools]) => `${name} (${String(Object.keys(tools).length)})`).join(", ") + (unpinned.length === 0 ? "" : style.quiet(`; not ${unpinned.join(", ")}`))}`
|
|
2803
|
+
);
|
|
2678
2804
|
out("");
|
|
2679
2805
|
out(` ${style.quiet("Anything not mentioned here is treated as irreversible and guarded.")}`);
|
|
2680
2806
|
out("");
|
|
@@ -2711,6 +2837,11 @@ async function runInstall(argv) {
|
|
|
2711
2837
|
for (const server of plan.servers) {
|
|
2712
2838
|
const note = server.adopted === void 0 ? style.accent("drafted, every tool held until you say how to undo it") : style.quiet(`the policy that ships for ${server.adopted} (${String(server.tools ?? 0)} tools)`);
|
|
2713
2839
|
out(` ${style.strong(server.name.padEnd(18))} ${note}`);
|
|
2840
|
+
if (server.provenance === "documented") {
|
|
2841
|
+
out(
|
|
2842
|
+
` ${" ".repeat(18)} ${style.accent("never run against the real server -- check it before trusting undo")}`
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2714
2845
|
}
|
|
2715
2846
|
for (const skip of plan.skipped) {
|
|
2716
2847
|
out(` ${style.quiet(skip.name.padEnd(18))} ${style.quiet(skip.why)}`);
|
|
@@ -2921,18 +3052,19 @@ function pick(candidates2, given, noun, newest = false) {
|
|
|
2921
3052
|
if (given === void 0) {
|
|
2922
3053
|
const [only, ...rest2] = candidates2;
|
|
2923
3054
|
if (only === void 0) {
|
|
2924
|
-
throw new UsageError(`there is no ${noun.one} to act on
|
|
3055
|
+
throw new UsageError(`there is no ${noun.one} to act on`, false);
|
|
2925
3056
|
}
|
|
2926
3057
|
if (rest2.length > 0 && !newest) {
|
|
2927
3058
|
throw new UsageError(
|
|
2928
3059
|
`there are ${String(candidates2.length)} ${noun.many}; name one, or use --all:
|
|
2929
|
-
${listed(candidates2)}
|
|
3060
|
+
${listed(candidates2)}`,
|
|
3061
|
+
false
|
|
2930
3062
|
);
|
|
2931
3063
|
}
|
|
2932
3064
|
return only;
|
|
2933
3065
|
}
|
|
2934
3066
|
if (given === "") {
|
|
2935
|
-
throw new UsageError(`no ${noun.one} was named; an empty id is usually an unset variable
|
|
3067
|
+
throw new UsageError(`no ${noun.one} was named; an empty id is usually an unset variable`, false);
|
|
2936
3068
|
}
|
|
2937
3069
|
const exact = candidates2.find((item) => item.id === given);
|
|
2938
3070
|
if (exact !== void 0) {
|
|
@@ -2941,12 +3073,13 @@ ${listed(candidates2)}`
|
|
|
2941
3073
|
const matches = candidates2.filter((item) => item.id.startsWith(given));
|
|
2942
3074
|
const [first, ...rest] = matches;
|
|
2943
3075
|
if (first === void 0) {
|
|
2944
|
-
throw new UsageError(`no ${noun.one} matches ${given}
|
|
3076
|
+
throw new UsageError(`no ${noun.one} matches ${given}`, false);
|
|
2945
3077
|
}
|
|
2946
3078
|
if (rest.length > 0) {
|
|
2947
3079
|
throw new UsageError(
|
|
2948
3080
|
`${given} matches ${String(matches.length)} ${noun.many}:
|
|
2949
|
-
${listed(matches)}
|
|
3081
|
+
${listed(matches)}`,
|
|
3082
|
+
false
|
|
2950
3083
|
);
|
|
2951
3084
|
}
|
|
2952
3085
|
return first;
|
|
@@ -2965,8 +3098,10 @@ function out(line2) {
|
|
|
2965
3098
|
}
|
|
2966
3099
|
function runList(journal, asJson, journalPath) {
|
|
2967
3100
|
const runs = [...journal.listRuns()].reverse();
|
|
3101
|
+
const tally2 = journal.tallyRuns();
|
|
3102
|
+
const counted = (id) => tally2.get(id) ?? { actions: 0, unknown: 0, waiting: 0, applied: 0 };
|
|
2968
3103
|
if (asJson) {
|
|
2969
|
-
out(JSON.stringify(runs.map((run) => ({ ...run, actions:
|
|
3104
|
+
out(JSON.stringify(runs.map((run) => ({ ...run, actions: counted(run.id).actions }))));
|
|
2970
3105
|
return 0;
|
|
2971
3106
|
}
|
|
2972
3107
|
if (runs.length === 0) {
|
|
@@ -2983,16 +3118,15 @@ function runList(journal, asJson, journalPath) {
|
|
|
2983
3118
|
)
|
|
2984
3119
|
);
|
|
2985
3120
|
for (const run of runs) {
|
|
2986
|
-
const actions =
|
|
2987
|
-
const unknown = actions
|
|
2988
|
-
const waiting = actions.filter((action) => action.status === "gated").length;
|
|
3121
|
+
const actions = counted(run.id);
|
|
3122
|
+
const { unknown, waiting } = actions;
|
|
2989
3123
|
const notes = [
|
|
2990
3124
|
unknown === 0 ? "" : `${String(unknown)} of unknown outcome`,
|
|
2991
3125
|
waiting === 0 ? "" : `${String(waiting)} awaiting approval`
|
|
2992
3126
|
].filter((note2) => note2 !== "");
|
|
2993
3127
|
const note = notes.length === 0 ? "" : ` ${style.accent(`(${notes.join("; ")})`)}`;
|
|
2994
3128
|
out(
|
|
2995
|
-
` ${style.strong(run.id)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${run.status.padEnd(12)} ${String(actions.
|
|
3129
|
+
` ${style.strong(run.id)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${run.status.padEnd(12)} ${String(actions.actions).padStart(7)} ${run.label ?? "-"}${note}`
|
|
2996
3130
|
);
|
|
2997
3131
|
}
|
|
2998
3132
|
out("");
|
|
@@ -3062,7 +3196,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3062
3196
|
out(` ${style.quiet(summariseArgs(action.args, 96))}`);
|
|
3063
3197
|
}
|
|
3064
3198
|
if (action.approvedAt !== void 0) {
|
|
3065
|
-
const verb = action
|
|
3199
|
+
const verb = labelFor(action) === "denied" ? "denied" : "approved";
|
|
3066
3200
|
out(
|
|
3067
3201
|
` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${fullTime(action.approvedAt)}`)}`
|
|
3068
3202
|
);
|
|
@@ -3178,7 +3312,8 @@ function truncate3(text, limit) {
|
|
|
3178
3312
|
function summarise2(actions) {
|
|
3179
3313
|
const counts = /* @__PURE__ */ new Map();
|
|
3180
3314
|
for (const action of actions) {
|
|
3181
|
-
|
|
3315
|
+
const label = labelFor(action);
|
|
3316
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
3182
3317
|
}
|
|
3183
3318
|
const parts = [...counts].sort(([a], [b]) => a < b ? -1 : 1).map(([k, v]) => `${String(v)} ${k}`);
|
|
3184
3319
|
const undoable = actions.filter((a) => a.inverse !== void 0).length;
|
|
@@ -3258,12 +3393,14 @@ function runPrune(argv, journal, journalPath) {
|
|
|
3258
3393
|
function runClose(argv, journal) {
|
|
3259
3394
|
const all = journal.listRuns();
|
|
3260
3395
|
const given = positional(argv)[1];
|
|
3261
|
-
const
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3396
|
+
const active = [...all.filter((candidate) => candidate.status === "active")].reverse();
|
|
3397
|
+
if (given === void 0 && active.length === 0) {
|
|
3398
|
+
out("");
|
|
3399
|
+
out(` ${style.quiet("nothing is open; every run has ended cleanly")}`);
|
|
3400
|
+
out("");
|
|
3401
|
+
return 0;
|
|
3402
|
+
}
|
|
3403
|
+
const run = given === void 0 ? pick(active, void 0, RUN, true) : pick([...all].reverse(), given, RUN, true);
|
|
3267
3404
|
const closed = journal.closeAbandonedRun(run.id);
|
|
3268
3405
|
out("");
|
|
3269
3406
|
out(
|
|
@@ -3313,7 +3450,7 @@ function runDecision(argv, journal, approving) {
|
|
|
3313
3450
|
const settled = journal.getAction(given);
|
|
3314
3451
|
if (settled !== void 0 && settled.status !== "gated") {
|
|
3315
3452
|
process.stderr.write(
|
|
3316
|
-
`synartesis: ${given} is no longer awaiting approval (it is ${settled
|
|
3453
|
+
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled)})
|
|
3317
3454
|
`
|
|
3318
3455
|
);
|
|
3319
3456
|
return 1;
|
|
@@ -3362,7 +3499,11 @@ function report(result, alreadyForcing = false) {
|
|
|
3362
3499
|
if (step.plan !== void 0 && step.kind === "revert") {
|
|
3363
3500
|
const verb = `${step.replanned === true ? "replanned, " : ""}${result.dryRun ? "would call" : "called"}`;
|
|
3364
3501
|
out(
|
|
3365
|
-
` ${style.quiet(verb)} ${step.plan.server}.${step.plan.tool} ` +
|
|
3502
|
+
` ${style.quiet(verb)} ${step.plan.server}.${step.plan.tool} ` + // summariseArgs, not truncated json: the raw form spent its whole
|
|
3503
|
+
// budget on a long path's leading directories and cut off before
|
|
3504
|
+
// the filename, so every step read "would call fs.write_file
|
|
3505
|
+
// {path: /Users/.../very/long/pre..." and named nothing.
|
|
3506
|
+
style.quiet(summariseArgs(step.plan.args, 120))
|
|
3366
3507
|
);
|
|
3367
3508
|
}
|
|
3368
3509
|
}
|
|
@@ -3589,7 +3730,8 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
3589
3730
|
"--help",
|
|
3590
3731
|
"-h",
|
|
3591
3732
|
"--version",
|
|
3592
|
-
"-V"
|
|
3733
|
+
"-V",
|
|
3734
|
+
"-v"
|
|
3593
3735
|
]);
|
|
3594
3736
|
function version() {
|
|
3595
3737
|
try {
|
|
@@ -3625,12 +3767,13 @@ async function main(argv) {
|
|
|
3625
3767
|
await import("./proxy.js");
|
|
3626
3768
|
return 0;
|
|
3627
3769
|
}
|
|
3628
|
-
|
|
3770
|
+
const first = positional(argv)[0];
|
|
3771
|
+
if (argv.includes("--help") || argv.includes("-h") || first === "help") {
|
|
3629
3772
|
process.stdout.write(`${banner()}
|
|
3630
3773
|
${COMMANDS}`);
|
|
3631
3774
|
return 0;
|
|
3632
3775
|
}
|
|
3633
|
-
if (argv.includes("--version") || argv.includes("-V")) {
|
|
3776
|
+
if (argv.includes("--version") || argv.includes("-V") || argv.includes("-v") || first === "version") {
|
|
3634
3777
|
process.stdout.write(`${version()}
|
|
3635
3778
|
`);
|
|
3636
3779
|
return 0;
|
|
@@ -3677,6 +3820,9 @@ ${COMMANDS}`);
|
|
|
3677
3820
|
if (command === "check") {
|
|
3678
3821
|
return await runCheck(argv);
|
|
3679
3822
|
}
|
|
3823
|
+
if (command === "pin") {
|
|
3824
|
+
return await runPin(argv);
|
|
3825
|
+
}
|
|
3680
3826
|
if (command === "install") {
|
|
3681
3827
|
return await runInstall(argv);
|
|
3682
3828
|
}
|
|
@@ -3764,9 +3910,12 @@ try {
|
|
|
3764
3910
|
process.exitCode = await main(process.argv.slice(2));
|
|
3765
3911
|
} catch (error) {
|
|
3766
3912
|
if (error instanceof UsageError) {
|
|
3767
|
-
process.stderr.write(
|
|
3913
|
+
process.stderr.write(
|
|
3914
|
+
error.listCommands ? `synartesis: ${error.message}
|
|
3768
3915
|
|
|
3769
|
-
${COMMANDS}`
|
|
3916
|
+
${COMMANDS}` : `synartesis: ${error.message}
|
|
3917
|
+
`
|
|
3918
|
+
);
|
|
3770
3919
|
process.exitCode = 2;
|
|
3771
3920
|
} else if (error instanceof ManifestError) {
|
|
3772
3921
|
process.stderr.write(`synartesis: ${error.message}
|
package/dist/proxy.js
CHANGED
|
@@ -18,8 +18,11 @@ import {
|
|
|
18
18
|
refusal,
|
|
19
19
|
runRead,
|
|
20
20
|
toPayload,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
untested,
|
|
22
|
+
verifyAgainstServers,
|
|
23
|
+
warnUntested,
|
|
24
|
+
withIdempotencyKey
|
|
25
|
+
} from "./chunk-6X7GPUGJ.js";
|
|
23
26
|
import {
|
|
24
27
|
SnapshotError,
|
|
25
28
|
UpstreamError,
|
|
@@ -730,7 +733,11 @@ function createProxyServer(options) {
|
|
|
730
733
|
}
|
|
731
734
|
const forwarded = {
|
|
732
735
|
method: "tools/call",
|
|
733
|
-
params: {
|
|
736
|
+
params: {
|
|
737
|
+
...request.params,
|
|
738
|
+
name: route.tool,
|
|
739
|
+
_meta: withIdempotencyKey(request.params._meta, pending.idempotencyKey)
|
|
740
|
+
}
|
|
734
741
|
};
|
|
735
742
|
try {
|
|
736
743
|
const result = await route.upstream.client.request(forwarded, PassthroughResult, {
|
|
@@ -775,6 +782,13 @@ function createProxyServer(options) {
|
|
|
775
782
|
warnings.push(`inverse could not be resolved: ${describe(error)}`);
|
|
776
783
|
}
|
|
777
784
|
}
|
|
785
|
+
if (verify === void 0 && policy.verify !== void 0) {
|
|
786
|
+
try {
|
|
787
|
+
verify = planRead(policy.verify, context);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
warnings.push(`the drift check could not be planned: ${describe(error)}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
778
792
|
let postSnapshot;
|
|
779
793
|
if (verify !== void 0) {
|
|
780
794
|
try {
|
|
@@ -1109,6 +1123,10 @@ async function main() {
|
|
|
1109
1123
|
process.stderr.write(mark());
|
|
1110
1124
|
}
|
|
1111
1125
|
const manifest = loadManifest(argv.manifest);
|
|
1126
|
+
const unproven = untested(manifest);
|
|
1127
|
+
if (unproven.length > 0) {
|
|
1128
|
+
log.warn({ servers: unproven }, warnUntested(unproven));
|
|
1129
|
+
}
|
|
1112
1130
|
const journal = openJournal(argv.journal);
|
|
1113
1131
|
const declared = Object.entries(manifest.servers);
|
|
1114
1132
|
const wanted = argv.server === void 0 ? declared : declared.filter(([name]) => name === argv.server);
|
|
@@ -14,6 +14,7 @@ servers:
|
|
|
14
14
|
fs:
|
|
15
15
|
command: node
|
|
16
16
|
args: ["node_modules/@modelcontextprotocol/server-filesystem/dist/index.js", "."]
|
|
17
|
+
provenance: live
|
|
17
18
|
|
|
18
19
|
tools:
|
|
19
20
|
# --- reads -------------------------------------------------------------
|
|
@@ -105,3 +106,10 @@ tools:
|
|
|
105
106
|
- match: "fs.create_directory"
|
|
106
107
|
class: irreversible
|
|
107
108
|
gate: always
|
|
109
|
+
|
|
110
|
+
# This policy is written for the server version named at the top. A later
|
|
111
|
+
# version can keep a tool's name and change what it takes, which would leave
|
|
112
|
+
# the policy above describing something else -- and undo would still produce an
|
|
113
|
+
# answer. `synartesis pin` prints a `pins:` block for the server you actually
|
|
114
|
+
# have; with it pasted in, a tool whose shape has moved stops the proxy at
|
|
115
|
+
# startup instead of being quietly trusted.
|
package/manifests/git.yaml
CHANGED
|
@@ -15,6 +15,7 @@ servers:
|
|
|
15
15
|
git:
|
|
16
16
|
command: uvx
|
|
17
17
|
args: ["--from", "mcp-server-git", "mcp-server-git"]
|
|
18
|
+
provenance: live
|
|
18
19
|
|
|
19
20
|
tools:
|
|
20
21
|
# --- reads -------------------------------------------------------------
|
|
@@ -75,3 +76,10 @@ tools:
|
|
|
75
76
|
- match: "git.git_checkout"
|
|
76
77
|
class: irreversible
|
|
77
78
|
gate: never
|
|
79
|
+
|
|
80
|
+
# This policy is written for the server version named at the top. A later
|
|
81
|
+
# version can keep a tool's name and change what it takes, which would leave
|
|
82
|
+
# the policy above describing something else -- and undo would still produce an
|
|
83
|
+
# answer. `synartesis pin` prints a `pins:` block for the server you actually
|
|
84
|
+
# have; with it pasted in, a tool whose shape has moved stops the proxy at
|
|
85
|
+
# startup instead of being quietly trusted.
|
package/manifests/github.yaml
CHANGED
|
@@ -24,6 +24,9 @@ servers:
|
|
|
24
24
|
# Read from your shell; never written into this file.
|
|
25
25
|
env:
|
|
26
26
|
GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
27
|
+
# Nothing here has met a real github; every shape below comes from
|
|
28
|
+
# documentation. That is a weaker claim than the others make.
|
|
29
|
+
provenance: documented
|
|
27
30
|
|
|
28
31
|
tools:
|
|
29
32
|
# --- reads -------------------------------------------------------------
|
|
@@ -188,3 +191,10 @@ tools:
|
|
|
188
191
|
- match: "github.create_release"
|
|
189
192
|
class: irreversible
|
|
190
193
|
gate: always
|
|
194
|
+
|
|
195
|
+
# This policy is written for the server version named at the top. A later
|
|
196
|
+
# version can keep a tool's name and change what it takes, which would leave
|
|
197
|
+
# the policy above describing something else -- and undo would still produce an
|
|
198
|
+
# answer. `synartesis pin` prints a `pins:` block for the server you actually
|
|
199
|
+
# have; with it pasted in, a tool whose shape has moved stops the proxy at
|
|
200
|
+
# startup instead of being quietly trusted.
|
package/manifests/memory.yaml
CHANGED
|
@@ -18,6 +18,7 @@ servers:
|
|
|
18
18
|
# or the agent will remember into a different file than it reads from.
|
|
19
19
|
env:
|
|
20
20
|
MEMORY_FILE_PATH: "${MEMORY_FILE_PATH}"
|
|
21
|
+
provenance: live
|
|
21
22
|
|
|
22
23
|
tools:
|
|
23
24
|
# --- reads -------------------------------------------------------------
|
|
@@ -54,6 +55,16 @@ tools:
|
|
|
54
55
|
tool: "memory.delete_entities"
|
|
55
56
|
args:
|
|
56
57
|
entityNames: "$result.entities[].name"
|
|
58
|
+
# Deleting an entity takes its observations and its relations with it. So
|
|
59
|
+
# the compensating delete is only safe while the entity is still the thing
|
|
60
|
+
# the agent made: if anybody has added to it since, undoing destroys work
|
|
61
|
+
# nobody recorded. There is no pre-read to compare against -- the entity
|
|
62
|
+
# did not exist before the call -- so this reads it back afterwards, and
|
|
63
|
+
# undo halts rather than deleting over somebody.
|
|
64
|
+
verify:
|
|
65
|
+
tool: "memory.open_nodes"
|
|
66
|
+
args:
|
|
67
|
+
names: "$result.entities[].name"
|
|
57
68
|
|
|
58
69
|
- match: "memory.create_relations"
|
|
59
70
|
class: compensable
|
|
@@ -94,3 +105,10 @@ tools:
|
|
|
94
105
|
- match: "memory.delete_observations"
|
|
95
106
|
class: irreversible
|
|
96
107
|
gate: always
|
|
108
|
+
|
|
109
|
+
# This policy is written for the server version named at the top. A later
|
|
110
|
+
# version can keep a tool's name and change what it takes, which would leave
|
|
111
|
+
# the policy above describing something else -- and undo would still produce an
|
|
112
|
+
# answer. `synartesis pin` prints a `pins:` block for the server you actually
|
|
113
|
+
# have; with it pasted in, a tool whose shape has moved stops the proxy at
|
|
114
|
+
# startup instead of being quietly trusted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synartesis",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "An undo layer for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"overrides": {
|
|
75
75
|
"fast-uri": "^3.1.7",
|
|
76
76
|
"hono": "^4.13.7",
|
|
77
|
-
"qs": "^6.16.0"
|
|
77
|
+
"qs": "^6.16.0",
|
|
78
|
+
"esbuild": "^0.28.1"
|
|
78
79
|
}
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|