synartesis 0.6.12 → 0.6.15
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 +308 -0
- package/README.md +103 -2
- package/dist/{chunk-FUVEHRJP.js → chunk-OODDXM36.js} +376 -66
- package/dist/cli.js +511 -88
- package/dist/proxy.js +39 -9
- package/manifests/filesystem.yaml +8 -0
- package/manifests/git.yaml +8 -0
- package/manifests/github.yaml +10 -0
- package/manifests/memory.yaml +18 -0
- package/package.json +3 -2
|
@@ -6,6 +6,19 @@ import {
|
|
|
6
6
|
describe
|
|
7
7
|
} from "./chunk-YVOO3PTV.js";
|
|
8
8
|
|
|
9
|
+
// src/proxy/flags.ts
|
|
10
|
+
var PROXY_FLAGS = [
|
|
11
|
+
"--manifest",
|
|
12
|
+
"--journal",
|
|
13
|
+
"--server",
|
|
14
|
+
"--gate-timeout",
|
|
15
|
+
"--log-level",
|
|
16
|
+
"--http",
|
|
17
|
+
"--http-host",
|
|
18
|
+
"--http-idle",
|
|
19
|
+
"--token"
|
|
20
|
+
];
|
|
21
|
+
|
|
9
22
|
// src/invocation.ts
|
|
10
23
|
import { spawnSync } from "child_process";
|
|
11
24
|
import { accessSync, constants } from "fs";
|
|
@@ -131,23 +144,25 @@ var INK = `${ESC}38;2;214;201;197m`;
|
|
|
131
144
|
var DIM = `${ESC}2m`;
|
|
132
145
|
var BOLD = `${ESC}1m`;
|
|
133
146
|
var RESET = `${ESC}0m`;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return enabled ? `${codes}${text}${RESET}` : text;
|
|
147
|
+
function usable(stream) {
|
|
148
|
+
return process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && stream.isTTY;
|
|
137
149
|
}
|
|
138
150
|
function spaced(text) {
|
|
139
151
|
return Array.from(text).join(" ");
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
};
|
|
153
|
+
function palette(on) {
|
|
154
|
+
const paint = (codes, text) => on ? `${codes}${text}${RESET}` : text;
|
|
155
|
+
return {
|
|
156
|
+
label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
|
|
157
|
+
heading: (text) => paint(INK, text.toUpperCase()),
|
|
158
|
+
accent: (text) => paint(ACCENT, text),
|
|
159
|
+
strong: (text) => paint(INK, text),
|
|
160
|
+
quiet: (text) => paint(DIM, text),
|
|
161
|
+
plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
var style = palette(usable(process.stdout));
|
|
165
|
+
var errorStyle = palette(usable(process.stderr));
|
|
151
166
|
var WORDMARK = spaced("SYNARTESIS");
|
|
152
167
|
function meander(width) {
|
|
153
168
|
const unit = "\u2517\u2501\u2513\u250F\u2501\u251B";
|
|
@@ -257,6 +272,47 @@ CREATE INDEX IF NOT EXISTS actions_by_run ON actions(run_id, seq);
|
|
|
257
272
|
-- two-kilobyte snapshots: 61ms to 0.01ms, and 56ms to 0.00ms.
|
|
258
273
|
CREATE INDEX IF NOT EXISTS actions_approved ON actions(server, tool, status, approved_at);
|
|
259
274
|
CREATE INDEX IF NOT EXISTS actions_gated ON actions(status, ts);
|
|
275
|
+
|
|
276
|
+
-- Covering, and that is the whole point. listRuns needs a count and three
|
|
277
|
+
-- status tallies per run, and without this the group-by scans the table --
|
|
278
|
+
-- which carries the snapshots, so the cost of listing sessions grew with the
|
|
279
|
+
-- size of the data those sessions touched, not with how many there were.
|
|
280
|
+
-- Adding status to the run index lets sqlite answer entirely from the index.
|
|
281
|
+
-- Measured on forty runs of five hundred actions with two-kilobyte snapshots,
|
|
282
|
+
-- a hundred-megabyte journal: 76ms to 2ms.
|
|
283
|
+
--
|
|
284
|
+
-- Added the same way as the two above and for the same reason: no row changes,
|
|
285
|
+
-- no meaning changes, IF NOT EXISTS makes it idempotent, and an older build
|
|
286
|
+
-- opening the same file afterwards neither notices nor cares.
|
|
287
|
+
CREATE INDEX IF NOT EXISTS actions_run_status ON actions(run_id, status);
|
|
288
|
+
|
|
289
|
+
-- Partial, which is what makes it small: newestUndoable asks for the newest run
|
|
290
|
+
-- holding something an undo would reverse, and that is a handful of rows out of
|
|
291
|
+
-- a table where every other row is a read, a refusal or something already put
|
|
292
|
+
-- back. Without it the question can only be answered by reaching into the table
|
|
293
|
+
-- to test inverse_json on every applied row -- and those rows carry the
|
|
294
|
+
-- snapshots, so the cost of the hint at the foot of a session list grew with
|
|
295
|
+
-- the size of the data rather than with the number of sessions, which is
|
|
296
|
+
-- exactly what the index above was added to stop. Measured on sixty runs of a
|
|
297
|
+
-- thousand actions with two-kilobyte snapshots, a 246MB journal: 56ms to
|
|
298
|
+
-- 0.01ms.
|
|
299
|
+
--
|
|
300
|
+
-- newestUndoable names this index with INDEXED BY, and has to: left to choose,
|
|
301
|
+
-- sqlite takes actions_gated for the status seek and then reads the rows. See
|
|
302
|
+
-- the query for why that is preferred to running ANALYZE.
|
|
303
|
+
--
|
|
304
|
+
-- Added the same way as the three above and for the same reason: no row
|
|
305
|
+
-- changes, no meaning changes, IF NOT EXISTS makes it idempotent, and an older
|
|
306
|
+
-- build opening the same file afterwards neither notices nor cares.
|
|
307
|
+
CREATE INDEX IF NOT EXISTS actions_undoable ON actions(run_id)
|
|
308
|
+
WHERE status = 'applied' AND inverse_json IS NOT NULL;
|
|
309
|
+
|
|
310
|
+
-- Partial for the same reason: findPending runs on the way in to every write,
|
|
311
|
+
-- looking for an earlier attempt at that exact call whose outcome was never
|
|
312
|
+
-- established. Those are rare, so the index holds almost nothing -- while the
|
|
313
|
+
-- rows it saves reading are the ones carrying the snapshots.
|
|
314
|
+
CREATE INDEX IF NOT EXISTS actions_unresolved ON actions(run_id, server, tool)
|
|
315
|
+
WHERE status = 'pending';
|
|
260
316
|
`;
|
|
261
317
|
|
|
262
318
|
// src/journal/journal.ts
|
|
@@ -268,6 +324,13 @@ var runSchema = z.object({
|
|
|
268
324
|
ended_at: z.string().nullable(),
|
|
269
325
|
status: z.enum(["active", "complete", "rolled_back", "partial"])
|
|
270
326
|
});
|
|
327
|
+
var tallySchema = z.object({
|
|
328
|
+
run_id: z.string(),
|
|
329
|
+
actions: z.number(),
|
|
330
|
+
unknown: z.number().nullable(),
|
|
331
|
+
waiting: z.number().nullable(),
|
|
332
|
+
applied: z.number().nullable()
|
|
333
|
+
});
|
|
271
334
|
var actionSchema = z.object({
|
|
272
335
|
id: z.string(),
|
|
273
336
|
run_id: z.string(),
|
|
@@ -661,6 +724,17 @@ var SqliteJournal = class {
|
|
|
661
724
|
return rows.find((row) => canonical(row.args) === wanted);
|
|
662
725
|
});
|
|
663
726
|
}
|
|
727
|
+
findPending(query) {
|
|
728
|
+
return this.#run("findPending", () => {
|
|
729
|
+
const rows = this.#db.prepare(
|
|
730
|
+
`SELECT * FROM actions INDEXED BY actions_unresolved
|
|
731
|
+
WHERE run_id = ? AND server = ? AND tool = ? AND status = 'pending'
|
|
732
|
+
ORDER BY seq`
|
|
733
|
+
).all(query.runId, query.server, query.tool).map(toAction);
|
|
734
|
+
const wanted = canonical(query.args ?? {});
|
|
735
|
+
return rows.find((row) => canonical(row.args) === wanted);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
664
738
|
getAction(actionId) {
|
|
665
739
|
return this.#run("getAction", () => {
|
|
666
740
|
const raw = this.#db.prepare("SELECT * FROM actions WHERE id = ?").get(actionId);
|
|
@@ -699,6 +773,57 @@ var SqliteJournal = class {
|
|
|
699
773
|
() => this.#db.prepare("SELECT * FROM actions WHERE run_id = ? ORDER BY seq").all(runId).map(toAction)
|
|
700
774
|
);
|
|
701
775
|
}
|
|
776
|
+
tallyRuns() {
|
|
777
|
+
return this.#run("tallyRuns", () => {
|
|
778
|
+
const rows = this.#db.prepare(
|
|
779
|
+
`SELECT run_id,
|
|
780
|
+
COUNT(*) AS actions,
|
|
781
|
+
SUM(status = 'pending') AS unknown,
|
|
782
|
+
SUM(status = 'gated') AS waiting,
|
|
783
|
+
SUM(status = 'applied') AS applied
|
|
784
|
+
FROM actions
|
|
785
|
+
GROUP BY run_id`
|
|
786
|
+
).all();
|
|
787
|
+
const tally = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const row of rows) {
|
|
789
|
+
const counts = tallySchema.parse(row);
|
|
790
|
+
tally.set(counts.run_id, {
|
|
791
|
+
actions: counts.actions,
|
|
792
|
+
unknown: counts.unknown ?? 0,
|
|
793
|
+
waiting: counts.waiting ?? 0,
|
|
794
|
+
applied: counts.applied ?? 0
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return tally;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The newest run that still holds something an undo would put back.
|
|
802
|
+
*
|
|
803
|
+
* Not simply the newest run, and not the newest run with an applied action
|
|
804
|
+
* either. A client that connects and reads opens a session like any other,
|
|
805
|
+
* and reads are recorded and left `applied` -- rollback steps past them --
|
|
806
|
+
* so `applied` alone regularly points at a session in which nothing
|
|
807
|
+
* happened. What makes a run worth offering is an action that is still
|
|
808
|
+
* applied and carries the inverse that would reverse it, which is the same
|
|
809
|
+
* test `undo` itself uses when it goes looking for the session somebody
|
|
810
|
+
* meant. That inverse is also what excludes the reads: the manifest loader
|
|
811
|
+
* refuses an inverse on a readonly tool, so a read can never carry one, and
|
|
812
|
+
* a second condition on the class would be a condition that cannot fire.
|
|
813
|
+
*/
|
|
814
|
+
newestUndoable() {
|
|
815
|
+
return this.#run("newestUndoable", () => {
|
|
816
|
+
const raw = this.#db.prepare(
|
|
817
|
+
`SELECT * FROM runs
|
|
818
|
+
WHERE id IN (SELECT run_id FROM actions INDEXED BY actions_undoable
|
|
819
|
+
WHERE status = 'applied'
|
|
820
|
+
AND inverse_json IS NOT NULL)
|
|
821
|
+
ORDER BY started_at DESC, rowid DESC
|
|
822
|
+
LIMIT 1`
|
|
823
|
+
).get();
|
|
824
|
+
return raw === void 0 ? void 0 : toRun(raw);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
702
827
|
recentActions(limit) {
|
|
703
828
|
return this.#run(
|
|
704
829
|
"recentActions",
|
|
@@ -973,17 +1098,20 @@ var toolPolicy = z2.strictObject({
|
|
|
973
1098
|
gate: z2.enum(["always", "on_write", "never"]).optional(),
|
|
974
1099
|
refusal: z2.enum(["uncertain", "clean"]).optional(),
|
|
975
1100
|
snapshot: callTemplate.optional(),
|
|
976
|
-
inverse: callTemplate.optional()
|
|
1101
|
+
inverse: callTemplate.optional(),
|
|
1102
|
+
verify: callTemplate.optional()
|
|
977
1103
|
});
|
|
978
1104
|
var serverSpec = z2.strictObject({
|
|
979
1105
|
command: z2.string().min(1),
|
|
980
1106
|
args: z2.array(z2.string()).default([]),
|
|
981
|
-
env: z2.record(z2.string(), z2.string()).optional()
|
|
1107
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
1108
|
+
provenance: z2.enum(["live", "documented"]).optional()
|
|
982
1109
|
});
|
|
983
1110
|
var manifestSchema = z2.strictObject({
|
|
984
1111
|
version: z2.literal(1),
|
|
985
1112
|
servers: z2.record(z2.string(), serverSpec),
|
|
986
|
-
tools: z2.array(toolPolicy).default([])
|
|
1113
|
+
tools: z2.array(toolPolicy).default([]),
|
|
1114
|
+
pins: z2.record(z2.string(), z2.record(z2.string(), z2.string().min(1))).optional()
|
|
987
1115
|
});
|
|
988
1116
|
var Source = class {
|
|
989
1117
|
constructor(doc, lines, file) {
|
|
@@ -1066,6 +1194,11 @@ function validate(source, manifest) {
|
|
|
1066
1194
|
if (servers.length === 0) {
|
|
1067
1195
|
source.fail(["servers"], "at least one server must be declared");
|
|
1068
1196
|
}
|
|
1197
|
+
for (const name of Object.keys(manifest.pins ?? {})) {
|
|
1198
|
+
if (!servers.includes(name)) {
|
|
1199
|
+
source.fail(["pins", name], `pins name server ${name}, which is not declared`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1069
1202
|
const seen = /* @__PURE__ */ new Map();
|
|
1070
1203
|
manifest.tools.forEach((policy, index) => {
|
|
1071
1204
|
const path = ["tools", index];
|
|
@@ -1087,6 +1220,9 @@ function validate(source, manifest) {
|
|
|
1087
1220
|
`${policy.match} names server ${segment}, which is not declared`
|
|
1088
1221
|
);
|
|
1089
1222
|
}
|
|
1223
|
+
if (policy.verify !== void 0 && policy.class === "readonly") {
|
|
1224
|
+
source.fail([...path, "verify"], "a readonly tool has no post-state to check for drift");
|
|
1225
|
+
}
|
|
1090
1226
|
const needsInverse = policy.class === "reversible" || policy.class === "compensable";
|
|
1091
1227
|
if (needsInverse && policy.inverse === void 0) {
|
|
1092
1228
|
source.fail(path, `a ${policy.class} tool must declare an inverse`);
|
|
@@ -1130,7 +1266,8 @@ function withGate(policy) {
|
|
|
1130
1266
|
gate,
|
|
1131
1267
|
refusal: policy.refusal ?? "uncertain",
|
|
1132
1268
|
...policy.snapshot === void 0 ? {} : { snapshot: toCall(policy.snapshot) },
|
|
1133
|
-
...policy.inverse === void 0 ? {} : { inverse: toCall(policy.inverse) }
|
|
1269
|
+
...policy.inverse === void 0 ? {} : { inverse: toCall(policy.inverse) },
|
|
1270
|
+
...policy.verify === void 0 ? {} : { verify: toCall(policy.verify) }
|
|
1134
1271
|
};
|
|
1135
1272
|
}
|
|
1136
1273
|
function parseManifest(text, file) {
|
|
@@ -1166,11 +1303,13 @@ function parseManifest(text, file) {
|
|
|
1166
1303
|
{
|
|
1167
1304
|
command: spec.command,
|
|
1168
1305
|
args: spec.args,
|
|
1169
|
-
...spec.env === void 0 ? {} : { env: expandEnvironment(source, ["servers", name], spec.env) }
|
|
1306
|
+
...spec.env === void 0 ? {} : { env: expandEnvironment(source, ["servers", name], spec.env) },
|
|
1307
|
+
...spec.provenance === void 0 ? {} : { provenance: spec.provenance }
|
|
1170
1308
|
}
|
|
1171
1309
|
])
|
|
1172
1310
|
),
|
|
1173
|
-
tools: parsed.data.tools.map(withGate)
|
|
1311
|
+
tools: parsed.data.tools.map(withGate),
|
|
1312
|
+
...parsed.data.pins === void 0 ? {} : { pins: parsed.data.pins }
|
|
1174
1313
|
};
|
|
1175
1314
|
validate(source, manifest);
|
|
1176
1315
|
return manifest;
|
|
@@ -1193,8 +1332,41 @@ function loadManifest(path) {
|
|
|
1193
1332
|
return parseManifest(text, path);
|
|
1194
1333
|
}
|
|
1195
1334
|
|
|
1196
|
-
// src/manifest/
|
|
1197
|
-
import {
|
|
1335
|
+
// src/manifest/pin.ts
|
|
1336
|
+
import { createHash } from "crypto";
|
|
1337
|
+
|
|
1338
|
+
// src/manifest/match.ts
|
|
1339
|
+
function toRegExp(pattern) {
|
|
1340
|
+
const source = pattern.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
|
|
1341
|
+
return new RegExp(`^${source}$`);
|
|
1342
|
+
}
|
|
1343
|
+
function literalLength(pattern) {
|
|
1344
|
+
return pattern.length - pattern.split("*").length + 1;
|
|
1345
|
+
}
|
|
1346
|
+
function failClosed(qualifiedName) {
|
|
1347
|
+
return { match: qualifiedName, class: "irreversible", gate: "always", refusal: "uncertain" };
|
|
1348
|
+
}
|
|
1349
|
+
function createPolicyResolver(manifest) {
|
|
1350
|
+
const compiled = manifest.tools.map((policy) => ({
|
|
1351
|
+
policy,
|
|
1352
|
+
test: toRegExp(policy.match),
|
|
1353
|
+
specificity: literalLength(policy.match),
|
|
1354
|
+
wildcards: policy.match.split("*").length - 1
|
|
1355
|
+
})).sort((a, b) => b.specificity - a.specificity || a.wildcards - b.wildcards);
|
|
1356
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1357
|
+
return {
|
|
1358
|
+
resolve(qualifiedName) {
|
|
1359
|
+
const cached2 = cache.get(qualifiedName);
|
|
1360
|
+
if (cached2 !== void 0) {
|
|
1361
|
+
return cached2;
|
|
1362
|
+
}
|
|
1363
|
+
const hit = compiled.find((candidate) => candidate.test.test(qualifiedName));
|
|
1364
|
+
const match = hit === void 0 ? { policy: failClosed(qualifiedName), matched: false } : { policy: hit.policy, matched: true };
|
|
1365
|
+
cache.set(qualifiedName, match);
|
|
1366
|
+
return match;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1198
1370
|
|
|
1199
1371
|
// src/manifest/types.ts
|
|
1200
1372
|
function qualify(server, tool) {
|
|
@@ -1208,13 +1380,77 @@ function splitQualified(qualified) {
|
|
|
1208
1380
|
return { server: qualified.slice(0, dot), tool: qualified.slice(dot + 1) };
|
|
1209
1381
|
}
|
|
1210
1382
|
|
|
1383
|
+
// src/manifest/pin.ts
|
|
1384
|
+
function fingerprint(inputSchema) {
|
|
1385
|
+
return `sha256:${createHash("sha256").update(canonical(inputSchema)).digest("hex")}`;
|
|
1386
|
+
}
|
|
1387
|
+
function auditPins(server, advertised, manifest) {
|
|
1388
|
+
const pins = manifest.pins?.[server];
|
|
1389
|
+
if (pins === void 0) {
|
|
1390
|
+
return [];
|
|
1391
|
+
}
|
|
1392
|
+
const resolver = createPolicyResolver(manifest);
|
|
1393
|
+
const faults = [];
|
|
1394
|
+
const present = /* @__PURE__ */ new Set();
|
|
1395
|
+
for (const tool of advertised) {
|
|
1396
|
+
present.add(tool.name);
|
|
1397
|
+
if (!resolver.resolve(qualify(server, tool.name)).matched) {
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const found = fingerprint(tool.inputSchema);
|
|
1401
|
+
const pinned = pins[tool.name];
|
|
1402
|
+
if (pinned === void 0) {
|
|
1403
|
+
faults.push({ kind: "unpinned", tool: tool.name, found });
|
|
1404
|
+
} else if (pinned !== found) {
|
|
1405
|
+
faults.push({ kind: "moved", tool: tool.name, pinned, found });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
for (const name of Object.keys(pins)) {
|
|
1409
|
+
if (!present.has(name)) {
|
|
1410
|
+
faults.push({ kind: "gone", tool: name });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return faults;
|
|
1414
|
+
}
|
|
1415
|
+
function pinBlock(shapes, manifest) {
|
|
1416
|
+
const resolver = createPolicyResolver(manifest);
|
|
1417
|
+
const lines = ["pins:"];
|
|
1418
|
+
for (const server of [...shapes.keys()].sort()) {
|
|
1419
|
+
const governed = (shapes.get(server) ?? []).filter((tool) => resolver.resolve(qualify(server, tool.name)).matched).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1420
|
+
if (governed.length === 0) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
lines.push(` ${server}:`);
|
|
1424
|
+
for (const tool of governed) {
|
|
1425
|
+
lines.push(` ${tool.name}: "${fingerprint(tool.inputSchema)}"`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return lines.join("\n");
|
|
1429
|
+
}
|
|
1430
|
+
function explainPins(server, faults) {
|
|
1431
|
+
return faults.map((fault) => {
|
|
1432
|
+
switch (fault.kind) {
|
|
1433
|
+
case "moved":
|
|
1434
|
+
return `${server}.${fault.tool} no longer has the shape it was pinned at. Its policy was written for the old one, so the snapshot and inverse it carries may no longer describe this tool.
|
|
1435
|
+
pinned ${fault.pinned}
|
|
1436
|
+
now ${fault.found}`;
|
|
1437
|
+
case "unpinned":
|
|
1438
|
+
return `${server}.${fault.tool} is governed by a policy and has no pin, on a server where everything else is pinned.
|
|
1439
|
+
add ${fault.tool}: "${fault.found}"`;
|
|
1440
|
+
case "gone":
|
|
1441
|
+
return `${server}.${fault.tool} is pinned but the server does not expose it, so the pin vouches for nothing. Remove it, or connect the server that has it.`;
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1211
1446
|
// src/manifest/verify.ts
|
|
1447
|
+
import { z as z3 } from "zod";
|
|
1212
1448
|
var listSchema = z3.looseObject({
|
|
1213
|
-
tools: z3.array(z3.looseObject({ name: z3.string() })),
|
|
1449
|
+
tools: z3.array(z3.looseObject({ name: z3.string(), inputSchema: z3.unknown() })),
|
|
1214
1450
|
nextCursor: z3.string().optional()
|
|
1215
1451
|
});
|
|
1216
|
-
async function
|
|
1217
|
-
const
|
|
1452
|
+
async function toolShapes(upstream) {
|
|
1453
|
+
const shapes = [];
|
|
1218
1454
|
let cursor;
|
|
1219
1455
|
do {
|
|
1220
1456
|
const page = listSchema.parse(
|
|
@@ -1224,16 +1460,19 @@ async function toolNames(upstream) {
|
|
|
1224
1460
|
)
|
|
1225
1461
|
);
|
|
1226
1462
|
for (const tool of page.tools) {
|
|
1227
|
-
|
|
1463
|
+
shapes.push({ name: tool.name, inputSchema: tool.inputSchema });
|
|
1228
1464
|
}
|
|
1229
1465
|
cursor = page.nextCursor;
|
|
1230
1466
|
} while (cursor !== void 0);
|
|
1231
|
-
return
|
|
1467
|
+
return shapes;
|
|
1232
1468
|
}
|
|
1233
1469
|
async function verifyAgainstServers(upstreams, manifest) {
|
|
1470
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
1234
1471
|
const available = /* @__PURE__ */ new Map();
|
|
1235
1472
|
for (const upstream of upstreams) {
|
|
1236
|
-
|
|
1473
|
+
const advertised = await toolShapes(upstream);
|
|
1474
|
+
shapes.set(upstream.name, advertised);
|
|
1475
|
+
available.set(upstream.name, new Set(advertised.map((tool) => tool.name)));
|
|
1237
1476
|
}
|
|
1238
1477
|
const problems = [];
|
|
1239
1478
|
const check = (qualified, role, match) => {
|
|
@@ -1259,11 +1498,50 @@ async function verifyAgainstServers(upstreams, manifest) {
|
|
|
1259
1498
|
if (policy.inverse !== void 0) {
|
|
1260
1499
|
check(policy.inverse.tool, "inverse", policy.match);
|
|
1261
1500
|
}
|
|
1501
|
+
if (policy.verify !== void 0) {
|
|
1502
|
+
check(policy.verify.tool, "verify", policy.match);
|
|
1503
|
+
}
|
|
1262
1504
|
}
|
|
1263
1505
|
if (problems.length > 0) {
|
|
1264
1506
|
throw new ManifestError(`the manifest calls tools that do not exist:
|
|
1265
1507
|
${problems.join("\n ")}`);
|
|
1266
1508
|
}
|
|
1509
|
+
const drifted = [];
|
|
1510
|
+
for (const upstream of upstreams) {
|
|
1511
|
+
const faults = auditPins(upstream.name, shapes.get(upstream.name) ?? [], manifest);
|
|
1512
|
+
drifted.push(...explainPins(upstream.name, faults));
|
|
1513
|
+
}
|
|
1514
|
+
if (drifted.length > 0) {
|
|
1515
|
+
throw new ManifestError(
|
|
1516
|
+
"a pinned tool no longer matches the policy written for it:\n " + drifted.join("\n ") + "\n\n Review what changed before trusting undo on these tools. `synartesis pin` prints the block for the servers you have now."
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/manifest/standing.ts
|
|
1522
|
+
function standing(manifest) {
|
|
1523
|
+
return Object.entries(manifest.servers).map(([server, spec]) => ({
|
|
1524
|
+
server,
|
|
1525
|
+
provenance: spec.provenance ?? "unstated"
|
|
1526
|
+
}));
|
|
1527
|
+
}
|
|
1528
|
+
function untested(manifest) {
|
|
1529
|
+
return standing(manifest).filter((entry) => entry.provenance === "documented").map((entry) => entry.server);
|
|
1530
|
+
}
|
|
1531
|
+
function describeStanding(entry) {
|
|
1532
|
+
switch (entry.provenance) {
|
|
1533
|
+
case "live":
|
|
1534
|
+
return "checked against the real server";
|
|
1535
|
+
case "documented":
|
|
1536
|
+
return "written from documentation, never run against the real server";
|
|
1537
|
+
case "unstated":
|
|
1538
|
+
return "no claim either way";
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function warnUntested(servers) {
|
|
1542
|
+
const names = servers.join(", ");
|
|
1543
|
+
const these = servers.length === 1 ? "this policy has" : "these policies have";
|
|
1544
|
+
return `${names}: ${these} never been run against the real server. The classes and inverses here come from documentation, so undo may not work where it says it will. Run \`synartesis check\` against your own credentials, and expect to correct something.`;
|
|
1267
1545
|
}
|
|
1268
1546
|
|
|
1269
1547
|
// src/proxy/routing.ts
|
|
@@ -1329,6 +1607,25 @@ function describeError(error) {
|
|
|
1329
1607
|
return error instanceof Error ? error.message : String(error);
|
|
1330
1608
|
}
|
|
1331
1609
|
var PROXY_CLIENT_INFO = { name: "synartesis-proxy", version: "0.0.0" };
|
|
1610
|
+
var STDERR_KEPT = 256 * 1024;
|
|
1611
|
+
var STDERR_SETTLE_MS = 200;
|
|
1612
|
+
function settled(stream, ms) {
|
|
1613
|
+
if (stream.readableEnded === true) {
|
|
1614
|
+
return Promise.resolve();
|
|
1615
|
+
}
|
|
1616
|
+
return new Promise((resolve2) => {
|
|
1617
|
+
const done = () => {
|
|
1618
|
+
clearTimeout(timer);
|
|
1619
|
+
resolve2();
|
|
1620
|
+
};
|
|
1621
|
+
const timer = setTimeout(done, ms);
|
|
1622
|
+
timer.unref?.();
|
|
1623
|
+
stream.on("end", done);
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
function isReadable(stream) {
|
|
1627
|
+
return typeof stream === "object" && stream !== null && "on" in stream && typeof stream.on === "function";
|
|
1628
|
+
}
|
|
1332
1629
|
function bufferedText(stream) {
|
|
1333
1630
|
if (typeof stream !== "object" || stream === null || !("read" in stream)) {
|
|
1334
1631
|
return "";
|
|
@@ -1337,14 +1634,26 @@ function bufferedText(stream) {
|
|
|
1337
1634
|
if (typeof read2 !== "function") {
|
|
1338
1635
|
return "";
|
|
1339
1636
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1637
|
+
let text = "";
|
|
1638
|
+
for (; ; ) {
|
|
1639
|
+
const chunk = read2.call(stream);
|
|
1640
|
+
if (typeof chunk === "string") {
|
|
1641
|
+
text += chunk;
|
|
1642
|
+
} else if (Buffer.isBuffer(chunk)) {
|
|
1643
|
+
text += chunk.toString("utf8");
|
|
1644
|
+
} else {
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1343
1647
|
}
|
|
1344
|
-
return
|
|
1648
|
+
return text;
|
|
1345
1649
|
}
|
|
1650
|
+
var NAMES_A_FAULT = /^(?:[A-Za-z]*(?:Error|Exception)\b|Cannot find |ENOENT\b|EACCES\b|EADDRINUSE\b|.*: command not found)/;
|
|
1346
1651
|
function lastWords(text) {
|
|
1347
|
-
const lines = text.split("\n").map((line) => line.
|
|
1652
|
+
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !/^at\s/.test(line));
|
|
1653
|
+
const named = lines.find((line) => NAMES_A_FAULT.test(line));
|
|
1654
|
+
if (named !== void 0) {
|
|
1655
|
+
return named;
|
|
1656
|
+
}
|
|
1348
1657
|
const kept = lines.slice(-4).join("; ");
|
|
1349
1658
|
return kept === "" ? void 0 : kept;
|
|
1350
1659
|
}
|
|
@@ -1376,11 +1685,22 @@ async function start(spec) {
|
|
|
1376
1685
|
});
|
|
1377
1686
|
const client = new Client({ ...PROXY_CLIENT_INFO });
|
|
1378
1687
|
let said = "";
|
|
1688
|
+
const stderr = transport.stderr;
|
|
1689
|
+
const listening = isReadable(stderr);
|
|
1690
|
+
if (listening) {
|
|
1691
|
+
stderr.on("data", (chunk) => {
|
|
1692
|
+
if (chunk !== void 0 && said.length < STDERR_KEPT) {
|
|
1693
|
+
said += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1379
1697
|
try {
|
|
1380
1698
|
await client.connect(transport);
|
|
1381
1699
|
} catch (error) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1700
|
+
if (listening) {
|
|
1701
|
+
await settled(stderr, STDERR_SETTLE_MS);
|
|
1702
|
+
}
|
|
1703
|
+
const reason = lastWords(said === "" ? bufferedText(transport.stderr) : said);
|
|
1384
1704
|
throw new UpstreamError(
|
|
1385
1705
|
spec.name,
|
|
1386
1706
|
"connect",
|
|
@@ -1390,37 +1710,17 @@ async function start(spec) {
|
|
|
1390
1710
|
return { client };
|
|
1391
1711
|
}
|
|
1392
1712
|
|
|
1393
|
-
// src/
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
function failClosed(qualifiedName) {
|
|
1402
|
-
return { match: qualifiedName, class: "irreversible", gate: "always", refusal: "uncertain" };
|
|
1403
|
-
}
|
|
1404
|
-
function createPolicyResolver(manifest) {
|
|
1405
|
-
const compiled = manifest.tools.map((policy) => ({
|
|
1406
|
-
policy,
|
|
1407
|
-
test: toRegExp(policy.match),
|
|
1408
|
-
specificity: literalLength(policy.match),
|
|
1409
|
-
wildcards: policy.match.split("*").length - 1
|
|
1410
|
-
})).sort((a, b) => b.specificity - a.specificity || a.wildcards - b.wildcards);
|
|
1411
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1412
|
-
return {
|
|
1413
|
-
resolve(qualifiedName) {
|
|
1414
|
-
const cached2 = cache.get(qualifiedName);
|
|
1415
|
-
if (cached2 !== void 0) {
|
|
1416
|
-
return cached2;
|
|
1417
|
-
}
|
|
1418
|
-
const hit = compiled.find((candidate) => candidate.test.test(qualifiedName));
|
|
1419
|
-
const match = hit === void 0 ? { policy: failClosed(qualifiedName), matched: false } : { policy: hit.policy, matched: true };
|
|
1420
|
-
cache.set(qualifiedName, match);
|
|
1421
|
-
return match;
|
|
1713
|
+
// src/idempotency.ts
|
|
1714
|
+
var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
|
|
1715
|
+
function withIdempotencyKey(meta, key) {
|
|
1716
|
+
const merged = {};
|
|
1717
|
+
if (typeof meta === "object" && meta !== null) {
|
|
1718
|
+
for (const [name, value] of Object.entries(meta)) {
|
|
1719
|
+
merged[name] = value;
|
|
1422
1720
|
}
|
|
1423
|
-
}
|
|
1721
|
+
}
|
|
1722
|
+
merged[IDEMPOTENCY_META_KEY] = key;
|
|
1723
|
+
return merged;
|
|
1424
1724
|
}
|
|
1425
1725
|
|
|
1426
1726
|
// src/proxy/snapshot.ts
|
|
@@ -1564,6 +1864,7 @@ async function observeState(router, read2, signal) {
|
|
|
1564
1864
|
}
|
|
1565
1865
|
|
|
1566
1866
|
export {
|
|
1867
|
+
PROXY_FLAGS,
|
|
1567
1868
|
cliCommand,
|
|
1568
1869
|
cliCommandFrom,
|
|
1569
1870
|
proxyCommand,
|
|
@@ -1571,6 +1872,7 @@ export {
|
|
|
1571
1872
|
findManifest,
|
|
1572
1873
|
findJournal,
|
|
1573
1874
|
style,
|
|
1875
|
+
errorStyle,
|
|
1574
1876
|
WORDMARK,
|
|
1575
1877
|
rule,
|
|
1576
1878
|
mark,
|
|
@@ -1582,9 +1884,17 @@ export {
|
|
|
1582
1884
|
wasRefused,
|
|
1583
1885
|
parseManifest,
|
|
1584
1886
|
loadManifest,
|
|
1887
|
+
createPolicyResolver,
|
|
1585
1888
|
qualify,
|
|
1889
|
+
pinBlock,
|
|
1890
|
+
toolShapes,
|
|
1586
1891
|
verifyAgainstServers,
|
|
1587
|
-
|
|
1892
|
+
standing,
|
|
1893
|
+
untested,
|
|
1894
|
+
describeStanding,
|
|
1895
|
+
warnUntested,
|
|
1896
|
+
IDEMPOTENCY_META_KEY,
|
|
1897
|
+
withIdempotencyKey,
|
|
1588
1898
|
createRouter,
|
|
1589
1899
|
refusal,
|
|
1590
1900
|
toPayload,
|
|
@@ -1597,4 +1907,4 @@ export {
|
|
|
1597
1907
|
observeState,
|
|
1598
1908
|
connectStdioUpstream
|
|
1599
1909
|
};
|
|
1600
|
-
//# sourceMappingURL=chunk-
|
|
1910
|
+
//# sourceMappingURL=chunk-OODDXM36.js.map
|