@synapsor/runner 1.4.0 → 1.4.12

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/dist/runner.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // apps/runner/src/cli.ts
4
4
  import fs5 from "node:fs/promises";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
- import crypto8 from "node:crypto";
6
+ import crypto9 from "node:crypto";
7
7
  import net from "node:net";
8
8
  import os from "node:os";
9
9
  import path5 from "node:path";
@@ -12,7 +12,42 @@ import readline from "node:readline/promises";
12
12
  import { fileURLToPath, pathToFileURL } from "node:url";
13
13
 
14
14
  // packages/protocol/src/index.ts
15
+ import crypto2 from "node:crypto";
15
16
  import { z } from "zod";
17
+ function canonicalJsonStringify(input) {
18
+ return JSON.stringify(canonicalJsonValue(input, /* @__PURE__ */ new Set()));
19
+ }
20
+ function canonicalJsonDigest(input) {
21
+ return `sha256:${crypto2.createHash("sha256").update(canonicalJsonStringify(input)).digest("hex")}`;
22
+ }
23
+ function canonicalJsonValue(input, ancestors) {
24
+ if (input === null || typeof input === "string" || typeof input === "boolean") return input;
25
+ if (typeof input === "number") {
26
+ if (!Number.isFinite(input)) throw new TypeError("canonical JSON accepts only finite numbers");
27
+ return Object.is(input, -0) ? 0 : input;
28
+ }
29
+ if (Array.isArray(input)) {
30
+ if (ancestors.has(input)) throw new TypeError("canonical JSON does not accept circular values");
31
+ ancestors.add(input);
32
+ const output2 = input.map((item) => canonicalJsonValue(item, ancestors));
33
+ ancestors.delete(input);
34
+ return output2;
35
+ }
36
+ if (typeof input !== "object" || input === void 0) throw new TypeError("canonical JSON accepts only JSON values");
37
+ const record = input;
38
+ const prototype = Object.getPrototypeOf(record);
39
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("canonical JSON accepts only plain objects");
40
+ if (ancestors.has(record)) throw new TypeError("canonical JSON does not accept circular values");
41
+ ancestors.add(record);
42
+ const output = {};
43
+ for (const key of Object.keys(record).sort()) {
44
+ const value = record[key];
45
+ if (value === void 0) throw new TypeError("canonical JSON does not accept undefined values");
46
+ output[key] = canonicalJsonValue(value, ancestors);
47
+ }
48
+ ancestors.delete(record);
49
+ return output;
50
+ }
16
51
  var scalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);
17
52
  var scalarMap = z.record(scalar).refine((value) => Object.keys(value).length > 0, "object must not be empty");
18
53
  var boundedScalarRecord = z.record(scalar).refine((value) => Object.keys(value).length <= 256, "object exceeds 256 fields");
@@ -2755,7 +2790,7 @@ function isRecord(value) {
2755
2790
  }
2756
2791
 
2757
2792
  // packages/mcp-server/src/index.ts
2758
- import crypto4 from "node:crypto";
2793
+ import crypto5 from "node:crypto";
2759
2794
  import fs2 from "node:fs";
2760
2795
  import { createServer } from "node:http";
2761
2796
  import { createServer as createHttpsServer } from "node:https";
@@ -2766,11 +2801,11 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
2766
2801
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
2767
2802
 
2768
2803
  // packages/postgres/src/index.ts
2769
- import crypto3 from "node:crypto";
2804
+ import crypto4 from "node:crypto";
2770
2805
  import { Pool, types as pgTypes } from "pg";
2771
2806
 
2772
2807
  // packages/worker-core/src/index.ts
2773
- import crypto2 from "node:crypto";
2808
+ import crypto3 from "node:crypto";
2774
2809
 
2775
2810
  // packages/worker-core/src/mcp-audit.ts
2776
2811
  var MCP_AUDIT_DISCLAIMER = "This is a static risk review, not proof that an MCP server is secure.";
