synartesis 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +78 -4
- package/dist/{chunk-JE7MOCZO.js → chunk-AEEKBR5D.js} +1813 -52
- package/dist/cli.js +710 -1005
- package/dist/proxy.js +182 -49
- package/manifests/aws-docs.yaml +26 -0
- package/manifests/brave.yaml +33 -0
- package/manifests/chrome-devtools.yaml +115 -0
- package/manifests/exa.yaml +20 -0
- package/manifests/fetch.yaml +20 -0
- package/manifests/github.yaml +67 -85
- package/manifests/playwright.yaml +106 -0
- package/manifests/tavily.yaml +27 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,368 +1,235 @@
|
|
|
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,
|
|
20
36
|
loadManifest,
|
|
21
37
|
observeState,
|
|
22
38
|
openJournal,
|
|
23
39
|
parseManifest,
|
|
24
|
-
pathBinaryMatches,
|
|
25
40
|
pinBlock,
|
|
41
|
+
planInstall,
|
|
26
42
|
planInverse,
|
|
27
43
|
planRead,
|
|
28
44
|
proxyCommand,
|
|
29
45
|
qualify,
|
|
30
46
|
resolvedRead,
|
|
31
47
|
rule,
|
|
48
|
+
serversAt,
|
|
49
|
+
splitQualified,
|
|
32
50
|
standing,
|
|
33
51
|
style,
|
|
34
52
|
toPayload,
|
|
35
53
|
toResolvedRead,
|
|
36
54
|
toolShapes,
|
|
55
|
+
trustsMarks,
|
|
37
56
|
ungoverned,
|
|
38
57
|
untested,
|
|
58
|
+
upstreamEnv,
|
|
39
59
|
verifyAgainstServers,
|
|
40
60
|
warnUntested,
|
|
41
61
|
wasRefused
|
|
42
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-AEEKBR5D.js";
|
|
43
63
|
import {
|
|
44
64
|
DriftConflict,
|
|
45
65
|
ManifestError,
|
|
46
66
|
RollbackHalted,
|
|
47
67
|
SynartesisError,
|
|
48
|
-
UpstreamError,
|
|
49
68
|
changedLines,
|
|
50
69
|
describe
|
|
51
70
|
} from "./chunk-YVOO3PTV.js";
|
|
52
71
|
|
|
53
72
|
// src/cli.ts
|
|
73
|
+
import { platform } from "os";
|
|
54
74
|
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";
|
|
75
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
76
|
+
import { createInterface } from "readline/promises";
|
|
77
|
+
import { dirname, join as join2, resolve } from "path";
|
|
64
78
|
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
|
-
|
|
79
|
+
|
|
80
|
+
// src/manifest/edit.ts
|
|
81
|
+
import { isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
82
|
+
var PolicyEditError = class extends Error {
|
|
83
|
+
name = "PolicyEditError";
|
|
84
|
+
};
|
|
85
|
+
function allowAlways(input) {
|
|
86
|
+
const { text, file, server, tool } = input;
|
|
87
|
+
const qualified = `${server}.${tool}`;
|
|
88
|
+
const before = parseManifest(text, file);
|
|
89
|
+
if (before.servers[server] === void 0) {
|
|
90
|
+
throw new PolicyEditError(`${file} has no server called ${server}`);
|
|
91
|
+
}
|
|
92
|
+
const current = createPolicyResolver(before).resolve(qualified);
|
|
93
|
+
const doc = parseDocument(text, { keepSourceTokens: true });
|
|
94
|
+
const tools = doc.get("tools", true);
|
|
95
|
+
const exact = isSeq(tools) ? tools.items.find((item) => isMap(item) && item.get("match") === qualified) : void 0;
|
|
96
|
+
let edited;
|
|
97
|
+
let how;
|
|
98
|
+
if (exact !== void 0) {
|
|
99
|
+
if (!isMap(exact) || exact.flow === true) {
|
|
100
|
+
throw new PolicyEditError(`the rule for ${qualified} is written on one line; change its gate by hand`);
|
|
101
|
+
}
|
|
102
|
+
const gate = exact.items.find((pair) => isScalar(pair.key) && pair.key.value === "gate");
|
|
103
|
+
if (gate !== void 0 && isScalar(gate.value) && gate.value.value === "never") {
|
|
104
|
+
how = "already";
|
|
105
|
+
edited = text;
|
|
106
|
+
} else if (gate !== void 0) {
|
|
107
|
+
const at = rangeOf(gate.value, qualified);
|
|
108
|
+
edited = text.slice(0, at[0]) + "never" + text.slice(at[1]);
|
|
109
|
+
how = "changed";
|
|
110
|
+
} else {
|
|
111
|
+
const cls = exact.items.find((pair) => isScalar(pair.key) && pair.key.value === "class");
|
|
112
|
+
const key = cls?.key;
|
|
113
|
+
if (cls === void 0 || !isScalar(key)) {
|
|
114
|
+
throw new PolicyEditError(`the rule for ${qualified} has no class; add one by hand first`);
|
|
115
|
+
}
|
|
116
|
+
const keyAt = rangeOf(key, qualified)[0];
|
|
117
|
+
const indent = " ".repeat(keyAt - lineStart(text, keyAt));
|
|
118
|
+
const lineEnd = endOfLine(text, rangeOf(cls.value, qualified)[1]);
|
|
119
|
+
edited = `${text.slice(0, lineEnd)}
|
|
120
|
+
${indent}gate: never${text.slice(lineEnd)}`;
|
|
121
|
+
how = "changed";
|
|
99
122
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
} else {
|
|
124
|
+
if (tools !== void 0 && (!isSeq(tools) || tools.flow === true || tools.items.length === 0)) {
|
|
125
|
+
throw new PolicyEditError(`the tools list in ${file} is not written one rule to a line; add the rule by hand`);
|
|
126
|
+
}
|
|
127
|
+
const base = current.matched ? { ...current.policy } : { class: "irreversible" };
|
|
128
|
+
delete base["match"];
|
|
129
|
+
delete base["refusal"];
|
|
130
|
+
const rule2 = { match: qualified, ...base, gate: "never" };
|
|
131
|
+
const said = [`# Let through without asking, by ${input.by} on ${input.date}.`];
|
|
132
|
+
if (base["class"] === "irreversible") {
|
|
133
|
+
said.push("# It still cannot be undone: it is recorded, and no longer held.");
|
|
134
|
+
}
|
|
135
|
+
const body = stringify([rule2], { lineWidth: 0 }).trimEnd().replace(/^- match: .*$/m, `- match: ${JSON.stringify(qualified)}`);
|
|
136
|
+
if (isSeq(tools)) {
|
|
137
|
+
const seqAt = rangeOf(tools, "tools")[0];
|
|
138
|
+
const indent = " ".repeat(seqAt - lineStart(text, seqAt));
|
|
139
|
+
const lines = [...said, ...body.split("\n")].map((line2) => indent + line2);
|
|
140
|
+
edited = insertAfter(text, rangeOf(tools, "tools")[2], `
|
|
141
|
+
${lines.join("\n")}
|
|
142
|
+
`);
|
|
143
|
+
} else {
|
|
144
|
+
edited = `${text.replace(/\n*$/, "\n")}
|
|
105
145
|
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;
|
|
146
|
+
${[...said, ...body.split("\n")].map((line2) => ` ${line2}`).join("\n")}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
how = "added";
|
|
126
150
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
break;
|
|
151
|
+
const pins = before.pins?.[server];
|
|
152
|
+
if (how !== "already" && pins !== void 0 && pins[tool] === void 0) {
|
|
153
|
+
if (input.pin === void 0) {
|
|
154
|
+
throw new PolicyEditError(`${server} is pinned, and ${tool} has no pin yet`);
|
|
132
155
|
}
|
|
133
|
-
|
|
156
|
+
edited = addPin(edited, server, tool, input.pin, qualified);
|
|
134
157
|
}
|
|
135
|
-
|
|
158
|
+
const after = parseManifest(edited, file);
|
|
159
|
+
const policy = createPolicyResolver(after).resolve(qualified).policy;
|
|
160
|
+
confirmOnly(before, after, { server, tool }, current.policy, policy, input.pin);
|
|
161
|
+
return { text: edited, how, policy };
|
|
136
162
|
}
|
|
137
|
-
function
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
163
|
+
function rangeOf(node, what) {
|
|
164
|
+
const range = isNode(node) ? node.range : void 0;
|
|
165
|
+
if (range === void 0 || range === null) {
|
|
166
|
+
throw new PolicyEditError(`could not find where ${what} is written in the policy`);
|
|
141
167
|
}
|
|
142
|
-
|
|
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
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return void 0;
|
|
168
|
+
return range;
|
|
153
169
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
|
|
157
|
-
(name) => name !== void 0
|
|
158
|
-
);
|
|
170
|
+
function lineStart(text, at) {
|
|
171
|
+
return text.lastIndexOf("\n", at - 1) + 1;
|
|
159
172
|
}
|
|
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);
|
|
173
|
+
function endOfLine(text, at) {
|
|
174
|
+
const next = text.indexOf("\n", Math.max(0, at - 1));
|
|
175
|
+
return next === -1 ? text.length : next;
|
|
177
176
|
}
|
|
178
|
-
function
|
|
179
|
-
if (text ===
|
|
180
|
-
return
|
|
177
|
+
function insertAfter(text, at, block2) {
|
|
178
|
+
if (at > 0 && text[at - 1] === "\n") {
|
|
179
|
+
return text.slice(0, at) + block2.slice(1) + text.slice(at);
|
|
181
180
|
}
|
|
182
|
-
const
|
|
183
|
-
return
|
|
181
|
+
const end = endOfLine(text, at);
|
|
182
|
+
return text.slice(0, end) + block2.replace(/\n$/, "") + text.slice(end);
|
|
184
183
|
}
|
|
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;
|
|
184
|
+
function addPin(text, server, tool, pin, qualified) {
|
|
185
|
+
const doc = parseDocument(text, { keepSourceTokens: true });
|
|
186
|
+
const map = doc.getIn(["pins", server], true);
|
|
187
|
+
if (!isMap(map) || map.flow === true || map.items.length === 0) {
|
|
188
|
+
throw new PolicyEditError(`the pins for ${server} are not written one to a line; pin ${qualified} by hand`);
|
|
229
189
|
}
|
|
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();
|
|
190
|
+
const at = rangeOf(map, "pins")[0];
|
|
191
|
+
const indent = " ".repeat(at - lineStart(text, at));
|
|
192
|
+
return insertAfter(text, rangeOf(map, "pins")[2], `
|
|
193
|
+
${indent}${tool}: ${JSON.stringify(pin)}
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
function confirmOnly(before, after, { server, tool }, was, now, pin) {
|
|
197
|
+
const qualified = `${server}.${tool}`;
|
|
198
|
+
const refuse = (why) => {
|
|
199
|
+
throw new PolicyEditError(`the edit came out wrong (${why}), so nothing was written`);
|
|
200
|
+
};
|
|
201
|
+
if (now.match !== qualified || now.gate !== "never") {
|
|
202
|
+
refuse(`${qualified} is not let through`);
|
|
265
203
|
}
|
|
266
|
-
if (
|
|
267
|
-
|
|
204
|
+
if (now.class !== was.class) {
|
|
205
|
+
refuse(`${qualified} would change from ${was.class} to ${now.class}`);
|
|
268
206
|
}
|
|
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
|
-
);
|
|
207
|
+
const others = (manifest) => manifest.tools.filter((rule2) => rule2.match !== qualified).map((rule2) => canonical(rule2));
|
|
208
|
+
if (canonical(others(before)) !== canonical(others(after))) {
|
|
209
|
+
refuse("another rule changed");
|
|
275
210
|
}
|
|
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
|
-
};
|
|
211
|
+
if (canonical(before.servers) !== canonical(after.servers)) {
|
|
212
|
+
refuse("a server changed");
|
|
322
213
|
}
|
|
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
|
-
);
|
|
214
|
+
const pinned = before.pins?.[server];
|
|
215
|
+
const expected = pin === void 0 || pinned === void 0 || pinned[tool] !== void 0 ? before.pins : { ...before.pins, [server]: { ...pinned, [tool]: pin } };
|
|
216
|
+
if (canonical(expected ?? null) !== canonical(after.pins ?? null)) {
|
|
217
|
+
refuse("the pins changed");
|
|
340
218
|
}
|
|
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
219
|
}
|
|
353
220
|
|
|
354
221
|
// src/rollback/rollback.ts
|
|
355
|
-
import { z
|
|
356
|
-
var inversePlan =
|
|
357
|
-
server:
|
|
358
|
-
tool:
|
|
359
|
-
args:
|
|
222
|
+
import { z } from "zod";
|
|
223
|
+
var inversePlan = z.object({
|
|
224
|
+
server: z.string(),
|
|
225
|
+
tool: z.string(),
|
|
226
|
+
args: z.record(z.string(), z.unknown())
|
|
360
227
|
});
|
|
361
|
-
var observation =
|
|
362
|
-
|
|
363
|
-
|
|
228
|
+
var observation = z.union([
|
|
229
|
+
z.object({ present: z.literal(true), value: z.unknown() }),
|
|
230
|
+
z.object({ present: z.literal(false) })
|
|
364
231
|
]);
|
|
365
|
-
var toolResult =
|
|
232
|
+
var toolResult = z.looseObject({ isError: z.boolean().default(false) });
|
|
366
233
|
function sameState(a, b) {
|
|
367
234
|
return canonical(a) === canonical(b);
|
|
368
235
|
}
|
|
@@ -582,7 +449,7 @@ ${seen}` : seen;
|
|
|
582
449
|
steps.push({
|
|
583
450
|
...describeStep(action),
|
|
584
451
|
kind: "revert",
|
|
585
|
-
reason: verified ? "
|
|
452
|
+
reason: verified ? "unchanged since, so safe to put back" : forcedOver ?? unverifiedBecause(action),
|
|
586
453
|
verified,
|
|
587
454
|
plan,
|
|
588
455
|
...rebuilt.inverse === void 0 ? {} : { replanned: true },
|
|
@@ -696,11 +563,11 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
696
563
|
_meta: { [IDEMPOTENCY_META_KEY]: idempotencyKey }
|
|
697
564
|
}
|
|
698
565
|
},
|
|
699
|
-
|
|
566
|
+
z.looseObject({}),
|
|
700
567
|
{ signal }
|
|
701
568
|
);
|
|
702
569
|
} catch (error) {
|
|
703
|
-
return { ok: false, rejected:
|
|
570
|
+
return { ok: false, rejected: upstream.classify?.(error) !== void 0, message: describe(error) };
|
|
704
571
|
}
|
|
705
572
|
const parsed = toolResult.safeParse(raw);
|
|
706
573
|
if (parsed.success && parsed.data.isError) {
|
|
@@ -714,10 +581,10 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
714
581
|
}
|
|
715
582
|
|
|
716
583
|
// src/rollback/inspect.ts
|
|
717
|
-
import { z as
|
|
718
|
-
var observation2 =
|
|
719
|
-
|
|
720
|
-
|
|
584
|
+
import { z as z2 } from "zod";
|
|
585
|
+
var observation2 = z2.union([
|
|
586
|
+
z2.object({ present: z2.literal(true), value: z2.unknown() }),
|
|
587
|
+
z2.object({ present: z2.literal(false) })
|
|
721
588
|
]);
|
|
722
589
|
function sameState2(a, b) {
|
|
723
590
|
return canonical(a) === canonical(b);
|
|
@@ -1035,7 +902,7 @@ function shortList() {
|
|
|
1035
902
|
}
|
|
1036
903
|
|
|
1037
904
|
// src/watch.ts
|
|
1038
|
-
import { existsSync
|
|
905
|
+
import { existsSync } from "fs";
|
|
1039
906
|
|
|
1040
907
|
// src/keys.ts
|
|
1041
908
|
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
@@ -1157,6 +1024,7 @@ function summariseArgs(args, limit = 60) {
|
|
|
1157
1024
|
}
|
|
1158
1025
|
|
|
1159
1026
|
// src/watch.ts
|
|
1027
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
1160
1028
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1161
1029
|
var NOTICE_TICKS = 26;
|
|
1162
1030
|
function line(action, now) {
|
|
@@ -1216,7 +1084,7 @@ function render(journal, options, tick, view) {
|
|
|
1216
1084
|
});
|
|
1217
1085
|
out2.push("");
|
|
1218
1086
|
out2.push(
|
|
1219
|
-
canDecide(options) ? ` ${keyHint("a", "approve")} ${keyHint("d", "deny")} ${keyHint("j/k", "move")} ${keyHint("q", "quit")}` : ` ${style.quiet(`${options.approveWith} approve --all`)}`
|
|
1087
|
+
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
1088
|
);
|
|
1221
1089
|
}
|
|
1222
1090
|
if (view.notice !== "") {
|
|
@@ -1262,7 +1130,7 @@ async function* terminalKeys() {
|
|
|
1262
1130
|
async function watch(options) {
|
|
1263
1131
|
let journal;
|
|
1264
1132
|
const open = () => {
|
|
1265
|
-
if (journal === void 0 &&
|
|
1133
|
+
if (journal === void 0 && existsSync(options.journalPath)) {
|
|
1266
1134
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
1267
1135
|
}
|
|
1268
1136
|
return journal;
|
|
@@ -1275,7 +1143,7 @@ async function watch(options) {
|
|
|
1275
1143
|
const ready = open();
|
|
1276
1144
|
return ready === void 0 ? waitingForJournal(options, tick2) : render(ready, options, tick2, view);
|
|
1277
1145
|
};
|
|
1278
|
-
const decide = (approve) => {
|
|
1146
|
+
const decide = (approve, forAnHour = false) => {
|
|
1279
1147
|
const ready = open();
|
|
1280
1148
|
if (ready === void 0 || options.decideAs === void 0) {
|
|
1281
1149
|
return;
|
|
@@ -1285,8 +1153,11 @@ async function watch(options) {
|
|
|
1285
1153
|
if (action === void 0) {
|
|
1286
1154
|
return;
|
|
1287
1155
|
}
|
|
1288
|
-
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.
|
|
1289
|
-
|
|
1156
|
+
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.denyByPerson(action.id, options.decideAs, "denied from the watch view");
|
|
1157
|
+
if (changed && approve && forAnHour) {
|
|
1158
|
+
ready.allow(action.server, action.tool, options.decideAs, new Date(Date.now() + HOUR_MS).toISOString());
|
|
1159
|
+
}
|
|
1160
|
+
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
1161
|
view.noticeUntil = tick + NOTICE_TICKS;
|
|
1291
1162
|
view.cursor = 0;
|
|
1292
1163
|
};
|
|
@@ -1299,6 +1170,9 @@ async function watch(options) {
|
|
|
1299
1170
|
case "a":
|
|
1300
1171
|
decide(true);
|
|
1301
1172
|
return;
|
|
1173
|
+
case "A":
|
|
1174
|
+
decide(true, true);
|
|
1175
|
+
return;
|
|
1302
1176
|
case "d":
|
|
1303
1177
|
decide(false);
|
|
1304
1178
|
return;
|
|
@@ -1349,7 +1223,7 @@ async function watch(options) {
|
|
|
1349
1223
|
if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
|
|
1350
1224
|
break;
|
|
1351
1225
|
}
|
|
1352
|
-
await new Promise((
|
|
1226
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
1353
1227
|
}
|
|
1354
1228
|
return 0;
|
|
1355
1229
|
} finally {
|
|
@@ -1364,613 +1238,17 @@ async function watch(options) {
|
|
|
1364
1238
|
await reader?.return?.(void 0);
|
|
1365
1239
|
await reading;
|
|
1366
1240
|
})(),
|
|
1367
|
-
new Promise((
|
|
1241
|
+
new Promise((resolve2) => setTimeout(resolve2, 50).unref())
|
|
1368
1242
|
]);
|
|
1369
1243
|
journal?.close();
|
|
1370
1244
|
}
|
|
1371
1245
|
}
|
|
1372
1246
|
|
|
1373
1247
|
// 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
|
-
}
|
|
1248
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1972
1249
|
|
|
1973
1250
|
// src/install/connections.ts
|
|
1251
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1974
1252
|
var ACTIVE_WITHIN_MS = 2 * 60 * 1e3;
|
|
1975
1253
|
function lastSeenByServer(journal) {
|
|
1976
1254
|
return journal.lastSeenPerServer();
|
|
@@ -1979,7 +1257,7 @@ function commandMissing(command) {
|
|
|
1979
1257
|
if (command === void 0) {
|
|
1980
1258
|
return false;
|
|
1981
1259
|
}
|
|
1982
|
-
return (command.includes("/") || command.includes("\\")) && !
|
|
1260
|
+
return (command.includes("/") || command.includes("\\")) && !existsSync2(command);
|
|
1983
1261
|
}
|
|
1984
1262
|
function scan(journal, cwd) {
|
|
1985
1263
|
const seen = journal === void 0 ? /* @__PURE__ */ new Map() : lastSeenByServer(journal);
|
|
@@ -2036,6 +1314,7 @@ function needsConnecting(groups) {
|
|
|
2036
1314
|
}
|
|
2037
1315
|
|
|
2038
1316
|
// src/console.ts
|
|
1317
|
+
var HOUR_MS2 = 60 * 60 * 1e3;
|
|
2039
1318
|
var FRAMES2 = [
|
|
2040
1319
|
"\u280B",
|
|
2041
1320
|
"\u2819",
|
|
@@ -2208,7 +1487,7 @@ function connectionsView(screen, options) {
|
|
|
2208
1487
|
return [
|
|
2209
1488
|
` ${style.quiet("No MCP client config was found on this machine.")}`,
|
|
2210
1489
|
"",
|
|
2211
|
-
` ${style.quiet(
|
|
1490
|
+
` ${style.quiet(LOOKED_FOR)}`
|
|
2212
1491
|
];
|
|
2213
1492
|
}
|
|
2214
1493
|
const rows = connectionRows(screen);
|
|
@@ -2263,7 +1542,7 @@ function footer(screen, options) {
|
|
|
2263
1542
|
const what = screen.confirmingForce ? `undo ${screen.confirmingLabel ?? "this session"} anyway, losing that change?` : `undo ${screen.confirmingLabel ?? "this session"}?`;
|
|
2264
1543
|
return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
|
|
2265
1544
|
}
|
|
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" ? [
|
|
1545
|
+
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
1546
|
keyHint2("l", "check now"),
|
|
2268
1547
|
keyHint2("p", "preview undo"),
|
|
2269
1548
|
keyHint2("u", "undo"),
|
|
@@ -2314,7 +1593,7 @@ async function* terminalKeys2() {
|
|
|
2314
1593
|
async function openConsole(options) {
|
|
2315
1594
|
let journal;
|
|
2316
1595
|
const open = () => {
|
|
2317
|
-
if (journal === void 0 &&
|
|
1596
|
+
if (journal === void 0 && existsSync3(options.journalPath)) {
|
|
2318
1597
|
journal = openJournal(options.journalPath, { mustExist: true });
|
|
2319
1598
|
}
|
|
2320
1599
|
return journal;
|
|
@@ -2401,7 +1680,7 @@ async function openConsole(options) {
|
|
|
2401
1680
|
}
|
|
2402
1681
|
return ["", ` ${style.quiet("nothing to undo in this session")}`];
|
|
2403
1682
|
};
|
|
2404
|
-
const decide = (approve) => {
|
|
1683
|
+
const decide = (approve, forAnHour = false) => {
|
|
2405
1684
|
const ready = open();
|
|
2406
1685
|
if (ready === void 0) {
|
|
2407
1686
|
return;
|
|
@@ -2411,9 +1690,12 @@ async function openConsole(options) {
|
|
|
2411
1690
|
if (action === void 0) {
|
|
2412
1691
|
return;
|
|
2413
1692
|
}
|
|
2414
|
-
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.
|
|
1693
|
+
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.denyByPerson(action.id, options.decideAs, "denied from the console");
|
|
1694
|
+
if (changed && approve && forAnHour) {
|
|
1695
|
+
ready.allow(action.server, action.tool, options.decideAs, new Date(Date.now() + HOUR_MS2).toISOString());
|
|
1696
|
+
}
|
|
2415
1697
|
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}
|
|
1698
|
+
!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
1699
|
);
|
|
2418
1700
|
screen.cursor = 0;
|
|
2419
1701
|
};
|
|
@@ -2566,6 +1848,11 @@ async function openConsole(options) {
|
|
|
2566
1848
|
}
|
|
2567
1849
|
return;
|
|
2568
1850
|
}
|
|
1851
|
+
case "A":
|
|
1852
|
+
if (screen.mode === "gates") {
|
|
1853
|
+
decide(true, true);
|
|
1854
|
+
}
|
|
1855
|
+
return;
|
|
2569
1856
|
case "a":
|
|
2570
1857
|
if (screen.mode === "gates") {
|
|
2571
1858
|
decide(true);
|
|
@@ -2690,7 +1977,7 @@ async function openConsole(options) {
|
|
|
2690
1977
|
if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
|
|
2691
1978
|
break;
|
|
2692
1979
|
}
|
|
2693
|
-
await new Promise((
|
|
1980
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
2694
1981
|
}
|
|
2695
1982
|
return 0;
|
|
2696
1983
|
} finally {
|
|
@@ -2706,40 +1993,40 @@ async function openConsole(options) {
|
|
|
2706
1993
|
await reader?.return?.(void 0);
|
|
2707
1994
|
await reading;
|
|
2708
1995
|
})(),
|
|
2709
|
-
new Promise((
|
|
1996
|
+
new Promise((resolve2) => setTimeout(resolve2, 50).unref())
|
|
2710
1997
|
]);
|
|
2711
1998
|
journal?.close();
|
|
2712
1999
|
}
|
|
2713
2000
|
}
|
|
2714
2001
|
|
|
2715
2002
|
// src/desktop.ts
|
|
2716
|
-
import { existsSync as
|
|
2717
|
-
import { homedir
|
|
2718
|
-
import { join
|
|
2003
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2004
|
+
import { homedir } from "os";
|
|
2005
|
+
import { join } from "path";
|
|
2719
2006
|
var RELEASES = "https://github.com/ArhaanDev24/Synartesis/releases";
|
|
2720
2007
|
function candidates(platform2 = process.platform) {
|
|
2721
|
-
const home =
|
|
2008
|
+
const home = homedir();
|
|
2722
2009
|
if (platform2 === "darwin") {
|
|
2723
2010
|
return [
|
|
2724
2011
|
"/Applications/Synartesis.app",
|
|
2725
|
-
|
|
2012
|
+
join(home, "Applications/Synartesis.app")
|
|
2726
2013
|
];
|
|
2727
2014
|
}
|
|
2728
2015
|
if (platform2 === "win32") {
|
|
2729
|
-
const local = process.env["LOCALAPPDATA"] ??
|
|
2016
|
+
const local = process.env["LOCALAPPDATA"] ?? join(home, "AppData/Local");
|
|
2730
2017
|
return [
|
|
2731
|
-
|
|
2732
|
-
|
|
2018
|
+
join(local, "Programs/Synartesis/Synartesis.exe"),
|
|
2019
|
+
join(process.env["PROGRAMFILES"] ?? "C:/Program Files", "Synartesis/Synartesis.exe")
|
|
2733
2020
|
];
|
|
2734
2021
|
}
|
|
2735
2022
|
return [
|
|
2736
2023
|
"/opt/Synartesis/synartesis-desktop",
|
|
2737
2024
|
"/usr/bin/synartesis-desktop",
|
|
2738
|
-
|
|
2739
|
-
|
|
2025
|
+
join(home, ".local/bin/synartesis-desktop"),
|
|
2026
|
+
join(home, "Applications/Synartesis.AppImage")
|
|
2740
2027
|
];
|
|
2741
2028
|
}
|
|
2742
|
-
function findDesktop(platform2 = process.platform, here =
|
|
2029
|
+
function findDesktop(platform2 = process.platform, here = existsSync4) {
|
|
2743
2030
|
for (const path of candidates(platform2)) {
|
|
2744
2031
|
if (!here(path)) {
|
|
2745
2032
|
continue;
|
|
@@ -2782,7 +2069,7 @@ var COMMANDS = `
|
|
|
2782
2069
|
undo -- all in one place, with
|
|
2783
2070
|
the arrow keys. Everything
|
|
2784
2071
|
below can be done from it.
|
|
2785
|
-
synartesis install [--client <name>] [--dry-run] [--print]
|
|
2072
|
+
synartesis install [--client <name>] [--remote] [--dry-run] [--print]
|
|
2786
2073
|
synartesis uninstall [--client <name>]
|
|
2787
2074
|
synartesis status
|
|
2788
2075
|
synartesis init <server> -- <command> [args...] [--manifest <path>]
|
|
@@ -2801,14 +2088,19 @@ var COMMANDS = `
|
|
|
2801
2088
|
synartesis watch [--by <name>] [--journal <path>]
|
|
2802
2089
|
synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
|
|
2803
2090
|
synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
|
|
2091
|
+
synartesis allow [<server.tool> --for <30m|2h> | --always | --stop]
|
|
2804
2092
|
synartesis resolve [actionId] --applied|--failed [--by <name>] [--reason <text>]
|
|
2093
|
+
synartesis notify --test
|
|
2805
2094
|
synartesis undo [runId] [--to <seq>] [--dry-run] [--replan] [--force [--yes]]
|
|
2806
2095
|
[--manifest <path>] [--journal <path>]
|
|
2807
2096
|
|
|
2808
2097
|
install is the short way in: it finds what Claude Code, Claude Desktop,
|
|
2809
|
-
Cursor
|
|
2810
|
-
|
|
2098
|
+
Cursor, Codex, Gemini CLI, Copilot CLI, Antigravity and Devin Desktop (or
|
|
2099
|
+
Windsurf) already list, writes a policy covering all of it -- using the ones
|
|
2100
|
+
that ship where they fit -- and points each entry at the proxy. The original config
|
|
2811
2101
|
is copied aside first, and uninstall puts it back. status says what is covered.
|
|
2102
|
+
A hosted server (a url rather than a command) is covered only with --remote,
|
|
2103
|
+
through mcp-remote, which opens your browser to sign in.
|
|
2812
2104
|
|
|
2813
2105
|
desktop opens the window, if it is installed. It is a separate download --
|
|
2814
2106
|
shipping it through npm would put a browser engine inside every install of
|
|
@@ -2840,7 +2132,8 @@ undo stops when somebody has changed the resource since, rather than writing
|
|
|
2840
2132
|
over them. Three ways past that, and it prints all three: leave it, put the
|
|
2841
2133
|
resource back as the run left it and --replan, or --force to overwrite.
|
|
2842
2134
|
|
|
2843
|
-
--client claude-code, claude-desktop, cursor
|
|
2135
|
+
--client claude-code, claude-desktop, cursor, codex, gemini-cli,
|
|
2136
|
+
copilot-cli, antigravity, devin or windsurf; all by default
|
|
2844
2137
|
--print show the entries install would write, and write nothing
|
|
2845
2138
|
--full show every argument, snapshot and inverse in full, nothing elided
|
|
2846
2139
|
--live read each resource as it is now and say what has changed since
|
|
@@ -2853,6 +2146,8 @@ resource back as the run left it and --replan, or --force to overwrite.
|
|
|
2853
2146
|
--once watch prints the current state and exits
|
|
2854
2147
|
--json machine-readable output for list, show and gates
|
|
2855
2148
|
--dry-run read current state and print the plan without changing anything
|
|
2149
|
+
--unattended approve with no terminal, from a script. Recorded as unattended,
|
|
2150
|
+
so it can be told apart from a yes a person typed
|
|
2856
2151
|
--applied resolve: the call did land, though nothing recorded it
|
|
2857
2152
|
--failed resolve: the call never landed
|
|
2858
2153
|
--replan rebuild each undo from the current manifest, for a run recorded
|
|
@@ -2912,7 +2207,9 @@ var TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
|
2912
2207
|
"--by",
|
|
2913
2208
|
"--reason",
|
|
2914
2209
|
"--older-than",
|
|
2915
|
-
"--client"
|
|
2210
|
+
"--client",
|
|
2211
|
+
"--for",
|
|
2212
|
+
"--confirm"
|
|
2916
2213
|
]);
|
|
2917
2214
|
function positional(argv) {
|
|
2918
2215
|
const values = [];
|
|
@@ -2929,6 +2226,31 @@ function positional(argv) {
|
|
|
2929
2226
|
}
|
|
2930
2227
|
return values;
|
|
2931
2228
|
}
|
|
2229
|
+
async function startAsTheClientWould(manifestPath, name, spec, session) {
|
|
2230
|
+
const client = clientEnvFor(manifestPath, name);
|
|
2231
|
+
const source = client === void 0 ? { kind: "manifest" } : { kind: "client", env: client.env };
|
|
2232
|
+
const recorded = session?.journal.runServer(session.runId, name);
|
|
2233
|
+
if (session !== void 0 && recorded !== void 0) {
|
|
2234
|
+
const now = fingerprint2(
|
|
2235
|
+
session.journal.fingerprintKey(),
|
|
2236
|
+
upstreamEnv(name, spec, source),
|
|
2237
|
+
Object.keys(recorded.fingerprints)
|
|
2238
|
+
);
|
|
2239
|
+
const changed = differing(recorded.fingerprints, now);
|
|
2240
|
+
if (changed.length > 0) {
|
|
2241
|
+
const one = changed.length === 1;
|
|
2242
|
+
throw new ConfigError(
|
|
2243
|
+
`${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.`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const cwd = recorded?.cwd ?? client?.cwd;
|
|
2248
|
+
return await connectUpstream(name, spec, {
|
|
2249
|
+
env: source,
|
|
2250
|
+
stderr: "capture",
|
|
2251
|
+
...cwd === void 0 ? {} : { cwd }
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2932
2254
|
async function runPin(argv) {
|
|
2933
2255
|
const path = findManifest(flag(argv, "--manifest"));
|
|
2934
2256
|
const manifest = loadManifest(path);
|
|
@@ -2936,13 +2258,7 @@ async function runPin(argv) {
|
|
|
2936
2258
|
const upstreams = [];
|
|
2937
2259
|
try {
|
|
2938
2260
|
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2939
|
-
const upstream = await
|
|
2940
|
-
name,
|
|
2941
|
-
command: spec.command,
|
|
2942
|
-
args: spec.args,
|
|
2943
|
-
stderr: "capture",
|
|
2944
|
-
...spec.env === void 0 ? {} : { env: spec.env }
|
|
2945
|
-
});
|
|
2261
|
+
const upstream = await startAsTheClientWould(path, name, spec);
|
|
2946
2262
|
upstreams.push(upstream);
|
|
2947
2263
|
shapes.set(name, await toolShapes(upstream));
|
|
2948
2264
|
}
|
|
@@ -2988,23 +2304,40 @@ async function runCheck(argv) {
|
|
|
2988
2304
|
out(` ${style.accent(line2)}`);
|
|
2989
2305
|
}
|
|
2990
2306
|
}
|
|
2307
|
+
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2308
|
+
for (const [header2, value] of Object.entries(spec.url === void 0 ? {} : spec.headers ?? {})) {
|
|
2309
|
+
if (!value.includes("${")) {
|
|
2310
|
+
out("");
|
|
2311
|
+
for (const line2 of wrapped(
|
|
2312
|
+
`${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.`,
|
|
2313
|
+
74
|
|
2314
|
+
)) {
|
|
2315
|
+
out(` ${style.accent(line2)}`);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2991
2320
|
const upstreams = [];
|
|
2992
2321
|
const offered = /* @__PURE__ */ new Map();
|
|
2322
|
+
const trustedReads = /* @__PURE__ */ new Map();
|
|
2993
2323
|
try {
|
|
2994
2324
|
for (const [name, spec] of Object.entries(manifest.servers)) {
|
|
2995
|
-
upstreams.push(
|
|
2996
|
-
await connectStdioUpstream({
|
|
2997
|
-
name,
|
|
2998
|
-
command: spec.command,
|
|
2999
|
-
args: spec.args,
|
|
3000
|
-
stderr: "capture",
|
|
3001
|
-
...spec.env === void 0 ? {} : { env: spec.env }
|
|
3002
|
-
})
|
|
3003
|
-
);
|
|
2325
|
+
upstreams.push(await startAsTheClientWould(path, name, spec));
|
|
3004
2326
|
}
|
|
3005
2327
|
await verifyAgainstServers(upstreams, manifest);
|
|
2328
|
+
const resolver = createPolicyResolver(manifest);
|
|
3006
2329
|
for (const upstream of upstreams) {
|
|
3007
|
-
|
|
2330
|
+
const shapes = await toolShapes(upstream);
|
|
2331
|
+
const trusted = shapes.filter(
|
|
2332
|
+
(tool) => tool.readOnly === true && trustsMarks(manifest, upstream.name) && !resolver.resolve(`${upstream.name}.${tool.name}`).matched
|
|
2333
|
+
).map((tool) => tool.name);
|
|
2334
|
+
offered.set(
|
|
2335
|
+
upstream.name,
|
|
2336
|
+
shapes.map((tool) => tool.name).filter((tool) => !trusted.includes(tool))
|
|
2337
|
+
);
|
|
2338
|
+
if (trusted.length > 0) {
|
|
2339
|
+
trustedReads.set(upstream.name, trusted);
|
|
2340
|
+
}
|
|
3008
2341
|
}
|
|
3009
2342
|
} finally {
|
|
3010
2343
|
for (const upstream of upstreams) {
|
|
@@ -3051,8 +2384,8 @@ async function runCheck(argv) {
|
|
|
3051
2384
|
`${String(total)} tool${total === 1 ? "" : "s"} here ${total === 1 ? "has" : "have"} no policy, so ${total === 1 ? "it is" : "they are"} treated as`
|
|
3052
2385
|
)}`
|
|
3053
2386
|
);
|
|
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.`)}`);
|
|
2387
|
+
out(` ${style.quiet("irreversible and held for a person every time an agent calls")}`);
|
|
2388
|
+
out(` ${style.quiet(`${total === 1 ? "it" : "one"}. Write a policy, or allow it, for any you would rather it got on with.`)}`);
|
|
3056
2389
|
out("");
|
|
3057
2390
|
for (const entry of uncovered) {
|
|
3058
2391
|
for (const line2 of wrapped(entry.tools.join(", "), 60)) {
|
|
@@ -3060,10 +2393,23 @@ async function runCheck(argv) {
|
|
|
3060
2393
|
}
|
|
3061
2394
|
}
|
|
3062
2395
|
}
|
|
2396
|
+
if (trustedReads.size > 0) {
|
|
2397
|
+
out("");
|
|
2398
|
+
out(
|
|
2399
|
+
` ${style.label("read as reads")} ${style.quiet("no rule, but the server marks these read-only, so they are not held:")}`
|
|
2400
|
+
);
|
|
2401
|
+
for (const [server, tools] of trustedReads) {
|
|
2402
|
+
for (const line2 of wrapped(tools.join(", "), 60)) {
|
|
2403
|
+
out(` ${style.quiet(server.padEnd(8))} ${line2}`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
out(` ${style.quiet("trust_annotations: false on a server turns this off for it.")}`);
|
|
2407
|
+
}
|
|
3063
2408
|
out("");
|
|
2409
|
+
const used = existsSync5(findJournal(flag(argv, "--journal"), path));
|
|
3064
2410
|
hint(
|
|
3065
2411
|
firstOf(
|
|
3066
|
-
() => notPinned(manifest),
|
|
2412
|
+
() => used ? notPinned(manifest) : void 0,
|
|
3067
2413
|
() => ({
|
|
3068
2414
|
why: "this is sound; to see which clients it covers",
|
|
3069
2415
|
run: "status",
|
|
@@ -3084,12 +2430,19 @@ async function runInstall(argv) {
|
|
|
3084
2430
|
if (sites.length === 0) {
|
|
3085
2431
|
out("");
|
|
3086
2432
|
out(` ${style.quiet("No MCP client config was found on this machine.")}`);
|
|
3087
|
-
out(` ${style.quiet(
|
|
2433
|
+
out(` ${style.quiet(LOOKED_FOR)}`);
|
|
3088
2434
|
out("");
|
|
3089
2435
|
return 0;
|
|
3090
2436
|
}
|
|
3091
|
-
const invoker = invokerFor(version(),
|
|
3092
|
-
const { plans, yaml } = await planInstall(sites, manifestPath, invoker
|
|
2437
|
+
const invoker = invokerFor(version(), fileURLToPath(import.meta.url));
|
|
2438
|
+
const { plans, yaml } = await planInstall(sites, manifestPath, invoker, void 0, {
|
|
2439
|
+
remote: argv.includes("--remote"),
|
|
2440
|
+
start: !printOnly,
|
|
2441
|
+
starting: (name) => {
|
|
2442
|
+
process.stderr.write(` ${style.quiet(`starting ${name} to see what it offers...`)}
|
|
2443
|
+
`);
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
3093
2446
|
const total = plans.reduce((sum, plan) => sum + plan.servers.length, 0);
|
|
3094
2447
|
out("");
|
|
3095
2448
|
if (invoker.note !== void 0 && total > 0) {
|
|
@@ -3102,16 +2455,38 @@ async function runInstall(argv) {
|
|
|
3102
2455
|
out(` ${style.strong(plan.site.label)} ${style.quiet(plan.site.scope)}`);
|
|
3103
2456
|
out(` ${style.quiet(plan.site.path)}`);
|
|
3104
2457
|
for (const server of plan.servers) {
|
|
3105
|
-
const note = server.
|
|
2458
|
+
const note = server.again === true ? style.quiet("already in your policy from an earlier install; covered again") : server.unstarted === true ? style.quiet(
|
|
2459
|
+
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`
|
|
2460
|
+
) : 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
2461
|
out(` ${style.strong(server.name.padEnd(18))} ${note}`);
|
|
3107
2462
|
if (server.provenance === "documented") {
|
|
3108
2463
|
out(
|
|
3109
2464
|
` ${" ".repeat(18)} ${style.accent("never run against the real server -- check it before trusting undo")}`
|
|
3110
2465
|
);
|
|
3111
2466
|
}
|
|
2467
|
+
if (server.direct !== void 0) {
|
|
2468
|
+
for (const line2 of [
|
|
2469
|
+
`hosted at ${server.direct}, reached directly with the headers your`,
|
|
2470
|
+
"client gave it. They now sit in this entry's env; the policy names them."
|
|
2471
|
+
]) {
|
|
2472
|
+
out(` ${" ".repeat(18)} ${style.quiet(line2)}`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
if (server.bridged !== void 0) {
|
|
2476
|
+
for (const line2 of [
|
|
2477
|
+
`hosted at ${server.bridged}, reached through mcp-remote,`,
|
|
2478
|
+
"which signs you in and keeps that sign-in itself; synartesis never sees it.",
|
|
2479
|
+
"Hosted tools are named differently from local packages, so no shipped",
|
|
2480
|
+
"policy applies: its writes are held until you write rules for them."
|
|
2481
|
+
]) {
|
|
2482
|
+
out(` ${" ".repeat(18)} ${style.quiet(line2)}`);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
3112
2485
|
}
|
|
3113
2486
|
for (const skip of plan.skipped) {
|
|
3114
|
-
|
|
2487
|
+
for (const [at, line2] of wrapped(skip.why, 58).entries()) {
|
|
2488
|
+
out(` ${style.quiet((at === 0 ? skip.name : "").padEnd(18))} ${style.quiet(line2)}`);
|
|
2489
|
+
}
|
|
3115
2490
|
}
|
|
3116
2491
|
if (plan.servers.length === 0 && plan.skipped.length === 0) {
|
|
3117
2492
|
out(` ${style.quiet("no servers listed")}`);
|
|
@@ -3195,13 +2570,13 @@ async function runUninstall(argv) {
|
|
|
3195
2570
|
}
|
|
3196
2571
|
function openIfPresent(journalPath) {
|
|
3197
2572
|
try {
|
|
3198
|
-
return
|
|
2573
|
+
return existsSync5(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
|
|
3199
2574
|
} catch {
|
|
3200
2575
|
return void 0;
|
|
3201
2576
|
}
|
|
3202
2577
|
}
|
|
3203
2578
|
async function connectThese(targets, manifestPath) {
|
|
3204
|
-
const invoker = invokerFor(version(),
|
|
2579
|
+
const invoker = invokerFor(version(), fileURLToPath(import.meta.url));
|
|
3205
2580
|
const wanted = /* @__PURE__ */ new Map();
|
|
3206
2581
|
const sites = /* @__PURE__ */ new Map();
|
|
3207
2582
|
for (const target of targets) {
|
|
@@ -3209,14 +2584,15 @@ async function connectThese(targets, manifestPath) {
|
|
|
3209
2584
|
sites.set(key, target.site);
|
|
3210
2585
|
(wanted.get(key) ?? wanted.set(key, /* @__PURE__ */ new Set()).get(key))?.add(target.server);
|
|
3211
2586
|
}
|
|
3212
|
-
const { plans, yaml } = await planInstall(
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
2587
|
+
const { plans, yaml } = await planInstall(
|
|
2588
|
+
[...sites.values()],
|
|
2589
|
+
manifestPath,
|
|
2590
|
+
invoker,
|
|
2591
|
+
(site, name) => wanted.get(`${site.path}${site.scope}`)?.has(name) === true,
|
|
2592
|
+
// Picked by name from the list, which is the asking.
|
|
2593
|
+
{ remote: true }
|
|
2594
|
+
);
|
|
2595
|
+
const applied = applyInstall(plans, manifestPath, yaml);
|
|
3220
2596
|
const count = applied.reduce((sum, entry) => sum + entry.servers.length, 0);
|
|
3221
2597
|
if (count === 0) {
|
|
3222
2598
|
return "nothing was connected; see the reasons above";
|
|
@@ -3228,7 +2604,7 @@ function runStatus(argv) {
|
|
|
3228
2604
|
const journalPath = findJournal(flag(argv, "--journal"), manifestPath);
|
|
3229
2605
|
out("");
|
|
3230
2606
|
out(
|
|
3231
|
-
` ${style.label("policy")} ${
|
|
2607
|
+
` ${style.label("policy")} ${existsSync5(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
|
|
3232
2608
|
);
|
|
3233
2609
|
out(
|
|
3234
2610
|
` ${style.label("journal")} ${bytesOf(journalPath) === void 0 ? style.quiet(`${journalPath} (none yet)`) : `${style.strong(journalPath)} ${style.quiet(sizeOf(journalPath))}`}`
|
|
@@ -3239,7 +2615,7 @@ function runStatus(argv) {
|
|
|
3239
2615
|
const groups = scan(journal, process.cwd());
|
|
3240
2616
|
if (groups.length === 0) {
|
|
3241
2617
|
out(` ${style.quiet("No MCP client config found.")}`);
|
|
3242
|
-
out(` ${style.quiet(
|
|
2618
|
+
out(` ${style.quiet(LOOKED_FOR)}`);
|
|
3243
2619
|
out("");
|
|
3244
2620
|
return 0;
|
|
3245
2621
|
}
|
|
@@ -3284,7 +2660,7 @@ async function runInit(argv) {
|
|
|
3284
2660
|
}
|
|
3285
2661
|
const path = findManifest(flag(argv, "--manifest"));
|
|
3286
2662
|
const force = argv.includes("--force");
|
|
3287
|
-
const present =
|
|
2663
|
+
const present = existsSync5(path);
|
|
3288
2664
|
if (present && force) {
|
|
3289
2665
|
throw new UsageError(
|
|
3290
2666
|
`--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
|
|
@@ -3294,11 +2670,11 @@ async function runInit(argv) {
|
|
|
3294
2670
|
name,
|
|
3295
2671
|
command,
|
|
3296
2672
|
args: argv.slice(separator + 2),
|
|
3297
|
-
...present ? { existing:
|
|
2673
|
+
...present ? { existing: readFileSync(path, "utf8") } : {}
|
|
3298
2674
|
});
|
|
3299
2675
|
parseManifest(draft.yaml, path);
|
|
3300
|
-
|
|
3301
|
-
|
|
2676
|
+
mkdirSync(dirname(resolve(path)), { recursive: true, mode: 448 });
|
|
2677
|
+
writeFileSync(path, draft.yaml);
|
|
3302
2678
|
out("");
|
|
3303
2679
|
out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
|
|
3304
2680
|
out(` ${rule(54)}`);
|
|
@@ -3313,7 +2689,7 @@ async function runInit(argv) {
|
|
|
3313
2689
|
out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
|
|
3314
2690
|
}
|
|
3315
2691
|
out("");
|
|
3316
|
-
out(` ${style.accent(`${proxyCommand()} --manifest ${
|
|
2692
|
+
out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
|
|
3317
2693
|
out("");
|
|
3318
2694
|
return 0;
|
|
3319
2695
|
}
|
|
@@ -3459,8 +2835,36 @@ function runList(journal, asJson, journalPath) {
|
|
|
3459
2835
|
` ${"session".padEnd(width)} ${"started".padEnd(13)} ${"did".padEnd(26)} ${"state".padEnd(26)} agent`
|
|
3460
2836
|
)
|
|
3461
2837
|
);
|
|
2838
|
+
let quiet = [];
|
|
2839
|
+
const flushQuiet = () => {
|
|
2840
|
+
if (quiet.length === 1) {
|
|
2841
|
+
const [only] = quiet;
|
|
2842
|
+
if (only !== void 0) {
|
|
2843
|
+
const did = didWhat(journal, only, 0);
|
|
2844
|
+
out(
|
|
2845
|
+
` ${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 ?? "-")}`
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
} else if (quiet.length > 1) {
|
|
2849
|
+
const newest = quiet[0];
|
|
2850
|
+
const oldest = quiet[quiet.length - 1];
|
|
2851
|
+
if (newest !== void 0 && oldest !== void 0) {
|
|
2852
|
+
out(
|
|
2853
|
+
` ${style.quiet(
|
|
2854
|
+
`${"".padEnd(width)} ${String(quiet.length)} sessions with nothing in them, ${shortTime(oldest.startedAt).trim()} to ${shortTime(newest.startedAt).trim()}`
|
|
2855
|
+
)}`
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
quiet = [];
|
|
2860
|
+
};
|
|
3462
2861
|
for (const run of runs) {
|
|
3463
2862
|
const actions = counted2(run.id);
|
|
2863
|
+
if (actions.actions === 0) {
|
|
2864
|
+
quiet.push(run);
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
flushQuiet();
|
|
3464
2868
|
const { unknown, waiting } = actions;
|
|
3465
2869
|
const notes = [
|
|
3466
2870
|
unknown === 0 ? "" : `${String(unknown)} of unknown outcome`,
|
|
@@ -3472,6 +2876,7 @@ function runList(journal, asJson, journalPath) {
|
|
|
3472
2876
|
` ${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
2877
|
);
|
|
3474
2878
|
}
|
|
2879
|
+
flushQuiet();
|
|
3475
2880
|
out("");
|
|
3476
2881
|
if ((bytesOf(journalPath) ?? 0) > PRUNE_NAG_BYTES) {
|
|
3477
2882
|
out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
|
|
@@ -3486,12 +2891,15 @@ function runList(journal, asJson, journalPath) {
|
|
|
3486
2891
|
async function runShow(argv, journal, asJson) {
|
|
3487
2892
|
const full = argv.includes("--full");
|
|
3488
2893
|
const runs = [...journal.listRuns()].reverse();
|
|
3489
|
-
const
|
|
2894
|
+
const given = positional(argv)[1];
|
|
2895
|
+
const tally2 = journal.tallyRuns();
|
|
2896
|
+
const run = (given === void 0 ? runs.find((one) => (tally2.get(one.id)?.actions ?? 0) > 0) : void 0) ?? pick(runs, given, RUN, true);
|
|
3490
2897
|
const runId = run.id;
|
|
3491
2898
|
const inspection = argv.includes("--live") && journal.getActions(runId).length > 0 ? await withUpstreams(
|
|
3492
2899
|
findManifest(flag(argv, "--manifest")),
|
|
3493
2900
|
async (router) => await inspect({ journal, router, runId }),
|
|
3494
|
-
serversUsedBy(journal, runId)
|
|
2901
|
+
serversUsedBy(journal, runId),
|
|
2902
|
+
{ journal, runId }
|
|
3495
2903
|
) : void 0;
|
|
3496
2904
|
if (asJson) {
|
|
3497
2905
|
out(
|
|
@@ -3514,7 +2922,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3514
2922
|
out(` ${style.quiet("agent ")} ${run.label ?? "-"}`);
|
|
3515
2923
|
out(` ${style.quiet("started")} ${fullTime(run.startedAt)} ${style.quiet(ago(run.startedAt))}`);
|
|
3516
2924
|
out(
|
|
3517
|
-
` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
|
|
2925
|
+
` ${style.quiet("status ")} ${RUN_STATUS[run.status]}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
|
|
3518
2926
|
);
|
|
3519
2927
|
const actions = journal.getActions(runId);
|
|
3520
2928
|
if (actions.length === 0) {
|
|
@@ -3576,7 +2984,7 @@ async function runShow(argv, journal, asJson) {
|
|
|
3576
2984
|
}
|
|
3577
2985
|
out("");
|
|
3578
2986
|
}
|
|
3579
|
-
out(` ${
|
|
2987
|
+
out(` ${summarise(actions)}`);
|
|
3580
2988
|
if (inspection !== void 0) {
|
|
3581
2989
|
out("");
|
|
3582
2990
|
const spoiled = inspection.resources.some((found) => found.condition === "changed");
|
|
@@ -3629,14 +3037,20 @@ var CLASS_MARK = {
|
|
|
3629
3037
|
unclassified: "?"
|
|
3630
3038
|
};
|
|
3631
3039
|
var BADGE_WIDTH = "irreversible".length + 2;
|
|
3040
|
+
var RUN_STATUS = {
|
|
3041
|
+
active: "still running",
|
|
3042
|
+
complete: "finished",
|
|
3043
|
+
rolled_back: "undone",
|
|
3044
|
+
partial: "partly undone"
|
|
3045
|
+
};
|
|
3632
3046
|
function badgeOf(action, pad2) {
|
|
3633
3047
|
const name = `${CLASS_MARK[action.class]} ${action.class}`;
|
|
3634
3048
|
const plain = pad2 ? name.padEnd(BADGE_WIDTH) : name;
|
|
3635
3049
|
return action.class === "irreversible" ? style.accent(plain) : style.quiet(plain);
|
|
3636
3050
|
}
|
|
3637
3051
|
function statusOf(action, pad2) {
|
|
3638
|
-
const label =
|
|
3639
|
-
const text = pad2 ? label.padEnd(
|
|
3052
|
+
const label = plainly(action).text;
|
|
3053
|
+
const text = pad2 ? label.padEnd(22) : label;
|
|
3640
3054
|
if (wasRefused(action)) {
|
|
3641
3055
|
return style.accent(text);
|
|
3642
3056
|
}
|
|
@@ -3675,7 +3089,7 @@ function inverseArgs(inverse) {
|
|
|
3675
3089
|
function truncate3(text, limit) {
|
|
3676
3090
|
return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
|
|
3677
3091
|
}
|
|
3678
|
-
function
|
|
3092
|
+
function summarise(actions) {
|
|
3679
3093
|
const counts = /* @__PURE__ */ new Map();
|
|
3680
3094
|
for (const action of actions) {
|
|
3681
3095
|
const label = labelFor(action);
|
|
@@ -3869,12 +3283,28 @@ function runGates(journal, asJson) {
|
|
|
3869
3283
|
return 0;
|
|
3870
3284
|
}
|
|
3871
3285
|
function runDecision(argv, journal, approving) {
|
|
3286
|
+
const unattended = argv.includes("--unattended");
|
|
3287
|
+
if (approving && !process.stdin.isTTY && !unattended) {
|
|
3288
|
+
throw new UsageError(
|
|
3289
|
+
"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.",
|
|
3290
|
+
false
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3872
3293
|
const waiting = journal.listGated();
|
|
3873
3294
|
const given = positional(argv)[1];
|
|
3874
|
-
const
|
|
3295
|
+
const named = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3296
|
+
const by = approving && unattended ? `${named} (unattended)` : named;
|
|
3875
3297
|
const reason = flag(argv, "--reason") ?? "denied by operator";
|
|
3876
3298
|
if (given !== void 0) {
|
|
3877
3299
|
const settled2 = journal.getAction(given);
|
|
3300
|
+
if (approving && settled2?.status === "denied" && journal.reverseDenial(settled2.id, by)) {
|
|
3301
|
+
out(
|
|
3302
|
+
` ${style.accent("approved")} ${style.strong(`${settled2.server}.${settled2.tool}`)} ${style.quiet(settled2.id)} ${style.quiet("(was denied)")}`
|
|
3303
|
+
);
|
|
3304
|
+
out("");
|
|
3305
|
+
hint({ why: "the agent can make that call again now, and it will go through" });
|
|
3306
|
+
return 0;
|
|
3307
|
+
}
|
|
3878
3308
|
if (settled2 !== void 0 && settled2.status !== "gated") {
|
|
3879
3309
|
process.stderr.write(
|
|
3880
3310
|
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled2)})
|
|
@@ -3891,7 +3321,7 @@ function runDecision(argv, journal, approving) {
|
|
|
3891
3321
|
let failed = 0;
|
|
3892
3322
|
let settled = 0;
|
|
3893
3323
|
for (const action of targets) {
|
|
3894
|
-
const changed = approving ? journal.approve(action.id, by) : journal.
|
|
3324
|
+
const changed = approving ? journal.approve(action.id, by) : journal.denyByPerson(action.id, by, reason);
|
|
3895
3325
|
if (!changed) {
|
|
3896
3326
|
const now = journal.getAction(action.id);
|
|
3897
3327
|
process.stderr.write(
|
|
@@ -3911,26 +3341,249 @@ function runDecision(argv, journal, approving) {
|
|
|
3911
3341
|
hint(
|
|
3912
3342
|
firstOf(
|
|
3913
3343
|
() => heldCalls(journal),
|
|
3914
|
-
() => approving ? { why: "the agent can make that call again now, and it will go through" } :
|
|
3344
|
+
() => approving ? { why: "the agent can make that call again now, and it will go through" } : settled === 1 ? {
|
|
3345
|
+
why: "refused; if the agent asks again it is told you said no. Changed your mind?",
|
|
3346
|
+
run: `approve ${targets[0]?.id.slice(0, 8) ?? ""}`,
|
|
3347
|
+
needs: ["journal"]
|
|
3348
|
+
} : { why: "refused; if the agent asks again it is told you said no" }
|
|
3915
3349
|
)
|
|
3916
3350
|
);
|
|
3917
3351
|
}
|
|
3918
3352
|
return failed === 0 ? 0 : 1;
|
|
3919
3353
|
}
|
|
3354
|
+
var ALLOW_MAX_MINUTES = 24 * 60;
|
|
3355
|
+
function allowFor(given) {
|
|
3356
|
+
const found = /^(\d+)(m|h)$/.exec(given.trim());
|
|
3357
|
+
const minutes = found === null ? NaN : Number(found[1]) * (found[2] === "h" ? 60 : 1);
|
|
3358
|
+
if (!Number.isFinite(minutes) || minutes < 1 || minutes > ALLOW_MAX_MINUTES) {
|
|
3359
|
+
throw new UsageError(
|
|
3360
|
+
`--for takes minutes or hours up to a day, like 30m or 2h, not ${given}. For good, use --always.`
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
return minutes;
|
|
3364
|
+
}
|
|
3365
|
+
async function runAllow(argv, journalPath) {
|
|
3366
|
+
const given = positional(argv)[1];
|
|
3367
|
+
const forFlag = flag(argv, "--for");
|
|
3368
|
+
const always = argv.includes("--always");
|
|
3369
|
+
const stop = argv.includes("--stop");
|
|
3370
|
+
const now = /* @__PURE__ */ new Date();
|
|
3371
|
+
if (given === void 0) {
|
|
3372
|
+
if (forFlag !== void 0 || always || stop) {
|
|
3373
|
+
throw new UsageError("allow needs the tool, as server.tool -- for example crm.send_email");
|
|
3374
|
+
}
|
|
3375
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3376
|
+
try {
|
|
3377
|
+
const current = journal.listAllowances(now.toISOString());
|
|
3378
|
+
out("");
|
|
3379
|
+
if (current.length === 0) {
|
|
3380
|
+
out(` ${style.quiet("Nothing is being let through without asking.")}`);
|
|
3381
|
+
}
|
|
3382
|
+
for (const one of current) {
|
|
3383
|
+
out(
|
|
3384
|
+
` ${style.strong(`${one.server}.${one.tool}`)} ${style.quiet(`until ${shortTime(one.until, now)}, allowed by ${one.by}`)}`
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
out("");
|
|
3388
|
+
return 0;
|
|
3389
|
+
} finally {
|
|
3390
|
+
journal.close();
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
const named = splitQualified(given);
|
|
3394
|
+
if (named === void 0) {
|
|
3395
|
+
throw new UsageError(`${given} is not a tool name; write it as server.tool, for example crm.send_email`);
|
|
3396
|
+
}
|
|
3397
|
+
if ([forFlag !== void 0, always, stop].filter(Boolean).length !== 1) {
|
|
3398
|
+
throw new UsageError(
|
|
3399
|
+
`say how long: allow ${given} --for 1h, allow ${given} --always, or allow ${given} --stop`
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
const who = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3403
|
+
if (stop) {
|
|
3404
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3405
|
+
try {
|
|
3406
|
+
const stopped = journal.stopAllowance(named.server, named.tool, who, now.toISOString());
|
|
3407
|
+
out(
|
|
3408
|
+
stopped ? ` ${style.accent("stopped")} ${style.strong(given)} ${style.quiet("is held again from its next call")}` : ` ${style.quiet(`${given} was not being let through`)}`
|
|
3409
|
+
);
|
|
3410
|
+
if (stopped) {
|
|
3411
|
+
out(` ${style.quiet("A rule written with --always stays in the policy; edit it there.")}`);
|
|
3412
|
+
}
|
|
3413
|
+
return 0;
|
|
3414
|
+
} finally {
|
|
3415
|
+
journal.close();
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const unattended = argv.includes("--unattended");
|
|
3419
|
+
const interactive = process.stdin.isTTY;
|
|
3420
|
+
if (!interactive && !unattended) {
|
|
3421
|
+
throw new UsageError(
|
|
3422
|
+
"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.",
|
|
3423
|
+
false
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
const by = unattended ? `${who} (unattended)` : who;
|
|
3427
|
+
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3428
|
+
const manifest = loadManifest(manifestPath);
|
|
3429
|
+
const spec = manifest.servers[named.server];
|
|
3430
|
+
if (spec === void 0) {
|
|
3431
|
+
const near = didYouMean(named.server, Object.keys(manifest.servers));
|
|
3432
|
+
throw new UsageError(
|
|
3433
|
+
`${manifestPath} has no server called ${named.server}${near === void 0 ? "" : `; did you mean ${near}?`}`
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
const { policy } = createPolicyResolver(manifest).resolve(given);
|
|
3437
|
+
if (forFlag !== void 0) {
|
|
3438
|
+
const minutes = allowFor(forFlag);
|
|
3439
|
+
const until = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
3440
|
+
const journal = openJournalOrExplain(journalPath);
|
|
3441
|
+
try {
|
|
3442
|
+
journal.allow(named.server, named.tool, by, until);
|
|
3443
|
+
} finally {
|
|
3444
|
+
journal.close();
|
|
3445
|
+
}
|
|
3446
|
+
out("");
|
|
3447
|
+
out(
|
|
3448
|
+
` ${style.accent("allowed")} ${style.strong(given)} ${style.quiet(`until ${shortTime(until, now)} -- its calls go out without asking, and are still recorded`)}`
|
|
3449
|
+
);
|
|
3450
|
+
if (policy.gate !== "always" && policy.gate !== "on_write") {
|
|
3451
|
+
out(` ${style.quiet("(the policy was not holding it anyway)")}`);
|
|
3452
|
+
} else if (policy.class === "irreversible") {
|
|
3453
|
+
out(` ${style.quiet("These cannot be undone. Nothing will ask before each one goes out.")}`);
|
|
3454
|
+
}
|
|
3455
|
+
out(` ${style.quiet(`Takes effect on its next call. To end it sooner: synartesis allow ${given} --stop`)}`);
|
|
3456
|
+
out("");
|
|
3457
|
+
return 0;
|
|
3458
|
+
}
|
|
3459
|
+
const text = readFileSync(manifestPath, "utf8");
|
|
3460
|
+
const pins = manifest.pins?.[named.server];
|
|
3461
|
+
let pin;
|
|
3462
|
+
if (pins !== void 0 && pins[named.tool] === void 0) {
|
|
3463
|
+
const upstream = await startAsTheClientWould(manifestPath, named.server, spec);
|
|
3464
|
+
try {
|
|
3465
|
+
const shape = (await toolShapes(upstream)).find((tool) => tool.name === named.tool);
|
|
3466
|
+
if (shape === void 0) {
|
|
3467
|
+
throw new UsageError(`${named.server} has no tool called ${named.tool}`);
|
|
3468
|
+
}
|
|
3469
|
+
pin = fingerprint(shape.inputSchema);
|
|
3470
|
+
} finally {
|
|
3471
|
+
await upstream.close();
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
let edit;
|
|
3475
|
+
try {
|
|
3476
|
+
edit = allowAlways({
|
|
3477
|
+
text,
|
|
3478
|
+
file: manifestPath,
|
|
3479
|
+
server: named.server,
|
|
3480
|
+
tool: named.tool,
|
|
3481
|
+
by,
|
|
3482
|
+
date: now.toISOString().slice(0, 10),
|
|
3483
|
+
...pin === void 0 ? {} : { pin }
|
|
3484
|
+
});
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
if (error instanceof PolicyEditError) {
|
|
3487
|
+
process.stderr.write(`synartesis: ${error.message}
|
|
3488
|
+
`);
|
|
3489
|
+
return 1;
|
|
3490
|
+
}
|
|
3491
|
+
throw error;
|
|
3492
|
+
}
|
|
3493
|
+
if (edit.how === "already") {
|
|
3494
|
+
out(` ${style.quiet(`${given} is already let through by ${manifestPath}`)}`);
|
|
3495
|
+
return 0;
|
|
3496
|
+
}
|
|
3497
|
+
if (edit.policy.class === "irreversible") {
|
|
3498
|
+
const confirmed = unattended ? flag(argv, "--confirm") : await ask(` ${given} cannot be undone. Type its name to stop holding it for good: `);
|
|
3499
|
+
if (confirmed?.trim() !== given) {
|
|
3500
|
+
process.stderr.write(`synartesis: not confirmed, so ${manifestPath} was not changed
|
|
3501
|
+
`);
|
|
3502
|
+
return 1;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (readFileSync(manifestPath, "utf8") !== text) {
|
|
3506
|
+
process.stderr.write(`synartesis: ${manifestPath} changed while this was running; nothing was written
|
|
3507
|
+
`);
|
|
3508
|
+
return 1;
|
|
3509
|
+
}
|
|
3510
|
+
const beside = `${manifestPath}.${String(process.pid)}.tmp`;
|
|
3511
|
+
writeFileSync(beside, edit.text, { mode: statSync(manifestPath).mode });
|
|
3512
|
+
renameSync(beside, manifestPath);
|
|
3513
|
+
out("");
|
|
3514
|
+
out(
|
|
3515
|
+
` ${style.accent("allowed")} ${style.strong(given)} ${style.quiet(`for good -- ${edit.how === "added" ? "a rule was added to" : "its rule was changed in"} ${manifestPath}`)}`
|
|
3516
|
+
);
|
|
3517
|
+
if (edit.policy.class === "irreversible") {
|
|
3518
|
+
out(` ${style.quiet("It still cannot be undone: each call is recorded, and none is held.")}`);
|
|
3519
|
+
}
|
|
3520
|
+
if (pin !== void 0) {
|
|
3521
|
+
out(` ${style.quiet("Pinned at the shape it has now, as the rest of the server is.")}`);
|
|
3522
|
+
}
|
|
3523
|
+
out(` ${style.quiet("Takes effect when your client next starts the server. Until then:")}`);
|
|
3524
|
+
out(` ${style.quiet(`synartesis allow ${given} --for 1h`)}`);
|
|
3525
|
+
out("");
|
|
3526
|
+
return 0;
|
|
3527
|
+
}
|
|
3528
|
+
async function ask(question) {
|
|
3529
|
+
const reader = createInterface({ input: process.stdin, output: process.stdout });
|
|
3530
|
+
try {
|
|
3531
|
+
return await reader.question(question);
|
|
3532
|
+
} finally {
|
|
3533
|
+
reader.close();
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
function stepWords(kind, dryRun) {
|
|
3537
|
+
switch (kind) {
|
|
3538
|
+
case "revert":
|
|
3539
|
+
return dryRun ? "would put back" : "put back";
|
|
3540
|
+
case "skip":
|
|
3541
|
+
return "nothing to undo";
|
|
3542
|
+
case "already-reverted":
|
|
3543
|
+
return "already undone";
|
|
3544
|
+
case "permanent":
|
|
3545
|
+
return "cannot undo";
|
|
3546
|
+
case "kept":
|
|
3547
|
+
return "left alone";
|
|
3548
|
+
case "halt":
|
|
3549
|
+
return "stopped";
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
function resultWords(status, dryRun) {
|
|
3553
|
+
switch (status) {
|
|
3554
|
+
case "rolled_back":
|
|
3555
|
+
return dryRun ? "would all be undone (nothing was written)" : "all undone";
|
|
3556
|
+
case "partial":
|
|
3557
|
+
return dryRun ? "would be partly undone (nothing was written)" : "partly undone";
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3920
3560
|
function report(result, alreadyForcing = false, as = "") {
|
|
3921
3561
|
out("");
|
|
3922
3562
|
out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
|
|
3923
3563
|
out(` ${rule(72)}`);
|
|
3924
3564
|
out("");
|
|
3925
3565
|
let separated = false;
|
|
3566
|
+
let reads = 0;
|
|
3567
|
+
const flushReads = () => {
|
|
3568
|
+
if (reads > 0) {
|
|
3569
|
+
out(` ${style.quiet(`${String(reads)} read${reads === 1 ? "" : "s"}, nothing to undo`)}`);
|
|
3570
|
+
reads = 0;
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3926
3573
|
for (const step of result.steps) {
|
|
3574
|
+
if (step.kind === "skip" && step.reason === "readonly") {
|
|
3575
|
+
reads += 1;
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
flushReads();
|
|
3927
3579
|
if (step.kind === "kept" && !separated) {
|
|
3928
3580
|
separated = true;
|
|
3929
3581
|
out("");
|
|
3930
3582
|
out(` ${style.quiet("left alone")}`);
|
|
3931
3583
|
}
|
|
3932
3584
|
const unverified = step.kind === "revert" && !step.verified ? ` ${style.accent("[unverified]")}` : "";
|
|
3933
|
-
const
|
|
3585
|
+
const said = stepWords(step.kind, result.dryRun);
|
|
3586
|
+
const kind = step.kind === "halt" || step.kind === "permanent" ? style.accent(said.padEnd(16)) : said.padEnd(16);
|
|
3934
3587
|
out(
|
|
3935
3588
|
` ${style.quiet(String(step.seq).padStart(3))} ${kind} ${style.strong(`${step.server}.${step.tool}`)} ${style.quiet(step.reason)}${unverified}`
|
|
3936
3589
|
);
|
|
@@ -3950,6 +3603,7 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
3950
3603
|
);
|
|
3951
3604
|
}
|
|
3952
3605
|
}
|
|
3606
|
+
flushReads();
|
|
3953
3607
|
if (result.halted !== void 0) {
|
|
3954
3608
|
const halt = result.halted;
|
|
3955
3609
|
out("");
|
|
@@ -3993,8 +3647,9 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
3993
3647
|
);
|
|
3994
3648
|
}
|
|
3995
3649
|
out("");
|
|
3650
|
+
const outcome = resultWords(result.status, result.dryRun);
|
|
3996
3651
|
out(
|
|
3997
|
-
` ${style.label("result")} ${result.status === "rolled_back" ?
|
|
3652
|
+
` ${style.label("result")} ${result.status === "rolled_back" ? outcome : style.accent(outcome)}`
|
|
3998
3653
|
);
|
|
3999
3654
|
out("");
|
|
4000
3655
|
if (result.dryRun && result.status === "rolled_back") {
|
|
@@ -4006,7 +3661,7 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
4006
3661
|
}
|
|
4007
3662
|
return result.halted === void 0 && permanent.length === 0 ? 0 : 1;
|
|
4008
3663
|
}
|
|
4009
|
-
async function withUpstreams(manifestPath, use, only) {
|
|
3664
|
+
async function withUpstreams(manifestPath, use, only, session) {
|
|
4010
3665
|
const manifest = loadManifest(manifestPath);
|
|
4011
3666
|
const upstreams = [];
|
|
4012
3667
|
const missing = [];
|
|
@@ -4016,15 +3671,7 @@ async function withUpstreams(manifestPath, use, only) {
|
|
|
4016
3671
|
continue;
|
|
4017
3672
|
}
|
|
4018
3673
|
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
|
-
);
|
|
3674
|
+
upstreams.push(await startAsTheClientWould(manifestPath, name, spec, session));
|
|
4028
3675
|
} catch (error) {
|
|
4029
3676
|
missing.push(`${name}: ${describe(error)}`);
|
|
4030
3677
|
}
|
|
@@ -4077,7 +3724,8 @@ async function performUndo(manifestPath, journal, runId, options) {
|
|
|
4077
3724
|
}),
|
|
4078
3725
|
// A replan re-resolves inverses from the current policy, which may name a
|
|
4079
3726
|
// server this run never used; everything else needs only what it touched.
|
|
4080
|
-
options.replan === true ? void 0 : serversUsedBy(journal, runId)
|
|
3727
|
+
options.replan === true ? void 0 : serversUsedBy(journal, runId),
|
|
3728
|
+
{ journal, runId }
|
|
4081
3729
|
);
|
|
4082
3730
|
}
|
|
4083
3731
|
async function runUndo(argv, journal) {
|
|
@@ -4087,9 +3735,16 @@ async function runUndo(argv, journal) {
|
|
|
4087
3735
|
throw new UsageError("--to needs a positive whole number", false);
|
|
4088
3736
|
}
|
|
4089
3737
|
const given = positional(argv)[1];
|
|
4090
|
-
const
|
|
3738
|
+
const runs = [...journal.listRuns()].reverse();
|
|
3739
|
+
const standing3 = journal.standingPerRun();
|
|
3740
|
+
const hasWork = (id) => {
|
|
3741
|
+
const here = standing3.get(id);
|
|
3742
|
+
return here !== void 0 && (here.undoable > 0 || here.conflicted > 0);
|
|
3743
|
+
};
|
|
3744
|
+
const newestWithWork = given === void 0 ? runs.find((run) => hasWork(run.id)) : void 0;
|
|
3745
|
+
const chosen = newestWithWork ?? pick(runs, given, RUN, true);
|
|
4091
3746
|
const runId = chosen.id;
|
|
4092
|
-
if (journal.getRun(runId)?.status === "active" && !argv.includes("--yes") && !argv.includes("--dry-run")) {
|
|
3747
|
+
if (journal.getRun(runId)?.status === "active" && hasWork(runId) && !argv.includes("--yes") && !argv.includes("--dry-run")) {
|
|
4093
3748
|
throw new UsageError(
|
|
4094
3749
|
`${runId.slice(0, 8)} has not ended, so an agent may still be writing to it.
|
|
4095
3750
|
See what an undo would do: ${cliCommand()} undo ${runId.slice(0, 8)} --dry-run
|
|
@@ -4105,7 +3760,9 @@ async function runUndo(argv, journal) {
|
|
|
4105
3760
|
).length;
|
|
4106
3761
|
out("");
|
|
4107
3762
|
out(
|
|
4108
|
-
` ${style.quiet(
|
|
3763
|
+
` ${style.quiet(
|
|
3764
|
+
newestWithWork === void 0 ? "no session named, so the most recent:" : "no session named, so the most recent with something to undo:"
|
|
3765
|
+
)} ${style.strong(runId.slice(0, 8))} ` + style.quiet(`${chosen.label ?? "an agent"}, ${shortTime(chosen.startedAt).trim()}`)
|
|
4109
3766
|
);
|
|
4110
3767
|
if (left === 0) {
|
|
4111
3768
|
out("");
|
|
@@ -4155,7 +3812,8 @@ async function runUndo(argv, journal) {
|
|
|
4155
3812
|
const over = (await withUpstreams(
|
|
4156
3813
|
manifestPath,
|
|
4157
3814
|
async (router) => await inspect({ journal, router, runId }),
|
|
4158
|
-
serversUsedBy(journal, runId)
|
|
3815
|
+
serversUsedBy(journal, runId),
|
|
3816
|
+
{ journal, runId }
|
|
4159
3817
|
)).resources.filter(
|
|
4160
3818
|
// Below --to nothing is undone, so a change down there is not something
|
|
4161
3819
|
// this command would write over and must not stand in its way.
|
|
@@ -4217,7 +3875,9 @@ var KNOWN_COMMANDS = [
|
|
|
4217
3875
|
"watch",
|
|
4218
3876
|
"approve",
|
|
4219
3877
|
"deny",
|
|
3878
|
+
"allow",
|
|
4220
3879
|
"resolve",
|
|
3880
|
+
"notify",
|
|
4221
3881
|
"undo",
|
|
4222
3882
|
"help",
|
|
4223
3883
|
"version"
|
|
@@ -4247,6 +3907,18 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
4247
3907
|
"--force",
|
|
4248
3908
|
"--yes",
|
|
4249
3909
|
"--older-than",
|
|
3910
|
+
// install: cover hosted servers too, through mcp-remote.
|
|
3911
|
+
"--remote",
|
|
3912
|
+
// notify: send one to see whether they reach you.
|
|
3913
|
+
"--test",
|
|
3914
|
+
// approve without a terminal, from a script; recorded as unattended.
|
|
3915
|
+
"--unattended",
|
|
3916
|
+
// allow: for a while, for good, or no longer; and the typed confirmation
|
|
3917
|
+
// for a tool that cannot be undone, given without a terminal.
|
|
3918
|
+
"--for",
|
|
3919
|
+
"--always",
|
|
3920
|
+
"--stop",
|
|
3921
|
+
"--confirm",
|
|
4250
3922
|
"--help",
|
|
4251
3923
|
"-h",
|
|
4252
3924
|
"--version",
|
|
@@ -4255,8 +3927,8 @@ var FLAGS = /* @__PURE__ */ new Set([
|
|
|
4255
3927
|
]);
|
|
4256
3928
|
function version() {
|
|
4257
3929
|
try {
|
|
4258
|
-
const root =
|
|
4259
|
-
const parsed = JSON.parse(
|
|
3930
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
3931
|
+
const parsed = JSON.parse(readFileSync(join2(root, "..", "package.json"), "utf8"));
|
|
4260
3932
|
const found = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
|
|
4261
3933
|
return typeof found === "string" ? found : "unknown";
|
|
4262
3934
|
} catch {
|
|
@@ -4287,7 +3959,7 @@ function rejectUnknownFlags(argv) {
|
|
|
4287
3959
|
}
|
|
4288
3960
|
}
|
|
4289
3961
|
function openJournalOrExplain(journalPath) {
|
|
4290
|
-
if (!
|
|
3962
|
+
if (!existsSync5(journalPath)) {
|
|
4291
3963
|
throw new UsageError(
|
|
4292
3964
|
`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
3965
|
);
|
|
@@ -4317,8 +3989,8 @@ ${COMMANDS}`);
|
|
|
4317
3989
|
}
|
|
4318
3990
|
const givenJournal = flag(argv, "--journal");
|
|
4319
3991
|
const givenManifest = flag(argv, "--manifest");
|
|
4320
|
-
journalArg = givenJournal === void 0 ? "" : ` --journal ${
|
|
4321
|
-
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${
|
|
3992
|
+
journalArg = givenJournal === void 0 ? "" : ` --journal ${resolve(givenJournal)}`;
|
|
3993
|
+
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${resolve(givenManifest)}`;
|
|
4322
3994
|
if (command === void 0) {
|
|
4323
3995
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
4324
3996
|
const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
|
|
@@ -4335,7 +4007,8 @@ ${COMMANDS}`);
|
|
|
4335
4007
|
return await withUpstreams(
|
|
4336
4008
|
manifestPath,
|
|
4337
4009
|
async (router) => await inspect({ journal: journal2, router, runId }),
|
|
4338
|
-
serversUsedBy(journal2, runId)
|
|
4010
|
+
serversUsedBy(journal2, runId),
|
|
4011
|
+
{ journal: journal2, runId }
|
|
4339
4012
|
);
|
|
4340
4013
|
} finally {
|
|
4341
4014
|
journal2.close();
|
|
@@ -4387,7 +4060,7 @@ ${COMMANDS}`);
|
|
|
4387
4060
|
decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
|
|
4388
4061
|
});
|
|
4389
4062
|
}
|
|
4390
|
-
if (!
|
|
4063
|
+
if (!existsSync5(journalPath) && (command === "list" || command === "show" || command === "gates")) {
|
|
4391
4064
|
if (asJson) {
|
|
4392
4065
|
out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
|
|
4393
4066
|
return 0;
|
|
@@ -4401,6 +4074,12 @@ ${COMMANDS}`);
|
|
|
4401
4074
|
out("");
|
|
4402
4075
|
return 0;
|
|
4403
4076
|
}
|
|
4077
|
+
if (command === "notify") {
|
|
4078
|
+
return runNotifyTest(argv);
|
|
4079
|
+
}
|
|
4080
|
+
if (command === "allow") {
|
|
4081
|
+
return await runAllow(argv, journalPath);
|
|
4082
|
+
}
|
|
4404
4083
|
if (command === "desktop") {
|
|
4405
4084
|
return runDesktop();
|
|
4406
4085
|
}
|
|
@@ -4432,6 +4111,32 @@ ${COMMANDS}`);
|
|
|
4432
4111
|
journal.close();
|
|
4433
4112
|
}
|
|
4434
4113
|
}
|
|
4114
|
+
function runNotifyTest(argv) {
|
|
4115
|
+
if (!argv.includes("--test")) {
|
|
4116
|
+
throw new UsageError("notify takes --test, which sends one to see whether they reach you");
|
|
4117
|
+
}
|
|
4118
|
+
const why = canNotify();
|
|
4119
|
+
if (why !== void 0) {
|
|
4120
|
+
out(` ${style.quiet(`No notification sent: ${why}.`)}`);
|
|
4121
|
+
return 1;
|
|
4122
|
+
}
|
|
4123
|
+
desktopNotifier()({
|
|
4124
|
+
server: "synartesis",
|
|
4125
|
+
tool: "test",
|
|
4126
|
+
actionId: "00000000",
|
|
4127
|
+
approve: "this is only a test"
|
|
4128
|
+
});
|
|
4129
|
+
out("");
|
|
4130
|
+
out(` ${style.quiet("Sent one. If nothing appeared, notifications for it are switched off:")}`);
|
|
4131
|
+
out(
|
|
4132
|
+
` ${style.quiet(
|
|
4133
|
+
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."
|
|
4134
|
+
)}`
|
|
4135
|
+
);
|
|
4136
|
+
out(` ${style.quiet("Either way, synartesis watch shows every held call as it happens.")}`);
|
|
4137
|
+
out("");
|
|
4138
|
+
return 0;
|
|
4139
|
+
}
|
|
4435
4140
|
function runDesktop() {
|
|
4436
4141
|
const found = findDesktop();
|
|
4437
4142
|
if (found === void 0) {
|