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
package/dist/cli.js
CHANGED
|
@@ -1,368 +1,238 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CLIENT_IDS,
|
|
4
|
+
ConfigError,
|
|
3
5
|
IDEMPOTENCY_META_KEY,
|
|
4
6
|
LIVE_IS_NOT_RECOVERY,
|
|
7
|
+
LOOKED_FOR,
|
|
5
8
|
NOTHING_RECORDED_YET,
|
|
6
9
|
PROXY_FLAGS,
|
|
7
10
|
WORDMARK,
|
|
11
|
+
applyInstall,
|
|
12
|
+
applyUninstall,
|
|
8
13
|
banner,
|
|
14
|
+
canNotify,
|
|
9
15
|
canonical,
|
|
10
16
|
cliCommand,
|
|
11
|
-
|
|
17
|
+
clientEnvFor,
|
|
18
|
+
connectUpstream,
|
|
12
19
|
counted,
|
|
13
20
|
createPolicyResolver,
|
|
14
21
|
createRouter,
|
|
15
22
|
describeStanding,
|
|
23
|
+
desktopNotifier,
|
|
24
|
+
differing,
|
|
25
|
+
discover,
|
|
26
|
+
draftManifest,
|
|
16
27
|
errorStyle,
|
|
17
28
|
findJournal,
|
|
18
29
|
findManifest,
|
|
30
|
+
fingerprint,
|
|
31
|
+
fingerprint2,
|
|
32
|
+
invokerFor,
|
|
33
|
+
isClientId,
|
|
34
|
+
isWrapped,
|
|
19
35
|
labelFor,
|
|
36
|
+
listAll,
|
|
20
37
|
loadManifest,
|
|
21
38
|
observeState,
|
|
22
39
|
openJournal,
|
|
23
40
|
parseManifest,
|
|
24
|
-
pathBinaryMatches,
|
|
25
41
|
pinBlock,
|
|
42
|
+
planInstall,
|
|
26
43
|
planInverse,
|
|
27
44
|
planRead,
|
|
28
45
|
proxyCommand,
|
|
29
46
|
qualify,
|
|
30
47
|
resolvedRead,
|
|
31
48
|
rule,
|
|
49
|
+
serversAt,
|
|
50
|
+
splitQualified,
|
|
32
51
|
standing,
|
|
52
|
+
startAll,
|
|
53
|
+
startTogether,
|
|
33
54
|
style,
|
|
34
55
|
toPayload,
|
|
35
56
|
toResolvedRead,
|
|
36
57
|
toolShapes,
|
|
58
|
+
trustsMarks,
|
|
37
59
|
ungoverned,
|
|
38
60
|
untested,
|
|
61
|
+
upstreamEnv,
|
|
39
62
|
verifyAgainstServers,
|
|
40
63
|
warnUntested,
|
|
41
64
|
wasRefused
|
|
42
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-H4SPMXQW.js";
|
|
43
66
|
import {
|
|
44
67
|
DriftConflict,
|
|
45
68
|
ManifestError,
|
|
46
69
|
RollbackHalted,
|
|
47
70
|
SynartesisError,
|
|
48
|
-
UpstreamError,
|
|
49
71
|
changedLines,
|
|
50
72
|
describe
|
|
51
73
|
} from "./chunk-YVOO3PTV.js";
|
|
52
74
|
|
|
53
75
|
// src/cli.ts
|
|
76
|
+
import { platform } from "os";
|
|
54
77
|
import { spawn } from "child_process";
|
|
55
|
-
import { existsSync as
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
// src/init/draft.ts
|
|
60
|
-
import { z } from "zod";
|
|
61
|
-
|
|
62
|
-
// src/init/known.ts
|
|
63
|
-
import { existsSync, readFileSync } from "fs";
|
|
78
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
79
|
+
import { createInterface } from "readline/promises";
|
|
80
|
+
import { dirname, join as join2, resolve } from "path";
|
|
64
81
|
import { fileURLToPath } from "url";
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
|
|
83
|
+
// src/manifest/edit.ts
|
|
84
|
+
import { isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
85
|
+
var PolicyEditError = class extends Error {
|
|
86
|
+
name = "PolicyEditError";
|
|
87
|
+
};
|
|
88
|
+
function allowAlways(input) {
|
|
89
|
+
const { text, file, server, tool } = input;
|
|
90
|
+
const qualified = `${server}.${tool}`;
|
|
91
|
+
const before = parseManifest(text, file);
|
|
92
|
+
if (before.servers[server] === void 0) {
|
|
93
|
+
throw new PolicyEditError(`${file} has no server called ${server}`);
|
|
94
|
+
}
|
|
95
|
+
const current = createPolicyResolver(before).resolve(qualified);
|
|
96
|
+
const doc = parseDocument(text, { keepSourceTokens: true });
|
|
97
|
+
const tools = doc.get("tools", true);
|
|
98
|
+
const exact = isSeq(tools) ? tools.items.find((item) => isMap(item) && item.get("match") === qualified) : void 0;
|
|
99
|
+
let edited;
|
|
100
|
+
let how;
|
|
101
|
+
if (exact !== void 0) {
|
|
102
|
+
if (!isMap(exact) || exact.flow === true) {
|
|
103
|
+
throw new PolicyEditError(`the rule for ${qualified} is written on one line; change its gate by hand`);
|
|
104
|
+
}
|
|
105
|
+
const gate = exact.items.find((pair) => isScalar(pair.key) && pair.key.value === "gate");
|
|
106
|
+
if (gate !== void 0 && isScalar(gate.value) && gate.value.value === "never") {
|
|
107
|
+
how = "already";
|
|
108
|
+
edited = text;
|
|
109
|
+
} else if (gate !== void 0) {
|
|
110
|
+
const at = rangeOf(gate.value, qualified);
|
|
111
|
+
edited = text.slice(0, at[0]) + "never" + text.slice(at[1]);
|
|
112
|
+
how = "changed";
|
|
113
|
+
} else {
|
|
114
|
+
const cls = exact.items.find((pair) => isScalar(pair.key) && pair.key.value === "class");
|
|
115
|
+
const key = cls?.key;
|
|
116
|
+
if (cls === void 0 || !isScalar(key)) {
|
|
117
|
+
throw new PolicyEditError(`the rule for ${qualified} has no class; add one by hand first`);
|
|
118
|
+
}
|
|
119
|
+
const keyAt = rangeOf(key, qualified)[0];
|
|
120
|
+
const indent = " ".repeat(keyAt - lineStart(text, keyAt));
|
|
121
|
+
const lineEnd = endOfLine(text, rangeOf(cls.value, qualified)[1]);
|
|
122
|
+
edited = `${text.slice(0, lineEnd)}
|
|
123
|
+
${indent}gate: never${text.slice(lineEnd)}`;
|
|
124
|
+
how = "changed";
|
|
99
125
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
} else {
|
|
127
|
+
if (tools !== void 0 && (!isSeq(tools) || tools.flow === true || tools.items.length === 0)) {
|
|
128
|
+
throw new PolicyEditError(`the tools list in ${file} is not written one rule to a line; add the rule by hand`);
|
|
129
|
+
}
|
|
130
|
+
const base = current.matched ? { ...current.policy } : { class: "irreversible" };
|
|
131
|
+
delete base["match"];
|
|
132
|
+
delete base["refusal"];
|
|
133
|
+
const rule2 = { match: qualified, ...base, gate: "never" };
|
|
134
|
+
const said = [`# Let through without asking, by ${input.by} on ${input.date}.`];
|
|
135
|
+
if (base["class"] === "irreversible") {
|
|
136
|
+
said.push("# It still cannot be undone: it is recorded, and no longer held.");
|
|
137
|
+
}
|
|
138
|
+
const body = stringify([rule2], { lineWidth: 0 }).trimEnd().replace(/^- match: .*$/m, `- match: ${JSON.stringify(qualified)}`);
|
|
139
|
+
if (isSeq(tools)) {
|
|
140
|
+
const seqAt = rangeOf(tools, "tools")[0];
|
|
141
|
+
const indent = " ".repeat(seqAt - lineStart(text, seqAt));
|
|
142
|
+
const lines = [...said, ...body.split("\n")].map((line2) => indent + line2);
|
|
143
|
+
edited = insertAfter(text, rangeOf(tools, "tools")[2], `
|
|
144
|
+
${lines.join("\n")}
|
|
145
|
+
`);
|
|
146
|
+
} else {
|
|
147
|
+
edited = `${text.replace(/\n*$/, "\n")}
|
|
105
148
|
tools:
|
|
106
|
-
${
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const claimed = /^\s*provenance:\s*(live|documented)\s*$/m.exec(text)?.[1];
|
|
111
|
-
return {
|
|
112
|
-
key,
|
|
113
|
-
rules,
|
|
114
|
-
name: hit.manifest,
|
|
115
|
-
source,
|
|
116
|
-
...claimed === "live" || claimed === "documented" ? { provenance: claimed } : {}
|
|
117
|
-
};
|
|
118
|
-
} catch {
|
|
119
|
-
return void 0;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function toolsBlock(text) {
|
|
123
|
-
const at = text.search(/^tools:[ \t]*$/m);
|
|
124
|
-
if (at === -1) {
|
|
125
|
-
return void 0;
|
|
149
|
+
${[...said, ...body.split("\n")].map((line2) => ` ${line2}`).join("\n")}
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
how = "added";
|
|
126
153
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
break;
|
|
154
|
+
const pins = before.pins?.[server];
|
|
155
|
+
if (how !== "already" && pins !== void 0 && pins[tool] === void 0) {
|
|
156
|
+
if (input.pin === void 0) {
|
|
157
|
+
throw new PolicyEditError(`${server} is pinned, and ${tool} has no pin yet`);
|
|
132
158
|
}
|
|
133
|
-
|
|
159
|
+
edited = addPin(edited, server, tool, input.pin, qualified);
|
|
134
160
|
}
|
|
135
|
-
|
|
161
|
+
const after = parseManifest(edited, file);
|
|
162
|
+
const policy = createPolicyResolver(after).resolve(qualified).policy;
|
|
163
|
+
confirmOnly(before, after, { server, tool }, current.policy, policy, input.pin);
|
|
164
|
+
return { text: edited, how, policy };
|
|
136
165
|
}
|
|
137
|
-
function
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
const body = text.slice(text.indexOf("\n", at) + 1);
|
|
143
|
-
for (const line2 of body.split("\n")) {
|
|
144
|
-
if (/^[^\s#]/.test(line2)) {
|
|
145
|
-
return void 0;
|
|
146
|
-
}
|
|
147
|
-
const named = /^ {2}([A-Za-z0-9_-]+):[ \t]*$/.exec(line2);
|
|
148
|
-
if (named?.[1] !== void 0) {
|
|
149
|
-
return named[1];
|
|
150
|
-
}
|
|
166
|
+
function rangeOf(node, what) {
|
|
167
|
+
const range = isNode(node) ? node.range : void 0;
|
|
168
|
+
if (range === void 0 || range === null) {
|
|
169
|
+
throw new PolicyEditError(`could not find where ${what} is written in the policy`);
|
|
151
170
|
}
|
|
152
|
-
return
|
|
171
|
+
return range;
|
|
153
172
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
|
|
157
|
-
(name) => name !== void 0
|
|
158
|
-
);
|
|
173
|
+
function lineStart(text, at) {
|
|
174
|
+
return text.lastIndexOf("\n", at - 1) + 1;
|
|
159
175
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
name: z.string(),
|
|
164
|
-
description: z.string().optional(),
|
|
165
|
-
annotations: z.looseObject({
|
|
166
|
-
readOnlyHint: z.boolean().optional(),
|
|
167
|
-
destructiveHint: z.boolean().optional(),
|
|
168
|
-
idempotentHint: z.boolean().optional()
|
|
169
|
-
}).optional()
|
|
170
|
-
});
|
|
171
|
-
var listSchema = z.looseObject({
|
|
172
|
-
tools: z.array(toolSchema),
|
|
173
|
-
nextCursor: z.string().optional()
|
|
174
|
-
});
|
|
175
|
-
function quote(value) {
|
|
176
|
-
return JSON.stringify(value);
|
|
176
|
+
function endOfLine(text, at) {
|
|
177
|
+
const next = text.indexOf("\n", Math.max(0, at - 1));
|
|
178
|
+
return next === -1 ? text.length : next;
|
|
177
179
|
}
|
|
178
|
-
function
|
|
179
|
-
if (text ===
|
|
180
|
-
return
|
|
180
|
+
function insertAfter(text, at, block2) {
|
|
181
|
+
if (at > 0 && text[at - 1] === "\n") {
|
|
182
|
+
return text.slice(0, at) + block2.slice(1) + text.slice(at);
|
|
181
183
|
}
|
|
182
|
-
const
|
|
183
|
-
return
|
|
184
|
+
const end = endOfLine(text, at);
|
|
185
|
+
return text.slice(0, end) + block2.replace(/\n$/, "") + text.slice(end);
|
|
184
186
|
}
|
|
185
|
-
function
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
lines.push(` # ${description}`);
|
|
191
|
-
}
|
|
192
|
-
if (tool.annotations?.readOnlyHint === true) {
|
|
193
|
-
lines.push(` # classified readonly from the server's readOnlyHint; verify it before relying on it.`);
|
|
194
|
-
lines.push(` - match: ${quote(match)}`);
|
|
195
|
-
lines.push(` class: readonly`);
|
|
196
|
-
return lines.join("\n");
|
|
197
|
-
}
|
|
198
|
-
lines.push(` # TODO: this is gated on every call until you describe how to undo it.`);
|
|
199
|
-
lines.push(` # reversible needs a snapshot (a pre-read) and an inverse.`);
|
|
200
|
-
lines.push(` # compensable needs an inverse only, usually built from $result.`);
|
|
201
|
-
lines.push(` # irreversible is correct when neither exists; leave gate: always.`);
|
|
202
|
-
lines.push(` - match: ${quote(match)}`);
|
|
203
|
-
lines.push(` class: irreversible`);
|
|
204
|
-
lines.push(` gate: always`);
|
|
205
|
-
return lines.join("\n");
|
|
206
|
-
}
|
|
207
|
-
function patternFor(match) {
|
|
208
|
-
const source = match.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
|
|
209
|
-
return new RegExp(`^${source}$`);
|
|
210
|
-
}
|
|
211
|
-
function adopt(known, name, tools) {
|
|
212
|
-
const advertised = new Set(tools.map((tool) => tool.name));
|
|
213
|
-
const covered = /* @__PURE__ */ new Set();
|
|
214
|
-
for (const rule2 of known.rules) {
|
|
215
|
-
for (const needed of toolsReferencedBy(rule2, known.key)) {
|
|
216
|
-
if (!advertised.has(needed)) {
|
|
217
|
-
return void 0;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const test = patternFor(rule2.match);
|
|
221
|
-
for (const tool of tools) {
|
|
222
|
-
if (test.test(`${known.key}.${tool.name}`)) {
|
|
223
|
-
covered.add(tool.name);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (covered.size === 0) {
|
|
228
|
-
return void 0;
|
|
187
|
+
function addPin(text, server, tool, pin, qualified) {
|
|
188
|
+
const doc = parseDocument(text, { keepSourceTokens: true });
|
|
189
|
+
const map = doc.getIn(["pins", server], true);
|
|
190
|
+
if (!isMap(map) || map.flow === true || map.items.length === 0) {
|
|
191
|
+
throw new PolicyEditError(`the pins for ${server} are not written one to a line; pin ${qualified} by hand`);
|
|
229
192
|
}
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
args: options.args,
|
|
244
|
-
stderr: "capture"
|
|
245
|
-
});
|
|
246
|
-
let tools;
|
|
247
|
-
try {
|
|
248
|
-
const collected = [];
|
|
249
|
-
let cursor;
|
|
250
|
-
do {
|
|
251
|
-
const page = listSchema.parse(
|
|
252
|
-
await upstream.client.request(
|
|
253
|
-
{ method: "tools/list", params: cursor === void 0 ? {} : { cursor } },
|
|
254
|
-
z.looseObject({})
|
|
255
|
-
)
|
|
256
|
-
);
|
|
257
|
-
collected.push(...page.tools);
|
|
258
|
-
cursor = page.nextCursor;
|
|
259
|
-
} while (cursor !== void 0);
|
|
260
|
-
tools = collected;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
throw new UpstreamError(options.name, "tools/list", error);
|
|
263
|
-
} finally {
|
|
264
|
-
await upstream.close();
|
|
193
|
+
const at = rangeOf(map, "pins")[0];
|
|
194
|
+
const indent = " ".repeat(at - lineStart(text, at));
|
|
195
|
+
return insertAfter(text, rangeOf(map, "pins")[2], `
|
|
196
|
+
${indent}${tool}: ${JSON.stringify(pin)}
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
function confirmOnly(before, after, { server, tool }, was, now, pin) {
|
|
200
|
+
const qualified = `${server}.${tool}`;
|
|
201
|
+
const refuse = (why) => {
|
|
202
|
+
throw new PolicyEditError(`the edit came out wrong (${why}), so nothing was written`);
|
|
203
|
+
};
|
|
204
|
+
if (now.match !== qualified || now.gate !== "never") {
|
|
205
|
+
refuse(`${qualified} is not let through`);
|
|
265
206
|
}
|
|
266
|
-
if (
|
|
267
|
-
|
|
207
|
+
if (now.class !== was.class) {
|
|
208
|
+
refuse(`${qualified} would change from ${was.class} to ${now.class}`);
|
|
268
209
|
}
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
throw new ManifestError(
|
|
273
|
-
`${options.name} is already declared in the manifest; remove it first or choose another name`
|
|
274
|
-
);
|
|
210
|
+
const others = (manifest) => manifest.tools.filter((rule2) => rule2.match !== qualified).map((rule2) => canonical(rule2));
|
|
211
|
+
if (canonical(others(before)) !== canonical(others(after))) {
|
|
212
|
+
refuse("another rule changed");
|
|
275
213
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
...known.provenance === "documented" ? [
|
|
279
|
-
` # This policy has never been run against the real server. Check it`,
|
|
280
|
-
` # against your own setup before trusting undo on it.`
|
|
281
|
-
] : [],
|
|
282
|
-
` provenance: ${known.provenance}`
|
|
283
|
-
];
|
|
284
|
-
const server = [
|
|
285
|
-
` ${options.name}:`,
|
|
286
|
-
` command: ${quote(options.command)}`,
|
|
287
|
-
` args: [${options.args.map(quote).join(", ")}]`,
|
|
288
|
-
...claim
|
|
289
|
-
].join("\n");
|
|
290
|
-
const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
|
|
291
|
-
const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
|
|
292
|
-
if (existing === void 0) {
|
|
293
|
-
const yaml = [
|
|
294
|
-
`# Generated by synartesis init from ${options.name}'s tools/list.`,
|
|
295
|
-
...adopted === void 0 ? [
|
|
296
|
-
`# Every tool starts gated. Working through the TODOs is the whole job:`,
|
|
297
|
-
`# a tool with no inverse is one an agent cannot use unsupervised.`
|
|
298
|
-
] : [
|
|
299
|
-
`# ${String(adopted.covered)} of its tools were recognised, so the policy that ships`,
|
|
300
|
-
`# with Synartesis for ${known?.name ?? "this server"} was used and checked against what this`,
|
|
301
|
-
`# server actually advertises. Read it before trusting it: it is a starting`,
|
|
302
|
-
`# point that happens to be finished, not a promise about your setup.`
|
|
303
|
-
],
|
|
304
|
-
``,
|
|
305
|
-
`version: 1`,
|
|
306
|
-
``,
|
|
307
|
-
`servers:`,
|
|
308
|
-
server,
|
|
309
|
-
``,
|
|
310
|
-
`tools:`,
|
|
311
|
-
policies,
|
|
312
|
-
``
|
|
313
|
-
].join("\n");
|
|
314
|
-
return adopted === void 0 || known === void 0 ? { yaml } : {
|
|
315
|
-
yaml,
|
|
316
|
-
adopted: {
|
|
317
|
-
server: known.name,
|
|
318
|
-
tools: adopted.covered,
|
|
319
|
-
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
320
|
-
}
|
|
321
|
-
};
|
|
214
|
+
if (canonical(before.servers) !== canonical(after.servers)) {
|
|
215
|
+
refuse("a server changed");
|
|
322
216
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
server: known.name,
|
|
328
|
-
tools: adopted.covered,
|
|
329
|
-
...known.provenance === void 0 ? {} : { provenance: known.provenance }
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
function mergeInto(existing, server, policies, name) {
|
|
334
|
-
const serversAt2 = existing.indexOf("\nservers:");
|
|
335
|
-
const toolsAt = existing.indexOf("\ntools:");
|
|
336
|
-
if (serversAt2 === -1 || toolsAt === -1 || toolsAt < serversAt2) {
|
|
337
|
-
throw new ManifestError(
|
|
338
|
-
"the existing manifest does not have a servers: block followed by a tools: block, so it cannot be extended automatically"
|
|
339
|
-
);
|
|
217
|
+
const pinned = before.pins?.[server];
|
|
218
|
+
const expected = pin === void 0 || pinned === void 0 || pinned[tool] !== void 0 ? before.pins : { ...before.pins, [server]: { ...pinned, [tool]: pin } };
|
|
219
|
+
if (canonical(expected ?? null) !== canonical(after.pins ?? null)) {
|
|
220
|
+
refuse("the pins changed");
|
|
340
221
|
}
|
|
341
|
-
const head = existing.slice(0, toolsAt);
|
|
342
|
-
const tail = existing.slice(toolsAt);
|
|
343
|
-
return [
|
|
344
|
-
head.trimEnd(),
|
|
345
|
-
server,
|
|
346
|
-
tail.trimEnd(),
|
|
347
|
-
``,
|
|
348
|
-
` # --- added by synartesis init for ${name} ---`,
|
|
349
|
-
policies,
|
|
350
|
-
``
|
|
351
|
-
].join("\n");
|
|
352
222
|
}
|
|
353
223
|
|
|
354
224
|
// src/rollback/rollback.ts
|
|
355
|
-
import { z
|
|
356
|
-
var inversePlan =
|
|
357
|
-
server:
|
|
358
|
-
tool:
|
|
359
|
-
args:
|
|
225
|
+
import { z } from "zod";
|
|
226
|
+
var inversePlan = z.object({
|
|
227
|
+
server: z.string(),
|
|
228
|
+
tool: z.string(),
|
|
229
|
+
args: z.record(z.string(), z.unknown())
|
|
360
230
|
});
|
|
361
|
-
var observation =
|
|
362
|
-
|
|
363
|
-
|
|
231
|
+
var observation = z.union([
|
|
232
|
+
z.object({ present: z.literal(true), value: z.unknown() }),
|
|
233
|
+
z.object({ present: z.literal(false) })
|
|
364
234
|
]);
|
|
365
|
-
var toolResult =
|
|
235
|
+
var toolResult = z.looseObject({ isError: z.boolean().default(false) });
|
|
366
236
|
function sameState(a, b) {
|
|
367
237
|
return canonical(a) === canonical(b);
|
|
368
238
|
}
|
|
@@ -505,7 +375,11 @@ ${seen}` : seen;
|
|
|
505
375
|
}
|
|
506
376
|
if (sameState(current, recordedPost.data)) {
|
|
507
377
|
verified = true;
|
|
508
|
-
} else if (sameState(current, intendedAfterInverse(action))
|
|
378
|
+
} else if (sameState(current, intendedAfterInverse(action)) || // An inverse this undo sent before it was killed, whose owner is gone:
|
|
379
|
+
// the resource having reached the state it produces is the inverse
|
|
380
|
+
// having landed. Found by SIGKILLing undo mid-compensation: the delete
|
|
381
|
+
// had gone through, and every later attempt called its absence drift.
|
|
382
|
+
action.status === "rolling_back" && journal.leaseFor(action.id)?.alive === false && looksUndone(current, action)) {
|
|
509
383
|
steps.push({
|
|
510
384
|
...describeStep(action),
|
|
511
385
|
kind: "already-reverted",
|
|
@@ -582,7 +456,7 @@ ${seen}` : seen;
|
|
|
582
456
|
steps.push({
|
|
583
457
|
...describeStep(action),
|
|
584
458
|
kind: "revert",
|
|
585
|
-
reason: verified ? "
|
|
459
|
+
reason: verified ? "unchanged since, so safe to put back" : forcedOver ?? unverifiedBecause(action),
|
|
586
460
|
verified,
|
|
587
461
|
plan,
|
|
588
462
|
...rebuilt.inverse === void 0 ? {} : { replanned: true },
|
|
@@ -620,6 +494,19 @@ ${seen}` : seen;
|
|
|
620
494
|
journal.markRolledBack(action.id);
|
|
621
495
|
continue;
|
|
622
496
|
}
|
|
497
|
+
const landed = outcome.rejected && recordedPost.success && verifyRead.success ? await landedAnyway(router, toResolvedRead(verifyRead.data), recordedPost.data, action, signal) : void 0;
|
|
498
|
+
if (landed !== void 0) {
|
|
499
|
+
journal.markRolledBack(action.id);
|
|
500
|
+
steps[steps.length - 1] = {
|
|
501
|
+
...describeStep(action),
|
|
502
|
+
kind: "revert",
|
|
503
|
+
reason: landed,
|
|
504
|
+
verified,
|
|
505
|
+
plan,
|
|
506
|
+
note: `the server reported an error (${truncated(outcome.message)}), but the resource had changed as this inverse changes it`
|
|
507
|
+
};
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
623
510
|
const halt = new RollbackHalted(action.seq, outcome.message);
|
|
624
511
|
if (outcome.rejected) {
|
|
625
512
|
journal.markInverseRejected(action.id, halt.message);
|
|
@@ -677,6 +564,28 @@ function overwriteText(current, action) {
|
|
|
677
564
|
}
|
|
678
565
|
return changedLines(current, intended2);
|
|
679
566
|
}
|
|
567
|
+
async function landedAnyway(router, read, post, action, signal) {
|
|
568
|
+
let now;
|
|
569
|
+
try {
|
|
570
|
+
now = await observeState(router, read, signal);
|
|
571
|
+
} catch {
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
if (sameState(now, post)) {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
return looksUndone(now, action) ? "done, despite the server reporting an error" : void 0;
|
|
578
|
+
}
|
|
579
|
+
function looksUndone(now, action) {
|
|
580
|
+
const intended2 = intendedAfterInverse(action);
|
|
581
|
+
if (intended2 !== void 0) {
|
|
582
|
+
return sameState(now, intended2);
|
|
583
|
+
}
|
|
584
|
+
return action.class === "compensable" && !now.present;
|
|
585
|
+
}
|
|
586
|
+
function truncated(text) {
|
|
587
|
+
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
588
|
+
}
|
|
680
589
|
function intendedAfterInverse(action) {
|
|
681
590
|
return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
|
|
682
591
|
}
|
|
@@ -696,11 +605,11 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
696
605
|
_meta: { [IDEMPOTENCY_META_KEY]: idempotencyKey }
|
|
697
606
|
}
|
|
698
607
|
},
|
|
699
|
-
|
|
608
|
+
z.looseObject({}),
|
|
700
609
|
{ signal }
|
|
701
610
|
);
|
|
702
611
|
} catch (error) {
|
|
703
|
-
return { ok: false, rejected:
|
|
612
|
+
return { ok: false, rejected: upstream.classify?.(error) !== void 0, message: describe(error) };
|
|
704
613
|
}
|
|
705
614
|
const parsed = toolResult.safeParse(raw);
|
|
706
615
|
if (parsed.success && parsed.data.isError) {
|
|
@@ -714,10 +623,10 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
714
623
|
}
|
|
715
624
|
|
|
716
625
|
// src/rollback/inspect.ts
|
|
717
|
-
import { z as
|
|
718
|
-
var observation2 =
|
|
719
|
-
|
|
720
|
-
|
|
626
|
+
import { z as z2 } from "zod";
|
|
627
|
+
var observation2 = z2.union([
|
|
628
|
+
z2.object({ present: z2.literal(true), value: z2.unknown() }),
|
|
629
|
+
z2.object({ present: z2.literal(false) })
|
|
721
630
|
]);
|
|
722
631
|
function sameState2(a, b) {
|
|
723
632
|
return canonical(a) === canonical(b);
|
|
@@ -1035,7 +944,7 @@ function shortList() {
|
|
|
1035
944
|
}
|
|
1036
945
|
|
|
1037
946
|
// src/watch.ts
|
|
1038
|
-
import { existsSync
|
|
947
|
+
import { existsSync } from "fs";
|
|
1039
948
|
|
|
1040
949
|
// src/keys.ts
|
|
1041
950
|
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
@@ -1157,6 +1066,7 @@ function summariseArgs(args, limit = 60) {
|
|
|
1157
1066
|
}
|
|
1158
1067
|
|
|
1159
1068
|
// src/watch.ts
|
|
1069
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
1160
1070
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1161
1071
|
var NOTICE_TICKS = 26;
|
|
1162
1072
|
function line(action, now) {
|
|
@@ -1216,7 +1126,7 @@ function render(journal, options, tick, view) {
|
|
|
1216
1126
|
});
|
|
1217
1127
|
out2.push("");
|
|
1218
1128
|
out2.push(
|
|
1219
|
-
canDecide(options) ? ` ${keyHint("a", "approve")} ${keyHint("d", "deny")} ${keyHint("j/k", "move")} ${keyHint("q", "quit")}` : ` ${style.quiet(`${options.approveWith} approve --all`)}`
|
|
1129
|
+
canDecide(options) ? ` ${keyHint("a", "approve")} ${keyHint("A", "and for an hour")} ${keyHint("d", "deny")} ${keyHint("j/k", "move")} ${keyHint("q", "quit")}` : ` ${style.quiet(`${options.approveWith} approve --all`)}`
|
|
1220
1130
|
);
|
|
1221
1131
|
}
|
|
1222
1132
|
if (view.notice !== "") {
|
|
@@ -1262,7 +1172,7 @@ async function* terminalKeys() {
|
|
|
1262
1172
|
async function watch(options) {
|
|
1263
1173
|
let journal;
|
|
1264
1174
|
const open = () => {
|
|
1265
|
-
if (journal === void 0 &&
|
|
1175
|
+
if (journal === void 0 && existsSync(options.journalPath)) {
|
|
1266
1176
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
1267
1177
|
}
|
|
1268
1178
|
return journal;
|
|
@@ -1275,7 +1185,7 @@ async function watch(options) {
|
|
|
1275
1185
|
const ready = open();
|
|
1276
1186
|
return ready === void 0 ? waitingForJournal(options, tick2) : render(ready, options, tick2, view);
|
|
1277
1187
|
};
|
|
1278
|
-
const decide = (approve) => {
|
|
1188
|
+
const decide = (approve, forAnHour = false) => {
|
|
1279
1189
|
const ready = open();
|
|
1280
1190
|
if (ready === void 0 || options.decideAs === void 0) {
|
|
1281
1191
|
return;
|
|
@@ -1285,8 +1195,11 @@ async function watch(options) {
|
|
|
1285
1195
|
if (action === void 0) {
|
|
1286
1196
|
return;
|
|
1287
1197
|
}
|
|
1288
|
-
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.
|
|
1289
|
-
|
|
1198
|
+
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.denyByPerson(action.id, options.decideAs, "denied from the watch view");
|
|
1199
|
+
if (changed && approve && forAnHour) {
|
|
1200
|
+
ready.allow(action.server, action.tool, options.decideAs, new Date(Date.now() + HOUR_MS).toISOString());
|
|
1201
|
+
}
|
|
1202
|
+
view.notice = !changed ? `${action.server}.${action.tool} was already settled` : approve ? forAnHour ? `approved ${action.server}.${action.tool} \xB7 not asked again for an hour \xB7 tell the agent to try again` : `approved ${action.server}.${action.tool} \xB7 now tell the agent to try again` : `denied ${action.server}.${action.tool} \xB7 the agent is told if it asks again`;
|
|
1290
1203
|
view.noticeUntil = tick + NOTICE_TICKS;
|
|
1291
1204
|
view.cursor = 0;
|
|
1292
1205
|
};
|
|
@@ -1299,6 +1212,9 @@ async function watch(options) {
|
|
|
1299
1212
|
case "a":
|
|
1300
1213
|
decide(true);
|
|
1301
1214
|
return;
|
|
1215
|
+
case "A":
|
|
1216
|
+
decide(true, true);
|
|
1217
|
+
return;
|
|
1302
1218
|
case "d":
|
|
1303
1219
|
decide(false);
|
|
1304
1220
|
return;
|
|
@@ -1349,7 +1265,7 @@ async function watch(options) {
|
|
|
1349
1265
|
if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
|
|
1350
1266
|
break;
|
|
1351
1267
|
}
|
|
1352
|
-
await new Promise((
|
|
1268
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
1353
1269
|
}
|
|
1354
1270
|
return 0;
|
|
1355
1271
|
} finally {
|
|
@@ -1364,613 +1280,17 @@ async function watch(options) {
|
|
|
1364
1280
|
await reader?.return?.(void 0);
|
|
1365
1281
|
await reading;
|
|
1366
1282
|
})(),
|
|
1367
|
-
new Promise((
|
|
1283
|
+
new Promise((resolve2) => setTimeout(resolve2, 50).unref())
|
|
1368
1284
|
]);
|
|
1369
1285
|
journal?.close();
|
|
1370
1286
|
}
|
|
1371
1287
|
}
|
|
1372
1288
|
|
|
1373
1289
|
// src/console.ts
|
|
1374
|
-
import { existsSync as
|
|
1375
|
-
|
|
1376
|
-
// src/install/connections.ts
|
|
1377
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1378
|
-
|
|
1379
|
-
// src/install/clients.ts
|
|
1380
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
1381
|
-
import { homedir, platform } from "os";
|
|
1382
|
-
import { basename as basename2, dirname, join, resolve } from "path";
|
|
1383
|
-
|
|
1384
|
-
// src/install/toml.ts
|
|
1385
|
-
var HEADER = /^\s*\[(?!\[)([^[\]]+)\]\s*$/;
|
|
1386
|
-
function serverTables(lines) {
|
|
1387
|
-
const tables = [];
|
|
1388
|
-
let open;
|
|
1389
|
-
const close = (at) => {
|
|
1390
|
-
if (open !== void 0) {
|
|
1391
|
-
tables.push({ name: open.name, start: open.start, end: at });
|
|
1392
|
-
open = void 0;
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
lines.forEach((line2, index) => {
|
|
1396
|
-
const header2 = HEADER.exec(line2)?.[1];
|
|
1397
|
-
if (header2 === void 0) {
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
const parts = header2.split(".");
|
|
1401
|
-
if (parts[0] === "mcp_servers" && parts.length === 2 && parts[1] !== void 0) {
|
|
1402
|
-
close(index);
|
|
1403
|
-
open = { name: unquote(parts[1]), start: index };
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
close(index);
|
|
1407
|
-
});
|
|
1408
|
-
close(lines.length);
|
|
1409
|
-
return tables;
|
|
1410
|
-
}
|
|
1411
|
-
function unquote(text) {
|
|
1412
|
-
const trimmed = text.trim();
|
|
1413
|
-
if (/^'.*'$/s.test(trimmed)) {
|
|
1414
|
-
return trimmed.slice(1, -1);
|
|
1415
|
-
}
|
|
1416
|
-
if (!/^".*"$/s.test(trimmed)) {
|
|
1417
|
-
return trimmed;
|
|
1418
|
-
}
|
|
1419
|
-
return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
1420
|
-
}
|
|
1421
|
-
function readKey(lines, table, key) {
|
|
1422
|
-
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`);
|
|
1423
|
-
for (let index = table.start + 1; index < table.end; index += 1) {
|
|
1424
|
-
const line2 = lines[index];
|
|
1425
|
-
if (line2 === void 0 || HEADER.test(line2)) {
|
|
1426
|
-
break;
|
|
1427
|
-
}
|
|
1428
|
-
const value = pattern.exec(line2)?.[1];
|
|
1429
|
-
if (value !== void 0) {
|
|
1430
|
-
return value.trim();
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
return void 0;
|
|
1434
|
-
}
|
|
1435
|
-
function splitItems(inner) {
|
|
1436
|
-
const items = [];
|
|
1437
|
-
let current = "";
|
|
1438
|
-
let quote3;
|
|
1439
|
-
let escaped = false;
|
|
1440
|
-
for (const character of inner) {
|
|
1441
|
-
if (escaped) {
|
|
1442
|
-
current += character;
|
|
1443
|
-
escaped = false;
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
if (character === "\\" && quote3 === '"') {
|
|
1447
|
-
current += character;
|
|
1448
|
-
escaped = true;
|
|
1449
|
-
continue;
|
|
1450
|
-
}
|
|
1451
|
-
if (quote3 === void 0 && (character === '"' || character === "'")) {
|
|
1452
|
-
quote3 = character;
|
|
1453
|
-
current += character;
|
|
1454
|
-
continue;
|
|
1455
|
-
}
|
|
1456
|
-
if (character === quote3) {
|
|
1457
|
-
quote3 = void 0;
|
|
1458
|
-
current += character;
|
|
1459
|
-
continue;
|
|
1460
|
-
}
|
|
1461
|
-
if (character === "," && quote3 === void 0) {
|
|
1462
|
-
items.push(current);
|
|
1463
|
-
current = "";
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
current += character;
|
|
1467
|
-
}
|
|
1468
|
-
items.push(current);
|
|
1469
|
-
return items;
|
|
1470
|
-
}
|
|
1471
|
-
function parseArray(value) {
|
|
1472
|
-
if (value === void 0 || !value.startsWith("[")) {
|
|
1473
|
-
return void 0;
|
|
1474
|
-
}
|
|
1475
|
-
if (!value.endsWith("]")) {
|
|
1476
|
-
return void 0;
|
|
1477
|
-
}
|
|
1478
|
-
const inner = value.slice(1, -1).trim();
|
|
1479
|
-
if (inner === "") {
|
|
1480
|
-
return [];
|
|
1481
|
-
}
|
|
1482
|
-
return splitItems(inner).map((item) => item.trim()).filter((item, index, all) => item !== "" || index !== all.length - 1).map(unquote);
|
|
1483
|
-
}
|
|
1484
|
-
function readServers(text) {
|
|
1485
|
-
const lines = text.split("\n");
|
|
1486
|
-
const servers = {};
|
|
1487
|
-
for (const table of serverTables(lines)) {
|
|
1488
|
-
const command = readKey(lines, table, "command");
|
|
1489
|
-
const entry = {};
|
|
1490
|
-
if (command !== void 0) {
|
|
1491
|
-
entry["command"] = unquote(command);
|
|
1492
|
-
}
|
|
1493
|
-
const args = parseArray(readKey(lines, table, "args"));
|
|
1494
|
-
if (args !== void 0) {
|
|
1495
|
-
entry["args"] = args;
|
|
1496
|
-
}
|
|
1497
|
-
const url = readKey(lines, table, "url");
|
|
1498
|
-
if (url !== void 0) {
|
|
1499
|
-
entry["url"] = unquote(url);
|
|
1500
|
-
}
|
|
1501
|
-
const enabled = readKey(lines, table, "enabled");
|
|
1502
|
-
if (enabled !== void 0) {
|
|
1503
|
-
entry["enabled"] = enabled.trim() === "true";
|
|
1504
|
-
}
|
|
1505
|
-
servers[table.name] = entry;
|
|
1506
|
-
}
|
|
1507
|
-
return servers;
|
|
1508
|
-
}
|
|
1509
|
-
var quote2 = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1510
|
-
function writeServers(text, servers) {
|
|
1511
|
-
const lines = text.split("\n");
|
|
1512
|
-
const current = readServers(text);
|
|
1513
|
-
for (const table of serverTables(lines).reverse()) {
|
|
1514
|
-
const wanted = servers[table.name];
|
|
1515
|
-
if (wanted === void 0 || wanted.command === void 0) {
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const now = current[table.name];
|
|
1519
|
-
if (now?.command === wanted.command && JSON.stringify(now.args ?? []) === JSON.stringify(wanted.args ?? [])) {
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
1522
|
-
setKey(lines, table, "command", quote2(wanted.command));
|
|
1523
|
-
setKey(lines, table, "args", `[${(wanted.args ?? []).map(quote2).join(", ")}]`);
|
|
1524
|
-
}
|
|
1525
|
-
return lines.join("\n");
|
|
1526
|
-
}
|
|
1527
|
-
function setKey(lines, table, key, value) {
|
|
1528
|
-
const pattern = new RegExp(`^(\\s*)${key}\\s*=`);
|
|
1529
|
-
for (let index = table.start + 1; index < table.end; index += 1) {
|
|
1530
|
-
const line2 = lines[index];
|
|
1531
|
-
if (line2 === void 0 || HEADER.test(line2)) {
|
|
1532
|
-
break;
|
|
1533
|
-
}
|
|
1534
|
-
const indent = pattern.exec(line2)?.[1];
|
|
1535
|
-
if (indent !== void 0) {
|
|
1536
|
-
lines[index] = `${indent}${key} = ${value}`;
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
lines.splice(table.start + 1, 0, `${key} = ${value}`);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// src/install/clients.ts
|
|
1544
|
-
var LABELS = {
|
|
1545
|
-
"claude-code": "Claude Code",
|
|
1546
|
-
"claude-desktop": "Claude Desktop",
|
|
1547
|
-
cursor: "Cursor",
|
|
1548
|
-
codex: "Codex"
|
|
1549
|
-
};
|
|
1550
|
-
var CLIENT_IDS = Object.keys(LABELS).filter(
|
|
1551
|
-
(name) => name in LABELS
|
|
1552
|
-
);
|
|
1553
|
-
function isClientId(value) {
|
|
1554
|
-
return CLIENT_IDS.some((known) => known === value);
|
|
1555
|
-
}
|
|
1556
|
-
function claudeDesktopPath() {
|
|
1557
|
-
const home = homedir();
|
|
1558
|
-
switch (platform()) {
|
|
1559
|
-
case "darwin":
|
|
1560
|
-
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1561
|
-
case "win32":
|
|
1562
|
-
return join(process.env["APPDATA"] ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1563
|
-
default:
|
|
1564
|
-
return join(process.env["XDG_CONFIG_HOME"] ?? join(home, ".config"), "Claude", "claude_desktop_config.json");
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
function discover(cwd) {
|
|
1568
|
-
const home = homedir();
|
|
1569
|
-
const sites = [];
|
|
1570
|
-
const claudeCode = join(home, ".claude.json");
|
|
1571
|
-
if (existsSync3(claudeCode)) {
|
|
1572
|
-
const document = readJson(claudeCode);
|
|
1573
|
-
const projects = document?.["projects"];
|
|
1574
|
-
const here = resolve(cwd);
|
|
1575
|
-
if (isRecord2(projects) && Object.prototype.hasOwnProperty.call(projects, here)) {
|
|
1576
|
-
sites.push({
|
|
1577
|
-
client: "claude-code",
|
|
1578
|
-
label: LABELS["claude-code"],
|
|
1579
|
-
format: "json",
|
|
1580
|
-
path: claudeCode,
|
|
1581
|
-
scope: `project ${here}`,
|
|
1582
|
-
at: ["projects", here, "mcpServers"]
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
sites.push({
|
|
1586
|
-
client: "claude-code",
|
|
1587
|
-
label: LABELS["claude-code"],
|
|
1588
|
-
format: "json",
|
|
1589
|
-
path: claudeCode,
|
|
1590
|
-
scope: "global",
|
|
1591
|
-
at: ["mcpServers"]
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
const projectFile = join(resolve(cwd), ".mcp.json");
|
|
1595
|
-
if (existsSync3(projectFile)) {
|
|
1596
|
-
sites.push({
|
|
1597
|
-
client: "claude-code",
|
|
1598
|
-
label: LABELS["claude-code"],
|
|
1599
|
-
format: "json",
|
|
1600
|
-
path: projectFile,
|
|
1601
|
-
scope: "project file",
|
|
1602
|
-
at: ["mcpServers"]
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
const desktop = claudeDesktopPath();
|
|
1606
|
-
if (existsSync3(desktop)) {
|
|
1607
|
-
sites.push({
|
|
1608
|
-
client: "claude-desktop",
|
|
1609
|
-
label: LABELS["claude-desktop"],
|
|
1610
|
-
format: "json",
|
|
1611
|
-
path: desktop,
|
|
1612
|
-
scope: "global",
|
|
1613
|
-
at: ["mcpServers"]
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
const codex = join(process.env["CODEX_HOME"] ?? join(home, ".codex"), "config.toml");
|
|
1617
|
-
if (existsSync3(codex)) {
|
|
1618
|
-
sites.push({
|
|
1619
|
-
client: "codex",
|
|
1620
|
-
label: LABELS.codex,
|
|
1621
|
-
format: "toml",
|
|
1622
|
-
path: codex,
|
|
1623
|
-
scope: "global",
|
|
1624
|
-
at: ["mcp_servers"]
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
for (const [path, scope] of [
|
|
1628
|
-
[join(resolve(cwd), ".cursor", "mcp.json"), "project"],
|
|
1629
|
-
[join(home, ".cursor", "mcp.json"), "global"]
|
|
1630
|
-
]) {
|
|
1631
|
-
if (existsSync3(path)) {
|
|
1632
|
-
sites.push({ client: "cursor", label: LABELS.cursor, format: "json", path, scope, at: ["mcpServers"] });
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
return sites;
|
|
1636
|
-
}
|
|
1637
|
-
function isRecord2(value) {
|
|
1638
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1639
|
-
}
|
|
1640
|
-
function readJson(path) {
|
|
1641
|
-
try {
|
|
1642
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1643
|
-
return isRecord2(parsed) ? parsed : void 0;
|
|
1644
|
-
} catch {
|
|
1645
|
-
return void 0;
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
var ConfigError = class extends Error {
|
|
1649
|
-
};
|
|
1650
|
-
function readDocument(site) {
|
|
1651
|
-
let text;
|
|
1652
|
-
try {
|
|
1653
|
-
text = readFileSync2(site.path, "utf8");
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
throw new ConfigError(`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1656
|
-
}
|
|
1657
|
-
let parsed;
|
|
1658
|
-
try {
|
|
1659
|
-
parsed = JSON.parse(text);
|
|
1660
|
-
} catch (error) {
|
|
1661
|
-
throw new ConfigError(
|
|
1662
|
-
`${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.`
|
|
1663
|
-
);
|
|
1664
|
-
}
|
|
1665
|
-
if (!isRecord2(parsed)) {
|
|
1666
|
-
throw new ConfigError(`${site.path} is not a JSON object, so it has no server list to change`);
|
|
1667
|
-
}
|
|
1668
|
-
return parsed;
|
|
1669
|
-
}
|
|
1670
|
-
function readServers2(document, at) {
|
|
1671
|
-
let node = document;
|
|
1672
|
-
for (const key of at) {
|
|
1673
|
-
if (!isRecord2(node)) {
|
|
1674
|
-
return {};
|
|
1675
|
-
}
|
|
1676
|
-
node = node[key];
|
|
1677
|
-
}
|
|
1678
|
-
if (!isRecord2(node)) {
|
|
1679
|
-
return {};
|
|
1680
|
-
}
|
|
1681
|
-
const servers = {};
|
|
1682
|
-
for (const [name, entry] of Object.entries(node)) {
|
|
1683
|
-
if (isRecord2(entry)) {
|
|
1684
|
-
servers[name] = entry;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
return servers;
|
|
1688
|
-
}
|
|
1689
|
-
function withServers(document, at, servers) {
|
|
1690
|
-
const head = at[0];
|
|
1691
|
-
if (head === void 0) {
|
|
1692
|
-
throw new ConfigError("no path to the server list");
|
|
1693
|
-
}
|
|
1694
|
-
const rest = at.slice(1);
|
|
1695
|
-
const below = document[head];
|
|
1696
|
-
const child = rest.length === 0 ? servers : withServers(isRecord2(below) ? below : {}, rest, servers);
|
|
1697
|
-
return { ...document, [head]: child };
|
|
1698
|
-
}
|
|
1699
|
-
function indentOf(path) {
|
|
1700
|
-
try {
|
|
1701
|
-
const line2 = /\n([ \t]+)"/.exec(readFileSync2(path, "utf8"));
|
|
1702
|
-
const found = line2?.[1];
|
|
1703
|
-
if (found === void 0) {
|
|
1704
|
-
return 2;
|
|
1705
|
-
}
|
|
1706
|
-
return found.startsWith(" ") ? " " : found.length;
|
|
1707
|
-
} catch {
|
|
1708
|
-
return 2;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
function backupPathFor(path) {
|
|
1712
|
-
return `${path}.synartesis-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
1713
|
-
}
|
|
1714
|
-
var KEEP_BACKUPS = 5;
|
|
1715
|
-
function pruneBackups(path) {
|
|
1716
|
-
try {
|
|
1717
|
-
const dir = dirname(path);
|
|
1718
|
-
const prefix = `${basename2(path)}.synartesis-backup-`;
|
|
1719
|
-
const ours2 = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
|
|
1720
|
-
for (const name of ours2.slice(0, Math.max(0, ours2.length - KEEP_BACKUPS))) {
|
|
1721
|
-
rmSync(join(dir, name), { force: true });
|
|
1722
|
-
}
|
|
1723
|
-
} catch {
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
function writeDocument(site, document) {
|
|
1727
|
-
return writeText(site, `${JSON.stringify(document, void 0, indentOf(site.path))}
|
|
1728
|
-
`);
|
|
1729
|
-
}
|
|
1730
|
-
function writeText(site, text) {
|
|
1731
|
-
const backup = backupPathFor(site.path);
|
|
1732
|
-
const original = readFileSync2(site.path);
|
|
1733
|
-
writeFileSync(backup, original);
|
|
1734
|
-
pruneBackups(site.path);
|
|
1735
|
-
const temporary = join(dirname(site.path), `.synartesis-write-${String(process.pid)}.tmp`);
|
|
1736
|
-
try {
|
|
1737
|
-
writeFileSync(temporary, text);
|
|
1738
|
-
renameSync(temporary, site.path);
|
|
1739
|
-
} catch (error) {
|
|
1740
|
-
try {
|
|
1741
|
-
unlinkSync(temporary);
|
|
1742
|
-
} catch {
|
|
1743
|
-
}
|
|
1744
|
-
throw new ConfigError(
|
|
1745
|
-
`could not write ${site.path}: ${error instanceof Error ? error.message : String(error)}. The original is untouched, and a copy is at ${backup}.`
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
return backup;
|
|
1749
|
-
}
|
|
1750
|
-
function serversAt(site) {
|
|
1751
|
-
if (site.format === "toml") {
|
|
1752
|
-
try {
|
|
1753
|
-
return readServers(readFileSync2(site.path, "utf8"));
|
|
1754
|
-
} catch (error) {
|
|
1755
|
-
throw new ConfigError(
|
|
1756
|
-
`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
1757
|
-
);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
return readServers2(readDocument(site), site.at);
|
|
1761
|
-
}
|
|
1762
|
-
function saveServers(site, servers) {
|
|
1763
|
-
if (site.format === "toml") {
|
|
1764
|
-
const text = readFileSync2(site.path, "utf8");
|
|
1765
|
-
return writeText(site, writeServers(text, servers));
|
|
1766
|
-
}
|
|
1767
|
-
return writeDocument(site, withServers(readDocument(site), site.at, servers));
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// src/install/install.ts
|
|
1771
|
-
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1772
|
-
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1773
|
-
function recordPathFor(manifestPath) {
|
|
1774
|
-
return resolve2(dirname2(manifestPath), "installed.json");
|
|
1775
|
-
}
|
|
1776
|
-
var EMPTY = { version: 1, wrapped: {} };
|
|
1777
|
-
function isRecord3(value) {
|
|
1778
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1779
|
-
}
|
|
1780
|
-
function asRecord(value) {
|
|
1781
|
-
if (!isRecord3(value)) {
|
|
1782
|
-
return void 0;
|
|
1783
|
-
}
|
|
1784
|
-
const wrapped2 = value["wrapped"];
|
|
1785
|
-
if (!isRecord3(wrapped2)) {
|
|
1786
|
-
return void 0;
|
|
1787
|
-
}
|
|
1788
|
-
const kept = {};
|
|
1789
|
-
for (const [key, entry] of Object.entries(wrapped2)) {
|
|
1790
|
-
if (!isRecord3(entry)) {
|
|
1791
|
-
continue;
|
|
1792
|
-
}
|
|
1793
|
-
const original = entry["original"];
|
|
1794
|
-
const at = entry["at"];
|
|
1795
|
-
if (!isRecord3(original)) {
|
|
1796
|
-
continue;
|
|
1797
|
-
}
|
|
1798
|
-
if (!Array.isArray(at) || !at.every((step) => typeof step === "string")) {
|
|
1799
|
-
continue;
|
|
1800
|
-
}
|
|
1801
|
-
kept[key] = { original, at };
|
|
1802
|
-
}
|
|
1803
|
-
return { version: 1, wrapped: kept };
|
|
1804
|
-
}
|
|
1805
|
-
function keyFor(site, server) {
|
|
1806
|
-
return [site.path, site.scope, server].join("");
|
|
1807
|
-
}
|
|
1808
|
-
function readRecord(manifestPath) {
|
|
1809
|
-
const path = recordPathFor(manifestPath);
|
|
1810
|
-
if (!existsSync4(path)) {
|
|
1811
|
-
return EMPTY;
|
|
1812
|
-
}
|
|
1813
|
-
try {
|
|
1814
|
-
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1815
|
-
const record = asRecord(parsed);
|
|
1816
|
-
if (record !== void 0) {
|
|
1817
|
-
return record;
|
|
1818
|
-
}
|
|
1819
|
-
} catch {
|
|
1820
|
-
}
|
|
1821
|
-
return EMPTY;
|
|
1822
|
-
}
|
|
1823
|
-
function writeRecord(manifestPath, record) {
|
|
1824
|
-
mkdirSync(dirname2(recordPathFor(manifestPath)), { recursive: true, mode: 448 });
|
|
1825
|
-
writeFileSync2(recordPathFor(manifestPath), `${JSON.stringify(record, void 0, 2)}
|
|
1826
|
-
`);
|
|
1827
|
-
}
|
|
1828
|
-
function proxyEntry(manifestPath, server, original, invoker) {
|
|
1829
|
-
const command = { command: invoker.command, args: [...invoker.args] };
|
|
1830
|
-
return {
|
|
1831
|
-
...command,
|
|
1832
|
-
args: [...command.args, "--manifest", resolve2(manifestPath), "--server", server],
|
|
1833
|
-
// The agent's environment, not ours: the upstream is started by the proxy
|
|
1834
|
-
// from the manifest, but a client that set `env` here meant it for the
|
|
1835
|
-
// server, and the manifest reads `${VAR}` out of exactly this environment.
|
|
1836
|
-
...original.env === void 0 ? {} : { env: original.env },
|
|
1837
|
-
...original.cwd === void 0 ? {} : { cwd: original.cwd }
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1840
|
-
function isWrapped(entry) {
|
|
1841
|
-
const args = entry.args ?? [];
|
|
1842
|
-
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")));
|
|
1843
|
-
}
|
|
1844
|
-
function invokerFor(ourVersion, cliPath) {
|
|
1845
|
-
if (pathBinaryMatches(ourVersion)) {
|
|
1846
|
-
return { command: "synartesis", args: ["proxy"] };
|
|
1847
|
-
}
|
|
1848
|
-
return {
|
|
1849
|
-
command: process.execPath,
|
|
1850
|
-
args: [cliPath, "proxy"],
|
|
1851
|
-
note: "the synartesis on your PATH is a different build, so the entries name this one directly"
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
async function planInstall(sites, manifestPath, invoker) {
|
|
1855
|
-
let yaml = existsSync4(manifestPath) ? readFileSync3(manifestPath, "utf8") : void 0;
|
|
1856
|
-
const plans = [];
|
|
1857
|
-
const claimed = new Set(
|
|
1858
|
-
yaml === void 0 ? [] : Object.keys(parseManifest(yaml, manifestPath).servers)
|
|
1859
|
-
);
|
|
1860
|
-
for (const site of sites) {
|
|
1861
|
-
const servers = serversAt(site);
|
|
1862
|
-
const planned = [];
|
|
1863
|
-
const skipped = [];
|
|
1864
|
-
for (const [name, entry] of Object.entries(servers)) {
|
|
1865
|
-
if (isWrapped(entry)) {
|
|
1866
|
-
skipped.push({ name, why: "already covered" });
|
|
1867
|
-
continue;
|
|
1868
|
-
}
|
|
1869
|
-
if (entry.command === void 0) {
|
|
1870
|
-
skipped.push({ name, why: entry.url === void 0 ? "no command to start" : "remote (http); stdio only today" });
|
|
1871
|
-
continue;
|
|
1872
|
-
}
|
|
1873
|
-
if (entry.enabled === false) {
|
|
1874
|
-
skipped.push({ name, why: "switched off in the config" });
|
|
1875
|
-
continue;
|
|
1876
|
-
}
|
|
1877
|
-
const key = claimed.has(name) ? `${name}-${site.client}` : name;
|
|
1878
|
-
if (claimed.has(key)) {
|
|
1879
|
-
skipped.push({ name, why: `already in the policy as ${key}` });
|
|
1880
|
-
continue;
|
|
1881
|
-
}
|
|
1882
|
-
let draft;
|
|
1883
|
-
try {
|
|
1884
|
-
draft = await draftManifest({
|
|
1885
|
-
name: key,
|
|
1886
|
-
command: entry.command,
|
|
1887
|
-
args: [...entry.args ?? []],
|
|
1888
|
-
...yaml === void 0 ? {} : { existing: yaml }
|
|
1889
|
-
});
|
|
1890
|
-
} catch (error) {
|
|
1891
|
-
skipped.push({
|
|
1892
|
-
name,
|
|
1893
|
-
why: `will not start: ${(error instanceof Error ? error.message : String(error)).slice(0, 60)}`
|
|
1894
|
-
});
|
|
1895
|
-
continue;
|
|
1896
|
-
}
|
|
1897
|
-
yaml = draft.yaml;
|
|
1898
|
-
claimed.add(key);
|
|
1899
|
-
planned.push({
|
|
1900
|
-
name,
|
|
1901
|
-
original: entry,
|
|
1902
|
-
wrapped: proxyEntry(manifestPath, key, entry, invoker),
|
|
1903
|
-
...draft.adopted === void 0 ? {} : {
|
|
1904
|
-
adopted: draft.adopted.server,
|
|
1905
|
-
tools: draft.adopted.tools,
|
|
1906
|
-
...draft.adopted.provenance === void 0 ? {} : { provenance: draft.adopted.provenance }
|
|
1907
|
-
}
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
plans.push({ site, servers: planned, skipped });
|
|
1911
|
-
}
|
|
1912
|
-
return { plans, yaml: yaml ?? "" };
|
|
1913
|
-
}
|
|
1914
|
-
function applyInstall(plans, manifestPath, yaml) {
|
|
1915
|
-
if (!plans.some((plan) => plan.servers.length > 0)) {
|
|
1916
|
-
return [];
|
|
1917
|
-
}
|
|
1918
|
-
parseManifest(yaml, manifestPath);
|
|
1919
|
-
mkdirSync(dirname2(resolve2(manifestPath)), { recursive: true, mode: 448 });
|
|
1920
|
-
writeFileSync2(manifestPath, yaml);
|
|
1921
|
-
const record = readRecord(manifestPath);
|
|
1922
|
-
const wrapped2 = { ...record.wrapped };
|
|
1923
|
-
const applied = [];
|
|
1924
|
-
for (const plan of plans) {
|
|
1925
|
-
if (plan.servers.length === 0) {
|
|
1926
|
-
continue;
|
|
1927
|
-
}
|
|
1928
|
-
const servers = { ...serversAt(plan.site) };
|
|
1929
|
-
for (const server of plan.servers) {
|
|
1930
|
-
servers[server.name] = server.wrapped;
|
|
1931
|
-
wrapped2[keyFor(plan.site, server.name)] = { original: server.original, at: plan.site.at };
|
|
1932
|
-
}
|
|
1933
|
-
writeRecord(manifestPath, { version: 1, wrapped: wrapped2 });
|
|
1934
|
-
const backup = saveServers(plan.site, servers);
|
|
1935
|
-
applied.push({ site: plan.site, backup, servers: plan.servers.map((server) => server.name) });
|
|
1936
|
-
}
|
|
1937
|
-
return applied;
|
|
1938
|
-
}
|
|
1939
|
-
function applyUninstall(sites, manifestPath) {
|
|
1940
|
-
const record = readRecord(manifestPath);
|
|
1941
|
-
const restoredKeys = /* @__PURE__ */ new Set();
|
|
1942
|
-
const restored = [];
|
|
1943
|
-
for (const site of sites) {
|
|
1944
|
-
const servers = { ...serversAt(site) };
|
|
1945
|
-
const put = [];
|
|
1946
|
-
const unknown = [];
|
|
1947
|
-
for (const [name, entry] of Object.entries(servers)) {
|
|
1948
|
-
if (!isWrapped(entry)) {
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
const known = record.wrapped[keyFor(site, name)];
|
|
1952
|
-
if (known === void 0) {
|
|
1953
|
-
unknown.push(name);
|
|
1954
|
-
continue;
|
|
1955
|
-
}
|
|
1956
|
-
servers[name] = known.original;
|
|
1957
|
-
put.push(name);
|
|
1958
|
-
restoredKeys.add(keyFor(site, name));
|
|
1959
|
-
}
|
|
1960
|
-
if (put.length === 0 && unknown.length === 0) {
|
|
1961
|
-
continue;
|
|
1962
|
-
}
|
|
1963
|
-
const backup = put.length === 0 ? "" : saveServers(site, servers);
|
|
1964
|
-
restored.push({ site, backup, servers: put, unknown });
|
|
1965
|
-
}
|
|
1966
|
-
const remaining = Object.fromEntries(
|
|
1967
|
-
Object.entries(record.wrapped).filter(([key]) => !restoredKeys.has(key))
|
|
1968
|
-
);
|
|
1969
|
-
writeRecord(manifestPath, { version: 1, wrapped: remaining });
|
|
1970
|
-
return restored;
|
|
1971
|
-
}
|
|
1290
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1972
1291
|
|
|
1973
1292
|
// src/install/connections.ts
|
|
1293
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1974
1294
|
var ACTIVE_WITHIN_MS = 2 * 60 * 1e3;
|
|
1975
1295
|
function lastSeenByServer(journal) {
|
|
1976
1296
|
return journal.lastSeenPerServer();
|
|
@@ -1979,7 +1299,7 @@ function commandMissing(command) {
|
|
|
1979
1299
|
if (command === void 0) {
|
|
1980
1300
|
return false;
|
|
1981
1301
|
}
|
|
1982
|
-
return (command.includes("/") || command.includes("\\")) && !
|
|
1302
|
+
return (command.includes("/") || command.includes("\\")) && !existsSync2(command);
|
|
1983
1303
|
}
|
|
1984
1304
|
function scan(journal, cwd) {
|
|
1985
1305
|
const seen = journal === void 0 ? /* @__PURE__ */ new Map() : lastSeenByServer(journal);
|
|
@@ -2000,7 +1320,9 @@ function scan(journal, cwd) {
|
|
|
2000
1320
|
}
|
|
2001
1321
|
const connections = Object.entries(servers).map(([server, entry]) => {
|
|
2002
1322
|
const covered = isWrapped(entry);
|
|
2003
|
-
const
|
|
1323
|
+
const args = entry.args ?? [];
|
|
1324
|
+
const named = covered && args.includes("--server") ? args[args.indexOf("--server") + 1] : void 0;
|
|
1325
|
+
const lastSeen = seen.get(named ?? server);
|
|
2004
1326
|
return {
|
|
2005
1327
|
client: site.client,
|
|
2006
1328
|
scope: site.scope,
|
|
@@ -2036,6 +1358,7 @@ function needsConnecting(groups) {
|
|
|
2036
1358
|
}
|
|
2037
1359
|
|
|
2038
1360
|
// src/console.ts
|
|
1361
|
+
var HOUR_MS2 = 60 * 60 * 1e3;
|
|
2039
1362
|
var FRAMES2 = [
|
|
2040
1363
|
"\u280B",
|
|
2041
1364
|
"\u2819",
|
|
@@ -2208,7 +1531,7 @@ function connectionsView(screen, options) {
|
|
|
2208
1531
|
return [
|
|
2209
1532
|
` ${style.quiet("No MCP client config was found on this machine.")}`,
|
|
2210
1533
|
"",
|
|
2211
|
-
` ${style.quiet(
|
|
1534
|
+
` ${style.quiet(LOOKED_FOR)}`
|
|
2212
1535
|
];
|
|
2213
1536
|
}
|
|
2214
1537
|
const rows = connectionRows(screen);
|
|
@@ -2263,7 +1586,7 @@ function footer(screen, options) {
|
|
|
2263
1586
|
const what = screen.confirmingForce ? `undo ${screen.confirmingLabel ?? "this session"} anyway, losing that change?` : `undo ${screen.confirmingLabel ?? "this session"}?`;
|
|
2264
1587
|
return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
|
|
2265
1588
|
}
|
|
2266
|
-
const keys = screen.mode === "connections" ? [keyHint2("enter", "connect"), keyHint2("a", "connect all"), keyHint2("r", "rescan"), keyHint2("j/k", "move"), keyHint2("h", "back")] : screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
|
|
1589
|
+
const keys = screen.mode === "connections" ? [keyHint2("enter", "connect"), keyHint2("a", "connect all"), keyHint2("r", "rescan"), keyHint2("j/k", "move"), keyHint2("h", "back")] : screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("A", "and for an hour"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
|
|
2267
1590
|
keyHint2("l", "check now"),
|
|
2268
1591
|
keyHint2("p", "preview undo"),
|
|
2269
1592
|
keyHint2("u", "undo"),
|
|
@@ -2314,7 +1637,7 @@ async function* terminalKeys2() {
|
|
|
2314
1637
|
async function openConsole(options) {
|
|
2315
1638
|
let journal;
|
|
2316
1639
|
const open = () => {
|
|
2317
|
-
if (journal === void 0 &&
|
|
1640
|
+
if (journal === void 0 && existsSync3(options.journalPath)) {
|
|
2318
1641
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
2319
1642
|
}
|
|
2320
1643
|
return journal;
|
|
@@ -2401,7 +1724,7 @@ async function openConsole(options) {
|
|
|
2401
1724
|
}
|
|
2402
1725
|
return ["", ` ${style.quiet("nothing to undo in this session")}`];
|
|
2403
1726
|
};
|
|
2404
|
-
const decide = (approve) => {
|
|
1727
|
+
const decide = (approve, forAnHour = false) => {
|
|
2405
1728
|
const ready = open();
|
|
2406
1729
|
if (ready === void 0) {
|
|
2407
1730
|
return;
|
|
@@ -2411,9 +1734,12 @@ async function openConsole(options) {
|
|
|
2411
1734
|
if (action === void 0) {
|
|
2412
1735
|
return;
|
|
2413
1736
|
}
|
|
2414
|
-
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.
|
|
1737
|
+
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.denyByPerson(action.id, options.decideAs, "denied from the console");
|
|
1738
|
+
if (changed && approve && forAnHour) {
|
|
1739
|
+
ready.allow(action.server, action.tool, options.decideAs, new Date(Date.now() + HOUR_MS2).toISOString());
|
|
1740
|
+
}
|
|
2415
1741
|
say(
|
|
2416
|
-
!changed ? `${action.server}.${action.tool} was already settled` : approve ? `approved ${action.server}.${action.tool} ${DOT} now tell the agent to try again` : `denied ${action.server}.${action.tool} ${DOT}
|
|
1742
|
+
!changed ? `${action.server}.${action.tool} was already settled` : approve ? forAnHour ? `approved ${action.server}.${action.tool} ${DOT} not asked again for an hour ${DOT} tell the agent to try again` : `approved ${action.server}.${action.tool} ${DOT} now tell the agent to try again` : `denied ${action.server}.${action.tool} ${DOT} the agent is told if it asks again`
|
|
2417
1743
|
);
|
|
2418
1744
|
screen.cursor = 0;
|
|
2419
1745
|
};
|
|
@@ -2566,6 +1892,11 @@ async function openConsole(options) {
|
|
|
2566
1892
|
}
|
|
2567
1893
|
return;
|
|
2568
1894
|
}
|
|
1895
|
+
case "A":
|
|
1896
|
+
if (screen.mode === "gates") {
|
|
1897
|
+
decide(true, true);
|
|
1898
|
+
}
|
|
1899
|
+
return;
|
|
2569
1900
|
case "a":
|
|
2570
1901
|
if (screen.mode === "gates") {
|
|
2571
1902
|
decide(true);
|
|
@@ -2690,7 +2021,7 @@ async function openConsole(options) {
|
|
|
2690
2021
|
if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
|
|
2691
2022
|
break;
|
|
2692
2023
|
}
|
|
2693
|
-
await new Promise((
|
|
2024
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
2694
2025
|
}
|
|
2695
2026
|
return 0;
|
|
2696
2027
|
} finally {
|
|
@@ -2706,40 +2037,40 @@ async function openConsole(options) {
|
|
|
2706
2037
|
await reader?.return?.(void 0);
|
|
2707
2038
|
await reading;
|
|
2708
2039
|
})(),
|
|
2709
|
-
new Promise((
|
|
2040
|
+
new Promise((resolve2) => setTimeout(resolve2, 50).unref())
|
|
2710
2041
|
]);
|
|
2711
2042
|
journal?.close();
|
|
2712
2043
|
}
|
|
2713
2044
|
}
|
|
2714
2045
|
|
|
2715
2046
|
// src/desktop.ts
|
|
2716
|
-
import { existsSync as
|
|
2717
|
-
import { homedir
|
|
2718
|
-
import { join
|
|
2047
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2048
|
+
import { homedir } from "os";
|
|
2049
|
+
import { join } from "path";
|
|
2719
2050
|
var RELEASES = "https://github.com/ArhaanDev24/Synartesis/releases";
|
|
2720
2051
|
function candidates(platform2 = process.platform) {
|
|
2721
|
-
const home =
|
|
2052
|
+
const home = homedir();
|
|
2722
2053
|
if (platform2 === "darwin") {
|
|
2723
2054
|
return [
|
|
2724
2055
|
"/Applications/Synartesis.app",
|
|
2725
|
-
|
|
2056
|
+
join(home, "Applications/Synartesis.app")
|
|
2726
2057
|
];
|
|
2727
2058
|
}
|
|
2728
2059
|
if (platform2 === "win32") {
|
|
2729
|
-
const local = process.env["LOCALAPPDATA"] ??
|
|
2060
|
+
const local = process.env["LOCALAPPDATA"] ?? join(home, "AppData/Local");
|
|
2730
2061
|
return [
|
|
2731
|
-
|
|
2732
|
-
|
|
2062
|
+
join(local, "Programs/Synartesis/Synartesis.exe"),
|
|
2063
|
+
join(process.env["PROGRAMFILES"] ?? "C:/Program Files", "Synartesis/Synartesis.exe")
|
|
2733
2064
|
];
|
|
2734
2065
|
}
|
|
2735
2066
|
return [
|
|
2736
2067
|
"/opt/Synartesis/synartesis-desktop",
|
|
2737
2068
|
"/usr/bin/synartesis-desktop",
|
|
2738
|
-
|
|
2739
|
-
|
|
2069
|
+
join(home, ".local/bin/synartesis-desktop"),
|
|
2070
|
+
join(home, "Applications/Synartesis.AppImage")
|
|
2740
2071
|
];
|
|
2741
2072
|
}
|
|
2742
|
-
function findDesktop(platform2 = process.platform, here =
|
|
2073
|
+
function findDesktop(platform2 = process.platform, here = existsSync4) {
|
|
2743
2074
|
for (const path of candidates(platform2)) {
|
|
2744
2075
|
if (!here(path)) {
|
|
2745
2076
|
continue;
|
|
@@ -2782,7 +2113,7 @@ var COMMANDS = `
|
|
|
2782
2113
|
undo -- all in one place, with
|
|
2783
2114
|
the arrow keys. Everything
|
|
2784
2115
|
below can be done from it.
|
|
2785
|
-
synartesis install [--client <name>] [--dry-run] [--print]
|
|
2116
|
+
synartesis install [--client <name>] [--remote] [--dry-run] [--print]
|
|
2786
2117
|
synartesis uninstall [--client <name>]
|
|
2787
2118
|
synartesis status
|
|
2788
2119
|
synartesis init <server> -- <command> [args...] [--manifest <path>]
|
|
@@ -2801,14 +2132,19 @@ var COMMANDS = `
|
|
|
2801
2132
|
synartesis watch [--by <name>] [--journal <path>]
|
|
2802
2133
|
synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
|
|
2803
2134
|
synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
|
|
2135
|
+
synartesis allow [<server.tool> --for <30m|2h> | --always | --stop]
|
|
2804
2136
|
synartesis resolve [actionId] --applied|--failed [--by <name>] [--reason <text>]
|
|
2137
|
+
synartesis notify --test
|
|
2805
2138
|
synartesis undo [runId] [--to <seq>] [--dry-run] [--replan] [--force [--yes]]
|
|
2806
2139
|
[--manifest <path>] [--journal <path>]
|
|
2807
2140
|
|
|
2808
2141
|
install is the short way in: it finds what Claude Code, Claude Desktop,
|
|
2809
|
-
Cursor
|
|
2810
|
-
|
|
2142
|
+
Cursor, Codex, Gemini CLI, Copilot CLI, Antigravity and Devin Desktop (or
|
|
2143
|
+
Windsurf) already list, writes a policy covering all of it -- using the ones
|
|
2144
|
+
that ship where they fit -- and points each entry at the proxy. The original config
|
|
2811
2145
|
is copied aside first, and uninstall puts it back. status says what is covered.
|
|
2146
|
+
A hosted server (a url rather than a command) is covered only with --remote,
|
|
2147
|
+
through mcp-remote, which opens your browser to sign in.
|
|
2812
2148
|
|
|
2813
2149
|
desktop opens the window, if it is installed. It is a separate download --
|
|
2814
2150
|
shipping it through npm would put a browser engine inside every install of
|
|
@@ -2840,7 +2176,8 @@ undo stops when somebody has changed the resource since, rather than writing
|
|
|
2840
2176
|
over them. Three ways past that, and it prints all three: leave it, put the
|
|
2841
2177
|
resource back as the run left it and --replan, or --force to overwrite.
|
|
2842
2178
|
|
|
2843
|
-
--client claude-code, claude-desktop, cursor
|
|
2179
|
+
--client claude-code, claude-desktop, cursor, codex, gemini-cli,
|
|
2180
|
+
copilot-cli, antigravity, devin or windsurf; all by default
|
|
2844
2181
|
--print show the entries install would write, and write nothing
|
|
2845
2182
|
--full show every argument, snapshot and inverse in full, nothing elided
|
|
2846
2183
|
--live read each resource as it is now and say what has changed since
|
|
@@ -2853,6 +2190,8 @@ resource back as the run left it and --replan, or --force to overwrite.
|
|
|
2853
2190
|
--once watch prints the current state and exits
|
|
2854
2191
|
--json machine-readable output for list, show and gates
|
|
2855
2192
|
--dry-run read current state and print the plan without changing anything
|
|
2193
|
+
--unattended approve with no terminal, from a script. Recorded as unattended,
|
|
2194
|
+
so it can be told apart from a yes a person typed
|
|
2856
2195
|
--applied resolve: the call did land, though nothing recorded it
|
|
2857
2196
|
--failed resolve: the call never landed
|
|
2858
2197
|
--replan rebuild each undo from the current manifest, for a run recorded
|
|
@@ -2912,7 +2251,9 @@ var TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
|
2912
2251
|
"--by",
|
|
2913
2252
|
"--reason",
|
|
2914
2253
|
"--older-than",
|
|
2915
|
-
"--client"
|
|
2254
|
+
"--client",
|
|
2255
|
+
"--for",
|
|
2256
|
+
"--confirm"
|
|
2916
2257
|
]);
|
|
2917
2258
|
function positional(argv) {
|
|
2918
2259
|
const values = [];
|
|
@@ -2929,23 +2270,44 @@ function positional(argv) {
|
|
|
2929
2270
|
}
|
|
2930
2271
|
return values;
|
|
2931
2272
|
}
|
|
2273
|
+
async function startAsTheClientWould(manifestPath, name, spec, session) {
|
|
2274
|
+
const client = clientEnvFor(manifestPath, name);
|
|
2275
|
+
const source = client === void 0 ? { kind: "manifest" } : { kind: "client", env: client.env };
|
|
2276
|
+
const recorded = session?.journal.runServer(session.runId, name);
|
|
2277
|
+
if (session !== void 0 && recorded !== void 0) {
|
|
2278
|
+
const now = fingerprint2(
|
|
2279
|
+
session.journal.fingerprintKey(),
|
|
2280
|
+
upstreamEnv(name, spec, source),
|
|
2281
|
+
Object.keys(recorded.fingerprints)
|
|
2282
|
+
);
|
|
2283
|
+
const changed = differing(recorded.fingerprints, now);
|
|
2284
|
+
if (changed.length > 0) {
|
|
2285
|
+
const one = changed.length === 1;
|
|
2286
|
+
throw new ConfigError(
|
|
2287
|
+
`${changed.join(", ")} ${one ? "is" : "are"} not what this session's ${name} server was started with, so undoing through it now could act on a different store from the one the session wrote to. Set ${one ? "it" : "them"} back as ${one ? "it was" : "they were"} in ${client?.from ?? "the client config"} to undo this session.`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const cwd = recorded?.cwd ?? client?.cwd;
|
|
2292
|
+
return await connectUpstream(name, spec, {
|
|
2293
|
+
env: source,
|
|
2294
|
+
stderr: "capture",
|
|
2295
|
+
...cwd === void 0 ? {} : { cwd }
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2932
2298
|
async function runPin(argv) {
|
|
2933
2299
|
const path = findManifest(flag(argv, "--manifest"));
|
|
2934
2300
|
const manifest = loadManifest(path);
|
|
2935
|
-
|
|
2301
|
+
let shapes = /* @__PURE__ */ new Map();
|
|
2936
2302
|
const upstreams = [];
|
|
2937
2303
|
try {
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
});
|
|
2946
|
-
upstreams.push(upstream);
|
|
2947
|
-
shapes.set(name, await toolShapes(upstream));
|
|
2948
|
-
}
|
|
2304
|
+
upstreams.push(
|
|
2305
|
+
...await startAll(
|
|
2306
|
+
Object.entries(manifest.servers),
|
|
2307
|
+
([name, spec]) => startAsTheClientWould(path, name, spec)
|
|
2308
|
+
)
|
|
2309
|
+
);
|
|
2310
|
+
shapes = await listAll(upstreams);
|
|
2949
2311
|
} finally {
|
|
2950
2312
|
for (const upstream of upstreams) {
|
|
2951
2313
|
await upstream.close();
|
|
@@ -2988,23 +2350,44 @@ async function runCheck(argv) {
|
|
|
2988
2350
|
out(` ${style.accent(line2)}`);
|
|
2989
2351
|
}
|
|
2990
2352
|
}
|
|
2353
|
+
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2354
|
+
for (const [header2, value] of Object.entries(spec.url === void 0 ? {} : spec.headers ?? {})) {
|
|
2355
|
+
if (!value.includes("${")) {
|
|
2356
|
+
out("");
|
|
2357
|
+
for (const line2 of wrapped(
|
|
2358
|
+
`${name}'s ${header2} header is written into the policy itself. If it is a token, move it to the client entry's env and write "\${NAME}" here instead.`,
|
|
2359
|
+
74
|
|
2360
|
+
)) {
|
|
2361
|
+
out(` ${style.accent(line2)}`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2991
2366
|
const upstreams = [];
|
|
2992
2367
|
const offered = /* @__PURE__ */ new Map();
|
|
2368
|
+
const trustedReads = /* @__PURE__ */ new Map();
|
|
2993
2369
|
try {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
);
|
|
3004
|
-
}
|
|
3005
|
-
await verifyAgainstServers(upstreams, manifest);
|
|
2370
|
+
upstreams.push(
|
|
2371
|
+
...await startAll(
|
|
2372
|
+
Object.entries(manifest.servers),
|
|
2373
|
+
([name, spec]) => startAsTheClientWould(path, name, spec)
|
|
2374
|
+
)
|
|
2375
|
+
);
|
|
2376
|
+
const listed = await listAll(upstreams);
|
|
2377
|
+
await verifyAgainstServers(upstreams, manifest, listed);
|
|
2378
|
+
const resolver = createPolicyResolver(manifest);
|
|
3006
2379
|
for (const upstream of upstreams) {
|
|
3007
|
-
|
|
2380
|
+
const shapes = listed.get(upstream.name) ?? [];
|
|
2381
|
+
const trusted = shapes.filter(
|
|
2382
|
+
(tool) => tool.readOnly === true && trustsMarks(manifest, upstream.name) && !resolver.resolve(`${upstream.name}.${tool.name}`).matched
|
|
2383
|
+
).map((tool) => tool.name);
|
|
2384
|
+
offered.set(
|
|
2385
|
+
upstream.name,
|
|
2386
|
+
shapes.map((tool) => tool.name).filter((tool) => !trusted.includes(tool))
|
|
2387
|
+
);
|
|
2388
|
+
if (trusted.length > 0) {
|
|
2389
|
+
trustedReads.set(upstream.name, trusted);
|
|
2390
|
+
}
|
|
3008
2391
|
}
|
|
3009
2392
|
} finally {
|
|
3010
2393
|
for (const upstream of upstreams) {
|
|
@@ -3051,8 +2434,8 @@ async function runCheck(argv) {
|
|
|
3051
2434
|
`${String(total)} tool${total === 1 ? "" : "s"} here ${total === 1 ? "has" : "have"} no policy, so ${total === 1 ? "it is" : "they are"} treated as`
|
|
3052
2435
|
)}`
|
|
3053
2436
|
);
|
|
3054
|
-
out(` ${style.quiet("irreversible and held for a person
|
|
3055
|
-
out(` ${style.quiet(`${total === 1 ? "it" : "one"}. Write a policy for any you would rather it got on with.`)}`);
|
|
2437
|
+
out(` ${style.quiet("irreversible and held for a person every time an agent calls")}`);
|
|
2438
|
+
out(` ${style.quiet(`${total === 1 ? "it" : "one"}. Write a policy, or allow it, for any you would rather it got on with.`)}`);
|
|
3056
2439
|
out("");
|
|
3057
2440
|
for (const entry of uncovered) {
|
|
3058
2441
|
for (const line2 of wrapped(entry.tools.join(", "), 60)) {
|
|
@@ -3060,10 +2443,23 @@ async function runCheck(argv) {
|
|
|
3060
2443
|
}
|
|
3061
2444
|
}
|
|
3062
2445
|
}
|
|
2446
|
+
if (trustedReads.size > 0) {
|
|
2447
|
+
out("");
|
|
2448
|
+
out(
|
|
2449
|
+
` ${style.label("read as reads")} ${style.quiet("no rule, but the server marks these read-only, so they are not held:")}`
|
|
2450
|
+
);
|
|
2451
|
+
for (const [server, tools] of trustedReads) {
|
|
2452
|
+
for (const line2 of wrapped(tools.join(", "), 60)) {
|
|
2453
|
+
out(` ${style.quiet(server.padEnd(8))} ${line2}`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
out(` ${style.quiet("trust_annotations: false on a server turns this off for it.")}`);
|
|
2457
|
+
}
|
|
3063
2458
|
out("");
|
|
2459
|
+
const used = existsSync5(findJournal(flag(argv, "--journal"), path));
|
|
3064
2460
|
hint(
|
|
3065
2461
|
firstOf(
|
|
3066
|
-
() => notPinned(manifest),
|
|
2462
|
+
() => used ? notPinned(manifest) : void 0,
|
|
3067
2463
|
() => ({
|
|
3068
2464
|
why: "this is sound; to see which clients it covers",
|
|
3069
2465
|
run: "status",
|
|
@@ -3084,12 +2480,19 @@ async function runInstall(argv) {
|
|
|
3084
2480
|
if (sites.length === 0) {
|
|
3085
2481
|
out("");
|
|
3086
2482
|
out(` ${style.quiet("No MCP client config was found on this machine.")}`);
|
|
3087
|
-
out(` ${style.quiet(
|
|
2483
|
+
out(` ${style.quiet(LOOKED_FOR)}`);
|
|
3088
2484
|
out("");
|
|
3089
2485
|
return 0;
|
|
3090
2486
|
}
|
|
3091
|
-
const invoker = invokerFor(version(),
|
|
3092
|
-
const { plans, yaml } = await planInstall(sites, manifestPath, invoker
|
|
2487
|
+
const invoker = invokerFor(version(), fileURLToPath(import.meta.url));
|
|
2488
|
+
const { plans, yaml } = await planInstall(sites, manifestPath, invoker, void 0, {
|
|
2489
|
+
remote: argv.includes("--remote"),
|
|
2490
|
+
start: !printOnly,
|
|
2491
|
+
starting: (name) => {
|
|
2492
|
+
process.stderr.write(` ${style.quiet(`starting ${name} to see what it offers...`)}
|
|
2493
|
+
`);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
3093
2496
|
const total = plans.reduce((sum, plan) => sum + plan.servers.length, 0);
|
|
3094
2497
|
out("");
|
|
3095
2498
|
if (invoker.note !== void 0 && total > 0) {
|
|
@@ -3102,16 +2505,38 @@ async function runInstall(argv) {
|
|
|
3102
2505
|
out(` ${style.strong(plan.site.label)} ${style.quiet(plan.site.scope)}`);
|
|
3103
2506
|
out(` ${style.quiet(plan.site.path)}`);
|
|
3104
2507
|
for (const server of plan.servers) {
|
|
3105
|
-
const note = server.
|
|
2508
|
+
const note = server.again === true ? style.quiet("already in your policy from an earlier install; covered again") : server.unstarted === true ? style.quiet(
|
|
2509
|
+
server.adopted === void 0 ? "not started here; a policy is drafted when you install" : `not started here; the policy that ships for ${server.adopted} would be used`
|
|
2510
|
+
) : 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)`);
|
|
3106
2511
|
out(` ${style.strong(server.name.padEnd(18))} ${note}`);
|
|
3107
2512
|
if (server.provenance === "documented") {
|
|
3108
2513
|
out(
|
|
3109
2514
|
` ${" ".repeat(18)} ${style.accent("never run against the real server -- check it before trusting undo")}`
|
|
3110
2515
|
);
|
|
3111
2516
|
}
|
|
2517
|
+
if (server.direct !== void 0) {
|
|
2518
|
+
for (const line2 of [
|
|
2519
|
+
`hosted at ${server.direct}, reached directly with the headers your`,
|
|
2520
|
+
"client gave it. They now sit in this entry's env; the policy names them."
|
|
2521
|
+
]) {
|
|
2522
|
+
out(` ${" ".repeat(18)} ${style.quiet(line2)}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (server.bridged !== void 0) {
|
|
2526
|
+
for (const line2 of [
|
|
2527
|
+
`hosted at ${server.bridged}, reached through mcp-remote,`,
|
|
2528
|
+
"which signs you in and keeps that sign-in itself; synartesis never sees it.",
|
|
2529
|
+
"Hosted tools are named differently from local packages, so no shipped",
|
|
2530
|
+
"policy applies: its writes are held until you write rules for them."
|
|
2531
|
+
]) {
|
|
2532
|
+
out(` ${" ".repeat(18)} ${style.quiet(line2)}`);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
3112
2535
|
}
|
|
3113
2536
|
for (const skip of plan.skipped) {
|
|
3114
|
-
|
|
2537
|
+
for (const [at, line2] of wrapped(skip.why, 58).entries()) {
|
|
2538
|
+
out(` ${style.quiet((at === 0 ? skip.name : "").padEnd(18))} ${style.quiet(line2)}`);
|
|
2539
|
+
}
|
|
3115
2540
|
}
|
|
3116
2541
|
if (plan.servers.length === 0 && plan.skipped.length === 0) {
|
|
3117
2542
|
out(` ${style.quiet("no servers listed")}`);
|
|
@@ -3195,13 +2620,13 @@ async function runUninstall(argv) {
|
|
|
3195
2620
|
}
|
|
3196
2621
|
function openIfPresent(journalPath) {
|
|
3197
2622
|
try {
|
|
3198
|
-
return
|
|
2623
|
+
return existsSync5(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
|
|
3199
2624
|
} catch {
|
|
3200
2625
|
return void 0;
|
|
3201
2626
|
}
|
|
3202
2627
|
}
|
|
3203
2628
|
async function connectThese(targets, manifestPath) {
|
|
3204
|
-
const invoker = invokerFor(version(),
|
|
2629
|
+
const invoker = invokerFor(version(), fileURLToPath(import.meta.url));
|
|
3205
2630
|
const wanted = /* @__PURE__ */ new Map();
|
|
3206
2631
|
const sites = /* @__PURE__ */ new Map();
|
|
3207
2632
|
for (const target of targets) {
|
|
@@ -3209,14 +2634,15 @@ async function connectThese(targets, manifestPath) {
|
|
|
3209
2634
|
sites.set(key, target.site);
|
|
3210
2635
|
(wanted.get(key) ?? wanted.set(key, /* @__PURE__ */ new Set()).get(key))?.add(target.server);
|
|
3211
2636
|
}
|
|
3212
|
-
const { plans, yaml } = await planInstall(
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
2637
|
+
const { plans, yaml } = await planInstall(
|
|
2638
|
+
[...sites.values()],
|
|
2639
|
+
manifestPath,
|
|
2640
|
+
invoker,
|
|
2641
|
+
(site, name) => wanted.get(`${site.path}${site.scope}`)?.has(name) === true,
|
|
2642
|
+
// Picked by name from the list, which is the asking.
|
|
2643
|
+
{ remote: true }
|
|
2644
|
+
);
|
|
2645
|
+
const applied = applyInstall(plans, manifestPath, yaml);
|
|
3220
2646
|
const count = applied.reduce((sum, entry) => sum + entry.servers.length, 0);
|
|
3221
2647
|
if (count === 0) {
|
|
3222
2648
|
return "nothing was connected; see the reasons above";
|
|
@@ -3228,7 +2654,7 @@ function runStatus(argv) {
|
|
|
3228
2654
|
const journalPath = findJournal(flag(argv, "--journal"), manifestPath);
|
|
3229
2655
|
out("");
|
|
3230
2656
|
out(
|
|
3231
|
-
` ${style.label("policy")} ${
|
|
2657
|
+
` ${style.label("policy")} ${existsSync5(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
|
|
3232
2658
|
);
|
|
3233
2659
|
out(
|
|
3234
2660
|
` ${style.label("journal")} ${bytesOf(journalPath) === void 0 ? style.quiet(`${journalPath} (none yet)`) : `${style.strong(journalPath)} ${style.quiet(sizeOf(journalPath))}`}`
|
|
@@ -3239,7 +2665,7 @@ function runStatus(argv) {
|
|
|
3239
2665
|
const groups = scan(journal, process.cwd());
|
|
3240
2666
|
if (groups.length === 0) {
|
|
3241
2667
|
out(` ${style.quiet("No MCP client config found.")}`);
|
|
3242
|
-
out(` ${style.quiet(
|
|
2668
|
+
out(` ${style.quiet(LOOKED_FOR)}`);
|
|
3243
2669
|
out("");
|
|
3244
2670
|
return 0;
|
|
3245
2671
|
}
|
|
@@ -3284,7 +2710,7 @@ async function runInit(argv) {
|
|
|
3284
2710
|
}
|
|
3285
2711
|
const path = findManifest(flag(argv, "--manifest"));
|
|
3286
2712
|
const force = argv.includes("--force");
|
|
3287
|
-
const present =
|
|
2713
|
+
const present = existsSync5(path);
|
|
3288
2714
|
if (present && force) {
|
|
3289
2715
|
throw new UsageError(
|
|
3290
2716
|
`--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
|
|
@@ -3294,11 +2720,11 @@ async function runInit(argv) {
|
|
|
3294
2720
|
name,
|
|
3295
2721
|
command,
|
|
3296
2722
|
args: argv.slice(separator + 2),
|
|
3297
|
-
...present ? { existing:
|
|
2723
|
+
...present ? { existing: readFileSync(path, "utf8") } : {}
|
|
3298
2724
|
});
|
|
3299
2725
|
parseManifest(draft.yaml, path);
|
|
3300
|
-
|
|
3301
|
-
|
|
2726
|
+
mkdirSync(dirname(resolve(path)), { recursive: true, mode: 448 });
|
|
2727
|
+
writeFileSync(path, draft.yaml);
|
|
3302
2728
|
out("");
|
|
3303
2729
|
out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
|
|
3304
2730
|
out(` ${rule(54)}`);
|
|
@@ -3313,7 +2739,7 @@ async function runInit(argv) {
|
|
|
3313
2739
|
out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
|
|
3314
2740
|
}
|
|
3315
2741
|
out("");
|
|
3316
|
-
out(` ${style.accent(`${proxyCommand()} --manifest ${
|
|
2742
|
+
out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
|
|
3317
2743
|
out("");
|
|
3318
2744
|
return 0;
|
|
3319
2745
|
}
|
|
@@ -3459,8 +2885,36 @@ function runList(journal, asJson, journalPath) {
|
|
|
3459
2885
|
` ${"session".padEnd(width)} ${"started".padEnd(13)} ${"did".padEnd(26)} ${"state".padEnd(26)} agent`
|
|
3460
2886
|
)
|
|
3461
2887
|
);
|
|
2888
|
+
let quiet = [];
|
|
2889
|
+
const flushQuiet = () => {
|
|
2890
|
+
if (quiet.length === 1) {
|
|
2891
|
+
const [only] = quiet;
|
|
2892
|
+
if (only !== void 0) {
|
|
2893
|
+
const did = didWhat(journal, only, 0);
|
|
2894
|
+
out(
|
|
2895
|
+
` ${style.strong(only.id.slice(0, width))} ${style.quiet(shortTime(only.startedAt).trimEnd().padEnd(13))} ${laid(did.what, 26)} ${laid(did.state, 26)} ${style.quiet(only.label ?? "-")}`
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
} else if (quiet.length > 1) {
|
|
2899
|
+
const newest = quiet[0];
|
|
2900
|
+
const oldest = quiet[quiet.length - 1];
|
|
2901
|
+
if (newest !== void 0 && oldest !== void 0) {
|
|
2902
|
+
out(
|
|
2903
|
+
` ${style.quiet(
|
|
2904
|
+
`${"".padEnd(width)} ${String(quiet.length)} sessions with nothing in them, ${shortTime(oldest.startedAt).trim()} to ${shortTime(newest.startedAt).trim()}`
|
|
2905
|
+
)}`
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
quiet = [];
|
|
2910
|
+
};
|
|
3462
2911
|
for (const run of runs) {
|
|
3463
2912
|
const actions = counted2(run.id);
|
|
2913
|
+
if (actions.actions === 0) {
|
|
2914
|
+
quiet.push(run);
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
flushQuiet();
|
|
3464
2918
|
const { unknown, waiting } = actions;
|
|
3465
2919
|
const notes = [
|
|
3466
2920
|
unknown === 0 ? "" : `${String(unknown)} of unknown outcome`,
|
|
@@ -3472,6 +2926,7 @@ function runList(journal, asJson, journalPath) {
|
|
|
3472
2926
|
` ${style.strong(run.id.slice(0, width))} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${laid(did.what, 26)} ${laid(did.state, 26)} ${style.quiet(run.label ?? "-")}${note}`
|
|
3473
2927
|
);
|
|
3474
2928
|
}
|
|
2929
|
+
flushQuiet();
|
|
3475
2930
|
out("");
|
|
3476
2931
|
if ((bytesOf(journalPath) ?? 0) > PRUNE_NAG_BYTES) {
|
|
3477
2932
|
out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
|
|
@@ -3486,12 +2941,15 @@ function runList(journal, asJson, journalPath) {
|
|
|
3486
2941
|
async function runShow(argv, journal, asJson) {
|
|
3487
2942
|
const full = argv.includes("--full");
|
|
3488
2943
|
const runs = [...journal.listRuns()].reverse();
|
|
3489
|
-
const
|
|
2944
|
+
const given = positional(argv)[1];
|
|
2945
|
+
const tally2 = journal.tallyRuns();
|
|
2946
|
+
const run = (given === void 0 ? runs.find((one) => (tally2.get(one.id)?.actions ?? 0) > 0) : void 0) ?? pick(runs, given, RUN, true);
|
|
3490
2947
|
const runId = run.id;
|
|
3491
2948
|
const inspection = argv.includes("--live") && journal.getActions(runId).length > 0 ? await withUpstreams(
|
|
3492
2949
|
findManifest(flag(argv, "--manifest")),
|
|
3493
2950
|
async (router) => await inspect({ journal, router, runId }),
|
|
3494
|
-
serversUsedBy(journal, runId)
|
|
2951
|
+
serversUsedBy(journal, runId),
|
|
2952
|
+
{ journal, runId }
|
|
3495
2953
|
) : void 0;
|
|
3496
2954
|
if (asJson) {
|
|
3497
2955
|
out(
|
|
@@ -3514,7 +2972,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3514
2972
|
out(` ${style.quiet("agent ")} ${run.label ?? "-"}`);
|
|
3515
2973
|
out(` ${style.quiet("started")} ${fullTime(run.startedAt)} ${style.quiet(ago(run.startedAt))}`);
|
|
3516
2974
|
out(
|
|
3517
|
-
` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
|
|
2975
|
+
` ${style.quiet("status ")} ${RUN_STATUS[run.status]}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
|
|
3518
2976
|
);
|
|
3519
2977
|
const actions = journal.getActions(runId);
|
|
3520
2978
|
if (actions.length === 0) {
|
|
@@ -3576,7 +3034,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3576
3034
|
}
|
|
3577
3035
|
out("");
|
|
3578
3036
|
}
|
|
3579
|
-
out(` ${
|
|
3037
|
+
out(` ${summarise(actions)}`);
|
|
3580
3038
|
if (inspection !== void 0) {
|
|
3581
3039
|
out("");
|
|
3582
3040
|
const spoiled = inspection.resources.some((found) => found.condition === "changed");
|
|
@@ -3629,14 +3087,20 @@ var CLASS_MARK = {
|
|
|
3629
3087
|
unclassified: "?"
|
|
3630
3088
|
};
|
|
3631
3089
|
var BADGE_WIDTH = "irreversible".length + 2;
|
|
3090
|
+
var RUN_STATUS = {
|
|
3091
|
+
active: "still running",
|
|
3092
|
+
complete: "finished",
|
|
3093
|
+
rolled_back: "undone",
|
|
3094
|
+
partial: "partly undone"
|
|
3095
|
+
};
|
|
3632
3096
|
function badgeOf(action, pad2) {
|
|
3633
3097
|
const name = `${CLASS_MARK[action.class]} ${action.class}`;
|
|
3634
3098
|
const plain = pad2 ? name.padEnd(BADGE_WIDTH) : name;
|
|
3635
3099
|
return action.class === "irreversible" ? style.accent(plain) : style.quiet(plain);
|
|
3636
3100
|
}
|
|
3637
3101
|
function statusOf(action, pad2) {
|
|
3638
|
-
const label =
|
|
3639
|
-
const text = pad2 ? label.padEnd(
|
|
3102
|
+
const label = plainly(action).text;
|
|
3103
|
+
const text = pad2 ? label.padEnd(22) : label;
|
|
3640
3104
|
if (wasRefused(action)) {
|
|
3641
3105
|
return style.accent(text);
|
|
3642
3106
|
}
|
|
@@ -3675,7 +3139,7 @@ function inverseArgs(inverse) {
|
|
|
3675
3139
|
function truncate3(text, limit) {
|
|
3676
3140
|
return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
|
|
3677
3141
|
}
|
|
3678
|
-
function
|
|
3142
|
+
function summarise(actions) {
|
|
3679
3143
|
const counts = /* @__PURE__ */ new Map();
|
|
3680
3144
|
for (const action of actions) {
|
|
3681
3145
|
const label = labelFor(action);
|
|
@@ -3869,12 +3333,28 @@ function runGates(journal, asJson) {
|
|
|
3869
3333
|
return 0;
|
|
3870
3334
|
}
|
|
3871
3335
|
function runDecision(argv, journal, approving) {
|
|
3336
|
+
const unattended = argv.includes("--unattended");
|
|
3337
|
+
if (approving && !process.stdin.isTTY && !unattended) {
|
|
3338
|
+
throw new UsageError(
|
|
3339
|
+
"approve needs a person at a terminal, so that an agent with a shell cannot approve its own calls. Run it in your terminal, or answer it in synartesis watch. For approving from a script, see synartesis --help.",
|
|
3340
|
+
false
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3872
3343
|
const waiting = journal.listGated();
|
|
3873
3344
|
const given = positional(argv)[1];
|
|
3874
|
-
const
|
|
3345
|
+
const named = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3346
|
+
const by = approving && unattended ? `${named} (unattended)` : named;
|
|
3875
3347
|
const reason = flag(argv, "--reason") ?? "denied by operator";
|
|
3876
3348
|
if (given !== void 0) {
|
|
3877
3349
|
const settled2 = journal.getAction(given);
|
|
3350
|
+
if (approving && settled2?.status === "denied" && journal.reverseDenial(settled2.id, by)) {
|
|
3351
|
+
out(
|
|
3352
|
+
` ${style.accent("approved")} ${style.strong(`${settled2.server}.${settled2.tool}`)} ${style.quiet(settled2.id)} ${style.quiet("(was denied)")}`
|
|
3353
|
+
);
|
|
3354
|
+
out("");
|
|
3355
|
+
hint({ why: "the agent can make that call again now, and it will go through" });
|
|
3356
|
+
return 0;
|
|
3357
|
+
}
|
|
3878
3358
|
if (settled2 !== void 0 && settled2.status !== "gated") {
|
|
3879
3359
|
process.stderr.write(
|
|
3880
3360
|
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled2)})
|
|
@@ -3891,7 +3371,7 @@ function runDecision(argv, journal, approving) {
|
|
|
3891
3371
|
let failed = 0;
|
|
3892
3372
|
let settled = 0;
|
|
3893
3373
|
for (const action of targets) {
|
|
3894
|
-
const changed = approving ? journal.approve(action.id, by) : journal.
|
|
3374
|
+
const changed = approving ? journal.approve(action.id, by) : journal.denyByPerson(action.id, by, reason);
|
|
3895
3375
|
if (!changed) {
|
|
3896
3376
|
const now = journal.getAction(action.id);
|
|
3897
3377
|
process.stderr.write(
|
|
@@ -3911,26 +3391,249 @@ function runDecision(argv, journal, approving) {
|
|
|
3911
3391
|
hint(
|
|
3912
3392
|
firstOf(
|
|
3913
3393
|
() => heldCalls(journal),
|
|
3914
|
-
() => approving ? { why: "the agent can make that call again now, and it will go through" } :
|
|
3394
|
+
() => approving ? { why: "the agent can make that call again now, and it will go through" } : settled === 1 ? {
|
|
3395
|
+
why: "refused; if the agent asks again it is told you said no. Changed your mind?",
|
|
3396
|
+
run: `approve ${targets[0]?.id.slice(0, 8) ?? ""}`,
|
|
3397
|
+
needs: ["journal"]
|
|
3398
|
+
} : { why: "refused; if the agent asks again it is told you said no" }
|
|
3915
3399
|
)
|
|
3916
3400
|
);
|
|
3917
3401
|
}
|
|
3918
3402
|
return failed === 0 ? 0 : 1;
|
|
3919
3403
|
}
|
|
3404
|
+
var ALLOW_MAX_MINUTES = 24 * 60;
|
|
3405
|
+
function allowFor(given) {
|
|
3406
|
+
const found = /^(\d+)(m|h)$/.exec(given.trim());
|
|
3407
|
+
const minutes = found === null ? NaN : Number(found[1]) * (found[2] === "h" ? 60 : 1);
|
|
3408
|
+
if (!Number.isFinite(minutes) || minutes < 1 || minutes > ALLOW_MAX_MINUTES) {
|
|
3409
|
+
throw new UsageError(
|
|
3410
|
+
`--for takes minutes or hours up to a day, like 30m or 2h, not ${given}. For good, use --always.`
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
return minutes;
|
|
3414
|
+
}
|
|
3415
|
+
async function runAllow(argv, journalPath) {
|
|
3416
|
+
const given = positional(argv)[1];
|
|
3417
|
+
const forFlag = flag(argv, "--for");
|
|
3418
|
+
const always = argv.includes("--always");
|
|
3419
|
+
const stop = argv.includes("--stop");
|
|
3420
|
+
const now = /* @__PURE__ */ new Date();
|
|
3421
|
+
if (given === void 0) {
|
|
3422
|
+
if (forFlag !== void 0 || always || stop) {
|
|
3423
|
+
throw new UsageError("allow needs the tool, as server.tool -- for example crm.send_email");
|
|
3424
|
+
}
|
|
3425
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3426
|
+
try {
|
|
3427
|
+
const current = journal.listAllowances(now.toISOString());
|
|
3428
|
+
out("");
|
|
3429
|
+
if (current.length === 0) {
|
|
3430
|
+
out(` ${style.quiet("Nothing is being let through without asking.")}`);
|
|
3431
|
+
}
|
|
3432
|
+
for (const one of current) {
|
|
3433
|
+
out(
|
|
3434
|
+
` ${style.strong(`${one.server}.${one.tool}`)} ${style.quiet(`until ${shortTime(one.until, now)}, allowed by ${one.by}`)}`
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
out("");
|
|
3438
|
+
return 0;
|
|
3439
|
+
} finally {
|
|
3440
|
+
journal.close();
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
const named = splitQualified(given);
|
|
3444
|
+
if (named === void 0) {
|
|
3445
|
+
throw new UsageError(`${given} is not a tool name; write it as server.tool, for example crm.send_email`);
|
|
3446
|
+
}
|
|
3447
|
+
if ([forFlag !== void 0, always, stop].filter(Boolean).length !== 1) {
|
|
3448
|
+
throw new UsageError(
|
|
3449
|
+
`say how long: allow ${given} --for 1h, allow ${given} --always, or allow ${given} --stop`
|
|
3450
|
+
);
|
|
3451
|
+
}
|
|
3452
|
+
const who = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3453
|
+
if (stop) {
|
|
3454
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3455
|
+
try {
|
|
3456
|
+
const stopped = journal.stopAllowance(named.server, named.tool, who, now.toISOString());
|
|
3457
|
+
out(
|
|
3458
|
+
stopped ? ` ${style.accent("stopped")} ${style.strong(given)} ${style.quiet("is held again from its next call")}` : ` ${style.quiet(`${given} was not being let through`)}`
|
|
3459
|
+
);
|
|
3460
|
+
if (stopped) {
|
|
3461
|
+
out(` ${style.quiet("A rule written with --always stays in the policy; edit it there.")}`);
|
|
3462
|
+
}
|
|
3463
|
+
return 0;
|
|
3464
|
+
} finally {
|
|
3465
|
+
journal.close();
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
const unattended = argv.includes("--unattended");
|
|
3469
|
+
const interactive = process.stdin.isTTY;
|
|
3470
|
+
if (!interactive && !unattended) {
|
|
3471
|
+
throw new UsageError(
|
|
3472
|
+
"allow needs a person at a terminal, so that an agent with a shell cannot allow its own calls. Run it in your terminal. For allowing from a script, see synartesis --help.",
|
|
3473
|
+
false
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
const by = unattended ? `${who} (unattended)` : who;
|
|
3477
|
+
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3478
|
+
const manifest = loadManifest(manifestPath);
|
|
3479
|
+
const spec = manifest.servers[named.server];
|
|
3480
|
+
if (spec === void 0) {
|
|
3481
|
+
const near = didYouMean(named.server, Object.keys(manifest.servers));
|
|
3482
|
+
throw new UsageError(
|
|
3483
|
+
`${manifestPath} has no server called ${named.server}${near === void 0 ? "" : `; did you mean ${near}?`}`
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
const { policy } = createPolicyResolver(manifest).resolve(given);
|
|
3487
|
+
if (forFlag !== void 0) {
|
|
3488
|
+
const minutes = allowFor(forFlag);
|
|
3489
|
+
const until = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
3490
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3491
|
+
try {
|
|
3492
|
+
journal.allow(named.server, named.tool, by, until);
|
|
3493
|
+
} finally {
|
|
3494
|
+
journal.close();
|
|
3495
|
+
}
|
|
3496
|
+
out("");
|
|
3497
|
+
out(
|
|
3498
|
+
` ${style.accent("allowed")} ${style.strong(given)} ${style.quiet(`until ${shortTime(until, now)} -- its calls go out without asking, and are still recorded`)}`
|
|
3499
|
+
);
|
|
3500
|
+
if (policy.gate !== "always" && policy.gate !== "on_write") {
|
|
3501
|
+
out(` ${style.quiet("(the policy was not holding it anyway)")}`);
|
|
3502
|
+
} else if (policy.class === "irreversible") {
|
|
3503
|
+
out(` ${style.quiet("These cannot be undone. Nothing will ask before each one goes out.")}`);
|
|
3504
|
+
}
|
|
3505
|
+
out(` ${style.quiet(`Takes effect on its next call. To end it sooner: synartesis allow ${given} --stop`)}`);
|
|
3506
|
+
out("");
|
|
3507
|
+
return 0;
|
|
3508
|
+
}
|
|
3509
|
+
const text = readFileSync(manifestPath, "utf8");
|
|
3510
|
+
const pins = manifest.pins?.[named.server];
|
|
3511
|
+
let pin;
|
|
3512
|
+
if (pins !== void 0 && pins[named.tool] === void 0) {
|
|
3513
|
+
const upstream = await startAsTheClientWould(manifestPath, named.server, spec);
|
|
3514
|
+
try {
|
|
3515
|
+
const shape = (await toolShapes(upstream)).find((tool) => tool.name === named.tool);
|
|
3516
|
+
if (shape === void 0) {
|
|
3517
|
+
throw new UsageError(`${named.server} has no tool called ${named.tool}`);
|
|
3518
|
+
}
|
|
3519
|
+
pin = fingerprint(shape.inputSchema);
|
|
3520
|
+
} finally {
|
|
3521
|
+
await upstream.close();
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
let edit;
|
|
3525
|
+
try {
|
|
3526
|
+
edit = allowAlways({
|
|
3527
|
+
text,
|
|
3528
|
+
file: manifestPath,
|
|
3529
|
+
server: named.server,
|
|
3530
|
+
tool: named.tool,
|
|
3531
|
+
by,
|
|
3532
|
+
date: now.toISOString().slice(0, 10),
|
|
3533
|
+
...pin === void 0 ? {} : { pin }
|
|
3534
|
+
});
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
if (error instanceof PolicyEditError) {
|
|
3537
|
+
process.stderr.write(`synartesis: ${error.message}
|
|
3538
|
+
`);
|
|
3539
|
+
return 1;
|
|
3540
|
+
}
|
|
3541
|
+
throw error;
|
|
3542
|
+
}
|
|
3543
|
+
if (edit.how === "already") {
|
|
3544
|
+
out(` ${style.quiet(`${given} is already let through by ${manifestPath}`)}`);
|
|
3545
|
+
return 0;
|
|
3546
|
+
}
|
|
3547
|
+
if (edit.policy.class === "irreversible") {
|
|
3548
|
+
const confirmed = unattended ? flag(argv, "--confirm") : await ask(` ${given} cannot be undone. Type its name to stop holding it for good: `);
|
|
3549
|
+
if (confirmed?.trim() !== given) {
|
|
3550
|
+
process.stderr.write(`synartesis: not confirmed, so ${manifestPath} was not changed
|
|
3551
|
+
`);
|
|
3552
|
+
return 1;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
if (readFileSync(manifestPath, "utf8") !== text) {
|
|
3556
|
+
process.stderr.write(`synartesis: ${manifestPath} changed while this was running; nothing was written
|
|
3557
|
+
`);
|
|
3558
|
+
return 1;
|
|
3559
|
+
}
|
|
3560
|
+
const beside = `${manifestPath}.${String(process.pid)}.tmp`;
|
|
3561
|
+
writeFileSync(beside, edit.text, { mode: statSync(manifestPath).mode });
|
|
3562
|
+
renameSync(beside, manifestPath);
|
|
3563
|
+
out("");
|
|
3564
|
+
out(
|
|
3565
|
+
` ${style.accent("allowed")} ${style.strong(given)} ${style.quiet(`for good -- ${edit.how === "added" ? "a rule was added to" : "its rule was changed in"} ${manifestPath}`)}`
|
|
3566
|
+
);
|
|
3567
|
+
if (edit.policy.class === "irreversible") {
|
|
3568
|
+
out(` ${style.quiet("It still cannot be undone: each call is recorded, and none is held.")}`);
|
|
3569
|
+
}
|
|
3570
|
+
if (pin !== void 0) {
|
|
3571
|
+
out(` ${style.quiet("Pinned at the shape it has now, as the rest of the server is.")}`);
|
|
3572
|
+
}
|
|
3573
|
+
out(` ${style.quiet("Takes effect when your client next starts the server. Until then:")}`);
|
|
3574
|
+
out(` ${style.quiet(`synartesis allow ${given} --for 1h`)}`);
|
|
3575
|
+
out("");
|
|
3576
|
+
return 0;
|
|
3577
|
+
}
|
|
3578
|
+
async function ask(question) {
|
|
3579
|
+
const reader = createInterface({ input: process.stdin, output: process.stdout });
|
|
3580
|
+
try {
|
|
3581
|
+
return await reader.question(question);
|
|
3582
|
+
} finally {
|
|
3583
|
+
reader.close();
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
function stepWords(kind, dryRun) {
|
|
3587
|
+
switch (kind) {
|
|
3588
|
+
case "revert":
|
|
3589
|
+
return dryRun ? "would put back" : "put back";
|
|
3590
|
+
case "skip":
|
|
3591
|
+
return "nothing to undo";
|
|
3592
|
+
case "already-reverted":
|
|
3593
|
+
return "already undone";
|
|
3594
|
+
case "permanent":
|
|
3595
|
+
return "cannot undo";
|
|
3596
|
+
case "kept":
|
|
3597
|
+
return "left alone";
|
|
3598
|
+
case "halt":
|
|
3599
|
+
return "stopped";
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
function resultWords(status, dryRun) {
|
|
3603
|
+
switch (status) {
|
|
3604
|
+
case "rolled_back":
|
|
3605
|
+
return dryRun ? "would all be undone (nothing was written)" : "all undone";
|
|
3606
|
+
case "partial":
|
|
3607
|
+
return dryRun ? "would be partly undone (nothing was written)" : "partly undone";
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3920
3610
|
function report(result, alreadyForcing = false, as = "") {
|
|
3921
3611
|
out("");
|
|
3922
3612
|
out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
|
|
3923
3613
|
out(` ${rule(72)}`);
|
|
3924
3614
|
out("");
|
|
3925
3615
|
let separated = false;
|
|
3616
|
+
let reads = 0;
|
|
3617
|
+
const flushReads = () => {
|
|
3618
|
+
if (reads > 0) {
|
|
3619
|
+
out(` ${style.quiet(`${String(reads)} read${reads === 1 ? "" : "s"}, nothing to undo`)}`);
|
|
3620
|
+
reads = 0;
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3926
3623
|
for (const step of result.steps) {
|
|
3624
|
+
if (step.kind === "skip" && step.reason === "readonly") {
|
|
3625
|
+
reads += 1;
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
flushReads();
|
|
3927
3629
|
if (step.kind === "kept" && !separated) {
|
|
3928
3630
|
separated = true;
|
|
3929
3631
|
out("");
|
|
3930
3632
|
out(` ${style.quiet("left alone")}`);
|
|
3931
3633
|
}
|
|
3932
3634
|
const unverified = step.kind === "revert" && !step.verified ? ` ${style.accent("[unverified]")}` : "";
|
|
3933
|
-
const
|
|
3635
|
+
const said = stepWords(step.kind, result.dryRun);
|
|
3636
|
+
const kind = step.kind === "halt" || step.kind === "permanent" ? style.accent(said.padEnd(16)) : said.padEnd(16);
|
|
3934
3637
|
out(
|
|
3935
3638
|
` ${style.quiet(String(step.seq).padStart(3))} ${kind} ${style.strong(`${step.server}.${step.tool}`)} ${style.quiet(step.reason)}${unverified}`
|
|
3936
3639
|
);
|
|
@@ -3950,6 +3653,7 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
3950
3653
|
);
|
|
3951
3654
|
}
|
|
3952
3655
|
}
|
|
3656
|
+
flushReads();
|
|
3953
3657
|
if (result.halted !== void 0) {
|
|
3954
3658
|
const halt = result.halted;
|
|
3955
3659
|
out("");
|
|
@@ -3993,8 +3697,9 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
3993
3697
|
);
|
|
3994
3698
|
}
|
|
3995
3699
|
out("");
|
|
3700
|
+
const outcome = resultWords(result.status, result.dryRun);
|
|
3996
3701
|
out(
|
|
3997
|
-
` ${style.label("result")} ${result.status === "rolled_back" ?
|
|
3702
|
+
` ${style.label("result")} ${result.status === "rolled_back" ? outcome : style.accent(outcome)}`
|
|
3998
3703
|
);
|
|
3999
3704
|
out("");
|
|
4000
3705
|
if (result.dryRun && result.status === "rolled_back") {
|
|
@@ -4006,29 +3711,16 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
4006
3711
|
}
|
|
4007
3712
|
return result.halted === void 0 && permanent.length === 0 ? 0 : 1;
|
|
4008
3713
|
}
|
|
4009
|
-
async function withUpstreams(manifestPath, use, only) {
|
|
3714
|
+
async function withUpstreams(manifestPath, use, only, session) {
|
|
4010
3715
|
const manifest = loadManifest(manifestPath);
|
|
4011
|
-
const
|
|
4012
|
-
const
|
|
3716
|
+
const wanted = Object.entries(manifest.servers).filter(([name]) => only === void 0 || only.has(name));
|
|
3717
|
+
const { started, failed } = await startTogether(
|
|
3718
|
+
wanted,
|
|
3719
|
+
([name, spec]) => startAsTheClientWould(manifestPath, name, spec, session)
|
|
3720
|
+
);
|
|
3721
|
+
const upstreams = [...started];
|
|
3722
|
+
const missing = failed.map(({ item: [name], error }) => `${name}: ${describe(error)}`);
|
|
4013
3723
|
try {
|
|
4014
|
-
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
4015
|
-
if (only !== void 0 && !only.has(name)) {
|
|
4016
|
-
continue;
|
|
4017
|
-
}
|
|
4018
|
-
try {
|
|
4019
|
-
upstreams.push(
|
|
4020
|
-
await connectStdioUpstream({
|
|
4021
|
-
name,
|
|
4022
|
-
command: spec.command,
|
|
4023
|
-
args: spec.args,
|
|
4024
|
-
stderr: "capture",
|
|
4025
|
-
...spec.env === void 0 ? {} : { env: spec.env }
|
|
4026
|
-
})
|
|
4027
|
-
);
|
|
4028
|
-
} catch (error) {
|
|
4029
|
-
missing.push(`${name}: ${describe(error)}`);
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
3724
|
if (upstreams.length === 0 && missing.length > 0) {
|
|
4033
3725
|
throw new ManifestError(`no server could be started. ${missing.join("; ")}`);
|
|
4034
3726
|
}
|
|
@@ -4077,7 +3769,8 @@ async function performUndo(manifestPath, journal, runId, options) {
|
|
|
4077
3769
|
}),
|
|
4078
3770
|
// A replan re-resolves inverses from the current policy, which may name a
|
|
4079
3771
|
// server this run never used; everything else needs only what it touched.
|
|
4080
|
-
options.replan === true ? void 0 : serversUsedBy(journal, runId)
|
|
3772
|
+
options.replan === true ? void 0 : serversUsedBy(journal, runId),
|
|
3773
|
+
{ journal, runId }
|
|
4081
3774
|
);
|
|
4082
3775
|
}
|
|
4083
3776
|
async function runUndo(argv, journal) {
|
|
@@ -4087,9 +3780,16 @@ async function runUndo(argv, journal) {
|
|
|
4087
3780
|
throw new UsageError("--to needs a positive whole number", false);
|
|
4088
3781
|
}
|
|
4089
3782
|
const given = positional(argv)[1];
|
|
4090
|
-
const
|
|
3783
|
+
const runs = [...journal.listRuns()].reverse();
|
|
3784
|
+
const standing3 = journal.standingPerRun();
|
|
3785
|
+
const hasWork = (id) => {
|
|
3786
|
+
const here = standing3.get(id);
|
|
3787
|
+
return here !== void 0 && (here.undoable > 0 || here.conflicted > 0);
|
|
3788
|
+
};
|
|
3789
|
+
const newestWithWork = given === void 0 ? runs.find((run) => hasWork(run.id)) : void 0;
|
|
3790
|
+
const chosen = newestWithWork ?? pick(runs, given, RUN, true);
|
|
4091
3791
|
const runId = chosen.id;
|
|
4092
|
-
if (journal.getRun(runId)?.status === "active" && !argv.includes("--yes") && !argv.includes("--dry-run")) {
|
|
3792
|
+
if (journal.getRun(runId)?.status === "active" && hasWork(runId) && !argv.includes("--yes") && !argv.includes("--dry-run")) {
|
|
4093
3793
|
throw new UsageError(
|
|
4094
3794
|
`${runId.slice(0, 8)} has not ended, so an agent may still be writing to it.
|
|
4095
3795
|
See what an undo would do: ${cliCommand()} undo ${runId.slice(0, 8)} --dry-run
|
|
@@ -4105,7 +3805,9 @@ async function runUndo(argv, journal) {
|
|
|
4105
3805
|
).length;
|
|
4106
3806
|
out("");
|
|
4107
3807
|
out(
|
|
4108
|
-
` ${style.quiet(
|
|
3808
|
+
` ${style.quiet(
|
|
3809
|
+
newestWithWork === void 0 ? "no session named, so the most recent:" : "no session named, so the most recent with something to undo:"
|
|
3810
|
+
)} ${style.strong(runId.slice(0, 8))} ` + style.quiet(`${chosen.label ?? "an agent"}, ${shortTime(chosen.startedAt).trim()}`)
|
|
4109
3811
|
);
|
|
4110
3812
|
if (left === 0) {
|
|
4111
3813
|
out("");
|
|
@@ -4155,7 +3857,8 @@ async function runUndo(argv, journal) {
|
|
|
4155
3857
|
const over = (await withUpstreams(
|
|
4156
3858
|
manifestPath,
|
|
4157
3859
|
async (router) => await inspect({ journal, router, runId }),
|
|
4158
|
-
serversUsedBy(journal, runId)
|
|
3860
|
+
serversUsedBy(journal, runId),
|
|
3861
|
+
{ journal, runId }
|
|
4159
3862
|
)).resources.filter(
|
|
4160
3863
|
// Below --to nothing is undone, so a change down there is not something
|
|
4161
3864
|
// this command would write over and must not stand in its way.
|
|
@@ -4217,7 +3920,9 @@ var KNOWN_COMMANDS = [
|
|
|
4217
3920
|
"watch",
|
|
4218
3921
|
"approve",
|
|
4219
3922
|
"deny",
|
|
3923
|
+
"allow",
|
|
4220
3924
|
"resolve",
|
|
3925
|
+
"notify",
|
|
4221
3926
|
"undo",
|
|
4222
3927
|
"help",
|
|
4223
3928
|
"version"
|
|
@@ -4247,6 +3952,18 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
4247
3952
|
"--force",
|
|
4248
3953
|
"--yes",
|
|
4249
3954
|
"--older-than",
|
|
3955
|
+
// install: cover hosted servers too, through mcp-remote.
|
|
3956
|
+
"--remote",
|
|
3957
|
+
// notify: send one to see whether they reach you.
|
|
3958
|
+
"--test",
|
|
3959
|
+
// approve without a terminal, from a script; recorded as unattended.
|
|
3960
|
+
"--unattended",
|
|
3961
|
+
// allow: for a while, for good, or no longer; and the typed confirmation
|
|
3962
|
+
// for a tool that cannot be undone, given without a terminal.
|
|
3963
|
+
"--for",
|
|
3964
|
+
"--always",
|
|
3965
|
+
"--stop",
|
|
3966
|
+
"--confirm",
|
|
4250
3967
|
"--help",
|
|
4251
3968
|
"-h",
|
|
4252
3969
|
"--version",
|
|
@@ -4255,8 +3972,8 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
4255
3972
|
]);
|
|
4256
3973
|
function version() {
|
|
4257
3974
|
try {
|
|
4258
|
-
const root =
|
|
4259
|
-
const parsed = JSON.parse(
|
|
3975
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
3976
|
+
const parsed = JSON.parse(readFileSync(join2(root, "..", "package.json"), "utf8"));
|
|
4260
3977
|
const found = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
|
|
4261
3978
|
return typeof found === "string" ? found : "unknown";
|
|
4262
3979
|
} catch {
|
|
@@ -4287,7 +4004,7 @@ function rejectUnknownFlags(argv) {
|
|
|
4287
4004
|
}
|
|
4288
4005
|
}
|
|
4289
4006
|
function openJournalOrExplain(journalPath) {
|
|
4290
|
-
if (!
|
|
4007
|
+
if (!existsSync5(journalPath)) {
|
|
4291
4008
|
throw new UsageError(
|
|
4292
4009
|
`nothing has been recorded yet: there is no journal at ${journalPath}. One appears the first time an agent calls a tool through synartesis proxy.`
|
|
4293
4010
|
);
|
|
@@ -4317,8 +4034,8 @@ ${COMMANDS}`);
|
|
|
4317
4034
|
}
|
|
4318
4035
|
const givenJournal = flag(argv, "--journal");
|
|
4319
4036
|
const givenManifest = flag(argv, "--manifest");
|
|
4320
|
-
journalArg = givenJournal === void 0 ? "" : ` --journal ${
|
|
4321
|
-
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${
|
|
4037
|
+
journalArg = givenJournal === void 0 ? "" : ` --journal ${resolve(givenJournal)}`;
|
|
4038
|
+
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${resolve(givenManifest)}`;
|
|
4322
4039
|
if (command === void 0) {
|
|
4323
4040
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
4324
4041
|
const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
|
|
@@ -4335,7 +4052,8 @@ ${COMMANDS}`);
|
|
|
4335
4052
|
return await withUpstreams(
|
|
4336
4053
|
manifestPath,
|
|
4337
4054
|
async (router) => await inspect({ journal: journal2, router, runId }),
|
|
4338
|
-
serversUsedBy(journal2, runId)
|
|
4055
|
+
serversUsedBy(journal2, runId),
|
|
4056
|
+
{ journal: journal2, runId }
|
|
4339
4057
|
);
|
|
4340
4058
|
} finally {
|
|
4341
4059
|
journal2.close();
|
|
@@ -4387,7 +4105,7 @@ ${COMMANDS}`);
|
|
|
4387
4105
|
decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
|
|
4388
4106
|
});
|
|
4389
4107
|
}
|
|
4390
|
-
if (!
|
|
4108
|
+
if (!existsSync5(journalPath) && (command === "list" || command === "show" || command === "gates")) {
|
|
4391
4109
|
if (asJson) {
|
|
4392
4110
|
out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
|
|
4393
4111
|
return 0;
|
|
@@ -4401,6 +4119,12 @@ ${COMMANDS}`);
|
|
|
4401
4119
|
out("");
|
|
4402
4120
|
return 0;
|
|
4403
4121
|
}
|
|
4122
|
+
if (command === "notify") {
|
|
4123
|
+
return runNotifyTest(argv);
|
|
4124
|
+
}
|
|
4125
|
+
if (command === "allow") {
|
|
4126
|
+
return await runAllow(argv, journalPath);
|
|
4127
|
+
}
|
|
4404
4128
|
if (command === "desktop") {
|
|
4405
4129
|
return runDesktop();
|
|
4406
4130
|
}
|
|
@@ -4432,6 +4156,32 @@ ${COMMANDS}`);
|
|
|
4432
4156
|
journal.close();
|
|
4433
4157
|
}
|
|
4434
4158
|
}
|
|
4159
|
+
function runNotifyTest(argv) {
|
|
4160
|
+
if (!argv.includes("--test")) {
|
|
4161
|
+
throw new UsageError("notify takes --test, which sends one to see whether they reach you");
|
|
4162
|
+
}
|
|
4163
|
+
const why = canNotify();
|
|
4164
|
+
if (why !== void 0) {
|
|
4165
|
+
out(` ${style.quiet(`No notification sent: ${why}.`)}`);
|
|
4166
|
+
return 1;
|
|
4167
|
+
}
|
|
4168
|
+
desktopNotifier()({
|
|
4169
|
+
server: "synartesis",
|
|
4170
|
+
tool: "test",
|
|
4171
|
+
actionId: "00000000",
|
|
4172
|
+
approve: "this is only a test"
|
|
4173
|
+
});
|
|
4174
|
+
out("");
|
|
4175
|
+
out(` ${style.quiet("Sent one. If nothing appeared, notifications for it are switched off:")}`);
|
|
4176
|
+
out(
|
|
4177
|
+
` ${style.quiet(
|
|
4178
|
+
platform() === "darwin" ? "macOS lists the ones osascript sends under Script Editor, in System Settings > Notifications." : "check that a notification daemon is running and notify-send is installed."
|
|
4179
|
+
)}`
|
|
4180
|
+
);
|
|
4181
|
+
out(` ${style.quiet("Either way, synartesis watch shows every held call as it happens.")}`);
|
|
4182
|
+
out("");
|
|
4183
|
+
return 0;
|
|
4184
|
+
}
|
|
4435
4185
|
function runDesktop() {
|
|
4436
4186
|
const found = findDesktop();
|
|
4437
4187
|
if (found === void 0) {
|