@@ -3308,21 +3343,21 @@ function assertFrozenSetJobIntegrity(job) {
3308
3343
  if (member.before[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("SET_TENANT_GUARD_MISMATCH");
3309
3344
  const expectedAfter = { ...member.before, ...job.patch, [job.version_advance.column]: member.expected_version.value + 1 };
3310
3345
  if (!recordsEqual(member.after, expectedAfter)) throw new Error("SET_AFTER_STATE_MISMATCH");
3311
- if (member.before_digest !== reconciliationDigest({ primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
3312
- if (member.after_digest !== reconciliationDigest({ primary_key: member.primary_key.value, after: member.after })) throw new Error("SET_AFTER_DIGEST_MISMATCH");
3346
+ if (!reviewedDigestMatches(member.before_digest, { primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
3347
+ if (!reviewedDigestMatches(member.after_digest, { primary_key: member.primary_key.value, after: member.after })) throw new Error("SET_AFTER_DIGEST_MISMATCH");
3313
3348
  } else if (job.operation === "set_delete") {
3314
3349
  if (!member.expected_version || member.before[member.expected_version.column] !== member.expected_version.value) throw new Error("SET_VERSION_GUARD_MISMATCH");
3315
3350
  if (member.before[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("SET_TENANT_GUARD_MISMATCH");
3316
3351
  if (Object.keys(member.after).length !== 0) throw new Error("SET_DELETE_PATCH_FORBIDDEN");
3317
- if (member.before_digest !== reconciliationDigest({ primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
3318
- if (member.tombstone_digest !== reconciliationDigest({ primary_key: member.primary_key.value, expected_version: member.expected_version })) throw new Error("SET_TOMBSTONE_DIGEST_MISMATCH");
3352
+ if (!reviewedDigestMatches(member.before_digest, { primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
3353
+ if (!reviewedDigestMatches(member.tombstone_digest, { primary_key: member.primary_key.value, expected_version: member.expected_version })) throw new Error("SET_TOMBSTONE_DIGEST_MISMATCH");
3319
3354
  } else {
3320
3355
  const components = member.deduplication?.components ?? [];
3321
3356
  const primary = components.find((component) => component.column === job.target.primary_key.column);
3322
3357
  const tenant = components.find((component) => component.column === job.target.tenant_guard.column);
3323
3358
  if (!primary || primary.value !== member.primary_key.value || !tenant || tenant.source !== "trusted_tenant" || tenant.value !== job.target.tenant_guard.value) throw new Error("BATCH_DEDUP_REQUIRED");
3324
3359
  if (member.after[job.target.primary_key.column] !== member.primary_key.value || member.after[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("BATCH_IDENTITY_MISMATCH");
3325
- if (member.after_digest !== reconciliationDigest({ primary_key: member.primary_key.value, after: member.after })) throw new Error("SET_AFTER_DIGEST_MISMATCH");
3360
+ if (!reviewedDigestMatches(member.after_digest, { primary_key: member.primary_key.value, after: member.after })) throw new Error("SET_AFTER_DIGEST_MISMATCH");
3326
3361
  }
3327
3362
  }
3328
3363
  for (const bound of set.aggregate_bounds) {
@@ -3333,8 +3368,24 @@ function assertFrozenSetJobIntegrity(job) {
3333
3368
  }, 0);
3334
3369
  if (actual !== bound.actual || actual > bound.maximum) throw new Error("SET_AGGREGATE_BOUND_MISMATCH");
3335
3370
  }
3336
- const expectedSetDigest = reconciliationDigest({ operation: job.operation, members: set.members, aggregate_bounds: set.aggregate_bounds });
3337
- if (set.set_digest !== expectedSetDigest) throw new Error("SET_DIGEST_MISMATCH");
3371
+ const setMaterial = { operation: job.operation, members: set.members, aggregate_bounds: set.aggregate_bounds };
3372
+ const legacyContractMaterial = {
3373
+ operation: job.operation,
3374
+ members: set.members,
3375
+ aggregate_bounds: set.aggregate_bounds.map((bound) => ({
3376
+ column: bound.column,
3377
+ maximum: bound.maximum,
3378
+ measure: bound.measure,
3379
+ actual: bound.actual
3380
+ }))
3381
+ };
3382
+ if (!reviewedDigestMatches(set.set_digest, setMaterial, legacyContractMaterial)) throw new Error("SET_DIGEST_MISMATCH");
3383
+ }
3384
+ function reviewedDigestMatches(actual, current, legacyContract) {
3385
+ if (!actual) return false;
3386
+ const expected = /* @__PURE__ */ new Set([canonicalJsonDigest(current), reconciliationDigest(current)]);
3387
+ if (legacyContract) expected.add(reconciliationDigest(legacyContract));
3388
+ return expected.has(actual);
3338
3389
  }
3339
3390
  function recordValuesMatch(actual, expected, valuesEqual) {
3340
3391
  return Object.entries(expected).every(([column, value]) => valuesEqual(actual[column], value));
@@ -3352,7 +3403,7 @@ function asScalar(value) {
3352
3403
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : String(value);
3353
3404
  }
3354
3405
  function reconciliationDigest(value) {
3355
- return `sha256:${crypto2.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
3406
+ return `sha256:${crypto3.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
3356
3407
  }
3357
3408
  function loadConfig(env = process.env) {
3358
3409
  const engine = (env.SYNAPSOR_ENGINE || "postgres").toLowerCase();
@@ -3606,7 +3657,7 @@ function scalar2(value) {
3606
3657
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : String(value);
3607
3658
  }
3608
3659
  function digest(value) {
3609
- return `sha256:${crypto3.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
3660
+ return `sha256:${crypto4.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
3610
3661
  }
3611
3662
  function reconciliationProjection(job) {
3612
3663
  const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column, ...job.allowed_columns]);
@@ -10897,7 +10948,7 @@ async function handleStreamableHttpMcpRequest(input) {
10897
10948
  }
10898
10949
  let session;
10899
10950
  const transport = new StreamableHTTPServerTransport({
10900
- sessionIdGenerator: () => crypto4.randomUUID(),
10951
+ sessionIdGenerator: () => crypto5.randomUUID(),
10901
10952
  onsessioninitialized: (newSessionId) => {
10902
10953
  if (session) {
10903
10954
  session.sessionId = newSessionId;
@@ -11038,7 +11089,7 @@ function validBearerToken(header, expected) {
11038
11089
  const actual = header.slice("Bearer ".length);
11039
11090
  const actualBuffer = Buffer.from(actual);
11040
11091
  const expectedBuffer = Buffer.from(expected);
11041
- return actualBuffer.length === expectedBuffer.length && crypto4.timingSafeEqual(actualBuffer, expectedBuffer);
11092
+ return actualBuffer.length === expectedBuffer.length && crypto5.timingSafeEqual(actualBuffer, expectedBuffer);
11042
11093
  }
11043
11094
  function resolveMetricsEndpointAccess(config, env, host) {
11044
11095
  if (config.metrics?.enabled !== true) return { enabled: false };
@@ -11178,7 +11229,7 @@ async function checkRunnerReadiness(config, env = process.env, timeoutMs = 3e3)
11178
11229
  const table = `${quotePostgresIdentifier(shared.schema ?? "synapsor_runner")}.ledger_entries`;
11179
11230
  await client.query(
11180
11231
  `INSERT INTO ${table} (entry_key, kind, payload_json) VALUES ($1, 'readiness_probe', '{}'::jsonb)`,
11181
- [`readiness:${crypto4.randomUUID()}`]
11232
+ [`readiness:${crypto5.randomUUID()}`]
11182
11233
  );
11183
11234
  await client.query("ROLLBACK");
11184
11235
  } catch (error) {
@@ -11274,7 +11325,7 @@ function bearerToken(header) {
11274
11325
  return token || void 0;
11275
11326
  }
11276
11327
  function tokenFingerprint(token) {
11277
- return `sha256:${crypto4.createHash("sha256").update(token).digest("hex")}`;
11328
+ return `sha256:${crypto5.createHash("sha256").update(token).digest("hex")}`;
11278
11329
  }
11279
11330
  function safeSessionClaim(value) {
11280
11331
  if (typeof value !== "string") return void 0;
@@ -11338,7 +11389,7 @@ function toolNameExposureMap(canonicalNames, style) {
11338
11389
  return exposedByCanonical;
11339
11390
  }
11340
11391
  function shortToolHash(value) {
11341
- return crypto4.createHash("sha256").update(value).digest("hex").slice(0, 8);
11392
+ return crypto5.createHash("sha256").update(value).digest("hex").slice(0, 8);
11342
11393
  }
11343
11394
  function setCorsHeaders(response, corsOrigin) {
11344
11395
  if (corsOrigin) {
@@ -12339,7 +12390,7 @@ function buildBoundedSetChangeSet(input) {
12339
12390
  primary_key: { column: input.capability.target.primary_key, value: primary.value },
12340
12391
  before: {},
12341
12392
  after,
12342
- after_digest: hashJson({ primary_key: primary.value, after }),
12393
+ after_digest: canonicalJsonDigest({ primary_key: primary.value, after }),
12343
12394
  deduplication
12344
12395
  };
12345
12396
  }) : input.currentRows.map((rawRow) => {
@@ -12354,8 +12405,8 @@ function buildBoundedSetChangeSet(input) {
12354
12405
  expected_version: expectedVersion,
12355
12406
  before,
12356
12407
  after: {},
12357
- before_digest: hashJson({ primary_key: primaryValue, before }),
12358
- tombstone_digest: hashJson({ primary_key: primaryValue, expected_version: expectedVersion })
12408
+ before_digest: canonicalJsonDigest({ primary_key: primaryValue, before }),
12409
+ tombstone_digest: canonicalJsonDigest({ primary_key: primaryValue, expected_version: expectedVersion })
12359
12410
  };
12360
12411
  }
12361
12412
  const after = { ...before, ...input.patch };
@@ -12369,15 +12420,20 @@ function buildBoundedSetChangeSet(input) {
12369
12420
  expected_version: expectedVersion,
12370
12421
  before,
12371
12422
  after,
12372
- before_digest: hashJson({ primary_key: primaryValue, before }),
12373
- after_digest: hashJson({ primary_key: primaryValue, after })
12423
+ before_digest: canonicalJsonDigest({ primary_key: primaryValue, before }),
12424
+ after_digest: canonicalJsonDigest({ primary_key: primaryValue, after })
12374
12425
  };
12375
12426
  });
12376
12427
  const members = rawMembers.sort((left, right) => JSON.stringify(left.primary_key.value).localeCompare(JSON.stringify(right.primary_key.value)));
12377
12428
  if (new Set(members.map((member) => JSON.stringify(member.primary_key.value))).size !== members.length) {
12378
12429
  throw new McpRuntimeError("SET_IDENTITY_NOT_UNIQUE", "Every frozen set member must have a unique primary-key identity.");
12379
12430
  }
12380
- const aggregateBounds = operation.aggregate_bounds.map((bound) => ({ ...bound, actual: aggregateValue(members, bound) }));
12431
+ const aggregateBounds = operation.aggregate_bounds.map((bound) => ({
12432
+ column: bound.column,
12433
+ measure: bound.measure,
12434
+ maximum: bound.maximum,
12435
+ actual: aggregateValue(members, bound)
12436
+ }));
12381
12437
  for (const bound of aggregateBounds) {
12382
12438
  if (bound.actual > bound.maximum) throw new McpRuntimeError("SET_AGGREGATE_BOUND_EXCEEDED", `${bound.measure} aggregate for ${bound.column} exceeds the reviewed maximum ${bound.maximum}.`);
12383
12439
  }
@@ -12386,7 +12442,7 @@ function buildBoundedSetChangeSet(input) {
12386
12442
  row_count: members.length,
12387
12443
  aggregate_bounds: aggregateBounds,
12388
12444
  members,
12389
- set_digest: hashJson({ operation: kind, members, aggregate_bounds: aggregateBounds })
12445
+ set_digest: canonicalJsonDigest({ operation: kind, members, aggregate_bounds: aggregateBounds })
12390
12446
  };
12391
12447
  const approvalMode = input.capability.approval?.mode === "operator" ? "operator" : "human";
12392
12448
  const proposalCore = {
@@ -12512,7 +12568,7 @@ var RuntimeRateLimiter = class {
12512
12568
  if (this.sharedPool && this.sharedSchema) {
12513
12569
  await this.migration;
12514
12570
  const table = `${quotePostgresIdentifier(this.sharedSchema)}.rate_limit_buckets`;
12515
- const bucketKey = crypto4.createHash("sha256").update(`${context.tenant_id}\0${capability}`).digest("hex");
12571
+ const bucketKey = crypto5.createHash("sha256").update(`${context.tenant_id}\0${capability}`).digest("hex");
12516
12572
  const result = await this.sharedPool.query(
12517
12573
  `INSERT INTO ${table} AS bucket (bucket_key, window_start, request_count, rejected_count)
12518
12574
  VALUES ($1, $2, 1, 0)
@@ -13102,10 +13158,10 @@ function proposalAlreadyExists(existing) {
13102
13158
  );
13103
13159
  }
13104
13160
  function stableId(prefix, input) {
13105
- return `${prefix}_${crypto4.createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 20)}`;
13161
+ return `${prefix}_${crypto5.createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 20)}`;
13106
13162
  }
13107
13163
  function hashJson(input) {
13108
- return `sha256:${crypto4.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
13164
+ return `sha256:${crypto5.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
13109
13165
  }
13110
13166
  function isRecord5(value) {
13111
13167
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -13127,7 +13183,7 @@ function toolErrorPayload(error) {
13127
13183
  }
13128
13184
 
13129
13185
  // packages/mysql/src/index.ts
13130
- import crypto5 from "node:crypto";
13186
+ import crypto6 from "node:crypto";
13131
13187
  import mysql2 from "mysql2/promise";
13132
13188
  var mysqlReceiptMigration = `CREATE TABLE IF NOT EXISTS synapsor_writeback_receipts (
13133
13189
  idempotency_key varchar(255) PRIMARY KEY,
@@ -13241,7 +13297,7 @@ function scalar4(value) {
13241
13297
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : String(value);
13242
13298
  }
13243
13299
  function digest2(value) {
13244
- return `sha256:${crypto5.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
13300
+ return `sha256:${crypto6.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
13245
13301
  }
13246
13302
  function resultHash2(job, status, version) {
13247
13303
  return digest2({ job_id: job.job_id, status, version });
@@ -15621,7 +15677,7 @@ function parseList(value) {
15621
15677
  }
15622
15678
  function unsupported(item, blockKind) {
15623
15679
  if (/ROOT\s+EXTERNAL|JOIN\s+EXTERNAL|RETURN\s+ANSWER|AUTO\s+BRANCH|AUTO\s+MERGE/i.test(item.text)) {
15624
- throw dslError(item.line, 1, "UNSUPPORTED_PREVIEW_SYNTAX", `${blockKind} clause is not supported by @synapsor/dsl 0.1 preview: ${item.text}`);
15680
+ throw dslError(item.line, 1, "UNSUPPORTED_PREVIEW_SYNTAX", `${blockKind} clause is not supported by the current @synapsor/dsl grammar: ${item.text}`);
15625
15681
  }
15626
15682
  throw dslError(item.line, 1, "UNSUPPORTED_DSL_CLAUSE", `unsupported ${blockKind} clause: ${item.text}`);
15627
15683
  }
@@ -15630,7 +15686,7 @@ function dslError(line, column, code, message2) {
15630
15686
  }
15631
15687
 
15632
15688
  // apps/runner/src/local-ui.ts
15633
- import crypto6 from "node:crypto";
15689
+ import crypto7 from "node:crypto";
15634
15690
  import fs3 from "node:fs/promises";
15635
15691
  import { createServer as createServer2 } from "node:http";
15636
15692
  import path3 from "node:path";
@@ -15641,8 +15697,8 @@ async function startLocalUiServer(options = {}) {
15641
15697
  }
15642
15698
  const configPath = path3.resolve(options.configPath ?? "synapsor.runner.json");
15643
15699
  const storePath = path3.resolve(options.storePath ?? "./.synapsor/local.db");
15644
- const token = options.token ?? crypto6.randomBytes(24).toString("base64url");
15645
- const csrfToken = options.csrfToken ?? crypto6.randomBytes(24).toString("base64url");
15700
+ const token = options.token ?? crypto7.randomBytes(24).toString("base64url");
15701
+ const csrfToken = options.csrfToken ?? crypto7.randomBytes(24).toString("base64url");
15646
15702
  const storeAccess = options.storeAccess ?? localStoreAccess(storePath);
15647
15703
  const server = createServer2(async (request, response) => {
15648
15704
  try {
@@ -16606,7 +16662,7 @@ function stringOrDefault(value, fallback) {
16606
16662
  }
16607
16663
 
16608
16664
  // apps/runner/src/operator-identity.ts
16609
- import crypto7 from "node:crypto";
16665
+ import crypto8 from "node:crypto";
16610
16666
  import fs4 from "node:fs/promises";
16611
16667
  import path4 from "node:path";
16612
16668
  async function resolveOperatorIdentity(input) {
@@ -16692,8 +16748,8 @@ async function resolveOperatorIdentity(input) {
16692
16748
  const decision = operatorDecision(input, subject);
16693
16749
  const canonical = stableJson(decision);
16694
16750
  const decisionHash = sha2562(canonical);
16695
- const signature = crypto7.sign("sha256", Buffer.from(canonical), privateKey).toString("base64url");
16696
- if (!crypto7.verify("sha256", Buffer.from(canonical), publicKey, Buffer.from(signature, "base64url"))) {
16751
+ const signature = crypto8.sign("sha256", Buffer.from(canonical), privateKey).toString("base64url");
16752
+ if (!crypto8.verify("sha256", Buffer.from(canonical), publicKey, Buffer.from(signature, "base64url"))) {
16697
16753
  throw new Error(`private key does not match the reviewed public key for operator ${subject}`);
16698
16754
  }
16699
16755
  const core = {
@@ -16717,7 +16773,7 @@ function verifyJwtOperatorProof(proof, attestationSecret) {
16717
16773
  const expected = hmacAttestation(unsigned, attestationSecret);
16718
16774
  const actualBuffer = Buffer.from(signature);
16719
16775
  const expectedBuffer = Buffer.from(expected);
16720
- if (actualBuffer.length !== expectedBuffer.length || !crypto7.timingSafeEqual(actualBuffer, expectedBuffer)) return false;
16776
+ if (actualBuffer.length !== expectedBuffer.length || !crypto8.timingSafeEqual(actualBuffer, expectedBuffer)) return false;
16721
16777
  const core = { ...unsigned, signature };
16722
16778
  return proof.integrity_hash === sha2562(stableJson(core));
16723
16779
  }
@@ -16727,7 +16783,7 @@ function verifySignedOperatorProof(proof, publicKey) {
16727
16783
  if (proof.decision_hash !== sha2562(canonical)) return false;
16728
16784
  const { integrity_hash: _integrity, ...core } = proof;
16729
16785
  if (proof.integrity_hash !== sha2562(stableJson(core))) return false;
16730
- return crypto7.verify("sha256", Buffer.from(canonical), publicKey, Buffer.from(proof.signature, "base64url"));
16786
+ return crypto8.verify("sha256", Buffer.from(canonical), publicKey, Buffer.from(proof.signature, "base64url"));
16731
16787
  }
16732
16788
  function operatorDecision(input, subject) {
16733
16789
  return {
@@ -16781,10 +16837,10 @@ function rolesFromClaim(value) {
16781
16837
  return [...new Set(roles)].sort();
16782
16838
  }
16783
16839
  function hmacAttestation(value, secret) {
16784
- return crypto7.createHmac("sha256", secret).update(stableJson(value)).digest("base64url");
16840
+ return crypto8.createHmac("sha256", secret).update(stableJson(value)).digest("base64url");
16785
16841
  }
16786
16842
  function sha2562(value) {
16787
- return `sha256:${crypto7.createHash("sha256").update(value).digest("hex")}`;
16843
+ return `sha256:${crypto8.createHash("sha256").update(value).digest("hex")}`;
16788
16844
  }
16789
16845
  function stableJson(value) {
16790
16846
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -16883,7 +16939,7 @@ function isRecord7(value) {
16883
16939
  // apps/runner/package.json
16884
16940
  var package_default = {
16885
16941
  name: "@synapsor/runner",
16886
- version: "1.4.0",
16942
+ version: "1.4.12",
16887
16943
  description: "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
16888
16944
  license: "Apache-2.0",
16889
16945
  type: "module",
@@ -16918,6 +16974,7 @@ var package_default = {
16918
16974
  "AGENTS.md",
16919
16975
  "CHANGELOG.md",
16920
16976
  "CONTRIBUTING.md",
16977
+ "SECURITY.md",
16921
16978
  "LICENSE",
16922
16979
  "NOTICE",
16923
16980
  "THREAT_MODEL.md",
@@ -16972,8 +17029,8 @@ var package_default = {
16972
17029
  // packages/dsl/package.json
16973
17030
  var package_default2 = {
16974
17031
  name: "@synapsor/dsl",
16975
- version: "1.4.0",
16976
- description: "Preview SQL-like Synapsor authoring DSL compiler for canonical @synapsor/spec contracts.",
17032
+ version: "1.4.1",
17033
+ description: "SQL-like Synapsor authoring frontend for canonical @synapsor/spec contracts.",
16977
17034
  license: "Apache-2.0",
16978
17035
  type: "module",
16979
17036
  main: "dist/index.js",
@@ -17900,6 +17957,7 @@ async function runInitWizard(args, options = {}) {
17900
17957
  `);
17901
17958
  const smoke2 = await maybeRunGeneratedSmokeCall({
17902
17959
  config: generated.config,
17960
+ configPath: outputPath,
17903
17961
  env: options.env ?? process2.env,
17904
17962
  input: { [lookupArg]: smokeObjectId },
17905
17963
  readUrlEnv: configDatabaseUrlEnv,
@@ -18342,14 +18400,18 @@ async function maybeRunGeneratedSmokeCall(input) {
18342
18400
  ""
18343
18401
  ].join("\n");
18344
18402
  }
18345
- const store = new ProposalStore(input.storePath);
18346
- const runtime = createMcpRuntime(input.config, { store, env: input.env, readRow: input.readRow });
18403
+ const runtime = createMcpRuntime(input.config, { storePath: input.storePath, env: input.env, readRow: input.readRow });
18347
18404
  try {
18348
18405
  const result = await runtime.callTool(input.toolName, input.input);
18349
18406
  return [
18350
- "Smoke call ran successfully.",
18407
+ result.ok === false ? "Smoke call attempted but did not pass." : "Smoke call ran successfully.",
18351
18408
  "",
18352
- formatSmokeCallResult(input.toolName, input.input, result, input.storePath)
18409
+ formatSmokeCallResult(input.toolName, input.input, result, {
18410
+ configPath: input.configPath,
18411
+ storePath: input.storePath,
18412
+ storeAuthority: "local_sqlite",
18413
+ sharedPostgresSchema: "synapsor_runner"
18414
+ })
18353
18415
  ].join("\n");
18354
18416
  } catch (error) {
18355
18417
  const message2 = error instanceof Error ? error.message : String(error);
@@ -20374,8 +20436,8 @@ function trustedCliContext(config, capability, env) {
20374
20436
  return { tenant_id: tenant, principal };
20375
20437
  }
20376
20438
  function createCompensationProposal(input) {
20377
- const proposalId = `wrp_revert_${crypto8.randomBytes(10).toString("hex")}`;
20378
- const evidenceId = `ev_revert_${crypto8.randomBytes(10).toString("hex")}`;
20439
+ const proposalId = `wrp_revert_${crypto9.randomBytes(10).toString("hex")}`;
20440
+ const evidenceId = `ev_revert_${crypto9.randomBytes(10).toString("hex")}`;
20379
20441
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
20380
20442
  const one = input.inverse.members.length === 1 ? input.inverse.members[0] : void 0;
20381
20443
  const before = one ? one.expected_state : { row_count: input.inverse.members.length };
@@ -20845,7 +20907,7 @@ function executorConfig(config, executorName) {
20845
20907
  throw new Error(`executor ${executorName} has unsupported type`);
20846
20908
  }
20847
20909
  function signHandlerRequestBody(body, secret) {
20848
- return `sha256=${crypto8.createHmac("sha256", secret).update(body).digest("hex")}`;
20910
+ return `sha256=${crypto9.createHmac("sha256", secret).update(body).digest("hex")}`;
20849
20911
  }
20850
20912
  async function applyHttpHandlerProposal(input) {
20851
20913
  const duplicate = duplicateHandlerReceipt(input.store, input.proposalId);
@@ -21050,7 +21112,7 @@ function buildHandlerReceipt(input) {
21050
21112
  };
21051
21113
  }
21052
21114
  function hashReceipt(input) {
21053
- return `sha256:${crypto8.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
21115
+ return `sha256:${crypto9.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
21054
21116
  }
21055
21117
  function formatApplyResult(job, result, dryRun, storePath) {
21056
21118
  const status = writebackResultStatus(result);
@@ -22113,7 +22175,7 @@ function reconciliationReceipt(intent, observation, outcome, runnerId, reason) {
22113
22175
  };
22114
22176
  return {
22115
22177
  ...base2,
22116
- receipt_hash: `sha256:${crypto8.createHash("sha256").update(JSON.stringify(base2)).digest("hex")}`
22178
+ receipt_hash: `sha256:${crypto9.createHash("sha256").update(JSON.stringify(base2)).digest("hex")}`
22117
22179
  };
22118
22180
  }
22119
22181
  if (job.protocol_version !== protocolVersions.normalizedWritebackJobV2) throw new Error("single-row reconciliation requires a writeback-job v2");
@@ -22140,7 +22202,7 @@ function reconciliationReceipt(intent, observation, outcome, runnerId, reason) {
22140
22202
  };
22141
22203
  return {
22142
22204
  ...base,
22143
- receipt_hash: `sha256:${crypto8.createHash("sha256").update(JSON.stringify(base)).digest("hex")}`
22205
+ receipt_hash: `sha256:${crypto9.createHash("sha256").update(JSON.stringify(base)).digest("hex")}`
22144
22206
  };
22145
22207
  }
22146
22208
  function publicWritebackIntent(intent) {
@@ -23429,8 +23491,7 @@ async function smokeCall(args) {
23429
23491
  const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? defaultStorePath;
23430
23492
  const config = await readRuntimeConfig(configPath);
23431
23493
  const env = envWithDemoDefaults(config, configPath);
23432
- const store = new ProposalStore(storePath);
23433
- const runtime = createMcpRuntime(config, { store, env });
23494
+ const runtime = createMcpRuntime(config, { storePath, env });
23434
23495
  try {
23435
23496
  const tools2 = runtime.listTools();
23436
23497
  const requestedTool = firstPositional(args);
@@ -23442,13 +23503,28 @@ async function smokeCall(args) {
23442
23503
  if (!capability && config.mode !== "cloud") throw new Error(`capability not found in ${configPath}: ${toolName}`);
23443
23504
  const input = capability ? await smokeToolInput(args, capability) : await smokeInputFromArgs(args);
23444
23505
  const result = await runtime.callTool(toolName, input);
23506
+ const ok = result.ok !== false;
23507
+ const storeAuthority = config.storage?.shared_postgres?.mode === "runtime_store" ? "shared_postgres" : "local_sqlite";
23445
23508
  if (args.includes("--json")) {
23446
- process2.stdout.write(`${JSON.stringify({ ok: true, tool: toolName, input, result, store_path: storePath }, null, 2)}
23509
+ process2.stdout.write(`${JSON.stringify({
23510
+ ok,
23511
+ tool: toolName,
23512
+ input,
23513
+ result,
23514
+ store_path: storePath,
23515
+ store_authority: storeAuthority,
23516
+ ...storeAuthority === "shared_postgres" ? { shared_postgres_schema: config.storage?.shared_postgres?.schema ?? "synapsor_runner" } : {}
23517
+ }, null, 2)}
23447
23518
  `);
23448
23519
  } else {
23449
- process2.stdout.write(formatSmokeCallResult(toolName, input, result, storePath));
23520
+ process2.stdout.write(formatSmokeCallResult(toolName, input, result, {
23521
+ configPath,
23522
+ storePath,
23523
+ storeAuthority,
23524
+ sharedPostgresSchema: config.storage?.shared_postgres?.schema ?? "synapsor_runner"
23525
+ }));
23450
23526
  }
23451
- return 0;
23527
+ return ok ? 0 : 1;
23452
23528
  } finally {
23453
23529
  await runtime.close();
23454
23530
  }
@@ -23665,13 +23741,25 @@ async function smokeInputFromArgs(args, capability) {
23665
23741
  if (sample) return {};
23666
23742
  return {};
23667
23743
  }
23668
- function formatSmokeCallResult(toolName, input, result, storePath) {
23669
- const evidenceId = stringField(result, "evidence_bundle_id");
23670
- const proposalId = stringField(result, "proposal_id");
23744
+ function formatSmokeCallResult(toolName, input, result, topology) {
23745
+ const proposal = isRecord8(result.proposal) ? result.proposal : void 0;
23746
+ const evidence2 = isRecord8(result.evidence) ? result.evidence : void 0;
23747
+ const error = isRecord8(result.error) ? result.error : void 0;
23748
+ const evidenceId = stringField(result, "evidence_bundle_id") ?? (evidence2 ? stringField(evidence2, "bundle_id") : void 0);
23749
+ const proposalId = stringField(result, "proposal_id") ?? (proposal ? stringField(proposal, "id") : void 0);
23671
23750
  const replayResource = stringField(result, "replay_resource");
23672
23751
  const sourceChanged = result.source_database_changed === true || result.source_database_mutated === true;
23752
+ const ok = result.ok !== false;
23753
+ const storeLines = topology.storeAuthority === "shared_postgres" ? [
23754
+ "Authoritative ledger:",
23755
+ `shared Postgres (${topology.sharedPostgresSchema})`,
23756
+ "",
23757
+ "Local --store path:",
23758
+ `${topology.storePath} (compatibility path only; no authoritative smoke records are written here)`,
23759
+ ""
23760
+ ] : ["Local ledger:", topology.storePath, ""];
23673
23761
  const lines = [
23674
- "Synapsor smoke call: ok",
23762
+ `Synapsor smoke call: ${ok ? "ok" : "failed"}`,
23675
23763
  "",
23676
23764
  "Tool:",
23677
23765
  toolName,
@@ -23684,20 +23772,32 @@ function formatSmokeCallResult(toolName, input, result, storePath) {
23684
23772
  "",
23685
23773
  "Evidence:",
23686
23774
  evidenceId || "(not returned)",
23687
- ""
23775
+ "",
23776
+ ...storeLines
23688
23777
  ];
23778
+ if (!ok) {
23779
+ lines.push("Error:", error ? stringField(error, "code") ?? "UNCLASSIFIED" : "UNCLASSIFIED");
23780
+ if (error?.retryable === true) {
23781
+ lines.push("Retryable:", "yes");
23782
+ const retryAfter = error.retry_after_ms;
23783
+ if (typeof retryAfter === "number") lines.push("Retry after:", `${retryAfter} ms`);
23784
+ }
23785
+ return `${lines.join("\n")}
23786
+ `;
23787
+ }
23689
23788
  if (proposalId) {
23690
23789
  lines.push("Proposal:", proposalId, "", "Replay:", replayResource || `synapsor://replay/replay_${proposalId}`, "");
23691
23790
  }
23791
+ const storeSuffix = topology.storeAuthority === "shared_postgres" ? ` --config ${topology.configPath} --store ${topology.storePath}` : ` --store ${topology.storePath}`;
23692
23792
  lines.push("Next:");
23693
- if (evidenceId) lines.push(` ${cliCommandName()} evidence show ${evidenceId} --store ${storePath}`);
23793
+ if (evidenceId) lines.push(` ${cliCommandName()} evidence show ${evidenceId}${storeSuffix}`);
23694
23794
  if (proposalId) {
23695
- lines.push(` ${cliCommandName()} proposals show ${proposalId} --store ${storePath}`);
23696
- lines.push(` ${cliCommandName()} proposals approve ${proposalId} --store ${storePath}`);
23697
- lines.push(` ${cliCommandName()} apply ${proposalId} --store ${storePath}`);
23698
- lines.push(` ${cliCommandName()} replay show --proposal ${proposalId} --store ${storePath}`);
23795
+ lines.push(` ${cliCommandName()} proposals show ${proposalId}${storeSuffix}`);
23796
+ lines.push(` ${cliCommandName()} proposals approve ${proposalId}${storeSuffix}`);
23797
+ lines.push(` ${cliCommandName()} apply ${proposalId}${storeSuffix}`);
23798
+ lines.push(` ${cliCommandName()} replay show --proposal ${proposalId}${storeSuffix}`);
23699
23799
  } else if (evidenceId) {
23700
- lines.push(` ${cliCommandName()} query-audit list --evidence ${evidenceId} --store ${storePath}`);
23800
+ lines.push(` ${cliCommandName()} query-audit list --evidence ${evidenceId}${storeSuffix}`);
23701
23801
  }
23702
23802
  return `${lines.join("\n")}
23703
23803
  `;
@@ -25616,7 +25716,7 @@ no rows deleted
25616
25716
  await pool.query(`DELETE FROM ${qualified} WHERE entry_key = ANY($1::text[])`, [archivedEntries.map((entry) => entry.entry_key)]);
25617
25717
  }
25618
25718
  const retentionEntry = {
25619
- entry_key: `retention:${crypto8.randomUUID()}`,
25719
+ entry_key: `retention:${crypto9.randomUUID()}`,
25620
25720
  kind: "retention_event",
25621
25721
  payload: {
25622
25722
  cutoff,
@@ -27647,7 +27747,7 @@ function toExecutionReceipt(job, result, dryRun) {
27647
27747
  const affectedRows2 = result.affected_rows ?? 0;
27648
27748
  const terminalStatus = result.status === "applied" && !dryRun && affectedRows2 === 0 ? "already_applied" : result.status;
27649
27749
  const previousVersion = job.conflict_guard.kind === "version_column" ? job.conflict_guard.expected_value : void 0;
27650
- const receiptHash = typeof result.result_hash === "string" && result.result_hash.startsWith("sha256:") ? result.result_hash : `sha256:${crypto8.createHash("sha256").update(JSON.stringify({
27750
+ const receiptHash = typeof result.result_hash === "string" && result.result_hash.startsWith("sha256:") ? result.result_hash : `sha256:${crypto9.createHash("sha256").update(JSON.stringify({
27651
27751
  job_id: job.job_id,
27652
27752
  status: terminalStatus,
27653
27753
  affected_rows: affectedRows2,
package/docs/README.md CHANGED
@@ -24,6 +24,10 @@ no-database demo, wire your database, then read deeper concepts.
24
24
  ## 02 Why Raw SQL Is Dangerous
25
25
 
26
26
  - [Security Boundary](security-boundary.md): what the model can and cannot see.
27
+ - [Why Synapsor Over Prompt And Application
28
+ Guardrails](why-synapsor-vs-app-guardrails.md): where SQL authority lives,
29
+ what a hand-built semantic tool already gets right, and when the shared
30
+ contract, approval, receipt, and replay layer is worth adopting.
27
31
  - [MCP Audit](mcp-audit.md): static review for risky database MCP tools such as
28
32
  `execute_sql`, broad query tools, model-controlled tenant filters, or
29
33
  model-facing approval/commit tools.
@@ -0,0 +1,27 @@
1
+ # Bounded Set Local Measurements
2
+
3
+ These measurements are test evidence, not throughput claims or capacity
4
+ guidance. They record one run of `corepack pnpm test:bounded-set` on 2026-07-13
5
+ using synthetic rows and disposable local Docker databases.
6
+
7
+ Environment:
8
+
9
+ - Linux laptop, Intel Core i7-13800H, 20 logical CPUs, 30 GiB RAM;
10
+ - Node.js 22.22.2 and pnpm 10.14.0;
11
+ - Docker 29.5.2;
12
+ - PostgreSQL 16 and MySQL 8 containers;
13
+ - source-database receipt authority with an administrator-precreated receipt
14
+ table;
15
+ - one sequential set UPDATE per measurement, including connection setup,
16
+ source receipt, lock/preflight, transaction, and exact receipt generation.
17
+
18
+ | Engine | 1 row | 10 rows | 100 rows |
19
+ | --- | ---: | ---: | ---: |
20
+ | PostgreSQL | 30.42 ms | 53.88 ms | 154.90 ms |
21
+ | MySQL | 25.30 ms | 53.91 ms | 163.20 ms |
22
+
23
+ These figures are not portable across hardware, network topology, source
24
+ indexes, lock contention, triggers, database configuration, or receipt mode.
25
+ The claim supported by this gate is only that execution stays bounded and the
26
+ hard 100-row ceiling is exercised on both adapters. Operators must benchmark
27
+ their own reviewed contracts and database topology.