@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/CHANGELOG.md +50 -0
- package/README.md +39 -33
- package/SECURITY.md +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +172 -72
- package/docs/README.md +4 -0
- package/docs/benchmarks/bounded-set-local.md +27 -0
- package/docs/bounded-set-writeback.md +298 -0
- package/docs/capability-authoring.md +6 -0
- package/docs/getting-started-own-database.md +8 -1
- package/docs/production.md +10 -0
- package/docs/release-notes.md +41 -0
- package/docs/result-envelope-v3.md +43 -0
- package/docs/security-boundary.md +7 -0
- package/docs/troubleshooting-first-run.md +20 -0
- package/docs/why-synapsor-vs-app-guardrails.md +169 -0
- package/examples/runner-fleet/seed/postgres.sql +1 -0
- package/package.json +2 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3312
|
-
if (member.after_digest
|
|
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
|
|
3318
|
-
if (member.tombstone_digest
|
|
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
|
|
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
|
|
3337
|
-
|
|
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:${
|
|
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:${
|
|
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: () =>
|
|
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 &&
|
|
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:${
|
|
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:${
|
|
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
|
|
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:
|
|
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:
|
|
12358
|
-
tombstone_digest:
|
|
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:
|
|
12373
|
-
after_digest:
|
|
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) => ({
|
|
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:
|
|
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 =
|
|
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}_${
|
|
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:${
|
|
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
|
|
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:${
|
|
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
|
|
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
|
|
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 ??
|
|
15645
|
-
const csrfToken = options.csrfToken ??
|
|
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
|
|
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 =
|
|
16696
|
-
if (!
|
|
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 || !
|
|
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
|
|
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
|
|
16840
|
+
return crypto8.createHmac("sha256", secret).update(stableJson(value)).digest("base64url");
|
|
16785
16841
|
}
|
|
16786
16842
|
function sha2562(value) {
|
|
16787
|
-
return `sha256:${
|
|
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.
|
|
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.
|
|
16976
|
-
description: "
|
|
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
|
|
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,
|
|
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_${
|
|
20378
|
-
const evidenceId = `ev_revert_${
|
|
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=${
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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
|
|
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({
|
|
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,
|
|
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,
|
|
23669
|
-
const
|
|
23670
|
-
const
|
|
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
|
-
|
|
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}
|
|
23793
|
+
if (evidenceId) lines.push(` ${cliCommandName()} evidence show ${evidenceId}${storeSuffix}`);
|
|
23694
23794
|
if (proposalId) {
|
|
23695
|
-
lines.push(` ${cliCommandName()} proposals show ${proposalId}
|
|
23696
|
-
lines.push(` ${cliCommandName()} proposals approve ${proposalId}
|
|
23697
|
-
lines.push(` ${cliCommandName()} apply ${proposalId}
|
|
23698
|
-
lines.push(` ${cliCommandName()} replay show --proposal ${proposalId}
|
|
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}
|
|
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:${
|
|
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:${
|
|
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.
|