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.
@@ -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
- var enabled = process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && process.stdout.isTTY;
135
- function paint(codes, text) {
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
- var style = {
142
- /** A section label: small, capital, spaced out. */
143
- label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
144
- heading: (text) => paint(INK, text.toUpperCase()),
145
- accent: (text) => paint(ACCENT, text),
146
- strong: (text) => paint(INK, text),
147
- quiet: (text) => paint(DIM, text),
148
- /** Off-white on oxblood, the way the wordmark is set. */
149
- plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
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/verify.ts
1197
- import { z as z3 } from "zod";
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 toolNames(upstream) {
1217
- const names = /* @__PURE__ */ new Set();
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
- names.add(tool.name);
1463
+ shapes.push({ name: tool.name, inputSchema: tool.inputSchema });
1228
1464
  }
1229
1465
  cursor = page.nextCursor;
1230
1466
  } while (cursor !== void 0);
1231
- return names;
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
- available.set(upstream.name, await toolNames(upstream));
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
- const chunk = read2.call(stream);
1341
- if (typeof chunk === "string") {
1342
- return chunk;
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 Buffer.isBuffer(chunk) ? chunk.toString("utf8") : "";
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.trimEnd()).filter((line) => line.trim() !== "");
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
- said = bufferedText(transport.stderr);
1383
- const reason = lastWords(said);
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/manifest/match.ts
1394
- function toRegExp(pattern) {
1395
- const source = pattern.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
1396
- return new RegExp(`^${source}$`);
1397
- }
1398
- function literalLength(pattern) {
1399
- return pattern.length - pattern.split("*").length + 1;
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
- createPolicyResolver,
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-FUVEHRJP.js.map
1910
+ //# sourceMappingURL=chunk-OODDXM36.js.map