@synapsor/runner 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
Files changed (44) hide show
  1. package/README.md +284 -13
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5 -0
  4. package/dist/runner.mjs +2982 -127
  5. package/docs/README.md +32 -54
  6. package/docs/getting-started-own-database.md +40 -8
  7. package/docs/http-mcp.md +200 -0
  8. package/docs/limitations.md +20 -0
  9. package/docs/local-mode.md +101 -2
  10. package/docs/mcp-audit.md +10 -4
  11. package/docs/mcp-client-setup.md +40 -1
  12. package/docs/security-boundary.md +17 -0
  13. package/examples/openai-agents-http/README.md +55 -0
  14. package/examples/openai-agents-http/agent.py +90 -0
  15. package/examples/openai-agents-http/requirements.txt +1 -0
  16. package/examples/openai-agents-stdio/README.md +62 -0
  17. package/examples/openai-agents-stdio/agent.py +70 -0
  18. package/examples/openai-agents-stdio/requirements.txt +1 -0
  19. package/examples/reference-support-billing-app/README.md +51 -0
  20. package/examples/reference-support-billing-app/schema.sql +14 -1
  21. package/examples/reference-support-billing-app/seed.sql +12 -5
  22. package/examples/reference-support-billing-app/synapsor.runner.json +105 -0
  23. package/package.json +3 -1
  24. package/docs/MCP_RUNNER_IMPLEMENTATION_PLAN.md +0 -187
  25. package/docs/architecture.md +0 -65
  26. package/docs/capability-config.md +0 -180
  27. package/docs/cloud-mode.md +0 -140
  28. package/docs/config-migrations.md +0 -67
  29. package/docs/demo-transcript.md +0 -73
  30. package/docs/dependency-license-inventory.md +0 -35
  31. package/docs/first-10-minutes.md +0 -147
  32. package/docs/licensing.md +0 -38
  33. package/docs/local-ui.md +0 -163
  34. package/docs/mcp-efficiency-benchmark.md +0 -84
  35. package/docs/operations.md +0 -38
  36. package/docs/own-db-20-minutes.md +0 -185
  37. package/docs/production-readiness.md +0 -39
  38. package/docs/protocol.md +0 -90
  39. package/docs/roadmap.md +0 -13
  40. package/docs/schema-inspection.md +0 -88
  41. package/docs/shadow-mode.md +0 -67
  42. package/docs/telemetry.md +0 -28
  43. package/docs/threat-model.md +0 -25
  44. package/docs/trusted-context.md +0 -70
package/dist/runner.mjs CHANGED
@@ -1180,6 +1180,7 @@ function isRecord(value) {
1180
1180
  // packages/mcp-server/src/index.ts
1181
1181
  import crypto from "node:crypto";
1182
1182
  import fs from "node:fs";
1183
+ import { createServer } from "node:http";
1183
1184
  import path from "node:path";
1184
1185
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1185
1186
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1196,13 +1197,89 @@ var ProposalStoreError = class extends Error {
1196
1197
  };
1197
1198
  var ProposalStore = class {
1198
1199
  db;
1200
+ path;
1199
1201
  constructor(path4 = ":memory:") {
1202
+ this.path = path4;
1200
1203
  this.db = new DatabaseSync(path4);
1201
1204
  this.migrate();
1202
1205
  }
1203
1206
  close() {
1204
1207
  this.db.close();
1205
1208
  }
1209
+ stats() {
1210
+ const pageCount = this.numberValue("PRAGMA page_count");
1211
+ const pageSize = this.numberValue("PRAGMA page_size");
1212
+ return {
1213
+ path: this.path,
1214
+ proposals: this.countTable("proposals"),
1215
+ evidence_bundles: this.countTable("evidence_bundles"),
1216
+ evidence_items: this.countTable("evidence_items"),
1217
+ query_audit: this.countTable("query_audit"),
1218
+ writeback_receipts: this.countTable("writeback_receipts"),
1219
+ writeback_jobs: this.countTable("writeback_jobs"),
1220
+ idempotency_receipts: this.countTable("idempotency_receipts"),
1221
+ replay_records: this.countTable("replay_records"),
1222
+ approvals: this.countTable("approvals"),
1223
+ proposal_events: this.countTable("proposal_events"),
1224
+ shadow_human_actions: this.countTable("shadow_human_actions"),
1225
+ page_count: pageCount,
1226
+ page_size: pageSize,
1227
+ approx_bytes: pageCount * pageSize
1228
+ };
1229
+ }
1230
+ vacuum() {
1231
+ this.db.exec("VACUUM");
1232
+ }
1233
+ pruneBefore(cutoffIso, options = {}) {
1234
+ const dryRun = options.dryRun !== false;
1235
+ const proposalIds = this.stringColumn("SELECT proposal_id FROM proposals WHERE created_at < ?", [cutoffIso], "proposal_id");
1236
+ const evidenceIds = this.evidenceIdsForPrune(cutoffIso, proposalIds);
1237
+ const deleted = {};
1238
+ const run = (table, where, params) => {
1239
+ deleted[table] = this.countWhere(table, where, params);
1240
+ if (!dryRun && deleted[table] > 0) this.db.prepare(`DELETE FROM ${table} WHERE ${where}`).run(...params);
1241
+ };
1242
+ const proposalWhere = inWhere("proposal_id", proposalIds);
1243
+ const evidenceWhere = inWhere("evidence_bundle_id", evidenceIds);
1244
+ this.transaction(() => {
1245
+ if (proposalWhere) {
1246
+ run("idempotency_receipts", proposalWhere.sql, proposalWhere.params);
1247
+ run("writeback_receipts", proposalWhere.sql, proposalWhere.params);
1248
+ run("writeback_jobs", proposalWhere.sql, proposalWhere.params);
1249
+ run("approvals", proposalWhere.sql, proposalWhere.params);
1250
+ run("proposal_events", proposalWhere.sql, proposalWhere.params);
1251
+ run("shadow_human_actions", proposalWhere.sql, proposalWhere.params);
1252
+ run("replay_records", proposalWhere.sql, proposalWhere.params);
1253
+ } else {
1254
+ for (const table of ["idempotency_receipts", "writeback_receipts", "writeback_jobs", "approvals", "proposal_events", "shadow_human_actions", "replay_records"]) {
1255
+ deleted[table] = 0;
1256
+ }
1257
+ }
1258
+ const auditClauses = [];
1259
+ const auditParams = [];
1260
+ if (proposalWhere) {
1261
+ auditClauses.push(proposalWhere.sql);
1262
+ auditParams.push(...proposalWhere.params);
1263
+ }
1264
+ if (evidenceWhere) {
1265
+ auditClauses.push(evidenceWhere.sql);
1266
+ auditParams.push(...evidenceWhere.params);
1267
+ }
1268
+ auditClauses.push("(proposal_id IS NULL AND evidence_bundle_id IS NULL AND created_at < ?)");
1269
+ auditParams.push(cutoffIso);
1270
+ run("query_audit", auditClauses.map((clause) => `(${clause})`).join(" OR "), auditParams);
1271
+ if (evidenceWhere) {
1272
+ run("evidence_items", evidenceWhere.sql, evidenceWhere.params);
1273
+ run("evidence_bundles", evidenceWhere.sql, evidenceWhere.params);
1274
+ } else {
1275
+ deleted.evidence_items = 0;
1276
+ deleted.evidence_bundles = 0;
1277
+ }
1278
+ if (proposalWhere) run("proposals", proposalWhere.sql, proposalWhere.params);
1279
+ else deleted.proposals = 0;
1280
+ });
1281
+ return { cutoff: cutoffIso, dry_run: dryRun, deleted };
1282
+ }
1206
1283
  migrate() {
1207
1284
  this.db.exec(`
1208
1285
  PRAGMA foreign_keys = ON;
@@ -1353,6 +1430,154 @@ var ProposalStore = class {
1353
1430
  INSERT OR IGNORE INTO proposal_store_schema(version, applied_at)
1354
1431
  VALUES (1, datetime('now'));
1355
1432
  `);
1433
+ this.ensureSearchColumns();
1434
+ this.backfillSearchColumns();
1435
+ this.ensureSearchIndexes();
1436
+ }
1437
+ ensureSearchColumns() {
1438
+ this.ensureColumn("proposals", "principal", "TEXT");
1439
+ this.ensureColumn("proposals", "capability", "TEXT");
1440
+ this.ensureColumn("proposals", "interaction_id", "TEXT");
1441
+ this.ensureColumn("proposals", "tool_call_id", "TEXT");
1442
+ this.ensureColumn("evidence_bundles", "principal", "TEXT");
1443
+ this.ensureColumn("evidence_bundles", "capability", "TEXT");
1444
+ this.ensureColumn("evidence_bundles", "source_id", "TEXT");
1445
+ this.ensureColumn("evidence_bundles", "source_table", "TEXT");
1446
+ this.ensureColumn("evidence_bundles", "business_object", "TEXT");
1447
+ this.ensureColumn("evidence_bundles", "object_id", "TEXT");
1448
+ this.ensureColumn("evidence_bundles", "query_fingerprint", "TEXT");
1449
+ this.ensureColumn("query_audit", "tenant_id", "TEXT");
1450
+ this.ensureColumn("query_audit", "principal", "TEXT");
1451
+ this.ensureColumn("query_audit", "capability", "TEXT");
1452
+ this.ensureColumn("query_audit", "business_object", "TEXT");
1453
+ this.ensureColumn("query_audit", "object_id", "TEXT");
1454
+ this.ensureColumn("query_audit", "primary_key_value", "TEXT");
1455
+ }
1456
+ ensureSearchIndexes() {
1457
+ this.db.exec(`
1458
+ CREATE INDEX IF NOT EXISTS idx_proposals_tenant_created ON proposals(tenant_id, created_at);
1459
+ CREATE INDEX IF NOT EXISTS idx_proposals_action_created ON proposals(action, created_at);
1460
+ CREATE INDEX IF NOT EXISTS idx_proposals_capability_created ON proposals(capability, created_at);
1461
+ CREATE INDEX IF NOT EXISTS idx_proposals_principal_created ON proposals(principal, created_at);
1462
+ CREATE INDEX IF NOT EXISTS idx_proposals_object_created ON proposals(business_object, object_id, created_at);
1463
+ CREATE INDEX IF NOT EXISTS idx_proposals_state_created ON proposals(state, created_at);
1464
+ CREATE INDEX IF NOT EXISTS idx_proposals_source_table_created ON proposals(source_id, source_table, created_at);
1465
+
1466
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_tenant_created ON evidence_bundles(tenant_id, created_at);
1467
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_proposal_id ON evidence_bundles(proposal_id);
1468
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_created ON evidence_bundles(created_at);
1469
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_capability_created ON evidence_bundles(capability, created_at);
1470
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_principal_created ON evidence_bundles(principal, created_at);
1471
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_object_created ON evidence_bundles(business_object, object_id, created_at);
1472
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_source_table_created ON evidence_bundles(source_id, source_table, created_at);
1473
+ CREATE INDEX IF NOT EXISTS idx_evidence_bundles_fingerprint_created ON evidence_bundles(query_fingerprint, created_at);
1474
+
1475
+ CREATE INDEX IF NOT EXISTS idx_evidence_items_bundle_id ON evidence_items(evidence_bundle_id);
1476
+
1477
+ CREATE INDEX IF NOT EXISTS idx_query_audit_evidence_id ON query_audit(evidence_bundle_id);
1478
+ CREATE INDEX IF NOT EXISTS idx_query_audit_source_table_created ON query_audit(source_id, table_name, created_at);
1479
+ CREATE INDEX IF NOT EXISTS idx_query_audit_fingerprint_created ON query_audit(query_fingerprint, created_at);
1480
+ CREATE INDEX IF NOT EXISTS idx_query_audit_created ON query_audit(created_at);
1481
+ CREATE INDEX IF NOT EXISTS idx_query_audit_tenant_created ON query_audit(tenant_id, created_at);
1482
+ CREATE INDEX IF NOT EXISTS idx_query_audit_capability_created ON query_audit(capability, created_at);
1483
+ CREATE INDEX IF NOT EXISTS idx_query_audit_principal_created ON query_audit(principal, created_at);
1484
+ CREATE INDEX IF NOT EXISTS idx_query_audit_object_created ON query_audit(business_object, object_id, created_at);
1485
+ CREATE INDEX IF NOT EXISTS idx_query_audit_primary_key_created ON query_audit(primary_key_value, created_at);
1486
+
1487
+ CREATE INDEX IF NOT EXISTS idx_writeback_receipts_writeback_job ON writeback_receipts(writeback_job_id);
1488
+ CREATE INDEX IF NOT EXISTS idx_writeback_receipts_idempotency_key ON writeback_receipts(idempotency_key);
1489
+ CREATE INDEX IF NOT EXISTS idx_writeback_receipts_status_created ON writeback_receipts(status, created_at);
1490
+
1491
+ CREATE INDEX IF NOT EXISTS idx_replay_records_created ON replay_records(created_at);
1492
+
1493
+ CREATE INDEX IF NOT EXISTS idx_approvals_proposal_id ON approvals(proposal_id);
1494
+ CREATE INDEX IF NOT EXISTS idx_approvals_status_created ON approvals(status, created_at);
1495
+
1496
+ CREATE INDEX IF NOT EXISTS idx_proposal_events_kind_created ON proposal_events(kind, created_at);
1497
+ `);
1498
+ }
1499
+ ensureColumn(table, column, definition) {
1500
+ const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
1501
+ if (columns.some((row) => isRecord2(row) && row.name === column)) return;
1502
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
1503
+ }
1504
+ backfillSearchColumns() {
1505
+ const proposals2 = this.db.prepare("SELECT proposal_id, action, change_set_json FROM proposals").all();
1506
+ for (const row of proposals2) {
1507
+ if (!isRecord2(row)) continue;
1508
+ try {
1509
+ const changeSet = parseChangeSet(JSON.parse(String(row.change_set_json)));
1510
+ this.db.prepare("UPDATE proposals SET principal = COALESCE(principal, ?), capability = COALESCE(capability, ?) WHERE proposal_id = ?").run(changeSet.principal.id, changeSet.action, String(row.proposal_id));
1511
+ } catch {
1512
+ }
1513
+ }
1514
+ const evidenceRows = this.db.prepare("SELECT evidence_bundle_id, proposal_id, payload_json FROM evidence_bundles").all();
1515
+ for (const row of evidenceRows) {
1516
+ if (!isRecord2(row)) continue;
1517
+ const proposal = row.proposal_id == null ? void 0 : this.getProposal(String(row.proposal_id));
1518
+ let payload = {};
1519
+ try {
1520
+ payload = JSON.parse(String(row.payload_json));
1521
+ } catch {
1522
+ payload = {};
1523
+ }
1524
+ const metadata = this.evidenceMetadata({
1525
+ proposal,
1526
+ payload,
1527
+ items: this.evidenceItems(String(row.evidence_bundle_id)).map((item) => item.item)
1528
+ });
1529
+ this.db.prepare(`
1530
+ UPDATE evidence_bundles
1531
+ SET principal = COALESCE(principal, ?),
1532
+ capability = COALESCE(capability, ?),
1533
+ source_id = COALESCE(source_id, ?),
1534
+ source_table = COALESCE(source_table, ?),
1535
+ business_object = COALESCE(business_object, ?),
1536
+ object_id = COALESCE(object_id, ?),
1537
+ query_fingerprint = COALESCE(query_fingerprint, ?)
1538
+ WHERE evidence_bundle_id = ?
1539
+ `).run(
1540
+ metadata.principal ?? null,
1541
+ metadata.capability ?? null,
1542
+ metadata.source_id ?? null,
1543
+ metadata.source_table ?? null,
1544
+ metadata.business_object ?? null,
1545
+ metadata.object_id ?? null,
1546
+ metadata.query_fingerprint ?? null,
1547
+ String(row.evidence_bundle_id)
1548
+ );
1549
+ }
1550
+ const auditRows = this.db.prepare("SELECT audit_id, proposal_id, evidence_bundle_id, payload_json FROM query_audit").all();
1551
+ for (const row of auditRows) {
1552
+ if (!isRecord2(row)) continue;
1553
+ const proposal = row.proposal_id == null ? void 0 : this.getProposal(String(row.proposal_id));
1554
+ const evidence2 = row.evidence_bundle_id == null ? void 0 : this.getEvidenceBundle(String(row.evidence_bundle_id));
1555
+ let payload = {};
1556
+ try {
1557
+ payload = JSON.parse(String(row.payload_json));
1558
+ } catch {
1559
+ payload = {};
1560
+ }
1561
+ const metadata = this.queryAuditMetadata({ proposal, evidence: evidence2, payload });
1562
+ this.db.prepare(`
1563
+ UPDATE query_audit
1564
+ SET tenant_id = COALESCE(tenant_id, ?),
1565
+ principal = COALESCE(principal, ?),
1566
+ capability = COALESCE(capability, ?),
1567
+ business_object = COALESCE(business_object, ?),
1568
+ object_id = COALESCE(object_id, ?),
1569
+ primary_key_value = COALESCE(primary_key_value, ?)
1570
+ WHERE audit_id = ?
1571
+ `).run(
1572
+ metadata.tenant_id ?? null,
1573
+ metadata.principal ?? null,
1574
+ metadata.capability ?? null,
1575
+ metadata.business_object ?? null,
1576
+ metadata.object_id ?? null,
1577
+ metadata.primary_key_value ?? null,
1578
+ Number(row.audit_id)
1579
+ );
1580
+ }
1356
1581
  }
1357
1582
  createProposal(input) {
1358
1583
  const changeSet = parseChangeSet(input);
@@ -1377,6 +1602,10 @@ var ProposalStore = class {
1377
1602
  action,
1378
1603
  state,
1379
1604
  tenant_id,
1605
+ principal,
1606
+ capability,
1607
+ interaction_id,
1608
+ tool_call_id,
1380
1609
  business_object,
1381
1610
  object_id,
1382
1611
  source_kind,
@@ -1387,7 +1616,7 @@ var ProposalStore = class {
1387
1616
  change_set_json,
1388
1617
  created_at,
1389
1618
  updated_at
1390
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1619
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1391
1620
  `);
1392
1621
  this.transaction(() => {
1393
1622
  insert.run(
@@ -1397,6 +1626,10 @@ var ProposalStore = class {
1397
1626
  changeSet.action,
1398
1627
  state,
1399
1628
  changeSet.scope.tenant_id,
1629
+ changeSet.principal.id,
1630
+ changeSet.action,
1631
+ null,
1632
+ null,
1400
1633
  changeSet.scope.business_object,
1401
1634
  changeSet.scope.object_id,
1402
1635
  changeSet.source.kind,
@@ -1424,10 +1657,44 @@ var ProposalStore = class {
1424
1657
  const row = this.db.prepare("SELECT * FROM proposals WHERE proposal_id = ?").get(proposalId);
1425
1658
  return rowToProposal(row);
1426
1659
  }
1427
- listProposals(state) {
1428
- const rows = state ? this.db.prepare("SELECT * FROM proposals WHERE state = ? ORDER BY created_at DESC").all(state) : this.db.prepare("SELECT * FROM proposals ORDER BY created_at DESC").all();
1660
+ listProposals(filters) {
1661
+ if (typeof filters === "string") filters = { state: filters };
1662
+ const query = buildProposalQuery(filters ?? {});
1663
+ const rows = this.db.prepare(query.sql).all(...query.params);
1429
1664
  return rows.map((row) => rowToProposal(row)).filter((proposal) => proposal !== void 0);
1430
1665
  }
1666
+ listEvidenceBundles(filters = {}) {
1667
+ const query = buildEvidenceQuery(filters);
1668
+ const rows = this.db.prepare(query.sql).all(...query.params);
1669
+ return rows.map((row) => this.rowToEvidenceBundle(row)).filter((evidence2) => evidence2 !== void 0);
1670
+ }
1671
+ listQueryAudit(filters = {}) {
1672
+ const query = buildQueryAuditQuery(filters);
1673
+ const rows = this.db.prepare(query.sql).all(...query.params);
1674
+ return rows.map(rowToQueryAudit).filter((record) => record !== void 0);
1675
+ }
1676
+ getQueryAudit(auditId) {
1677
+ return rowToQueryAudit(this.db.prepare("SELECT * FROM query_audit WHERE audit_id = ?").get(auditId));
1678
+ }
1679
+ listReceipts(filters = {}) {
1680
+ const query = buildReceiptQuery(filters);
1681
+ const rows = this.db.prepare(query.sql).all(...query.params);
1682
+ return rows.map(rowToReceipt).filter((receipt) => receipt !== void 0);
1683
+ }
1684
+ getReceipt(receiptId) {
1685
+ return rowToReceipt(this.db.prepare("SELECT * FROM writeback_receipts WHERE receipt_id = ?").get(receiptId));
1686
+ }
1687
+ getReplayByReplayId(replayId) {
1688
+ const prefix = "replay_";
1689
+ const proposalId = replayId.startsWith(prefix) ? replayId.slice(prefix.length) : replayId;
1690
+ return this.replay(proposalId);
1691
+ }
1692
+ proposalIdForEvidence(evidenceBundleId) {
1693
+ const evidence2 = this.getEvidenceBundle(evidenceBundleId);
1694
+ if (evidence2?.proposal_id) return evidence2.proposal_id;
1695
+ const row = this.db.prepare("SELECT proposal_id FROM query_audit WHERE evidence_bundle_id = ? AND proposal_id IS NOT NULL ORDER BY created_at DESC LIMIT 1").get(evidenceBundleId);
1696
+ return isRecord2(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
1697
+ }
1431
1698
  approveProposal(proposalId, options) {
1432
1699
  const proposal = this.requireProposal(proposalId);
1433
1700
  assertWritebackAllowed(proposal, "approved");
@@ -1691,17 +1958,38 @@ var ProposalStore = class {
1691
1958
  recordEvidenceBundle(input) {
1692
1959
  assertNoSecretMaterial({ payload: input.payload, items: input.items ?? [] }, "evidence_bundle");
1693
1960
  const now = (/* @__PURE__ */ new Date()).toISOString();
1694
- if (input.proposal_id) this.requireProposal(input.proposal_id);
1961
+ const proposal = input.proposal_id ? this.requireProposal(input.proposal_id) : void 0;
1962
+ const metadata = this.evidenceMetadata({ proposal, payload: input.payload, items: input.items ?? [] });
1695
1963
  this.transaction(() => {
1696
1964
  this.db.prepare(`
1697
1965
  INSERT OR REPLACE INTO evidence_bundles (
1698
1966
  evidence_bundle_id,
1699
1967
  proposal_id,
1700
1968
  tenant_id,
1969
+ principal,
1970
+ capability,
1971
+ source_id,
1972
+ source_table,
1973
+ business_object,
1974
+ object_id,
1975
+ query_fingerprint,
1701
1976
  payload_json,
1702
1977
  created_at
1703
- ) VALUES (?, ?, ?, ?, ?)
1704
- `).run(input.evidence_bundle_id, input.proposal_id ?? null, input.tenant_id, JSON.stringify(input.payload), now);
1978
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1979
+ `).run(
1980
+ input.evidence_bundle_id,
1981
+ input.proposal_id ?? null,
1982
+ input.tenant_id,
1983
+ metadata.principal ?? null,
1984
+ metadata.capability ?? null,
1985
+ metadata.source_id ?? null,
1986
+ metadata.source_table ?? null,
1987
+ metadata.business_object ?? null,
1988
+ metadata.object_id ?? null,
1989
+ metadata.query_fingerprint ?? null,
1990
+ JSON.stringify(input.payload),
1991
+ now
1992
+ );
1705
1993
  for (const item of input.items ?? []) {
1706
1994
  this.db.prepare(`
1707
1995
  INSERT INTO evidence_items (evidence_bundle_id, item_json, created_at)
@@ -1719,21 +2007,35 @@ var ProposalStore = class {
1719
2007
  recordQueryAudit(input) {
1720
2008
  assertNoSecretMaterial(input.payload, "query_audit");
1721
2009
  const now = (/* @__PURE__ */ new Date()).toISOString();
1722
- if (input.proposal_id) this.requireProposal(input.proposal_id);
2010
+ const proposal = input.proposal_id ? this.requireProposal(input.proposal_id) : void 0;
2011
+ const evidence2 = input.evidence_bundle_id ? this.getEvidenceBundle(input.evidence_bundle_id) : void 0;
2012
+ const metadata = this.queryAuditMetadata({ proposal, evidence: evidence2, payload: input.payload });
1723
2013
  this.db.prepare(`
1724
2014
  INSERT INTO query_audit (
1725
2015
  proposal_id,
1726
2016
  evidence_bundle_id,
2017
+ tenant_id,
2018
+ principal,
2019
+ capability,
2020
+ business_object,
2021
+ object_id,
2022
+ primary_key_value,
1727
2023
  source_id,
1728
2024
  query_fingerprint,
1729
2025
  table_name,
1730
2026
  row_count,
1731
2027
  payload_json,
1732
2028
  created_at
1733
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2029
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1734
2030
  `).run(
1735
2031
  input.proposal_id ?? null,
1736
2032
  input.evidence_bundle_id ?? null,
2033
+ metadata.tenant_id ?? null,
2034
+ metadata.principal ?? null,
2035
+ metadata.capability ?? null,
2036
+ metadata.business_object ?? null,
2037
+ metadata.object_id ?? null,
2038
+ metadata.primary_key_value ?? null,
1737
2039
  input.source_id,
1738
2040
  input.query_fingerprint,
1739
2041
  input.table_name,
@@ -1744,14 +2046,24 @@ var ProposalStore = class {
1744
2046
  }
1745
2047
  getEvidenceBundle(evidenceBundleId) {
1746
2048
  const row = this.db.prepare("SELECT * FROM evidence_bundles WHERE evidence_bundle_id = ?").get(evidenceBundleId);
2049
+ return this.rowToEvidenceBundle(row);
2050
+ }
2051
+ rowToEvidenceBundle(row) {
1747
2052
  if (!isRecord2(row)) return void 0;
1748
2053
  return {
1749
2054
  evidence_bundle_id: String(row.evidence_bundle_id),
1750
2055
  proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
1751
2056
  tenant_id: String(row.tenant_id),
2057
+ principal: row.principal == null ? void 0 : String(row.principal),
2058
+ capability: row.capability == null ? void 0 : String(row.capability),
2059
+ source_id: row.source_id == null ? void 0 : String(row.source_id),
2060
+ source_table: row.source_table == null ? void 0 : String(row.source_table),
2061
+ business_object: row.business_object == null ? void 0 : String(row.business_object),
2062
+ object_id: row.object_id == null ? void 0 : String(row.object_id),
2063
+ query_fingerprint: row.query_fingerprint == null ? void 0 : String(row.query_fingerprint),
1752
2064
  payload: JSON.parse(String(row.payload_json)),
1753
- items: this.evidenceItems(evidenceBundleId),
1754
- query_audit: this.queryAuditByEvidence(evidenceBundleId),
2065
+ items: this.evidenceItems(String(row.evidence_bundle_id)),
2066
+ query_audit: this.queryAuditByEvidence(String(row.evidence_bundle_id)),
1755
2067
  created_at: String(row.created_at)
1756
2068
  };
1757
2069
  }
@@ -1896,6 +2208,8 @@ var ProposalStore = class {
1896
2208
  proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
1897
2209
  tenant_id: String(row.tenant_id),
1898
2210
  payload: JSON.parse(String(row.payload_json)),
2211
+ items: this.evidenceItems(String(row.evidence_bundle_id)),
2212
+ query_audit: this.queryAuditByEvidence(String(row.evidence_bundle_id)),
1899
2213
  created_at: String(row.created_at)
1900
2214
  });
1901
2215
  }
@@ -1915,6 +2229,81 @@ var ProposalStore = class {
1915
2229
  }
1916
2230
  return records;
1917
2231
  }
2232
+ evidenceMetadata(input) {
2233
+ if (input.proposal) {
2234
+ return {
2235
+ principal: input.proposal.change_set.principal.id,
2236
+ capability: input.proposal.action,
2237
+ source_id: input.proposal.source_id,
2238
+ source_table: `${input.proposal.source_schema}.${input.proposal.source_table}`,
2239
+ business_object: input.proposal.business_object,
2240
+ object_id: input.proposal.object_id,
2241
+ query_fingerprint: input.proposal.change_set.evidence.query_fingerprint
2242
+ };
2243
+ }
2244
+ const firstItem = input.items.find(isRecord2);
2245
+ const primaryKey = isRecord2(firstItem?.primary_key) ? firstItem.primary_key : void 0;
2246
+ const principal = stringFromPrincipal(input.payload.principal);
2247
+ const table = stringFromUnknown(input.payload.target) ?? stringFromUnknown(firstItem?.table);
2248
+ return {
2249
+ principal,
2250
+ capability: stringFromUnknown(input.payload.capability),
2251
+ source_id: stringFromUnknown(input.payload.source_id) ?? stringFromUnknown(firstItem?.source_id),
2252
+ source_table: table,
2253
+ business_object: table ? lastIdentifier(table) : void 0,
2254
+ object_id: primaryKey ? stringFromUnknown(primaryKey.value) : void 0,
2255
+ query_fingerprint: stringFromUnknown(input.payload.query_fingerprint)
2256
+ };
2257
+ }
2258
+ queryAuditMetadata(input) {
2259
+ if (input.proposal) {
2260
+ return {
2261
+ tenant_id: input.proposal.tenant_id,
2262
+ principal: input.proposal.change_set.principal.id,
2263
+ capability: input.proposal.action,
2264
+ business_object: input.proposal.business_object,
2265
+ object_id: input.proposal.object_id,
2266
+ primary_key_value: String(input.proposal.change_set.source.primary_key.value)
2267
+ };
2268
+ }
2269
+ const firstItem = input.evidence?.items.find((item) => isRecord2(item.item))?.item;
2270
+ const primaryKey = isRecord2(firstItem?.primary_key) ? firstItem.primary_key : void 0;
2271
+ return {
2272
+ tenant_id: input.evidence?.tenant_id ?? stringFromUnknown(input.payload.tenant_id),
2273
+ principal: input.evidence?.principal ?? stringFromPrincipal(input.payload.principal),
2274
+ capability: input.evidence?.capability ?? stringFromUnknown(input.payload.capability),
2275
+ business_object: input.evidence?.business_object ?? stringFromUnknown(input.payload.business_object),
2276
+ object_id: input.evidence?.object_id ?? stringFromUnknown(input.payload.object_id),
2277
+ primary_key_value: primaryKey ? stringFromUnknown(primaryKey.value) : input.evidence?.object_id ?? stringFromUnknown(input.payload.primary_key_value)
2278
+ };
2279
+ }
2280
+ countTable(table) {
2281
+ return this.countWhere(table, "1 = 1", []);
2282
+ }
2283
+ countWhere(table, where, params) {
2284
+ const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get(...params);
2285
+ return isRecord2(row) ? Number(row.count ?? 0) : 0;
2286
+ }
2287
+ numberValue(sql) {
2288
+ const row = this.db.prepare(sql).get();
2289
+ if (!isRecord2(row)) return 0;
2290
+ const value = Object.values(row)[0];
2291
+ return typeof value === "number" ? value : Number(value ?? 0);
2292
+ }
2293
+ stringColumn(sql, params, column) {
2294
+ return this.db.prepare(sql).all(...params).map((row) => isRecord2(row) ? row[column] : void 0).filter((value) => typeof value === "string" || typeof value === "number").map(String);
2295
+ }
2296
+ evidenceIdsForPrune(cutoffIso, proposalIds) {
2297
+ const proposalWhere = inWhere("proposal_id", proposalIds);
2298
+ if (!proposalWhere) {
2299
+ return this.stringColumn("SELECT evidence_bundle_id FROM evidence_bundles WHERE created_at < ?", [cutoffIso], "evidence_bundle_id");
2300
+ }
2301
+ return this.stringColumn(
2302
+ `SELECT evidence_bundle_id FROM evidence_bundles WHERE created_at < ? OR ${proposalWhere.sql}`,
2303
+ [cutoffIso, ...proposalWhere.params],
2304
+ "evidence_bundle_id"
2305
+ );
2306
+ }
1918
2307
  transaction(fn) {
1919
2308
  this.db.exec("BEGIN IMMEDIATE");
1920
2309
  try {
@@ -1933,6 +2322,133 @@ function stateFromChangeSet(changeSet) {
1933
2322
  if (changeSet.approval.status === "canceled") return "canceled";
1934
2323
  return "pending_review";
1935
2324
  }
2325
+ function inWhere(column, values) {
2326
+ if (values.length === 0) return void 0;
2327
+ return {
2328
+ sql: `${column} IN (${values.map(() => "?").join(", ")})`,
2329
+ params: values
2330
+ };
2331
+ }
2332
+ function buildProposalQuery(filters) {
2333
+ const clauses = [];
2334
+ const params = [];
2335
+ addEqual(clauses, params, "proposal_id", filters.proposal);
2336
+ addEqual(clauses, params, "tenant_id", filters.tenant);
2337
+ addEqual(clauses, params, "principal", filters.principal);
2338
+ addEqual(clauses, params, "source_id", filters.source);
2339
+ addTableFilter(clauses, params, "source_table", filters.table);
2340
+ addEqual(clauses, params, "state", filters.status ?? filters.state);
2341
+ addEqual(clauses, params, "action", filters.capability ?? filters.action);
2342
+ addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
2343
+ addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2344
+ return finishQuery("SELECT * FROM proposals", clauses, params, filters.limit);
2345
+ }
2346
+ function buildEvidenceQuery(filters) {
2347
+ const clauses = [];
2348
+ const params = [];
2349
+ addEqual(clauses, params, "evidence_bundle_id", filters.evidence);
2350
+ addEqual(clauses, params, "tenant_id", filters.tenant);
2351
+ addEqual(clauses, params, "principal", filters.principal);
2352
+ addEqual(clauses, params, "capability", filters.capability);
2353
+ addEqual(clauses, params, "proposal_id", filters.proposal);
2354
+ addEqual(clauses, params, "source_id", filters.source);
2355
+ addTableFilter(clauses, params, "source_table", filters.table);
2356
+ addEqual(clauses, params, "query_fingerprint", filters.queryFingerprint);
2357
+ addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
2358
+ addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2359
+ return finishQuery("SELECT * FROM evidence_bundles", clauses, params, filters.limit);
2360
+ }
2361
+ function buildQueryAuditQuery(filters) {
2362
+ const clauses = [];
2363
+ const params = [];
2364
+ addEqual(clauses, params, "tenant_id", filters.tenant);
2365
+ addEqual(clauses, params, "principal", filters.principal);
2366
+ addEqual(clauses, params, "capability", filters.capability);
2367
+ addEqual(clauses, params, "proposal_id", filters.proposal);
2368
+ addEqual(clauses, params, "evidence_bundle_id", filters.evidence);
2369
+ addEqual(clauses, params, "source_id", filters.source);
2370
+ addTableFilter(clauses, params, "table_name", filters.table);
2371
+ addObjectFilter(clauses, params, "business_object", "table_name", "object_id", filters.objectType, filters.objectId);
2372
+ addEqual(clauses, params, "primary_key_value", filters.primaryKey);
2373
+ addEqual(clauses, params, "query_fingerprint", filters.queryFingerprint);
2374
+ addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2375
+ return finishQuery("SELECT * FROM query_audit", clauses, params, filters.limit);
2376
+ }
2377
+ function buildReceiptQuery(filters) {
2378
+ const clauses = [];
2379
+ const params = [];
2380
+ addEqual(clauses, params, "receipt_id", filters.receipt);
2381
+ addEqual(clauses, params, "proposal_id", filters.proposal);
2382
+ addEqual(clauses, params, "writeback_job_id", filters.writebackJob);
2383
+ addEqual(clauses, params, "idempotency_key", filters.idempotencyKey);
2384
+ addEqual(clauses, params, "status", filters.status);
2385
+ addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2386
+ return finishQuery("SELECT * FROM writeback_receipts", clauses, params, filters.limit);
2387
+ }
2388
+ function addEqual(clauses, params, column, value) {
2389
+ if (!value) return;
2390
+ clauses.push(`${column} = ?`);
2391
+ params.push(value);
2392
+ }
2393
+ function addTableFilter(clauses, params, column, value) {
2394
+ if (!value) return;
2395
+ if (value.includes(".")) {
2396
+ clauses.push(`${column} = ?`);
2397
+ params.push(value);
2398
+ return;
2399
+ }
2400
+ clauses.push(`(${column} = ? OR ${column} = ?)`);
2401
+ params.push(value, `public.${value}`);
2402
+ }
2403
+ function addObjectFilter(clauses, params, typeColumn, tableColumn, idColumn, objectType, objectId) {
2404
+ if (objectId) {
2405
+ clauses.push(`${idColumn} = ?`);
2406
+ params.push(objectId);
2407
+ }
2408
+ if (!objectType) return;
2409
+ const variants = objectTypeVariants(objectType);
2410
+ const placeholders = variants.map(() => "?").join(", ");
2411
+ clauses.push(`(${typeColumn} IN (${placeholders}) OR ${tableColumn} IN (${placeholders}))`);
2412
+ params.push(...variants, ...variants);
2413
+ }
2414
+ function addTimeRange(clauses, params, column, from, to) {
2415
+ if (from) {
2416
+ clauses.push(`${column} >= ?`);
2417
+ params.push(from);
2418
+ }
2419
+ if (to) {
2420
+ clauses.push(`${column} <= ?`);
2421
+ params.push(to);
2422
+ }
2423
+ }
2424
+ function finishQuery(base, clauses, params, limit) {
2425
+ const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
2426
+ const sql = `${base}${where} ORDER BY created_at DESC${limit ? " LIMIT ?" : ""}`;
2427
+ return { sql, params: limit ? [...params, limit] : params };
2428
+ }
2429
+ function objectTypeVariants(value) {
2430
+ const variants = /* @__PURE__ */ new Set([value]);
2431
+ if (value.endsWith("s")) variants.add(value.slice(0, -1));
2432
+ else variants.add(`${value}s`);
2433
+ for (const variant of [...variants]) {
2434
+ if (!variant.includes(".")) variants.add(`public.${variant}`);
2435
+ }
2436
+ return [...variants];
2437
+ }
2438
+ function stringFromUnknown(value) {
2439
+ if (typeof value === "string" && value.trim()) return value;
2440
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2441
+ return void 0;
2442
+ }
2443
+ function stringFromPrincipal(value) {
2444
+ if (typeof value === "string") return value;
2445
+ if (isRecord2(value)) return stringFromUnknown(value.id);
2446
+ return void 0;
2447
+ }
2448
+ function lastIdentifier(value) {
2449
+ const parts = value.split(".");
2450
+ return parts[parts.length - 1] ?? value;
2451
+ }
1936
2452
  function stateFromReceipt(receipt) {
1937
2453
  if (receipt.status === "applied" || receipt.status === "already_applied") return "applied";
1938
2454
  if (receipt.status === "conflict") return "conflict";
@@ -2007,6 +2523,10 @@ function rowToProposal(row) {
2007
2523
  action: String(row.action),
2008
2524
  state: String(row.state),
2009
2525
  tenant_id: String(row.tenant_id),
2526
+ principal: row.principal == null ? void 0 : String(row.principal),
2527
+ capability: row.capability == null ? void 0 : String(row.capability),
2528
+ interaction_id: row.interaction_id == null ? void 0 : String(row.interaction_id),
2529
+ tool_call_id: row.tool_call_id == null ? void 0 : String(row.tool_call_id),
2010
2530
  business_object: String(row.business_object),
2011
2531
  object_id: String(row.object_id),
2012
2532
  source_kind: String(row.source_kind),
@@ -2033,6 +2553,7 @@ function rowToEvent(row) {
2033
2553
  function rowToReceipt(row) {
2034
2554
  if (!isRecord2(row)) return void 0;
2035
2555
  return {
2556
+ receipt_id: Number(row.receipt_id),
2036
2557
  writeback_job_id: String(row.writeback_job_id),
2037
2558
  proposal_id: String(row.proposal_id),
2038
2559
  runner_id: String(row.runner_id),
@@ -2049,6 +2570,12 @@ function rowToQueryAudit(row) {
2049
2570
  audit_id: Number(row.audit_id),
2050
2571
  proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
2051
2572
  evidence_bundle_id: row.evidence_bundle_id == null ? void 0 : String(row.evidence_bundle_id),
2573
+ tenant_id: row.tenant_id == null ? void 0 : String(row.tenant_id),
2574
+ principal: row.principal == null ? void 0 : String(row.principal),
2575
+ capability: row.capability == null ? void 0 : String(row.capability),
2576
+ business_object: row.business_object == null ? void 0 : String(row.business_object),
2577
+ object_id: row.object_id == null ? void 0 : String(row.object_id),
2578
+ primary_key_value: row.primary_key_value == null ? void 0 : String(row.primary_key_value),
2052
2579
  source_id: String(row.source_id),
2053
2580
  query_fingerprint: String(row.query_fingerprint),
2054
2581
  table_name: String(row.table_name),
@@ -2158,7 +2685,7 @@ function createMcpRuntime(config, options = {}) {
2158
2685
  }
2159
2686
  function createSynapsorMcpServer(runtime) {
2160
2687
  const server = new McpServer(
2161
- { name: "synapsor-runner", version: "0.1.0-alpha.3" },
2688
+ { name: "synapsor-runner", version: "0.1.0-alpha.5" },
2162
2689
  { capabilities: { tools: {}, resources: {} } }
2163
2690
  );
2164
2691
  if (runtime.config.mode === "cloud") {
@@ -2252,6 +2779,233 @@ async function serveStdio(options = {}) {
2252
2779
  process.once("SIGTERM", close);
2253
2780
  });
2254
2781
  }
2782
+ async function startHttpMcpServer(options = {}) {
2783
+ const host = options.host ?? "127.0.0.1";
2784
+ const port = options.port ?? 8765;
2785
+ const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
2786
+ const env = options.env ?? process.env;
2787
+ const devNoAuth = options.devNoAuth === true;
2788
+ if (devNoAuth && !isLoopbackHost(host)) {
2789
+ throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
2790
+ }
2791
+ const authToken = devNoAuth ? void 0 : env[authTokenEnv];
2792
+ if (!devNoAuth && !authToken) {
2793
+ throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
2794
+ }
2795
+ const config = options.config ?? loadRuntimeConfigFromFile(options.configPath);
2796
+ const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
2797
+ const runtime = createMcpRuntime(config, {
2798
+ env,
2799
+ storePath: options.storePath,
2800
+ readRow: options.readRow,
2801
+ cloudTools
2802
+ });
2803
+ const server = createServer((request, response) => {
2804
+ void handleHttpMcpRequest({
2805
+ request,
2806
+ response,
2807
+ runtime,
2808
+ authToken,
2809
+ devNoAuth,
2810
+ corsOrigin: options.corsOrigin
2811
+ });
2812
+ });
2813
+ try {
2814
+ await new Promise((resolve, reject) => {
2815
+ server.once("error", reject);
2816
+ server.listen(port, host, () => {
2817
+ server.off("error", reject);
2818
+ resolve();
2819
+ });
2820
+ });
2821
+ } catch (error) {
2822
+ runtime.close();
2823
+ throw error;
2824
+ }
2825
+ const address = server.address();
2826
+ const actualHost = address.address === "::" ? host : address.address;
2827
+ const actualPort = address.port;
2828
+ const url = `http://${actualHost}:${actualPort}/mcp`;
2829
+ if (options.log !== false) {
2830
+ const log = options.log ?? process.stderr;
2831
+ log.write(`Synapsor Runner HTTP MCP listening on ${url}
2832
+ `);
2833
+ log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : `Auth: bearer token from ${authTokenEnv}
2834
+ `);
2835
+ log.write(`Config: ${options.configPath ?? "synapsor.runner.json"}
2836
+ `);
2837
+ log.write(`Store: ${options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db"}
2838
+ `);
2839
+ }
2840
+ return {
2841
+ host: actualHost,
2842
+ port: actualPort,
2843
+ url,
2844
+ close: () => closeHttpServer(server, runtime)
2845
+ };
2846
+ }
2847
+ async function handleHttpMcpRequest(input) {
2848
+ const { request, response, runtime, authToken, devNoAuth, corsOrigin } = input;
2849
+ try {
2850
+ setCommonHttpHeaders(response, corsOrigin);
2851
+ if (request.method === "OPTIONS" && corsOrigin) {
2852
+ response.statusCode = 204;
2853
+ response.end();
2854
+ return;
2855
+ }
2856
+ const url = new URL(request.url ?? "/", "http://localhost");
2857
+ if (request.method === "GET" && url.pathname === "/healthz") {
2858
+ writeJson(response, 200, {
2859
+ ok: true,
2860
+ transport: "http",
2861
+ tools: runtime.listTools().length,
2862
+ mode: runtime.config.mode
2863
+ });
2864
+ return;
2865
+ }
2866
+ if (url.pathname !== "/mcp") {
2867
+ writeJson(response, 404, { ok: false, error: "not_found" });
2868
+ return;
2869
+ }
2870
+ if (request.method !== "POST") {
2871
+ writeJson(response, 405, { ok: false, error: "method_not_allowed" });
2872
+ return;
2873
+ }
2874
+ if (!devNoAuth && !validBearerToken(request.headers.authorization, authToken ?? "")) {
2875
+ writeJson(response, 401, { ok: false, error: "unauthorized" });
2876
+ return;
2877
+ }
2878
+ const body = await readRequestBody(request);
2879
+ const payload = JSON.parse(body);
2880
+ if (!isRecord3(payload)) {
2881
+ writeJson(response, 400, jsonRpcError(null, -32600, "JSON-RPC request must be an object."));
2882
+ return;
2883
+ }
2884
+ const id = payload.id ?? null;
2885
+ const method = typeof payload.method === "string" ? payload.method : void 0;
2886
+ if (!method) {
2887
+ writeJson(response, 400, jsonRpcError(id, -32600, "JSON-RPC method is required."));
2888
+ return;
2889
+ }
2890
+ const result = await handleHttpJsonRpcMethod(runtime, method, isRecord3(payload.params) ? payload.params : {});
2891
+ writeJson(response, 200, {
2892
+ jsonrpc: "2.0",
2893
+ id,
2894
+ result: sanitizeHttpPayload(result, authToken)
2895
+ });
2896
+ } catch (error) {
2897
+ const message = sanitizeHttpError(error, authToken);
2898
+ writeJson(response, 200, jsonRpcError(null, -32e3, message));
2899
+ }
2900
+ }
2901
+ async function handleHttpJsonRpcMethod(runtime, method, params) {
2902
+ if (method === "tools/list") {
2903
+ return {
2904
+ tools: runtime.listTools().map(httpToolMetadata)
2905
+ };
2906
+ }
2907
+ if (method === "tools/call") {
2908
+ const name = typeof params.name === "string" ? params.name : void 0;
2909
+ if (!name) throw new McpRuntimeError("HTTP_TOOL_NAME_REQUIRED", "tools/call requires params.name.");
2910
+ const args = isRecord3(params.arguments) ? params.arguments : isRecord3(params.args) ? params.args : {};
2911
+ return await toolCallResult(runtime, name, args);
2912
+ }
2913
+ if (method === "resources/read") {
2914
+ const uri = typeof params.uri === "string" ? params.uri : void 0;
2915
+ if (!uri) throw new McpRuntimeError("HTTP_RESOURCE_URI_REQUIRED", "resources/read requires params.uri.");
2916
+ return resourceResult(uri, runtime.readResource);
2917
+ }
2918
+ throw new McpRuntimeError("HTTP_JSONRPC_METHOD_UNSUPPORTED", `Unsupported MCP HTTP method: ${method}`);
2919
+ }
2920
+ function httpToolMetadata(tool) {
2921
+ return {
2922
+ name: tool.name,
2923
+ title: tool.title,
2924
+ description: tool.description,
2925
+ inputSchema: tool.input_schema,
2926
+ annotations: {
2927
+ ...tool.annotations,
2928
+ raw_sql_exposed: false,
2929
+ approval_or_commit_tool: false
2930
+ },
2931
+ _meta: {
2932
+ "synapsor.raw_sql_exposed": false,
2933
+ "synapsor.approval_tool": false,
2934
+ "synapsor.database_credentials_exposed": false,
2935
+ "synapsor.model_controlled_tenant_authority": false
2936
+ }
2937
+ };
2938
+ }
2939
+ function validBearerToken(header, expected) {
2940
+ if (!header?.startsWith("Bearer ")) return false;
2941
+ const actual = header.slice("Bearer ".length);
2942
+ const actualBuffer = Buffer.from(actual);
2943
+ const expectedBuffer = Buffer.from(expected);
2944
+ return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
2945
+ }
2946
+ function setCommonHttpHeaders(response, corsOrigin) {
2947
+ response.setHeader("content-type", "application/json; charset=utf-8");
2948
+ if (corsOrigin) {
2949
+ response.setHeader("access-control-allow-origin", corsOrigin);
2950
+ response.setHeader("access-control-allow-methods", "POST, GET, OPTIONS");
2951
+ response.setHeader("access-control-allow-headers", "authorization, content-type");
2952
+ }
2953
+ }
2954
+ function writeJson(response, statusCode, payload) {
2955
+ response.statusCode = statusCode;
2956
+ response.end(`${JSON.stringify(payload, null, 2)}
2957
+ `);
2958
+ }
2959
+ async function readRequestBody(request) {
2960
+ const chunks = [];
2961
+ let bytes = 0;
2962
+ for await (const chunk of request) {
2963
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2964
+ bytes += buffer.length;
2965
+ if (bytes > 1024 * 1024) {
2966
+ throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", "HTTP MCP request body exceeds 1 MiB.");
2967
+ }
2968
+ chunks.push(buffer);
2969
+ }
2970
+ return Buffer.concat(chunks).toString("utf8");
2971
+ }
2972
+ function jsonRpcError(id, code, message) {
2973
+ return {
2974
+ jsonrpc: "2.0",
2975
+ id,
2976
+ error: { code, message }
2977
+ };
2978
+ }
2979
+ function sanitizeHttpError(error, authToken) {
2980
+ const raw = error instanceof Error ? error.message : String(error);
2981
+ return sanitizeHttpString(raw, authToken);
2982
+ }
2983
+ function sanitizeHttpPayload(value, authToken) {
2984
+ if (typeof value === "string") return sanitizeHttpString(value, authToken);
2985
+ if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, authToken));
2986
+ if (isRecord3(value)) {
2987
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, authToken)]));
2988
+ }
2989
+ return value;
2990
+ }
2991
+ function sanitizeHttpString(value, authToken) {
2992
+ let redacted = value.replace(/(?:postgres(?:ql)?|mysql):\/\/[^\s"']+/gi, "[redacted-database-url]");
2993
+ if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
2994
+ return redacted;
2995
+ }
2996
+ function isLoopbackHost(host) {
2997
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
2998
+ }
2999
+ async function closeHttpServer(server, runtime) {
3000
+ await new Promise((resolve, reject) => {
3001
+ server.close((error) => {
3002
+ if (error) reject(error);
3003
+ else resolve();
3004
+ });
3005
+ }).finally(() => {
3006
+ runtime.close();
3007
+ });
3008
+ }
2255
3009
  async function toolCallResult(runtime, toolName, args) {
2256
3010
  try {
2257
3011
  const structuredContent = await runtime.callTool(toolName, args);
@@ -2798,9 +3552,9 @@ function readLocalResource(store, uri) {
2798
3552
  return { proposal, events: store.events(id), receipts: store.receipts(id) };
2799
3553
  }
2800
3554
  if (collection === "evidence") {
2801
- const evidence = store.getEvidenceBundle(id);
2802
- if (!evidence) throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Evidence bundle not found: ${id}`);
2803
- return evidence;
3555
+ const evidence2 = store.getEvidenceBundle(id);
3556
+ if (!evidence2) throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Evidence bundle not found: ${id}`);
3557
+ return evidence2;
2804
3558
  }
2805
3559
  if (collection === "replay") {
2806
3560
  const proposalId = id.startsWith("replay_") ? id.slice("replay_".length) : id;
@@ -4337,7 +5091,7 @@ async function startPolling(config, adapters2, signal) {
4337
5091
  // apps/runner/src/local-ui.ts
4338
5092
  import crypto4 from "node:crypto";
4339
5093
  import fs2 from "node:fs/promises";
4340
- import { createServer } from "node:http";
5094
+ import { createServer as createServer2 } from "node:http";
4341
5095
  import path2 from "node:path";
4342
5096
  async function startLocalUiServer(options = {}) {
4343
5097
  const host = options.host ?? "127.0.0.1";
@@ -4348,7 +5102,7 @@ async function startLocalUiServer(options = {}) {
4348
5102
  const storePath = path2.resolve(options.storePath ?? "./.synapsor/local.db");
4349
5103
  const token = options.token ?? crypto4.randomBytes(24).toString("base64url");
4350
5104
  const csrfToken = options.csrfToken ?? crypto4.randomBytes(24).toString("base64url");
4351
- const server = createServer(async (request, response) => {
5105
+ const server = createServer2(async (request, response) => {
4352
5106
  try {
4353
5107
  await handleRequest({ request, response, configPath, storePath, token, csrfToken, tour: options.tour === true });
4354
5108
  } catch (error) {
@@ -4392,7 +5146,7 @@ async function handleRequest(input) {
4392
5146
  }
4393
5147
  if (request.method === "GET" && url.pathname === "/") {
4394
5148
  response.setHeader("set-cookie", `synapsor_ui_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/`);
4395
- sendHtml(response, renderShell(csrfToken, tour || url.searchParams.get("tour") === "1"));
5149
+ sendHtml(response, renderShell(csrfToken, tour || url.searchParams.get("tour") === "1", configPath, storePath));
4396
5150
  return;
4397
5151
  }
4398
5152
  if (request.method === "GET" && url.pathname === "/api/summary") {
@@ -4691,8 +5445,10 @@ function sendHtml(response, html) {
4691
5445
  response.setHeader("cache-control", "no-store");
4692
5446
  response.end(html);
4693
5447
  }
4694
- function renderShell(csrfToken, tour = false) {
5448
+ function renderShell(csrfToken, tour = false, configPath = "synapsor.runner.json", storePath = "./.synapsor/local.db") {
4695
5449
  const escapedCsrf = escapeScriptString(csrfToken);
5450
+ const escapedConfigPath = escapeScriptString(configPath);
5451
+ const escapedStorePath = escapeScriptString(storePath);
4696
5452
  const tourHtml = tour ? `
4697
5453
  <div class="card full tour">
4698
5454
  <h2>Commit-safe MCP in one loop</h2>
@@ -4822,6 +5578,8 @@ details.raw > summary { cursor:pointer; color:var(--blue); font-weight:600; font
4822
5578
  </main>
4823
5579
  <script>
4824
5580
  const csrfToken = "${escapedCsrf}";
5581
+ const configPath = "${escapedConfigPath}";
5582
+ const storePath = "${escapedStorePath}";
4825
5583
  const state = { selected: null, firstId: null };
4826
5584
  const byId = (id) => document.getElementById(id);
4827
5585
  const text = (tag, value, className = "") => { const node = document.createElement(tag); node.textContent = value == null ? "" : String(value); if (className) node.className = className; return node; };
@@ -4915,6 +5673,13 @@ function guardDrawer(gc) {
4915
5673
  d.append(kv);
4916
5674
  return d;
4917
5675
  }
5676
+ function shellQuote(value) {
5677
+ const text = String(value || "");
5678
+ return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : "'" + text.replace(/'/g, "'\\\\''") + "'";
5679
+ }
5680
+ function trustedApplyCommand(proposalId) {
5681
+ return "synapsor apply " + shellQuote(proposalId) + " --config " + shellQuote(configPath) + " --store " + shellQuote(storePath);
5682
+ }
4918
5683
  async function loadSummary() {
4919
5684
  const payload = await api("/api/summary");
4920
5685
  const root = byId("summary"); root.replaceChildren(text("h2", "Setup summary"));
@@ -5109,6 +5874,24 @@ async function loadDetail(proposalId) {
5109
5874
  const reject = el("button", { class: "danger", text: "Reject", onclick: async () => { await api("/api/proposals/" + encodeURIComponent(proposalId) + "/reject", { method: "POST", headers: { "x-synapsor-csrf": csrfToken }, body: JSON.stringify({ actor: actor.value, reason: reason.value || "rejected from local UI", confirm: "reject" }) }); await loadProposals(); await loadDetail(proposalId); } });
5110
5875
  actions.append(approve, reject);
5111
5876
  reviewPane.append(el("div", { class: "callout", text: "You are the approval authority here \u2014 the model cannot reach these controls." }), actor, reason, actions);
5877
+ } else if (proposal.state === "approved" || proposal.state === "pending_worker") {
5878
+ const command = trustedApplyCommand(proposalId);
5879
+ const commandBox = el("div", { class: "mono", text: command, style: "display:block;margin-top:8px" });
5880
+ const copied = el("span", { class: "status-line", text: "" });
5881
+ const copy = el("button", { text: "Copy guarded apply command", onclick: async () => {
5882
+ try {
5883
+ await navigator.clipboard.writeText(command);
5884
+ copied.textContent = "Copied. Run this from a trusted terminal with write credentials.";
5885
+ } catch {
5886
+ copied.textContent = "Copy this guarded apply command and run it from a trusted terminal with write credentials.";
5887
+ }
5888
+ } });
5889
+ reviewPane.append(
5890
+ el("div", { class: "callout", text: "Apply guarded writeback from a trusted terminal. This remains outside MCP, so the model still cannot commit." }),
5891
+ commandBox,
5892
+ el("div", { class: "actions" }, [copy]),
5893
+ copied,
5894
+ );
5112
5895
  }
5113
5896
 
5114
5897
  jsonPane.append(
@@ -5252,6 +6035,7 @@ var dangerousDatabaseMcpAuditExample = {
5252
6035
  };
5253
6036
  var defaultConfigPath = "synapsor.runner.json";
5254
6037
  var defaultStorePath = "./.synapsor/local.db";
6038
+ var quickDemoStorePath = "./.synapsor/quick-demo.db";
5255
6039
  var referenceDemoDir = "examples/reference-support-billing-app";
5256
6040
  var referenceDemoConfigPath = `${referenceDemoDir}/synapsor.runner.json`;
5257
6041
  var referenceDemoContainer = "synapsor_runner_reference_support_billing";
@@ -5274,6 +6058,14 @@ async function main(argv) {
5274
6058
  usage([]);
5275
6059
  return 0;
5276
6060
  }
6061
+ if (!isKnownTopLevelCommand(command)) {
6062
+ process2.stderr.write(`Unknown command: synapsor ${command}
6063
+
6064
+ Try:
6065
+ synapsor --help
6066
+ `);
6067
+ return 2;
6068
+ }
5277
6069
  if (isHelpRequest(rest)) {
5278
6070
  usage([command, ...rest.filter((arg) => arg !== "--help" && arg !== "-h")]);
5279
6071
  return 0;
@@ -5301,9 +6093,18 @@ async function main(argv) {
5301
6093
  if (command === "benchmark") return benchmark(rest);
5302
6094
  if (command === "proposals") return proposals(rest);
5303
6095
  if (command === "replay") return replay(rest);
6096
+ if (command === "evidence") return evidence(rest);
6097
+ if (command === "query-audit") return queryAudit(rest);
6098
+ if (command === "receipts") return receipts(rest);
6099
+ if (command === "activity") return activity(rest);
6100
+ if (command === "store") return storeCommand(rest);
5304
6101
  if (command === "shadow") return shadow(rest);
5305
6102
  if (command === "ui") return ui(rest);
5306
- usage([]);
6103
+ process2.stderr.write(`Unknown command: synapsor ${command}
6104
+
6105
+ Try:
6106
+ synapsor --help
6107
+ `);
5307
6108
  return 2;
5308
6109
  }
5309
6110
  async function init(args) {
@@ -5367,7 +6168,9 @@ async function runInitWizard(args, options = {}) {
5367
6168
  const ask = options.ask ?? askTtyQuestion;
5368
6169
  const stdout = options.stdout ?? process2.stdout;
5369
6170
  stdout.write("Synapsor Runner guided init\n");
5370
- stdout.write("Use a staging or disposable Postgres/MySQL database first. The wizard stores environment-variable names, not credentials.\n\n");
6171
+ stdout.write("Use a staging or disposable Postgres/MySQL database first. The wizard stores environment-variable names, not credentials.\n");
6172
+ stdout.write("Flow: inspect database -> create trusted context -> create capability -> expose MCP tool.\n\n");
6173
+ stdout.write("Step 1: Inspect database metadata\n");
5371
6174
  const engineInput = await askChoice(ask, "Engine", optionalArg(args, "--engine") ?? "auto", ["postgres", "mysql", "auto"]);
5372
6175
  const databaseInput = databaseInputFromArgs(args);
5373
6176
  if (databaseInput.inlineUrl) {
@@ -5390,21 +6193,27 @@ async function runInitWizard(args, options = {}) {
5390
6193
  stdout.write(` - ${table2.schema}.${table2.name} (${table2.type}, pk=${table2.primary_key.join(",") || "none"}, tenant=${table2.suggestions.tenant_columns.join(",") || "none"})
5391
6194
  `);
5392
6195
  }
5393
- const tableName = await askDefault(ask, "Table/view for this capability", optionalArg(args, "--table") ?? tables[0]?.name ?? "");
6196
+ stdout.write("\nStep 2: Create trusted context\n");
6197
+ stdout.write("Choose the source object and trusted scope. Tenant and principal values come from your backend/session, not from the model.\n");
6198
+ const tableName = await askDefault(ask, "Source table/view for this context", optionalArg(args, "--table") ?? tables[0]?.name ?? "");
5394
6199
  const table = findInspectionTable(inspection, tableName, schema);
5395
6200
  if (!table) throw new Error(`table not found in inspection: ${schema}.${tableName}`);
5396
6201
  const columns = table.columns.map((column) => column.name);
5397
6202
  const primaryKey = await askColumn(ask, "Primary-key column", optionalArg(args, "--primary-key") ?? table.primary_key[0] ?? inferPrimaryKeyCandidate(table), columns);
5398
6203
  const suggestedTenant = optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
5399
- const tenantAnswer = await askDefault(ask, "Tenant/scope column. Leave blank only for a reviewed single-tenant dev source", suggestedTenant ?? "");
6204
+ const tenantAnswer = await askDefault(ask, "Trusted tenant/scope column", suggestedTenant ?? "");
5400
6205
  const singleTenantDev = !tenantAnswer && (await askDefault(ask, "No tenant column selected. Type yes to mark this as a single-tenant dev source", "no")).toLowerCase() === "yes";
5401
6206
  if (!tenantAnswer && !singleTenantDev) throw new Error("tenant/scope column is required unless single-tenant dev source is explicitly confirmed");
5402
6207
  if (tenantAnswer && !columns.includes(tenantAnswer)) throw new Error(`tenant column ${tenantAnswer} does not exist on ${table.schema}.${table.name}`);
5403
- const mode = await askChoice(ask, "Mode", optionalArg(args, "--mode") ?? "read_only", ["read_only", "shadow", "review"]);
6208
+ const tenantEnv = await askEnvName(ask, "Trusted tenant env var", optionalArg(args, "--tenant-env") ?? "SYNAPSOR_TENANT_ID");
6209
+ const principalEnv = await askEnvName(ask, "Trusted principal env var", optionalArg(args, "--principal-env") ?? "SYNAPSOR_PRINCIPAL");
6210
+ stdout.write("\nStep 3: Create capability\n");
6211
+ stdout.write("Name the semantic tool the model can call. Table, key, visible fields, and mode define what that capability can do.\n");
6212
+ const mode = await askChoice(ask, "Capability mode", optionalArg(args, "--mode") ?? "read_only", ["read_only", "shadow", "review"]);
5404
6213
  const conflictAnswer = mode === "read_only" ? optionalArg(args, "--conflict-column") ?? "" : await askDefault(ask, "Conflict/version column", optionalArg(args, "--conflict-column") ?? table.suggestions.conflict_columns[0] ?? "");
5405
6214
  if (conflictAnswer && !columns.includes(conflictAnswer)) throw new Error(`conflict column ${conflictAnswer} does not exist on ${table.schema}.${table.name}`);
5406
6215
  const defaultVisible = table.suggestions.default_visible_columns.join(",");
5407
- let visibleColumns = parseColumnList(await askDefault(ask, "Read-visible columns", optionalArg(args, "--visible-columns") ?? defaultVisible));
6216
+ let visibleColumns = parseColumnList(await askDefault(ask, "Capability read-visible columns", optionalArg(args, "--visible-columns") ?? defaultVisible));
5408
6217
  ensureColumnsExist(visibleColumns, columns, "visible");
5409
6218
  if (mode !== "read_only" && !conflictAnswer) {
5410
6219
  const weak = await askDefault(ask, "No conflict/version column selected. Type yes to continue with a weak guard", "no");
@@ -5472,8 +6281,6 @@ async function runInitWizard(args, options = {}) {
5472
6281
  const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? "source");
5473
6282
  const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? recipeSpec?.object_name ?? safeObjectName(table.name));
5474
6283
  const lookupArg = await askDefault(ask, "Model-visible object id argument", optionalArg(args, "--lookup-arg") ?? recipeSpec?.lookup_arg ?? `${objectName}_id`);
5475
- const tenantEnv = await askEnvName(ask, "Trusted tenant env var", optionalArg(args, "--tenant-env") ?? "SYNAPSOR_TENANT_ID");
5476
- const principalEnv = await askEnvName(ask, "Trusted principal env var", optionalArg(args, "--principal-env") ?? "SYNAPSOR_PRINCIPAL");
5477
6284
  const writeUrlEnv = mode === "review" ? await askEnvName(ask, "Write URL env var for trusted apply path", optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL") : optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL";
5478
6285
  const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
5479
6286
  const spec = {
@@ -5511,6 +6318,8 @@ async function runInitWizard(args, options = {}) {
5511
6318
  const generated = generateRunnerConfigFromSpec(spec);
5512
6319
  const tools2 = generated.config.capabilities.map((capability) => `${capability.name} (${capability.kind})`);
5513
6320
  stdout.write("\nPreview:\n");
6321
+ stdout.write(` trusted context: tenant from ${tenantEnv}${singleTenantDev ? " (single-tenant dev source)" : tenantAnswer ? ` via ${tenantAnswer}` : ""}; principal from ${principalEnv}
6322
+ `);
5514
6323
  stdout.write(` source: ${inspection.engine} ${table.schema}.${table.name}
5515
6324
  `);
5516
6325
  stdout.write(` mode: ${mode}
@@ -6127,6 +6936,49 @@ function formatLocalDoctorReport(report) {
6127
6936
  return `${lines.join("\n")}
6128
6937
  `;
6129
6938
  }
6939
+ function formatLocalDoctorMarkdown(report) {
6940
+ const store = report.store_stats;
6941
+ const boundaryOk = report.checks.find((check) => check.name === "mcp-tool-boundary")?.ok === true;
6942
+ const lines = [
6943
+ "# Synapsor Runner Doctor Report",
6944
+ "",
6945
+ `- Runner package: @synapsor/runner`,
6946
+ `- Node version: ${process2.versions.node}`,
6947
+ `- Config: ${report.config_path}`,
6948
+ `- Mode: ${report.mode}`,
6949
+ `- Status: ${report.ok ? "ok" : "needs attention"}`,
6950
+ "",
6951
+ "## Semantic Tools",
6952
+ "",
6953
+ ...report.tools.length ? report.tools.map((tool) => `- ${tool}`) : ["- none listed"],
6954
+ "",
6955
+ "## Safety Boundary",
6956
+ "",
6957
+ `- Raw SQL / commit tools exposed: ${boundaryOk ? "no obvious forbidden tools detected" : "needs review"}`,
6958
+ "- Database URLs, passwords, bearer tokens, and private keys are intentionally not included in this report.",
6959
+ "",
6960
+ "## Local Store",
6961
+ "",
6962
+ `- Path: ${store?.path ?? "not configured"}`,
6963
+ `- Exists: ${store?.exists ? "yes" : "no"}`,
6964
+ ...store?.exists ? [
6965
+ `- Proposals: ${store.proposals ?? 0}`,
6966
+ `- Evidence bundles: ${store.evidence ?? 0}`,
6967
+ `- Query audit records: ${store.query_audit ?? 0}`,
6968
+ `- Receipts: ${store.receipts ?? 0}`
6969
+ ] : [],
6970
+ "",
6971
+ "## Checks",
6972
+ "",
6973
+ ...report.checks.map((check) => `- ${check.level.toUpperCase()} ${check.name}: ${check.message}`),
6974
+ "",
6975
+ "## Redaction Note",
6976
+ "",
6977
+ "This report is redacted by design. Do not attach raw database URLs, passwords, API keys, bearer tokens, private keys, cookies, or customer data when sharing diagnostics."
6978
+ ];
6979
+ return `${lines.join("\n")}
6980
+ `;
6981
+ }
6130
6982
  var moduleDir = path3.dirname(fileURLToPath(import.meta.url));
6131
6983
  var packageAssetRoot = path3.resolve(moduleDir, "..");
6132
6984
  var sourceAssetRoot = path3.resolve(moduleDir, "../../..");
@@ -6479,9 +7331,16 @@ async function localDoctor(args) {
6479
7331
  mode: String(parsed.mode),
6480
7332
  config_path: configPath,
6481
7333
  checks,
6482
- tools: tools2
7334
+ tools: tools2,
7335
+ store_stats: await localDoctorStoreStats(optionalArg(args, "--store") ?? parsed.storage?.sqlite_path)
6483
7336
  };
6484
- if (args.includes("--json")) {
7337
+ if (args.includes("--report")) {
7338
+ const output = outputArg(args) ?? "synapsor-doctor.md";
7339
+ await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
7340
+ await fs3.writeFile(output, formatLocalDoctorMarkdown(report), "utf8");
7341
+ process2.stdout.write(`wrote redacted doctor report: ${output}
7342
+ `);
7343
+ } else if (args.includes("--json")) {
6485
7344
  process2.stdout.write(`${JSON.stringify(report, null, 2)}
6486
7345
  `);
6487
7346
  } else {
@@ -6489,6 +7348,23 @@ async function localDoctor(args) {
6489
7348
  }
6490
7349
  return report.ok ? 0 : 1;
6491
7350
  }
7351
+ async function localDoctorStoreStats(storePath) {
7352
+ if (!storePath || storePath === ":memory:") return { path: storePath ?? "not configured", exists: storePath === ":memory:" };
7353
+ if (!await fileExists(storePath)) return { path: storePath, exists: false };
7354
+ const store = new ProposalStore(storePath);
7355
+ try {
7356
+ return {
7357
+ path: storePath,
7358
+ exists: true,
7359
+ proposals: store.listProposals({ limit: 1e6 }).length,
7360
+ evidence: store.listEvidenceBundles({ limit: 1e6 }).length,
7361
+ query_audit: store.listQueryAudit({ limit: 1e6 }).length,
7362
+ receipts: store.listReceipts({ limit: 1e6 }).length
7363
+ };
7364
+ } finally {
7365
+ store.close();
7366
+ }
7367
+ }
6492
7368
  async function validate(args) {
6493
7369
  const job = await readJob(args);
6494
7370
  parseWritebackJob(job);
@@ -6567,21 +7443,21 @@ async function applyProposal(args, proposalId) {
6567
7443
  lease_seconds: Number(optionalArg(args, "--lease-seconds") ?? "300")
6568
7444
  });
6569
7445
  const result = await applySqlJob(job, configPath, storePath, dryRun, envWithDemoDefaults(config, configPath));
6570
- process2.stdout.write(`${JSON.stringify(result, null, 2)}
6571
- `);
7446
+ process2.stdout.write(args.includes("--json") ? `${JSON.stringify(result, null, 2)}
7447
+ ` : formatApplyResult(parseWritebackJob(job), result, dryRun, storePath));
6572
7448
  return result.status === "failed" ? 1 : 0;
6573
7449
  }
6574
7450
  const executor = executorConfig(config, executorName);
6575
7451
  if (executor.type === "http_handler") {
6576
7452
  const result = await applyHttpHandlerProposal({ store, proposalId: resolvedProposalId, proposal, executorName, executor, runnerId, dryRun, env: envWithDemoDefaults(config, configPath) });
6577
- process2.stdout.write(`${JSON.stringify(redactConfig(result), null, 2)}
6578
- `);
7453
+ process2.stdout.write(args.includes("--json") ? `${JSON.stringify(redactConfig(result), null, 2)}
7454
+ ` : formatHandlerApplyResult(result, resolvedProposalId, storePath));
6579
7455
  return result.status === "failed" ? 1 : 0;
6580
7456
  }
6581
7457
  if (executor.type === "command_handler") {
6582
7458
  const result = await applyCommandHandlerProposal({ store, proposalId: resolvedProposalId, proposal, executorName, executor, runnerId, dryRun, env: envWithDemoDefaults(config, configPath) });
6583
- process2.stdout.write(`${JSON.stringify(redactConfig(result), null, 2)}
6584
- `);
7459
+ process2.stdout.write(args.includes("--json") ? `${JSON.stringify(redactConfig(result), null, 2)}
7460
+ ` : formatHandlerApplyResult(result, resolvedProposalId, storePath));
6585
7461
  return result.status === "failed" ? 1 : 0;
6586
7462
  }
6587
7463
  throw new Error(`unsupported executor type for ${executorName}`);
@@ -6757,8 +7633,8 @@ function prepareHandlerProposal(store, proposal, runnerId) {
6757
7633
  };
6758
7634
  }
6759
7635
  function duplicateHandlerReceipt(store, proposalId) {
6760
- const receipts = store.receipts(proposalId);
6761
- const existing = receipts.find((receipt) => receipt.writeback_job_id.startsWith("hwb_"));
7636
+ const receipts2 = store.receipts(proposalId);
7637
+ const existing = receipts2.find((receipt) => receipt.writeback_job_id.startsWith("hwb_"));
6762
7638
  return existing ? { receipt: existing.receipt } : void 0;
6763
7639
  }
6764
7640
  function alreadyAppliedReceipt(receipt, runnerId) {
@@ -6837,14 +7713,115 @@ function buildHandlerReceipt(input) {
6837
7713
  function hashReceipt(input) {
6838
7714
  return `sha256:${crypto5.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
6839
7715
  }
6840
- function parseOptionalJson(text) {
6841
- if (!text.trim()) return {};
6842
- try {
6843
- return JSON.parse(text);
6844
- } catch {
6845
- return { status: "failed", safe_error_code: "HANDLER_INVALID_JSON" };
7716
+ function formatApplyResult(job, result, dryRun, storePath) {
7717
+ const status = writebackResultStatus(result);
7718
+ const affectedRows = writebackAffectedRows(result);
7719
+ const errorCode = writebackErrorCode(result);
7720
+ const receiptHash = writebackReceiptHash(result);
7721
+ const conflictGuardPassed = status === "conflict" && errorCode === "VERSION_CONFLICT" ? "no" : status === "applied" ? "yes" : "not completed";
7722
+ const title = status === "conflict" ? "Guarded writeback returned conflict." : status === "failed" ? "Guarded writeback failed." : dryRun ? "Guarded writeback dry run passed." : affectedRows === 0 ? "Guarded writeback already applied." : "Guarded writeback applied.";
7723
+ const lines = [
7724
+ title,
7725
+ "",
7726
+ "Checks:",
7727
+ "* proposal approved: yes",
7728
+ `* primary key matched: ${status === "conflict" && errorCode === "ROW_NOT_FOUND" ? "no" : status === "failed" ? "not completed" : "yes"}`,
7729
+ `* tenant guard matched: ${status === "conflict" && errorCode === "ROW_NOT_FOUND" ? "no" : status === "failed" ? "not completed" : "yes"}`,
7730
+ "* allowed columns only: yes",
7731
+ `* conflict guard passed: ${conflictGuardPassed}`,
7732
+ `* affected rows: ${affectedRows}`,
7733
+ `* idempotency key: ${job.idempotency_key}`,
7734
+ ""
7735
+ ];
7736
+ if (status === "conflict") {
7737
+ lines.push(
7738
+ errorCode === "VERSION_CONFLICT" ? "The row changed after the agent saw it." : "The target row was not available under the primary-key and tenant guard.",
7739
+ "",
7740
+ "Result:",
7741
+ "conflict",
7742
+ "",
7743
+ "Source DB changed by Synapsor:",
7744
+ "no",
7745
+ "",
7746
+ "Why:",
7747
+ errorCode === "VERSION_CONFLICT" ? "conflict/version guard did not match" : errorCode || "guarded writeback returned conflict",
7748
+ ""
7749
+ );
6846
7750
  }
6847
- }
7751
+ if (status === "failed") {
7752
+ lines.push("Error:", errorCode || "writeback failed", "");
7753
+ }
7754
+ lines.push(
7755
+ "Receipt:",
7756
+ receiptHash || "(stored locally)",
7757
+ "",
7758
+ "Replay:",
7759
+ `${cliCommandName()} replay ${job.proposal_id} --store ${storePath}`,
7760
+ ""
7761
+ );
7762
+ return `${lines.join("\n")}
7763
+ `;
7764
+ }
7765
+ function formatHandlerApplyResult(receipt, proposalId, storePath) {
7766
+ const title = receipt.status === "conflict" ? "App-owned writeback returned conflict." : receipt.status === "failed" ? "App-owned writeback failed." : receipt.status === "already_applied" ? "App-owned writeback already applied." : "App-owned writeback applied.";
7767
+ const lines = [
7768
+ title,
7769
+ "",
7770
+ "Checks:",
7771
+ "* proposal approved: yes",
7772
+ "* execution authority: app-owned handler outside MCP",
7773
+ `* source database changed by handler: ${receipt.source_database_mutated ? "yes" : "no"}`,
7774
+ `* affected rows: ${receipt.rows_affected}`,
7775
+ `* idempotency key: ${receipt.idempotency_key}`,
7776
+ ""
7777
+ ];
7778
+ if (receipt.status === "conflict") {
7779
+ lines.push(
7780
+ "Result:",
7781
+ "conflict",
7782
+ "",
7783
+ "Why:",
7784
+ receipt.safe_error_code || "handler returned conflict",
7785
+ ""
7786
+ );
7787
+ }
7788
+ if (receipt.status === "failed") {
7789
+ lines.push("Error:", receipt.safe_error_code || "handler writeback failed", "");
7790
+ }
7791
+ lines.push(
7792
+ "Receipt:",
7793
+ receipt.receipt_hash,
7794
+ "",
7795
+ "Replay:",
7796
+ `${cliCommandName()} replay ${proposalId} --store ${storePath}`,
7797
+ ""
7798
+ );
7799
+ return `${lines.join("\n")}
7800
+ `;
7801
+ }
7802
+ function writebackResultStatus(result) {
7803
+ return String(result.status ?? "unknown");
7804
+ }
7805
+ function writebackAffectedRows(result) {
7806
+ const value = result.affected_rows ?? result.rows_affected ?? 0;
7807
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
7808
+ }
7809
+ function writebackErrorCode(result) {
7810
+ const value = result.error_code ?? result.safe_error_code;
7811
+ return typeof value === "string" && value ? value : void 0;
7812
+ }
7813
+ function writebackReceiptHash(result) {
7814
+ const value = result.result_hash ?? result.receipt_hash;
7815
+ return typeof value === "string" && value ? value : void 0;
7816
+ }
7817
+ function parseOptionalJson(text) {
7818
+ if (!text.trim()) return {};
7819
+ try {
7820
+ return JSON.parse(text);
7821
+ } catch {
7822
+ return { status: "failed", safe_error_code: "HANDLER_INVALID_JSON" };
7823
+ }
7824
+ }
6848
7825
  function scalarOrUndefined(value) {
6849
7826
  if (value === void 0) return void 0;
6850
7827
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
@@ -7000,7 +7977,7 @@ async function cloudConnect(args) {
7000
7977
  return 1;
7001
7978
  }
7002
7979
  const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
7003
- const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.3").trim();
7980
+ const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.5").trim();
7004
7981
  const engines = normalizeEngines(parsed.cloud.engines);
7005
7982
  const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
7006
7983
  const client = new ControlPlaneClient({
@@ -7050,6 +8027,7 @@ async function cloudConnect(args) {
7050
8027
  async function mcp(args) {
7051
8028
  const [subcommand, ...rest] = args;
7052
8029
  if (subcommand === "serve") return mcpServe(rest);
8030
+ if (subcommand === "serve-http") return mcpServeHttp(rest);
7053
8031
  if (subcommand === "audit") return mcpAudit(rest);
7054
8032
  if (subcommand === "config") return mcpConfig(rest);
7055
8033
  if (subcommand === "configure") return mcpConfigure(rest);
@@ -7095,16 +8073,98 @@ async function onboard(args) {
7095
8073
  }
7096
8074
  async function demo(args) {
7097
8075
  const [subcommand] = args;
8076
+ if (subcommand === "inspect") return demoInspect(args.slice(1));
7098
8077
  if (subcommand && !subcommand.startsWith("-") && subcommand !== "reference-support-billing") {
7099
8078
  usage(["demo"]);
7100
8079
  return 2;
7101
8080
  }
7102
- if (args.includes("--quick")) return quickDemo();
8081
+ if (args.includes("--quick")) return quickDemo(args);
7103
8082
  return prepareReferenceDemo(args);
7104
8083
  }
7105
- async function quickDemo() {
7106
- process2.stdout.write([
7107
- "Synapsor Runner quick demo (fixture-only)",
8084
+ async function quickDemo(args) {
8085
+ const allowed = /* @__PURE__ */ new Set(["--quick", "--guided", "--no-interactive", "--details", "--json"]);
8086
+ assertKnownOptions(args, allowed, "demo --quick");
8087
+ const seeded = await seedQuickDemoStore(quickDemoStorePath);
8088
+ const summary = quickDemoSummary(seeded);
8089
+ if (args.includes("--json")) {
8090
+ process2.stdout.write(`${JSON.stringify(summary, null, 2)}
8091
+ `);
8092
+ return 0;
8093
+ }
8094
+ if (args.includes("--details")) {
8095
+ process2.stdout.write(formatQuickDemoDetails(seeded));
8096
+ return 0;
8097
+ }
8098
+ const canPauseForInput = Boolean(process2.stdin.isTTY && process2.stdout.isTTY && !process2.env.CI && !process2.env.VITEST);
8099
+ const forceConcise = args.includes("--no-interactive");
8100
+ const forceGuided = args.includes("--guided") && !forceConcise;
8101
+ const shouldGuide = forceGuided || canPauseForInput && !forceConcise;
8102
+ if (shouldGuide) {
8103
+ await runGuidedQuickDemo(seeded, { pause: canPauseForInput });
8104
+ return 0;
8105
+ }
8106
+ process2.stdout.write(formatQuickDemoConcise(seeded));
8107
+ return 0;
8108
+ }
8109
+ async function demoInspect(args) {
8110
+ const allowed = /* @__PURE__ */ new Set(["--npx", "--json"]);
8111
+ assertKnownOptions(args, allowed, "demo inspect");
8112
+ const seeded = await seedQuickDemoStore(quickDemoStorePath);
8113
+ const commands = quickDemoInspectCommands(args.includes("--npx"));
8114
+ if (args.includes("--json")) {
8115
+ process2.stdout.write(`${JSON.stringify({ ...quickDemoSummary(seeded), commands }, null, 2)}
8116
+ `);
8117
+ return 0;
8118
+ }
8119
+ process2.stdout.write(formatQuickDemoInspect(commands));
8120
+ return 0;
8121
+ }
8122
+ function quickDemoSummary(seeded) {
8123
+ return {
8124
+ mode: "fixture_only",
8125
+ store: quickDemoStorePath,
8126
+ proposal_id: seeded.proposal_id,
8127
+ evidence_bundle_id: seeded.evidence_bundle_id,
8128
+ replay_id: seeded.replay_id,
8129
+ model_tool: "billing.propose_late_fee_waiver",
8130
+ business_object: "invoice:INV-3001",
8131
+ proposed_change: { late_fee_cents: { before: 5500, after: 0 } },
8132
+ source_database_changed: false,
8133
+ approval: "required outside MCP"
8134
+ };
8135
+ }
8136
+ function formatQuickDemoConcise(seeded) {
8137
+ void seeded;
8138
+ return [
8139
+ "Synapsor quick demo complete.",
8140
+ "",
8141
+ "The model asked to waive a late fee:",
8142
+ 'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
8143
+ "",
8144
+ "Result:",
8145
+ "* proposal created",
8146
+ "* source DB changed: no",
8147
+ "* approval required outside MCP",
8148
+ "* evidence + replay saved locally",
8149
+ "",
8150
+ "Local ledger:",
8151
+ quickDemoStorePath,
8152
+ "",
8153
+ "Next:",
8154
+ `${cliCommandName()} demo inspect`,
8155
+ ""
8156
+ ].join("\n");
8157
+ }
8158
+ function formatQuickDemoDetails(seeded) {
8159
+ return [
8160
+ "Synapsor Runner quick demo is ready.",
8161
+ "",
8162
+ "This is a fixture-only first look. It does not start Docker, connect a database,",
8163
+ "or require an MCP client. It writes an inspectable local ledger fixture to:",
8164
+ quickDemoStorePath,
8165
+ "",
8166
+ "If you ran this through one-off npx and did not install the package, prefix",
8167
+ "follow-up commands with: npx -y -p @synapsor/runner@alpha synapsor",
7108
8168
  "",
7109
8169
  "Raw MCP shape:",
7110
8170
  "execute_sql(sql: string)",
@@ -7115,7 +8175,7 @@ async function quickDemo() {
7115
8175
  "billing.propose_late_fee_waiver(invoice_id, reason)",
7116
8176
  "",
7117
8177
  "Agent requested:",
7118
- 'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
8178
+ 'billing.propose_late_fee_waiver(invoice_id="INV-3001", reason="approved support waiver")',
7119
8179
  "",
7120
8180
  "Trusted context:",
7121
8181
  "tenant_id = acme",
@@ -7131,14 +8191,360 @@ async function quickDemo() {
7131
8191
  "required outside MCP",
7132
8192
  "",
7133
8193
  "Replay:",
7134
- "available after approval/apply",
8194
+ `${seeded.replay_id} captures the local proposal, evidence handle, query audit, and events.`,
8195
+ `Proposal id: ${seeded.proposal_id}`,
8196
+ `Evidence id: ${seeded.evidence_bundle_id}`,
7135
8197
  "",
7136
- "Next:",
8198
+ "What this proves:",
8199
+ "* The model gets a business tool, not raw SQL.",
8200
+ "* The model created a proposal, not a write.",
8201
+ "* Source DB changed: no.",
8202
+ "* You can inspect evidence and replay locally.",
8203
+ "",
8204
+ "Try:",
8205
+ `1. ${cliCommandName()} proposals show latest --store ${quickDemoStorePath}`,
8206
+ `2. ${cliCommandName()} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`,
8207
+ `3. ${cliCommandName()} replay show latest --store ${quickDemoStorePath}`,
8208
+ "",
8209
+ "For full reviewer detail:",
8210
+ `${cliCommandName()} replay show latest --details --store ${quickDemoStorePath}`,
8211
+ "",
8212
+ "For real guarded writeback against disposable Postgres:",
7137
8213
  `${cliCommandName()} demo`,
7138
- `${cliCommandName()} audit --example dangerous-db-mcp`,
8214
+ ""
8215
+ ].join("\n");
8216
+ }
8217
+ async function runGuidedQuickDemo(seeded, options) {
8218
+ const screens = quickDemoGuidedScreens(seeded);
8219
+ for (const [index, screen] of screens.entries()) {
8220
+ printStep(screen.title, screen.body, index + 1, screens.length);
8221
+ if (index < screens.length - 1) {
8222
+ await waitForEnter("Press Enter to continue...", options);
8223
+ }
8224
+ }
8225
+ }
8226
+ function quickDemoGuidedScreens(seeded) {
8227
+ return [
8228
+ {
8229
+ title: "Synapsor Runner quick demo",
8230
+ body: [
8231
+ "This teaches the Synapsor safety model without Docker, a database, or an MCP client.",
8232
+ "",
8233
+ "It also creates a local fixture ledger you can inspect."
8234
+ ]
8235
+ },
8236
+ {
8237
+ title: "The risky default",
8238
+ body: [
8239
+ "Many database MCP demos expose this:",
8240
+ "",
8241
+ "execute_sql(sql: string)",
8242
+ "",
8243
+ "That means the model can receive database authority directly."
8244
+ ]
8245
+ },
8246
+ {
8247
+ title: "The Synapsor boundary",
8248
+ body: [
8249
+ "Synapsor gives the model business tools instead:",
8250
+ "",
8251
+ "billing.inspect_invoice(invoice_id)",
8252
+ "billing.propose_late_fee_waiver(invoice_id, reason)",
8253
+ "",
8254
+ "The model can ask for a business change.",
8255
+ "It cannot commit the write."
8256
+ ]
8257
+ },
8258
+ {
8259
+ title: "What the agent requested",
8260
+ body: [
8261
+ 'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
8262
+ "",
8263
+ "Proposed change:",
8264
+ "late_fee_cents: 5500 -> 0",
8265
+ "",
8266
+ "Source DB changed:",
8267
+ "no"
8268
+ ]
8269
+ },
8270
+ {
8271
+ title: "What Synapsor saved",
8272
+ body: [
8273
+ "Synapsor saved:",
8274
+ "",
8275
+ "- proposal: what the model requested",
8276
+ "- evidence: what data supported it",
8277
+ "- query audit: what was read",
8278
+ "- replay: what happened later",
8279
+ "",
8280
+ `Proposal: ${seeded.proposal_id}`,
8281
+ `Evidence: ${seeded.evidence_bundle_id}`,
8282
+ `Replay: ${seeded.replay_id}`,
8283
+ "",
8284
+ "Local ledger:",
8285
+ quickDemoStorePath
8286
+ ]
8287
+ },
8288
+ {
8289
+ title: "Inspect it",
8290
+ body: [
8291
+ "Run this next:",
8292
+ "",
8293
+ "npx -y -p @synapsor/runner@alpha synapsor demo inspect",
8294
+ "",
8295
+ "demo inspect shows the proposal, evidence, activity search, and replay commands.",
8296
+ "",
8297
+ "If installed globally, use:",
8298
+ "synapsor demo inspect"
8299
+ ]
8300
+ },
8301
+ {
8302
+ title: "Next paths",
8303
+ body: [
8304
+ "Full disposable Postgres demo:",
8305
+ `${cliCommandName()} demo`,
8306
+ "",
8307
+ "Audit risky MCP database tools:",
8308
+ `${cliCommandName()} audit --example dangerous-db-mcp`,
8309
+ "",
8310
+ "Use your own staging DB:",
8311
+ 'export DATABASE_URL="postgres://..."',
8312
+ `${cliCommandName()} inspect --from-env DATABASE_URL`,
8313
+ "",
8314
+ "Done. You just saw Synapsor's core boundary: business tools for the model, approval/writeback outside the model, and replay for inspection."
8315
+ ]
8316
+ }
8317
+ ];
8318
+ }
8319
+ function printStep(title, body, index, total) {
8320
+ const divider = "------------------------------------------------------------";
8321
+ process2.stdout.write([
8322
+ "",
8323
+ divider,
8324
+ `Step ${index}/${total}: ${title}`,
8325
+ divider,
8326
+ "",
8327
+ ...body,
7139
8328
  ""
7140
8329
  ].join("\n"));
7141
- return 0;
8330
+ }
8331
+ async function waitForEnter(message, options) {
8332
+ if (!options.pause) {
8333
+ process2.stdout.write(`${message}
8334
+ `);
8335
+ return;
8336
+ }
8337
+ const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
8338
+ try {
8339
+ await rl.question(`${message} `);
8340
+ } finally {
8341
+ rl.close();
8342
+ }
8343
+ }
8344
+ function quickDemoInspectCommands(useNpx) {
8345
+ const cmd = useNpx ? "npx -y -p @synapsor/runner@alpha synapsor" : cliCommandName();
8346
+ return [
8347
+ {
8348
+ label: "Proposal summary",
8349
+ description: "See what the model asked to change.",
8350
+ command: `${cmd} proposals show latest --store ${quickDemoStorePath}`
8351
+ },
8352
+ {
8353
+ label: "Evidence",
8354
+ description: "Inspect rows and evidence items captured for the proposal.",
8355
+ command: `${cmd} evidence show ev_quick_INV_3001 --store ${quickDemoStorePath}`
8356
+ },
8357
+ {
8358
+ label: "Activity search",
8359
+ description: "Find the local ledger records for invoice INV-3001.",
8360
+ command: `${cmd} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`
8361
+ },
8362
+ {
8363
+ label: "Replay",
8364
+ description: "Replay the local proposal/evidence/audit story.",
8365
+ command: `${cmd} replay show latest --store ${quickDemoStorePath}`
8366
+ },
8367
+ {
8368
+ label: "Approve outside MCP",
8369
+ description: "Approve the proposal through the local operator boundary.",
8370
+ command: `${cmd} proposals approve latest --yes --store ${quickDemoStorePath}`
8371
+ },
8372
+ {
8373
+ label: "Full Docker-backed demo",
8374
+ description: "Run the disposable Postgres-backed proof.",
8375
+ command: `${cmd} demo`
8376
+ },
8377
+ {
8378
+ label: "Audit risky MCP database tools",
8379
+ description: "Review common dangerous MCP tool shapes.",
8380
+ command: `${cmd} audit --example dangerous-db-mcp`
8381
+ }
8382
+ ];
8383
+ }
8384
+ function formatQuickDemoInspect(commands) {
8385
+ return [
8386
+ "Quick demo inspection",
8387
+ "",
8388
+ "Local ledger:",
8389
+ quickDemoStorePath,
8390
+ "",
8391
+ ...commands.flatMap((item, index) => [
8392
+ `${index + 1}. ${item.label}`,
8393
+ ` ${item.description}`,
8394
+ ` ${item.command}`,
8395
+ ""
8396
+ ])
8397
+ ].join("\n");
8398
+ }
8399
+ async function seedQuickDemoStore(storePath) {
8400
+ const resolved = path3.resolve(storePath);
8401
+ await fs3.mkdir(path3.dirname(resolved), { recursive: true });
8402
+ await fs3.rm(resolved, { force: true });
8403
+ const store = new ProposalStore(resolved);
8404
+ try {
8405
+ const changeSet = quickDemoChangeSet();
8406
+ const proposal = store.createProposal(changeSet);
8407
+ store.recordEvidenceBundle({
8408
+ evidence_bundle_id: changeSet.evidence.bundle_id,
8409
+ proposal_id: proposal.proposal_id,
8410
+ tenant_id: changeSet.scope.tenant_id,
8411
+ payload: {
8412
+ capability: changeSet.action,
8413
+ proposal_id: proposal.proposal_id,
8414
+ source_id: changeSet.source.source_id,
8415
+ target: `${changeSet.source.schema}.${changeSet.source.table}`,
8416
+ principal: changeSet.principal.id,
8417
+ tenant_id: changeSet.scope.tenant_id,
8418
+ business_object: changeSet.scope.business_object,
8419
+ object_id: changeSet.scope.object_id,
8420
+ query_fingerprint: changeSet.evidence.query_fingerprint,
8421
+ source_database_changed: false
8422
+ },
8423
+ items: [
8424
+ {
8425
+ kind: "external_row",
8426
+ source_id: changeSet.source.source_id,
8427
+ table: `${changeSet.source.schema}.${changeSet.source.table}`,
8428
+ primary_key: changeSet.source.primary_key,
8429
+ tenant: changeSet.guards.tenant,
8430
+ visible_row: changeSet.before
8431
+ },
8432
+ {
8433
+ kind: "proposal_diff",
8434
+ before: changeSet.before,
8435
+ patch: changeSet.patch,
8436
+ after: changeSet.after
8437
+ }
8438
+ ]
8439
+ });
8440
+ store.recordQueryAudit({
8441
+ proposal_id: proposal.proposal_id,
8442
+ evidence_bundle_id: changeSet.evidence.bundle_id,
8443
+ source_id: changeSet.source.source_id,
8444
+ query_fingerprint: changeSet.evidence.query_fingerprint,
8445
+ table_name: `${changeSet.source.schema}.${changeSet.source.table}`,
8446
+ row_count: 1,
8447
+ payload: {
8448
+ capability: changeSet.action,
8449
+ tenant_bound: true,
8450
+ statement_template: "SELECT id, tenant_id, updated_at, late_fee_cents FROM invoices WHERE id = ? AND tenant_id = ? LIMIT 1",
8451
+ parameters_redacted: true
8452
+ }
8453
+ });
8454
+ store.replay(proposal.proposal_id);
8455
+ return {
8456
+ proposal_id: proposal.proposal_id,
8457
+ evidence_bundle_id: changeSet.evidence.bundle_id,
8458
+ replay_id: `replay_${proposal.proposal_id}`
8459
+ };
8460
+ } finally {
8461
+ store.close();
8462
+ }
8463
+ }
8464
+ function quickDemoChangeSet() {
8465
+ const base = {
8466
+ schema_version: protocolVersions.changeSet,
8467
+ proposal_id: "wrp_quick_INV_3001",
8468
+ proposal_version: 1,
8469
+ action: "billing.propose_late_fee_waiver",
8470
+ mode: "review_required",
8471
+ principal: {
8472
+ id: "support.agent",
8473
+ source: "trusted_session"
8474
+ },
8475
+ scope: {
8476
+ tenant_id: "acme",
8477
+ business_object: "invoice",
8478
+ object_id: "INV-3001"
8479
+ },
8480
+ source: {
8481
+ kind: "external_postgres",
8482
+ source_id: "app_postgres",
8483
+ schema: "public",
8484
+ table: "invoices",
8485
+ primary_key: {
8486
+ column: "id",
8487
+ value: "INV-3001"
8488
+ }
8489
+ },
8490
+ before: {
8491
+ id: "INV-3001",
8492
+ tenant_id: "acme",
8493
+ updated_at: "2026-06-23T09:00:00Z",
8494
+ late_fee_cents: 5500
8495
+ },
8496
+ patch: {
8497
+ late_fee_cents: 0
8498
+ },
8499
+ after: {
8500
+ id: "INV-3001",
8501
+ tenant_id: "acme",
8502
+ updated_at: "2026-06-23T09:00:00Z",
8503
+ late_fee_cents: 0
8504
+ },
8505
+ guards: {
8506
+ tenant: {
8507
+ column: "tenant_id",
8508
+ value: "acme"
8509
+ },
8510
+ allowed_columns: ["late_fee_cents"],
8511
+ expected_version: {
8512
+ column: "updated_at",
8513
+ value: "2026-06-23T09:00:00Z"
8514
+ }
8515
+ },
8516
+ evidence: {
8517
+ bundle_id: "ev_quick_INV_3001",
8518
+ query_fingerprint: "sha256:quick-demo-invoice-read",
8519
+ items: [
8520
+ {
8521
+ kind: "external_row",
8522
+ source_id: "app_postgres",
8523
+ table: "public.invoices",
8524
+ primary_key: { column: "id", value: "INV-3001" }
8525
+ }
8526
+ ]
8527
+ },
8528
+ approval: {
8529
+ status: "pending",
8530
+ required_role: "local_operator"
8531
+ },
8532
+ writeback: {
8533
+ status: "not_applied",
8534
+ mode: "trusted_worker_required"
8535
+ },
8536
+ source_database_mutated: false,
8537
+ integrity: {
8538
+ proposal_hash: "sha256:placeholder"
8539
+ },
8540
+ created_at: "2026-06-23T09:00:00Z"
8541
+ };
8542
+ return {
8543
+ ...base,
8544
+ integrity: {
8545
+ proposal_hash: hashReceipt({ ...base, integrity: void 0 })
8546
+ }
8547
+ };
7142
8548
  }
7143
8549
  async function mcpServe(args) {
7144
8550
  const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
@@ -7151,8 +8557,46 @@ async function mcpServe(args) {
7151
8557
  });
7152
8558
  return 0;
7153
8559
  }
8560
+ async function mcpServeHttp(args) {
8561
+ const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
8562
+ const readOnly = args.includes("--read-only");
8563
+ const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
8564
+ const host = optionalArg(args, "--host") ?? "127.0.0.1";
8565
+ const port = Number(optionalArg(args, "--port") ?? "8765");
8566
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
8567
+ throw new Error("--port must be an integer from 1 to 65535");
8568
+ }
8569
+ if (host === "0.0.0.0") {
8570
+ process2.stderr.write("Warning: binding Synapsor Runner HTTP MCP to 0.0.0.0 exposes model-facing tools on the network. Use TLS, private networking, authentication, and rate limits.\n");
8571
+ }
8572
+ const server = await startHttpMcpServer({
8573
+ configPath,
8574
+ config,
8575
+ storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
8576
+ host,
8577
+ port,
8578
+ authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
8579
+ devNoAuth: args.includes("--dev-no-auth"),
8580
+ corsOrigin: optionalArg(args, "--cors-origin")
8581
+ });
8582
+ process2.stderr.write("Press Ctrl+C to stop.\n");
8583
+ await new Promise((resolve) => {
8584
+ const stop = async () => {
8585
+ process2.off("SIGINT", stop);
8586
+ process2.off("SIGTERM", stop);
8587
+ await server.close();
8588
+ resolve();
8589
+ };
8590
+ process2.once("SIGINT", stop);
8591
+ process2.once("SIGTERM", stop);
8592
+ });
8593
+ return 0;
8594
+ }
7154
8595
  async function mcpAudit(args) {
7155
- const json = args.includes("--json");
8596
+ const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
8597
+ if (!["text", "json", "markdown"].includes(format)) {
8598
+ throw new Error("audit --format must be text, json, or markdown");
8599
+ }
7156
8600
  const example = optionalArg(args, "--example");
7157
8601
  const target2 = example ? `example:${example}` : firstPositional(args);
7158
8602
  if (!target2) {
@@ -7161,8 +8605,10 @@ async function mcpAudit(args) {
7161
8605
  const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "5000");
7162
8606
  const payload = example ? builtInMcpAuditExample(example) : await readMcpAuditTarget(target2, args, timeoutMs);
7163
8607
  const report = auditMcpManifest(payload, { target: target2 });
7164
- process2.stdout.write(json ? `${JSON.stringify(report, null, 2)}
7165
- ` : formatMcpAuditReport(report));
8608
+ if (format === "json") process2.stdout.write(`${JSON.stringify(report, null, 2)}
8609
+ `);
8610
+ else if (format === "markdown") process2.stdout.write(formatMcpAuditMarkdown(report));
8611
+ else process2.stdout.write(formatMcpAuditReport(report));
7166
8612
  return 0;
7167
8613
  }
7168
8614
  async function propose(args) {
@@ -7227,13 +8673,13 @@ function sampleInputForCapability(capability) {
7227
8673
  const input = {};
7228
8674
  for (const [name, spec] of Object.entries(capability.args)) {
7229
8675
  if (name === capability.lookup.id_from_arg) input[name] = sampleIdForCapability(capability, name);
7230
- else if (/reason/i.test(name)) input[name] = "approved support waiver";
8676
+ else if (/reason/i.test(name)) input[name] = sampleReasonForCapability(capability);
7231
8677
  else if (/resolution/i.test(name)) input[name] = "Resolved after reviewing policy evidence.";
8678
+ else if (spec.enum?.length) input[name] = spec.enum[0];
7232
8679
  else if (/status/i.test(name)) input[name] = "pending_review";
7233
8680
  else if (/amount|cents|fee|credit|balance/i.test(name)) input[name] = typeof spec.maximum === "number" ? Math.min(spec.maximum, 1e3) : 0;
7234
8681
  else if (spec.type === "number") input[name] = spec.minimum ?? 1;
7235
8682
  else if (spec.type === "boolean") input[name] = true;
7236
- else if (spec.enum?.length) input[name] = spec.enum[0];
7237
8683
  else input[name] = `sample_${name}`;
7238
8684
  }
7239
8685
  const missing = Object.entries(capability.args).filter(([, spec]) => spec.required !== false).filter(([name]) => input[name] === void 0).map(([name]) => name);
@@ -7244,12 +8690,20 @@ function sampleInputForCapability(capability) {
7244
8690
  }
7245
8691
  function sampleIdForCapability(capability, argName) {
7246
8692
  const text = `${capability.name} ${capability.target.table} ${argName}`.toLowerCase();
8693
+ const arg = argName.toLowerCase();
7247
8694
  if (/invoice|billing/.test(text)) return "INV-3001";
8695
+ if (/account|customer/.test(arg) || /accounts|customers/.test(text)) return "cust_acme_1";
7248
8696
  if (/ticket|support/.test(text)) return "T-1042";
7249
- if (/order/.test(text)) return "ord_1001";
7250
- if (/account|customer/.test(text)) return "cust_acme_1";
8697
+ if (/order/.test(text)) return "O-1001";
7251
8698
  return "sample_1";
7252
8699
  }
8700
+ function sampleReasonForCapability(capability) {
8701
+ const text = `${capability.name} ${capability.target.table}`.toLowerCase();
8702
+ if (/order|status_change/.test(text)) return "payment cleared and ready for the next status";
8703
+ if (/credit|customer|account/.test(text)) return "support goodwill credit";
8704
+ if (/late_fee|waiver|billing|invoice/.test(text)) return "approved support waiver";
8705
+ return "reviewed and approved by support";
8706
+ }
7253
8707
  function formatProposeResult(capabilityName, result, storePath) {
7254
8708
  const proposalId = String(result.proposal_id ?? "");
7255
8709
  const evidenceId = String(result.evidence_bundle_id ?? "");
@@ -7907,12 +9361,50 @@ async function proposals(args) {
7907
9361
  }
7908
9362
  async function replay(args) {
7909
9363
  const [subcommand, ...rest] = args;
9364
+ if (subcommand === "list") return replayList(rest);
7910
9365
  if (subcommand && !["show", "export"].includes(subcommand)) return replayShow(args);
7911
9366
  if (subcommand === "show") return replayShow(rest);
7912
9367
  if (subcommand === "export") return replayExport(rest);
7913
9368
  usage(["replay"]);
7914
9369
  return 2;
7915
9370
  }
9371
+ async function evidence(args) {
9372
+ const [subcommand, ...rest] = args;
9373
+ if (subcommand === "show") return evidenceShow(rest);
9374
+ if (subcommand === "list") return evidenceList(rest);
9375
+ if (subcommand === "export") return evidenceExport(rest);
9376
+ usage(["evidence"]);
9377
+ return 2;
9378
+ }
9379
+ async function queryAudit(args) {
9380
+ const [subcommand, ...rest] = args;
9381
+ if (subcommand === "list") return queryAuditList(rest);
9382
+ if (subcommand === "show") return queryAuditShow(rest);
9383
+ if (subcommand === "export") return queryAuditExport(rest);
9384
+ usage(["query-audit"]);
9385
+ return 2;
9386
+ }
9387
+ async function receipts(args) {
9388
+ const [subcommand, ...rest] = args;
9389
+ if (subcommand === "list") return receiptsList(rest);
9390
+ if (subcommand === "show") return receiptsShow(rest);
9391
+ usage(["receipts"]);
9392
+ return 2;
9393
+ }
9394
+ async function activity(args) {
9395
+ const [subcommand, ...rest] = args;
9396
+ if (subcommand === "search") return activitySearch(rest);
9397
+ usage(["activity"]);
9398
+ return 2;
9399
+ }
9400
+ async function storeCommand(args) {
9401
+ const [subcommand, ...rest] = args;
9402
+ if (subcommand === "stats") return storeStats(rest);
9403
+ if (subcommand === "vacuum") return storeVacuum(rest);
9404
+ if (subcommand === "prune") return storePrune(rest);
9405
+ usage(["store"]);
9406
+ return 2;
9407
+ }
7916
9408
  async function shadow(args) {
7917
9409
  const [subcommand, ...rest] = args;
7918
9410
  if (subcommand === "list") return shadowList(rest);
@@ -8033,10 +9525,11 @@ async function shadowReport(args) {
8033
9525
  }
8034
9526
  }
8035
9527
  async function proposalsList(args) {
9528
+ assertKnownOptions(args, proposalListAllowedOptions, "proposals list");
8036
9529
  const store = await openLocalStore(args);
8037
9530
  try {
8038
- const state = optionalArg(args, "--state");
8039
- const rows = store.listProposals(state);
9531
+ const filters = proposalFiltersFromArgs(args);
9532
+ const rows = store.listProposals(filters);
8040
9533
  if (args.includes("--json")) {
8041
9534
  process2.stdout.write(`${JSON.stringify({ proposals: rows }, null, 2)}
8042
9535
  `);
@@ -8055,6 +9548,7 @@ async function proposalsList(args) {
8055
9548
  }
8056
9549
  }
8057
9550
  async function proposalsShow(args) {
9551
+ assertKnownOptions(args, showAllowedOptions, "proposals show");
8058
9552
  const proposalId = positional(args, 0);
8059
9553
  if (!proposalId) throw new Error("proposals show requires <proposal_id>");
8060
9554
  const store = await openLocalStore(args);
@@ -8062,16 +9556,17 @@ async function proposalsShow(args) {
8062
9556
  const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
8063
9557
  const proposal = store.getProposal(resolvedProposalId);
8064
9558
  if (!proposal) throw new Error(`proposal not found: ${resolvedProposalId}`);
8065
- const payload = { proposal, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId) };
9559
+ const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
9560
+ const payload = { proposal, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId), evidence: evidence2 };
8066
9561
  if (args.includes("--json")) {
8067
9562
  process2.stdout.write(`${JSON.stringify(payload, null, 2)}
8068
9563
  `);
9564
+ } else if (showDetails(args)) {
9565
+ process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
9566
+ process2.stdout.write(formatProposalEventDetail(payload.events));
9567
+ if (args.includes("--debug")) process2.stdout.write(formatProposalDebug(proposal, optionalArg(args, "--store")));
8069
9568
  } else {
8070
- process2.stdout.write(formatProposalDetail(proposal));
8071
- for (const event of payload.events) {
8072
- process2.stdout.write(`event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}
8073
- `);
8074
- }
9569
+ process2.stdout.write(formatProposalFirstLook(proposal, evidence2?.items.length, proposalId, storeOptionSuffix(args)));
8075
9570
  }
8076
9571
  return 0;
8077
9572
  } finally {
@@ -8086,7 +9581,8 @@ async function proposalsApprove(args) {
8086
9581
  const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
8087
9582
  const proposal = requireLocalProposal(store, resolvedProposalId);
8088
9583
  if (!args.includes("--json")) {
8089
- process2.stdout.write(formatProposalDetail(proposal));
9584
+ const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
9585
+ process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
8090
9586
  }
8091
9587
  await confirmDangerousAction(args, `Approve proposal ${resolvedProposalId} for guarded writeback?`);
8092
9588
  const updated = store.approveProposal(resolvedProposalId, {
@@ -8113,7 +9609,8 @@ async function proposalsReject(args) {
8113
9609
  const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
8114
9610
  const proposal = requireLocalProposal(store, resolvedProposalId);
8115
9611
  if (!args.includes("--json")) {
8116
- process2.stdout.write(formatProposalDetail(proposal));
9612
+ const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
9613
+ process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
8117
9614
  }
8118
9615
  await confirmDangerousAction(args, `Reject proposal ${resolvedProposalId}?`);
8119
9616
  const updated = store.rejectProposal(resolvedProposalId, {
@@ -8157,58 +9654,628 @@ async function proposalsWritebackJob(args) {
8157
9654
  store.close();
8158
9655
  }
8159
9656
  }
8160
- async function replayShow(args) {
8161
- const proposalId = positional(args, 0);
8162
- if (!proposalId) throw new Error("replay show requires <proposal_id>");
9657
+ async function evidenceList(args) {
9658
+ assertKnownOptions(args, evidenceListAllowedOptions, "evidence list");
8163
9659
  const store = await openLocalStore(args);
8164
9660
  try {
8165
- const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
8166
- const replayRecord = store.replay(resolvedProposalId);
9661
+ const rows = store.listEvidenceBundles(evidenceFiltersFromArgs(args));
8167
9662
  if (args.includes("--json")) {
8168
- process2.stdout.write(`${JSON.stringify(replayRecord, null, 2)}
9663
+ process2.stdout.write(`${JSON.stringify({ evidence: rows }, null, 2)}
8169
9664
  `);
9665
+ } else if (rows.length === 0) {
9666
+ process2.stdout.write("No evidence bundles found.\n");
8170
9667
  } else {
8171
- process2.stdout.write(`Replay ${replayRecord.replay_id}
8172
- `);
8173
- process2.stdout.write(formatProposalDetail(replayRecord.proposal));
8174
- process2.stdout.write(`events: ${replayRecord.events.length}
8175
- `);
8176
- process2.stdout.write(`receipts: ${replayRecord.receipts.length}
8177
- `);
8178
- process2.stdout.write(`evidence bundles: ${replayRecord.evidence.length}
8179
- `);
8180
- process2.stdout.write(`query audit records: ${replayRecord.query_audit.length}
8181
- `);
9668
+ for (const bundle of rows) process2.stdout.write(formatEvidenceSummary(bundle));
8182
9669
  }
8183
9670
  return 0;
8184
9671
  } finally {
8185
9672
  store.close();
8186
9673
  }
8187
9674
  }
8188
- async function replayExport(args) {
8189
- const proposalId = positional(args, 0);
8190
- if (!proposalId) throw new Error("replay export requires <proposal_id>");
8191
- const output = outputArg(args);
8192
- if (!output) throw new Error("replay export requires --output <path>");
9675
+ async function evidenceShow(args) {
9676
+ assertKnownOptions(args, showAllowedOptions, "evidence show");
9677
+ const evidenceId = positional(args, 0);
9678
+ if (!evidenceId) throw new Error("evidence show requires <evidence_bundle_id>");
8193
9679
  const store = await openLocalStore(args);
8194
9680
  try {
8195
- const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
8196
- const replayRecord = store.replay(resolvedProposalId);
8197
- await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
8198
- await fs3.writeFile(output, `${JSON.stringify(replayRecord, null, 2)}
8199
- `, "utf8");
8200
- process2.stdout.write(`exported ${replayRecord.replay_id} to ${output}
9681
+ const evidence2 = store.getEvidenceBundle(evidenceId);
9682
+ if (!evidence2) throw new Error(`evidence bundle not found: ${evidenceId}`);
9683
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(evidence2, null, 2)}
8201
9684
  `);
9685
+ else if (showDetails(args)) process2.stdout.write(formatEvidenceDetail(evidence2));
9686
+ else process2.stdout.write(formatEvidenceFirstLook(evidence2, storeOptionSuffix(args)));
8202
9687
  return 0;
8203
9688
  } finally {
8204
9689
  store.close();
8205
9690
  }
8206
9691
  }
8207
- async function openLocalStore(args) {
8208
- const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
8209
- if (storePath !== ":memory:") {
8210
- await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
8211
- }
9692
+ async function evidenceExport(args) {
9693
+ assertKnownOptions(args, exportAllowedOptions, "evidence export");
9694
+ const evidenceId = positional(args, 0) ?? optionalArg(args, "--evidence");
9695
+ if (!evidenceId) throw new Error("evidence export requires <evidence_bundle_id>");
9696
+ const output = outputArg(args);
9697
+ if (!output) throw new Error("evidence export requires --output <path>");
9698
+ const format = exportFormat(args);
9699
+ const store = await openLocalStore(args);
9700
+ try {
9701
+ const evidence2 = store.getEvidenceBundle(evidenceId);
9702
+ if (!evidence2) throw new Error(`evidence bundle not found: ${evidenceId}`);
9703
+ const text = format === "json" ? `${JSON.stringify(evidence2, null, 2)}
9704
+ ` : formatEvidenceMarkdown(evidence2);
9705
+ await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
9706
+ await fs3.writeFile(output, text, "utf8");
9707
+ process2.stdout.write(`exported ${evidence2.evidence_bundle_id} to ${output}
9708
+ `);
9709
+ return 0;
9710
+ } finally {
9711
+ store.close();
9712
+ }
9713
+ }
9714
+ async function queryAuditList(args) {
9715
+ assertKnownOptions(args, queryAuditListAllowedOptions, "query-audit list");
9716
+ const store = await openLocalStore(args);
9717
+ try {
9718
+ const rows = store.listQueryAudit(queryAuditFiltersFromArgs(args));
9719
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ query_audit: rows }, null, 2)}
9720
+ `);
9721
+ else if (rows.length === 0) process2.stdout.write("No query audit records found.\n");
9722
+ else for (const row of rows) process2.stdout.write(formatQueryAuditSummary(row, showDetails(args), storeOptionSuffix(args)));
9723
+ return 0;
9724
+ } finally {
9725
+ store.close();
9726
+ }
9727
+ }
9728
+ async function queryAuditShow(args) {
9729
+ assertKnownOptions(args, showAllowedOptions, "query-audit show");
9730
+ const auditId = Number(positional(args, 0));
9731
+ if (!Number.isInteger(auditId) || auditId <= 0) throw new Error("query-audit show requires <audit_id>");
9732
+ const store = await openLocalStore(args);
9733
+ try {
9734
+ const row = store.getQueryAudit(auditId);
9735
+ if (!row) throw new Error(`query audit record not found: ${auditId}`);
9736
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(row, null, 2)}
9737
+ `);
9738
+ else process2.stdout.write(showDetails(args) ? formatQueryAuditDetail(row) : formatQueryAuditFirstLook(row, storeOptionSuffix(args)));
9739
+ return 0;
9740
+ } finally {
9741
+ store.close();
9742
+ }
9743
+ }
9744
+ async function queryAuditExport(args) {
9745
+ assertKnownOptions(args, exportAllowedOptions, "query-audit export");
9746
+ const auditId = Number(positional(args, 0) ?? optionalArg(args, "--audit"));
9747
+ if (!Number.isInteger(auditId) || auditId <= 0) throw new Error("query-audit export requires <audit_id>");
9748
+ const output = outputArg(args);
9749
+ if (!output) throw new Error("query-audit export requires --output <path>");
9750
+ const format = exportFormat(args, ["json"]);
9751
+ const store = await openLocalStore(args);
9752
+ try {
9753
+ const row = store.getQueryAudit(auditId);
9754
+ if (!row) throw new Error(`query audit record not found: ${auditId}`);
9755
+ await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
9756
+ await fs3.writeFile(output, `${JSON.stringify(row, null, 2)}
9757
+ `, "utf8");
9758
+ process2.stdout.write(`exported query audit ${auditId} to ${output}
9759
+ `);
9760
+ return 0;
9761
+ } finally {
9762
+ store.close();
9763
+ }
9764
+ }
9765
+ async function receiptsList(args) {
9766
+ assertKnownOptions(args, receiptListAllowedOptions, "receipts list");
9767
+ const store = await openLocalStore(args);
9768
+ try {
9769
+ const rows = store.listReceipts(receiptFiltersFromArgs(args));
9770
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ receipts: rows }, null, 2)}
9771
+ `);
9772
+ else if (rows.length === 0) process2.stdout.write("No writeback receipts found.\n");
9773
+ else for (const receipt of rows) process2.stdout.write(formatReceiptSummary(receipt));
9774
+ return 0;
9775
+ } finally {
9776
+ store.close();
9777
+ }
9778
+ }
9779
+ async function receiptsShow(args) {
9780
+ assertKnownOptions(args, showAllowedOptions, "receipts show");
9781
+ const receiptId = Number(positional(args, 0));
9782
+ if (!Number.isInteger(receiptId) || receiptId <= 0) throw new Error("receipts show requires <receipt_id>");
9783
+ const store = await openLocalStore(args);
9784
+ try {
9785
+ const receipt = store.getReceipt(receiptId);
9786
+ if (!receipt) throw new Error(`writeback receipt not found: ${receiptId}`);
9787
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(receipt, null, 2)}
9788
+ `);
9789
+ else process2.stdout.write(showDetails(args) ? formatReceiptDetail(receipt) : formatReceiptFirstLook(receipt, storeOptionSuffix(args)));
9790
+ return 0;
9791
+ } finally {
9792
+ store.close();
9793
+ }
9794
+ }
9795
+ async function replayList(args) {
9796
+ assertKnownOptions(args, replayListAllowedOptions, "replay list");
9797
+ const store = await openLocalStore(args);
9798
+ try {
9799
+ const filters = proposalFiltersFromReplayArgs(args, store);
9800
+ const proposals2 = store.listProposals(filters);
9801
+ const rows = proposals2.map((proposal) => ({
9802
+ replay_id: `replay_${proposal.proposal_id}`,
9803
+ proposal_id: proposal.proposal_id,
9804
+ created_at: proposal.created_at,
9805
+ state: proposal.state,
9806
+ tenant_id: proposal.tenant_id,
9807
+ principal: proposal.principal ?? proposal.change_set.principal.id,
9808
+ capability: proposal.action,
9809
+ business_object: proposal.business_object,
9810
+ object_id: proposal.object_id
9811
+ }));
9812
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ replays: rows }, null, 2)}
9813
+ `);
9814
+ else if (rows.length === 0) process2.stdout.write("No replay records found.\n");
9815
+ else for (const row of rows) process2.stdout.write(formatReplaySummary(row));
9816
+ return 0;
9817
+ } finally {
9818
+ store.close();
9819
+ }
9820
+ }
9821
+ async function replayShow(args) {
9822
+ assertKnownOptions(args, replayShowAllowedOptions, "replay show");
9823
+ const store = await openLocalStore(args);
9824
+ try {
9825
+ const resolvedProposalId = resolveReplayProposalId(args, store);
9826
+ const replayRecord = store.replay(resolvedProposalId);
9827
+ if (args.includes("--json")) {
9828
+ process2.stdout.write(`${JSON.stringify(replayRecord, null, 2)}
9829
+ `);
9830
+ } else if (showDetails(args)) {
9831
+ process2.stdout.write(formatReplayDetail(replayRecord));
9832
+ if (args.includes("--debug")) process2.stdout.write(formatReplayDebug(replayRecord, optionalArg(args, "--store")));
9833
+ } else {
9834
+ process2.stdout.write(formatReplayFirstLook(replayRecord, storeOptionSuffix(args)));
9835
+ }
9836
+ return 0;
9837
+ } finally {
9838
+ store.close();
9839
+ }
9840
+ }
9841
+ async function replayExport(args) {
9842
+ assertKnownOptions(args, replayExportAllowedOptions, "replay export");
9843
+ const output = outputArg(args);
9844
+ if (!output) throw new Error("replay export requires --output <path>");
9845
+ const format = exportFormat(args);
9846
+ const store = await openLocalStore(args);
9847
+ try {
9848
+ const resolvedProposalId = resolveReplayProposalId(args, store);
9849
+ const replayRecord = store.replay(resolvedProposalId);
9850
+ const text = format === "json" ? `${JSON.stringify(replayRecord, null, 2)}
9851
+ ` : formatReplayMarkdown(replayRecord);
9852
+ await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
9853
+ await fs3.writeFile(output, text, "utf8");
9854
+ process2.stdout.write(`exported ${replayRecord.replay_id} to ${output}
9855
+ `);
9856
+ return 0;
9857
+ } finally {
9858
+ store.close();
9859
+ }
9860
+ }
9861
+ async function activitySearch(args) {
9862
+ assertKnownOptions(args, activitySearchAllowedOptions, "activity search");
9863
+ const store = await openLocalStore(args);
9864
+ try {
9865
+ const proposalFilters = proposalFiltersFromActivityArgs(args, store);
9866
+ const evidenceFilters = evidenceFiltersFromActivityArgs(args, store);
9867
+ const queryAuditFilters = queryAuditFiltersFromActivityArgs(args, store);
9868
+ const receiptFilters = receiptFiltersFromActivityArgs(args, store);
9869
+ const proposals2 = store.listProposals(proposalFilters);
9870
+ const evidenceRows = store.listEvidenceBundles(evidenceFilters);
9871
+ const queryAuditRows = store.listQueryAudit(queryAuditFilters);
9872
+ const receiptsRows = store.listReceipts(receiptFilters);
9873
+ const proposalIds = new Set(proposals2.map((proposal) => proposal.proposal_id));
9874
+ const evidenceIds = new Set(evidenceRows.map((evidence2) => evidence2.evidence_bundle_id));
9875
+ const results = proposals2.map((proposal) => activityFromProposal(proposal));
9876
+ for (const evidence2 of evidenceRows) {
9877
+ if (evidence2.proposal_id && proposalIds.has(evidence2.proposal_id)) continue;
9878
+ results.push(activityFromEvidence(evidence2));
9879
+ }
9880
+ for (const audit2 of queryAuditRows) {
9881
+ const proposalId = stringField(audit2, "proposal_id");
9882
+ const evidenceId = stringField(audit2, "evidence_bundle_id");
9883
+ if (proposalId && proposalIds.has(proposalId)) continue;
9884
+ if (evidenceId && evidenceIds.has(evidenceId)) continue;
9885
+ results.push(activityFromQueryAudit(audit2));
9886
+ }
9887
+ for (const receipt of receiptsRows) {
9888
+ if (proposalIds.has(receipt.proposal_id)) continue;
9889
+ results.push(activityFromReceipt(receipt));
9890
+ }
9891
+ const sorted = results.sort((left, right) => String(right.created_at ?? "").localeCompare(String(left.created_at ?? ""))).slice(0, limitFromArgs(args));
9892
+ if (args.includes("--json")) {
9893
+ process2.stdout.write(`${JSON.stringify({ interactions: sorted }, null, 2)}
9894
+ `);
9895
+ } else if (sorted.length === 0) {
9896
+ process2.stdout.write("No local interactions found.\n");
9897
+ } else {
9898
+ process2.stdout.write(`Found ${sorted.length} local interaction${sorted.length === 1 ? "" : "s"}
9899
+
9900
+ `);
9901
+ sorted.forEach((item, index) => process2.stdout.write(formatActivityItem(item, index + 1, showDetails(args))));
9902
+ process2.stdout.write(formatActivityNext(sorted, storeOptionSuffix(args)));
9903
+ }
9904
+ return 0;
9905
+ } finally {
9906
+ store.close();
9907
+ }
9908
+ }
9909
+ async function storeStats(args) {
9910
+ assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
9911
+ const store = await openLocalStore(args);
9912
+ try {
9913
+ const stats = store.stats();
9914
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(stats, null, 2)}
9915
+ `);
9916
+ else process2.stdout.write(formatStoreStats(stats));
9917
+ return 0;
9918
+ } finally {
9919
+ store.close();
9920
+ }
9921
+ }
9922
+ async function storeVacuum(args) {
9923
+ assertKnownOptions(args, storeVacuumAllowedOptions, "store vacuum");
9924
+ const store = await openLocalStore(args);
9925
+ try {
9926
+ const before = store.stats();
9927
+ store.vacuum();
9928
+ const after = store.stats();
9929
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ before, after }, null, 2)}
9930
+ `);
9931
+ else process2.stdout.write(`vacuumed local store ${before.path}
9932
+ approx bytes: ${before.approx_bytes} -> ${after.approx_bytes}
9933
+ `);
9934
+ return 0;
9935
+ } finally {
9936
+ store.close();
9937
+ }
9938
+ }
9939
+ async function storePrune(args) {
9940
+ assertKnownOptions(args, storePruneAllowedOptions, "store prune");
9941
+ const olderThan = optionalArg(args, "--older-than");
9942
+ if (!olderThan) throw new Error("store prune requires --older-than <duration>, for example --older-than 30d");
9943
+ if (args.includes("--yes") && args.includes("--dry-run")) throw new Error("store prune accepts either --dry-run or --yes, not both");
9944
+ const cutoff = cutoffFromOlderThan(olderThan);
9945
+ const dryRun = !args.includes("--yes");
9946
+ const store = await openLocalStore(args);
9947
+ try {
9948
+ const result = store.pruneBefore(cutoff, { dryRun });
9949
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
9950
+ `);
9951
+ else process2.stdout.write(formatStorePrune(result));
9952
+ return 0;
9953
+ } finally {
9954
+ store.close();
9955
+ }
9956
+ }
9957
+ var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json", "--details", "--debug"]);
9958
+ var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
9959
+ var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
9960
+ var proposalListAllowedOptions = /* @__PURE__ */ new Set([
9961
+ ...commonReadOptions,
9962
+ "--tenant",
9963
+ "--principal",
9964
+ "--capability",
9965
+ "--action",
9966
+ "--object",
9967
+ "--object-type",
9968
+ "--object-id",
9969
+ "--status",
9970
+ "--state",
9971
+ "--source",
9972
+ "--table",
9973
+ "--from",
9974
+ "--to",
9975
+ "--limit"
9976
+ ]);
9977
+ var evidenceListAllowedOptions = /* @__PURE__ */ new Set([
9978
+ ...commonReadOptions,
9979
+ "--tenant",
9980
+ "--principal",
9981
+ "--capability",
9982
+ "--proposal",
9983
+ "--object",
9984
+ "--object-type",
9985
+ "--object-id",
9986
+ "--source",
9987
+ "--table",
9988
+ "--query-fingerprint",
9989
+ "--from",
9990
+ "--to",
9991
+ "--limit"
9992
+ ]);
9993
+ var queryAuditListAllowedOptions = /* @__PURE__ */ new Set([
9994
+ ...commonReadOptions,
9995
+ "--tenant",
9996
+ "--proposal",
9997
+ "--evidence",
9998
+ "--source",
9999
+ "--table",
10000
+ "--primary-key",
10001
+ "--query-fingerprint",
10002
+ "--from",
10003
+ "--to",
10004
+ "--limit"
10005
+ ]);
10006
+ var receiptListAllowedOptions = /* @__PURE__ */ new Set([
10007
+ ...commonReadOptions,
10008
+ "--proposal",
10009
+ "--writeback-job",
10010
+ "--idempotency-key",
10011
+ "--status",
10012
+ "--from",
10013
+ "--to",
10014
+ "--limit"
10015
+ ]);
10016
+ var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
10017
+ var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
10018
+ var replayListAllowedOptions = /* @__PURE__ */ new Set([
10019
+ ...commonReadOptions,
10020
+ "--tenant",
10021
+ "--principal",
10022
+ "--capability",
10023
+ "--proposal",
10024
+ "--evidence",
10025
+ "--receipt",
10026
+ "--object",
10027
+ "--object-type",
10028
+ "--object-id",
10029
+ "--status",
10030
+ "--state",
10031
+ "--from",
10032
+ "--to",
10033
+ "--limit"
10034
+ ]);
10035
+ var activitySearchAllowedOptions = /* @__PURE__ */ new Set([
10036
+ ...commonReadOptions,
10037
+ "--tenant",
10038
+ "--principal",
10039
+ "--capability",
10040
+ "--object",
10041
+ "--object-type",
10042
+ "--object-id",
10043
+ "--proposal",
10044
+ "--evidence",
10045
+ "--replay",
10046
+ "--receipt",
10047
+ "--source",
10048
+ "--table",
10049
+ "--query-fingerprint",
10050
+ "--status",
10051
+ "--state",
10052
+ "--from",
10053
+ "--to",
10054
+ "--limit"
10055
+ ]);
10056
+ var storeStatsAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
10057
+ var storeVacuumAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
10058
+ var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes"]);
10059
+ function assertKnownOptions(args, allowed, commandName) {
10060
+ for (const arg of args) {
10061
+ if (!arg.startsWith("--")) continue;
10062
+ const option = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
10063
+ if (option === "--help" || option === "-h") continue;
10064
+ if (!allowed.has(option)) throw new Error(`Unknown option for ${commandName}: ${option}`);
10065
+ }
10066
+ }
10067
+ function proposalFiltersFromArgs(args) {
10068
+ const object = objectFilterFromArgs(args);
10069
+ return {
10070
+ proposal: optionalArg(args, "--proposal"),
10071
+ tenant: optionalArg(args, "--tenant"),
10072
+ principal: optionalArg(args, "--principal"),
10073
+ capability: optionalArg(args, "--capability"),
10074
+ action: optionalArg(args, "--action"),
10075
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10076
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10077
+ status: optionalArg(args, "--status"),
10078
+ state: optionalArg(args, "--state"),
10079
+ source: optionalArg(args, "--source"),
10080
+ table: optionalArg(args, "--table"),
10081
+ from: optionalArg(args, "--from"),
10082
+ to: optionalArg(args, "--to"),
10083
+ limit: limitFromArgs(args)
10084
+ };
10085
+ }
10086
+ function evidenceFiltersFromArgs(args) {
10087
+ const object = objectFilterFromArgs(args);
10088
+ return {
10089
+ evidence: optionalArg(args, "--evidence"),
10090
+ tenant: optionalArg(args, "--tenant"),
10091
+ principal: optionalArg(args, "--principal"),
10092
+ capability: optionalArg(args, "--capability"),
10093
+ proposal: optionalArg(args, "--proposal"),
10094
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10095
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10096
+ source: optionalArg(args, "--source"),
10097
+ table: optionalArg(args, "--table"),
10098
+ queryFingerprint: optionalArg(args, "--query-fingerprint"),
10099
+ from: optionalArg(args, "--from"),
10100
+ to: optionalArg(args, "--to"),
10101
+ limit: limitFromArgs(args)
10102
+ };
10103
+ }
10104
+ function queryAuditFiltersFromArgs(args) {
10105
+ const object = objectFilterFromArgs(args);
10106
+ return {
10107
+ tenant: optionalArg(args, "--tenant"),
10108
+ principal: optionalArg(args, "--principal"),
10109
+ capability: optionalArg(args, "--capability"),
10110
+ proposal: optionalArg(args, "--proposal"),
10111
+ evidence: optionalArg(args, "--evidence"),
10112
+ source: optionalArg(args, "--source"),
10113
+ table: optionalArg(args, "--table"),
10114
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10115
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10116
+ primaryKey: optionalArg(args, "--primary-key"),
10117
+ queryFingerprint: optionalArg(args, "--query-fingerprint"),
10118
+ from: optionalArg(args, "--from"),
10119
+ to: optionalArg(args, "--to"),
10120
+ limit: limitFromArgs(args)
10121
+ };
10122
+ }
10123
+ function receiptFiltersFromArgs(args) {
10124
+ return {
10125
+ receipt: optionalArg(args, "--receipt"),
10126
+ proposal: optionalArg(args, "--proposal"),
10127
+ writebackJob: optionalArg(args, "--writeback-job"),
10128
+ idempotencyKey: optionalArg(args, "--idempotency-key"),
10129
+ status: optionalArg(args, "--status"),
10130
+ from: optionalArg(args, "--from"),
10131
+ to: optionalArg(args, "--to"),
10132
+ limit: limitFromArgs(args)
10133
+ };
10134
+ }
10135
+ function proposalFiltersFromReplayArgs(args, store) {
10136
+ return proposalFiltersFromActivityArgs(args, store);
10137
+ }
10138
+ function proposalFiltersFromActivityArgs(args, store) {
10139
+ const object = objectFilterFromArgs(args);
10140
+ const linkedProposal = linkedProposalFilter(args, store);
10141
+ return {
10142
+ proposal: optionalArg(args, "--proposal") ?? linkedProposal,
10143
+ tenant: optionalArg(args, "--tenant"),
10144
+ principal: optionalArg(args, "--principal"),
10145
+ capability: optionalArg(args, "--capability"),
10146
+ action: optionalArg(args, "--capability"),
10147
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10148
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10149
+ status: optionalArg(args, "--status"),
10150
+ state: optionalArg(args, "--state"),
10151
+ source: optionalArg(args, "--source"),
10152
+ table: optionalArg(args, "--table"),
10153
+ from: optionalArg(args, "--from"),
10154
+ to: optionalArg(args, "--to"),
10155
+ limit: limitFromArgs(args)
10156
+ };
10157
+ }
10158
+ function evidenceFiltersFromActivityArgs(args, store) {
10159
+ const object = objectFilterFromArgs(args);
10160
+ const linkedProposal = linkedProposalFilter(args, store, { includeEvidence: false });
10161
+ return {
10162
+ evidence: optionalArg(args, "--evidence"),
10163
+ tenant: optionalArg(args, "--tenant"),
10164
+ principal: optionalArg(args, "--principal"),
10165
+ capability: optionalArg(args, "--capability"),
10166
+ proposal: optionalArg(args, "--proposal") ?? linkedProposal,
10167
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10168
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10169
+ source: optionalArg(args, "--source"),
10170
+ table: optionalArg(args, "--table"),
10171
+ queryFingerprint: optionalArg(args, "--query-fingerprint"),
10172
+ from: optionalArg(args, "--from"),
10173
+ to: optionalArg(args, "--to"),
10174
+ limit: limitFromArgs(args)
10175
+ };
10176
+ }
10177
+ function queryAuditFiltersFromActivityArgs(args, store) {
10178
+ const object = objectFilterFromArgs(args);
10179
+ const linkedProposal = linkedProposalFilter(args, store, { includeEvidence: false });
10180
+ return {
10181
+ tenant: optionalArg(args, "--tenant"),
10182
+ principal: optionalArg(args, "--principal"),
10183
+ capability: optionalArg(args, "--capability"),
10184
+ proposal: optionalArg(args, "--proposal") ?? linkedProposal,
10185
+ evidence: optionalArg(args, "--evidence"),
10186
+ source: optionalArg(args, "--source"),
10187
+ table: optionalArg(args, "--table"),
10188
+ objectType: optionalArg(args, "--object-type") ?? object.type,
10189
+ objectId: optionalArg(args, "--object-id") ?? object.id,
10190
+ queryFingerprint: optionalArg(args, "--query-fingerprint"),
10191
+ from: optionalArg(args, "--from"),
10192
+ to: optionalArg(args, "--to"),
10193
+ limit: limitFromArgs(args)
10194
+ };
10195
+ }
10196
+ function receiptFiltersFromActivityArgs(args, store) {
10197
+ const linkedProposal = linkedProposalFilter(args, store, { includeReceipt: false });
10198
+ return {
10199
+ receipt: optionalArg(args, "--receipt"),
10200
+ proposal: optionalArg(args, "--proposal") ?? linkedProposal,
10201
+ status: optionalArg(args, "--status") ?? optionalArg(args, "--state"),
10202
+ from: optionalArg(args, "--from"),
10203
+ to: optionalArg(args, "--to"),
10204
+ limit: limitFromArgs(args)
10205
+ };
10206
+ }
10207
+ function linkedProposalFilter(args, store, options = {}) {
10208
+ const noLinkedProposal = "__synapsor_no_linked_proposal__";
10209
+ const replay2 = optionalArg(args, "--replay");
10210
+ if (replay2) return proposalIdFromReplayId(replay2);
10211
+ if (!store) return void 0;
10212
+ if (options.includeEvidence !== false) {
10213
+ const evidence2 = optionalArg(args, "--evidence");
10214
+ if (evidence2) return store.proposalIdForEvidence(evidence2) ?? noLinkedProposal;
10215
+ }
10216
+ if (options.includeReceipt !== false) {
10217
+ const receiptValue = optionalArg(args, "--receipt");
10218
+ if (receiptValue) {
10219
+ const receiptId = Number(receiptValue);
10220
+ if (!Number.isInteger(receiptId) || receiptId <= 0) throw new Error("--receipt must be a positive receipt id");
10221
+ return store.getReceipt(receiptId)?.proposal_id ?? noLinkedProposal;
10222
+ }
10223
+ }
10224
+ return void 0;
10225
+ }
10226
+ function objectFilterFromArgs(args) {
10227
+ const value = optionalArg(args, "--object");
10228
+ if (!value) return {};
10229
+ const separator = value.indexOf(":");
10230
+ if (separator <= 0 || separator === value.length - 1) {
10231
+ throw new Error("--object must use type:id, for example invoice:INV-3001");
10232
+ }
10233
+ return { type: value.slice(0, separator), id: value.slice(separator + 1) };
10234
+ }
10235
+ function limitFromArgs(args) {
10236
+ const value = optionalArg(args, "--limit");
10237
+ if (!value) return 20;
10238
+ const parsed = Number(value);
10239
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error("--limit must be a positive integer");
10240
+ return Math.min(parsed, 200);
10241
+ }
10242
+ function exportFormat(args, supported = ["json", "markdown"]) {
10243
+ const format = optionalArg(args, "--format") ?? "json";
10244
+ if (!supported.includes(format)) {
10245
+ throw new Error(`unsupported export format: ${format}. Supported formats: ${supported.join(", ")}`);
10246
+ }
10247
+ return format;
10248
+ }
10249
+ function resolveReplayProposalId(args, store) {
10250
+ const explicitProposal = optionalArg(args, "--proposal");
10251
+ if (explicitProposal) return resolveProposalIdFromStore(explicitProposal, store);
10252
+ const explicitReplay = optionalArg(args, "--replay");
10253
+ if (explicitReplay) return proposalIdFromReplayId(explicitReplay);
10254
+ const explicitEvidence = optionalArg(args, "--evidence");
10255
+ if (explicitEvidence) {
10256
+ const proposalId = store.proposalIdForEvidence(explicitEvidence);
10257
+ if (!proposalId) throw new Error(`evidence bundle ${explicitEvidence} is not linked to a replayable proposal`);
10258
+ return proposalId;
10259
+ }
10260
+ const value = positional(args, 0);
10261
+ if (!value) throw new Error("replay show requires <proposal_id>, --proposal <proposal_id>, --replay <replay_id>, or --evidence <evidence_bundle_id>");
10262
+ if (value === "latest") return resolveProposalIdFromStore(value, store);
10263
+ if (value.startsWith("replay_")) return proposalIdFromReplayId(value);
10264
+ if (value.startsWith("ev_")) throw new Error(`Use --evidence ${value} to replay from an evidence bundle.`);
10265
+ return resolveProposalIdFromStore(value, store);
10266
+ }
10267
+ function proposalIdFromReplayId(replayId) {
10268
+ if (!replayId.startsWith("replay_")) throw new Error(`invalid replay id: ${replayId}`);
10269
+ const proposalId = replayId.slice("replay_".length);
10270
+ if (!proposalId) throw new Error(`invalid replay id: ${replayId}`);
10271
+ return proposalId;
10272
+ }
10273
+ async function openLocalStore(args) {
10274
+ const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
10275
+ if (storePath !== ":memory:") {
10276
+ if (!await fileExists(storePath)) throw missingLocalStoreError(storePath);
10277
+ await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
10278
+ }
8212
10279
  return new ProposalStore(storePath);
8213
10280
  }
8214
10281
  async function writeFileGuarded(filePath, content, force) {
@@ -8251,6 +10318,7 @@ function requireLocalProposal(store, proposalId) {
8251
10318
  }
8252
10319
  async function resolveProposalId(proposalId, storePath) {
8253
10320
  if (proposalId !== "latest") return proposalId;
10321
+ if (storePath !== ":memory:" && !await fileExists(storePath)) throw missingLocalStoreError(storePath);
8254
10322
  const store = new ProposalStore(storePath);
8255
10323
  try {
8256
10324
  return resolveProposalIdFromStore(proposalId, store);
@@ -8264,6 +10332,15 @@ function resolveProposalIdFromStore(proposalId, store) {
8264
10332
  if (!latest) throw new Error("no proposals found in the local store");
8265
10333
  return latest.proposal_id;
8266
10334
  }
10335
+ function missingLocalStoreError(storePath) {
10336
+ return new Error([
10337
+ `No local Synapsor proposal store was found at ${storePath}.`,
10338
+ "Run:",
10339
+ `${cliCommandName()} demo`,
10340
+ "or pass:",
10341
+ "--store /path/to/local.db"
10342
+ ].join("\n"));
10343
+ }
8267
10344
  async function readRuntimeConfig(configPath) {
8268
10345
  const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
8269
10346
  return parsed;
@@ -8414,25 +10491,38 @@ function firstPositional(args) {
8414
10491
  "--allowed-columns",
8415
10492
  "--approval-role",
8416
10493
  "--actor",
10494
+ "--action",
10495
+ "--auth-token-env",
10496
+ "--audit",
8417
10497
  "--bearer-env",
10498
+ "--capability",
8418
10499
  "--config",
8419
10500
  "--conflict-column",
10501
+ "--cors-origin",
8420
10502
  "--database-url-env",
8421
10503
  "--destination",
8422
10504
  "--engine",
10505
+ "--evidence",
8423
10506
  "--example",
10507
+ "--format",
8424
10508
  "--from",
8425
10509
  "--from-env",
8426
10510
  "--host",
10511
+ "--idempotency-key",
8427
10512
  "--input",
8428
10513
  "--job",
8429
10514
  "--lease-seconds",
10515
+ "--limit",
8430
10516
  "--lookup-arg",
8431
10517
  "--mode",
8432
10518
  "--mcp-config",
8433
10519
  "--namespace",
8434
10520
  "--numeric-bound",
10521
+ "--object",
10522
+ "--object-id",
10523
+ "--object-type",
8435
10524
  "--object-name",
10525
+ "--older-than",
8436
10526
  "--output",
8437
10527
  "--out",
8438
10528
  "--patch-fixed",
@@ -8440,22 +10530,31 @@ function firstPositional(args) {
8440
10530
  "--port",
8441
10531
  "--primary-key",
8442
10532
  "--principal-env",
10533
+ "--proposal",
8443
10534
  "--project",
10535
+ "--query-fingerprint",
8444
10536
  "--reason",
8445
10537
  "--recipe",
10538
+ "--receipt",
10539
+ "--replay",
8446
10540
  "--runner",
8447
10541
  "--schema",
8448
10542
  "--source-name",
10543
+ "--source",
8449
10544
  "--state",
10545
+ "--status",
8450
10546
  "--stdio",
8451
10547
  "--store",
8452
10548
  "--table",
10549
+ "--tenant",
8453
10550
  "--tenant-env",
8454
10551
  "--tenant-key",
8455
10552
  "--timeout-ms",
10553
+ "--to",
8456
10554
  "--transition-guard",
8457
10555
  "--url",
8458
10556
  "--visible-columns",
10557
+ "--writeback-job",
8459
10558
  "--write-url-env"
8460
10559
  ]);
8461
10560
  for (let index = 0; index < args.length; index += 1) {
@@ -8531,25 +10630,60 @@ function parseJsonRpcResponse(stdout, id) {
8531
10630
  }
8532
10631
  function formatProposalSummary(proposal) {
8533
10632
  return [
8534
- `${proposal.proposal_id} ${proposal.state} ${proposal.action}`,
10633
+ `${proposal.created_at} ${proposal.proposal_id} ${proposal.state} ${proposal.action}`,
10634
+ ` object: ${proposal.business_object}:${proposal.object_id}`,
8535
10635
  ` target: ${proposal.source_kind}:${proposal.source_id}/${proposal.source_schema}.${proposal.source_table}/${proposal.object_id}`,
8536
10636
  ` tenant: ${proposal.tenant_id} source changed: ${proposal.source_database_mutated ? "yes" : "no"}`
8537
10637
  ].join("\n") + "\n";
8538
10638
  }
8539
- function formatProposalDetail(proposal) {
10639
+ function formatProposalFirstLook(proposal, storedEvidenceItemCount, proposalRef, storeSuffix) {
10640
+ const evidenceItems = storedEvidenceItemCount ?? proposal.change_set.evidence.items?.length ?? 0;
10641
+ return [
10642
+ `Proposal ${proposal.proposal_id}`,
10643
+ `Status: ${humanStatus(proposal.state)}`,
10644
+ "",
10645
+ "Agent requested:",
10646
+ proposal.action,
10647
+ "",
10648
+ "Business object:",
10649
+ `${proposal.business_object} ${proposal.object_id}`,
10650
+ "",
10651
+ "Proposed change:",
10652
+ ...formatChangeLines(proposal).map((line) => line.replace(/^ /, "")),
10653
+ "",
10654
+ "Source DB changed:",
10655
+ proposal.source_database_mutated ? "yes" : "no",
10656
+ "",
10657
+ "Approval:",
10658
+ approvalBoundary(proposal),
10659
+ "",
10660
+ "Evidence:",
10661
+ `${proposal.change_set.evidence.bundle_id}${evidenceItems > 0 ? ` (${plural(evidenceItems, "item")})` : ""}`,
10662
+ "",
10663
+ "Next:",
10664
+ ...proposalNextCommands(proposal, proposalRef, storeSuffix).map((command) => `${command}`),
10665
+ "",
10666
+ "More detail:",
10667
+ `${cliCommandName()} proposals show ${proposalRef} --details${storeSuffix}`,
10668
+ ""
10669
+ ].join("\n");
10670
+ }
10671
+ function formatProposalDetail(proposal, storedEvidenceItemCount) {
8540
10672
  const changeSet = proposal.change_set;
8541
10673
  const conflictGuard = changeSet.guards.expected_version;
8542
- const evidenceItems = changeSet.evidence.items?.length ?? 0;
10674
+ const evidenceItems = storedEvidenceItemCount ?? changeSet.evidence.items?.length ?? 0;
8543
10675
  const approvalStatus = currentApprovalStatus(proposal);
8544
10676
  const writebackStatus = currentWritebackStatus(proposal);
8545
10677
  return [
8546
- `proposal: ${proposal.proposal_id}`,
8547
- `state: ${proposal.state}`,
8548
- `action: ${proposal.action}`,
10678
+ `Proposal details: ${proposal.proposal_id}`,
10679
+ "",
10680
+ "Review details:",
8549
10681
  `principal: ${changeSet.principal.id} (${changeSet.principal.source})`,
8550
10682
  `tenant: ${proposal.tenant_id}`,
8551
10683
  `target: ${proposal.source_kind}:${proposal.source_id}/${proposal.source_schema}.${proposal.source_table}/${proposal.object_id}`,
8552
10684
  `primary key: ${changeSet.source.primary_key.column}=${formatScalar(changeSet.source.primary_key.value)}`,
10685
+ `status: ${proposal.state}`,
10686
+ `action: ${proposal.action}`,
8553
10687
  `approval: ${approvalStatus}${changeSet.approval.required_role ? ` required role ${changeSet.approval.required_role}` : ""}`,
8554
10688
  `proposal hash: ${proposal.proposal_hash}`,
8555
10689
  `proposal version: ${proposal.proposal_version}`,
@@ -8558,14 +10692,565 @@ function formatProposalDetail(proposal) {
8558
10692
  `evidence: ${changeSet.evidence.bundle_id} query ${changeSet.evidence.query_fingerprint} items ${evidenceItems}`,
8559
10693
  `writeback: ${writebackStatus} via ${changeSet.writeback.mode}`,
8560
10694
  `source database changed: ${proposal.source_database_mutated ? "yes" : "no"}`,
8561
- "diff:",
8562
- ...Object.keys(changeSet.patch).map((column) => {
8563
- const before = changeSet.before[column];
8564
- const proposed = changeSet.after[column];
8565
- return ` ${column}: ${JSON.stringify(before)} -> ${JSON.stringify(proposed)}`;
8566
- })
10695
+ "",
10696
+ "Diff:",
10697
+ ...formatChangeLines(proposal)
10698
+ ].join("\n") + "\n";
10699
+ }
10700
+ function formatProposalEventDetail(events) {
10701
+ if (events.length === 0) return "Events:\n none\n";
10702
+ return [
10703
+ "Events:",
10704
+ ...events.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
10705
+ ].join("\n") + "\n";
10706
+ }
10707
+ function formatProposalDebug(proposal, storePath) {
10708
+ return [
10709
+ "Debug:",
10710
+ `store: ${storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db"}`,
10711
+ `interaction id: ${proposal.interaction_id ?? "none"}`,
10712
+ `tool call id: ${proposal.tool_call_id ?? "none"}`,
10713
+ `source kind: ${proposal.source_kind}`,
10714
+ `writeback mode: ${proposal.change_set.writeback.mode}`,
10715
+ ""
10716
+ ].join("\n");
10717
+ }
10718
+ function formatEvidenceSummary(evidence2) {
10719
+ return [
10720
+ `${evidence2.created_at} ${evidence2.evidence_bundle_id}`,
10721
+ ` tenant: ${evidence2.tenant_id} capability: ${evidence2.capability ?? "unknown"} proposal: ${evidence2.proposal_id ?? "none"}`,
10722
+ ` source: ${evidence2.source_id ?? "unknown"}/${evidence2.source_table ?? "unknown"} object: ${evidence2.business_object ?? "object"}:${evidence2.object_id ?? "unknown"}`
8567
10723
  ].join("\n") + "\n";
8568
10724
  }
10725
+ function formatEvidenceFirstLook(evidence2, storeSuffix) {
10726
+ const object = evidence2.business_object && evidence2.object_id ? `${evidence2.business_object} ${evidence2.object_id}` : "not linked";
10727
+ const lines = [
10728
+ `Evidence ${evidence2.evidence_bundle_id}`,
10729
+ "",
10730
+ "Used for:",
10731
+ evidence2.capability ?? "unknown capability",
10732
+ object,
10733
+ "",
10734
+ "Captured:",
10735
+ plural(evidence2.items.length, "evidence item"),
10736
+ plural(evidence2.query_audit.length, "query audit record"),
10737
+ "",
10738
+ "Source:",
10739
+ `${evidence2.source_id ?? "unknown"} / ${evidence2.source_table ?? "unknown"}`,
10740
+ "",
10741
+ "Rows:",
10742
+ ...evidence2.items.flatMap((item, index) => formatEvidenceItem(item, index + 1)),
10743
+ "",
10744
+ "Next:",
10745
+ ` ${cliCommandName()} query-audit list --evidence ${evidence2.evidence_bundle_id}${storeSuffix}`,
10746
+ ...evidence2.proposal_id ? [` ${cliCommandName()} replay show --proposal ${evidence2.proposal_id}${storeSuffix}`] : [],
10747
+ "",
10748
+ "More detail:",
10749
+ ` ${cliCommandName()} evidence show ${evidence2.evidence_bundle_id} --details${storeSuffix}`
10750
+ ];
10751
+ return `${lines.join("\n")}
10752
+ `;
10753
+ }
10754
+ function formatEvidenceDetail(evidence2) {
10755
+ const audit2 = evidence2.query_audit[0];
10756
+ const lines = [
10757
+ `Evidence bundle: ${evidence2.evidence_bundle_id}`,
10758
+ `Tenant: ${evidence2.tenant_id}`,
10759
+ `Proposal: ${evidence2.proposal_id ?? "none"}`,
10760
+ `Principal: ${evidence2.principal ?? "unknown"}`,
10761
+ `Capability: ${evidence2.capability ?? "unknown"}`,
10762
+ `Source: ${evidence2.source_id ?? "unknown"}`,
10763
+ `Table: ${evidence2.source_table ?? "unknown"}`,
10764
+ `Query fingerprint: ${evidence2.query_fingerprint ?? stringField(audit2, "query_fingerprint") ?? "unknown"}`,
10765
+ `Rows captured: ${evidence2.items.length}`,
10766
+ `Created at: ${evidence2.created_at}`,
10767
+ "Projection: captured visible fields only; credentials and secret-looking values are rejected before persistence.",
10768
+ "",
10769
+ "Items:",
10770
+ ...evidence2.items.flatMap((item, index) => formatEvidenceItem(item, index + 1)),
10771
+ "",
10772
+ "Related:",
10773
+ ...evidence2.proposal_id ? [` ${cliCommandName()} proposals show ${evidence2.proposal_id}`, ` ${cliCommandName()} replay show --proposal ${evidence2.proposal_id}`] : [],
10774
+ ` ${cliCommandName()} query-audit list --evidence ${evidence2.evidence_bundle_id}`
10775
+ ];
10776
+ return `${lines.join("\n")}
10777
+ `;
10778
+ }
10779
+ function formatEvidenceItem(item, index) {
10780
+ const payload = isRecord6(item.item) ? item.item : item;
10781
+ const visibleRow = isRecord6(payload.visible_row) ? payload.visible_row : payload;
10782
+ const title = stringField(payload, "kind") ?? "item";
10783
+ const primaryKey = isRecord6(payload.primary_key) ? payload.primary_key : void 0;
10784
+ const heading = primaryKey ? `* ${title} ${formatScalar(primaryKey.value)}` : `* ${title} ${index}`;
10785
+ const rows = Object.entries(visibleRow).filter(([key]) => !["kind", "source_id", "table", "primary_key", "tenant"].includes(key)).flatMap(([key, value]) => formatEvidenceFieldLines(key, value)).slice(0, 12);
10786
+ return [heading, ...rows.length ? rows : [" (no scalar preview fields)"]];
10787
+ }
10788
+ function formatEvidenceFieldLines(key, value) {
10789
+ if (isRecord6(value)) {
10790
+ const nested = Object.entries(value).filter(([, nestedValue]) => nestedValue === null || ["string", "number", "boolean"].includes(typeof nestedValue)).slice(0, 6).map(([nestedKey, nestedValue]) => ` ${key}.${nestedKey}: ${formatScalar(nestedValue)}`);
10791
+ return nested.length ? nested : [` ${key}: [object]`];
10792
+ }
10793
+ return [` ${key}: ${formatScalar(value)}`];
10794
+ }
10795
+ function formatEvidenceMarkdown(evidence2) {
10796
+ return [
10797
+ `# Evidence ${evidence2.evidence_bundle_id}`,
10798
+ "",
10799
+ `- Tenant: ${evidence2.tenant_id}`,
10800
+ `- Proposal: ${evidence2.proposal_id ?? "none"}`,
10801
+ `- Principal: ${evidence2.principal ?? "unknown"}`,
10802
+ `- Capability: ${evidence2.capability ?? "unknown"}`,
10803
+ `- Source: ${evidence2.source_id ?? "unknown"}`,
10804
+ `- Table: ${evidence2.source_table ?? "unknown"}`,
10805
+ `- Query fingerprint: ${evidence2.query_fingerprint ?? "unknown"}`,
10806
+ `- Created at: ${evidence2.created_at}`,
10807
+ "",
10808
+ "## Captured Items",
10809
+ "",
10810
+ "```json",
10811
+ JSON.stringify(evidence2.items, null, 2),
10812
+ "```",
10813
+ "",
10814
+ "## Query Audit",
10815
+ "",
10816
+ "```json",
10817
+ JSON.stringify(evidence2.query_audit, null, 2),
10818
+ "```"
10819
+ ].join("\n") + "\n";
10820
+ }
10821
+ function formatQueryAuditSummary(row, details = false, storeSuffix = "") {
10822
+ const lines = [
10823
+ `${row.created_at} audit ${row.audit_id}`,
10824
+ ` source: ${row.source_id}/${row.table_name} rows: ${row.row_count}`,
10825
+ ` proposal: ${row.proposal_id ?? "none"} evidence: ${row.evidence_bundle_id ?? "none"}`,
10826
+ ...details ? [` query fingerprint: ${row.query_fingerprint}`] : [],
10827
+ ` detail: ${cliCommandName()} query-audit show ${row.audit_id}${details ? "" : " --details"}${storeSuffix}`
10828
+ ];
10829
+ return lines.join("\n") + "\n";
10830
+ }
10831
+ function formatQueryAuditFirstLook(row, storeSuffix) {
10832
+ return [
10833
+ `Query audit ${row.audit_id}`,
10834
+ "",
10835
+ "Read:",
10836
+ `${row.source_id}/${row.table_name}`,
10837
+ "",
10838
+ "Rows returned:",
10839
+ String(row.row_count ?? "unknown"),
10840
+ "",
10841
+ "Linked records:",
10842
+ `proposal: ${row.proposal_id ?? "none"}`,
10843
+ `evidence: ${row.evidence_bundle_id ?? "none"}`,
10844
+ "",
10845
+ "More detail:",
10846
+ `${cliCommandName()} query-audit show ${row.audit_id} --details${storeSuffix}`,
10847
+ ""
10848
+ ].join("\n");
10849
+ }
10850
+ function formatQueryAuditDetail(row) {
10851
+ const payload = isRecord6(row.payload) ? row.payload : {};
10852
+ return [
10853
+ `Query audit: ${row.audit_id}`,
10854
+ `Created at: ${row.created_at}`,
10855
+ `Source: ${row.source_id}`,
10856
+ `Table: ${row.table_name}`,
10857
+ `Rows: ${row.row_count}`,
10858
+ `Query fingerprint: ${row.query_fingerprint}`,
10859
+ `Proposal: ${row.proposal_id ?? "none"}`,
10860
+ `Evidence: ${row.evidence_bundle_id ?? "none"}`,
10861
+ `Tenant: ${row.tenant_id ?? "unknown"}`,
10862
+ `Capability: ${row.capability ?? payload.capability ?? "unknown"}`,
10863
+ `Parameters redacted: ${payload.parameters_redacted === true ? "yes" : "unknown"}`,
10864
+ "",
10865
+ "Payload:",
10866
+ JSON.stringify(payload, null, 2)
10867
+ ].join("\n") + "\n";
10868
+ }
10869
+ function formatReceiptSummary(receipt) {
10870
+ return [
10871
+ `${receipt.created_at} receipt ${receipt.receipt_id} ${receipt.status}`,
10872
+ ` proposal: ${receipt.proposal_id} job: ${receipt.writeback_job_id}`,
10873
+ ` idempotency: ${receipt.idempotency_key} source changed: ${receipt.source_database_mutated ? "yes" : "no"}`
10874
+ ].join("\n") + "\n";
10875
+ }
10876
+ function formatReceiptFirstLook(receipt, storeSuffix) {
10877
+ const checks = receipt.status === "applied" ? ["primary key matched", "tenant guard matched", "allowed columns only", "conflict guard passed"] : receipt.status === "conflict" ? ["primary key matched", "tenant guard matched", "conflict guard blocked stale write"] : ["guarded writeback did not apply"];
10878
+ return [
10879
+ `Receipt ${formatReceiptId(receipt.receipt_id)}`,
10880
+ `Status: ${humanStatus(receipt.status)}`,
10881
+ "",
10882
+ "Proposal:",
10883
+ receipt.proposal_id,
10884
+ "",
10885
+ "Writeback:",
10886
+ "guarded single-row update",
10887
+ "",
10888
+ "Checks:",
10889
+ ...checks.map((check) => `${check}`),
10890
+ `affected rows: ${receipt.receipt.rows_affected}`,
10891
+ "",
10892
+ "Source DB changed:",
10893
+ receipt.source_database_mutated ? "yes" : "no",
10894
+ "",
10895
+ "Next:",
10896
+ `${cliCommandName()} replay show --proposal ${receipt.proposal_id}${storeSuffix}`,
10897
+ "",
10898
+ "More detail:",
10899
+ `${cliCommandName()} receipts show ${receipt.receipt_id} --details${storeSuffix}`,
10900
+ ""
10901
+ ].join("\n");
10902
+ }
10903
+ function formatReceiptDetail(receipt) {
10904
+ return [
10905
+ `Receipt: ${receipt.receipt_id}`,
10906
+ `Proposal: ${receipt.proposal_id}`,
10907
+ `Writeback job: ${receipt.writeback_job_id}`,
10908
+ `Runner: ${receipt.runner_id}`,
10909
+ `Status: ${receipt.status}`,
10910
+ `Idempotency key: ${receipt.idempotency_key}`,
10911
+ `Source database mutated: ${receipt.source_database_mutated ? "yes" : "no"}`,
10912
+ `Rows affected: ${receipt.receipt.rows_affected}`,
10913
+ `Safe error: ${receipt.receipt.safe_error_code ?? "none"}`,
10914
+ `Receipt hash: ${receipt.receipt.receipt_hash}`,
10915
+ `Created at: ${receipt.created_at}`,
10916
+ "",
10917
+ "Related:",
10918
+ ` ${cliCommandName()} replay show --proposal ${receipt.proposal_id}`
10919
+ ].join("\n") + "\n";
10920
+ }
10921
+ function formatReplaySummary(row) {
10922
+ return [
10923
+ `${row.created_at} ${row.replay_id}`,
10924
+ ` proposal: ${row.proposal_id} status: ${row.state}`,
10925
+ ` tenant: ${row.tenant_id} capability: ${row.capability} object: ${row.business_object}:${row.object_id}`
10926
+ ].join("\n") + "\n";
10927
+ }
10928
+ function formatReplayFirstLook(replay2, storeSuffix) {
10929
+ const proposal = replay2.proposal;
10930
+ const evidenceItems = replay2.evidence.reduce((count, item) => {
10931
+ const evidence2 = item;
10932
+ return count + (Array.isArray(evidence2.items) ? evidence2.items.length : 0);
10933
+ }, 0);
10934
+ const latestReceipt = replay2.receipts.at(-1);
10935
+ const writebackStatus = latestReceipt ? humanStatus(latestReceipt.status) : humanStatus(currentWritebackStatus(proposal));
10936
+ const approvalLine = proposal.state === "pending_review" ? "Approval is still pending" : `Proposal is ${humanStatus(proposal.state)}`;
10937
+ return [
10938
+ `Replay ${replay2.replay_id}`,
10939
+ "",
10940
+ "What happened:",
10941
+ `1. Agent called ${proposal.action}`,
10942
+ `2. Runner read ${proposal.business_object} ${proposal.object_id} under tenant ${proposal.tenant_id}`,
10943
+ `3. Runner created evidence bundle ${proposal.change_set.evidence.bundle_id}`,
10944
+ "4. Runner created a proposal",
10945
+ `5. Source DB changed: ${proposal.source_database_mutated ? "yes" : "no"}`,
10946
+ `6. ${approvalLine}`,
10947
+ "",
10948
+ "Proposed change:",
10949
+ ...formatChangeLines(proposal).map((line) => line.replace(/^ /, "")),
10950
+ "",
10951
+ "Evidence:",
10952
+ plural(replay2.query_audit.length, "query audit record"),
10953
+ plural(evidenceItems, "evidence item"),
10954
+ "",
10955
+ "Writeback:",
10956
+ writebackStatus,
10957
+ ...latestReceipt ? [`source DB changed after writeback: ${latestReceipt.source_database_mutated ? "yes" : "no"}`] : [],
10958
+ "",
10959
+ "Next:",
10960
+ ` ${cliCommandName()} evidence show ${proposal.change_set.evidence.bundle_id}${storeSuffix}`,
10961
+ ...proposal.state === "pending_review" ? [` ${cliCommandName()} proposals approve ${proposal.proposal_id} --yes${storeSuffix}`] : [],
10962
+ "",
10963
+ "More detail:",
10964
+ ` ${cliCommandName()} replay show --proposal ${proposal.proposal_id} --details${storeSuffix}`,
10965
+ ""
10966
+ ].join("\n");
10967
+ }
10968
+ function formatReplayDetail(replay2) {
10969
+ const evidenceItems = replay2.evidence.reduce((count, item) => {
10970
+ const evidence2 = item;
10971
+ return count + (Array.isArray(evidence2.items) ? evidence2.items.length : 0);
10972
+ }, 0);
10973
+ return [
10974
+ `Replay details ${replay2.replay_id}`,
10975
+ formatProposalDetail(replay2.proposal, evidenceItems).trimEnd(),
10976
+ `events: ${replay2.events.length}`,
10977
+ ...replay2.events.map((event) => ` ${event.kind} by ${event.actor} at ${event.created_at}`),
10978
+ `receipts: ${replay2.receipts.length}`,
10979
+ ...replay2.receipts.map((receipt) => ` receipt ${receipt.receipt_id}: ${receipt.status} job ${receipt.writeback_job_id}`),
10980
+ `evidence bundles: ${replay2.evidence.length}`,
10981
+ ...replay2.evidence.map((evidence2) => ` ${evidence2.evidence_bundle_id ?? "unknown"}`),
10982
+ `query audit records: ${replay2.query_audit.length}`,
10983
+ ...replay2.query_audit.map((record) => ` audit ${record.audit_id}: ${record.source_id}/${record.table_name} rows ${record.row_count}`)
10984
+ ].join("\n") + "\n";
10985
+ }
10986
+ function formatReplayDebug(replay2, storePath) {
10987
+ return [
10988
+ "Debug:",
10989
+ `store: ${storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db"}`,
10990
+ `generated at: ${replay2.generated_at}`,
10991
+ `event ids: ${replay2.events.map((event) => event.event_id).join(", ") || "none"}`,
10992
+ `receipt ids: ${replay2.receipts.map((receipt) => receipt.receipt_id).join(", ") || "none"}`,
10993
+ ""
10994
+ ].join("\n");
10995
+ }
10996
+ function formatReplayMarkdown(replay2) {
10997
+ const proposal = replay2.proposal;
10998
+ const principal = proposal.change_set.principal.id;
10999
+ const approvalEvents = replay2.events.filter((event) => /approved|rejected|canceled/i.test(event.kind));
11000
+ const evidenceLines = replay2.evidence.length > 0 ? replay2.evidence.flatMap((evidence2) => {
11001
+ const record = evidence2;
11002
+ const payload = isRecord6(record.payload) ? record.payload : {};
11003
+ const sourceId = stringField(payload, "source_id") ?? proposal.source_id;
11004
+ const table = stringField(payload, "target") ?? `${proposal.source_schema}.${proposal.source_table}`;
11005
+ const queryFingerprint = stringField(payload, "query_fingerprint") ?? proposal.change_set.evidence.query_fingerprint;
11006
+ return [
11007
+ `- evidence: ${record.evidence_bundle_id ?? proposal.change_set.evidence.bundle_id}`,
11008
+ ` - source: ${sourceId}.${table}`,
11009
+ ` - query fingerprint: ${queryFingerprint}`,
11010
+ ` - rows captured: ${Array.isArray(record.items) ? record.items.length : 0}`
11011
+ ];
11012
+ }) : [`- evidence: ${proposal.change_set.evidence.bundle_id}`, ` - source: ${proposal.source_id}.${proposal.source_schema}.${proposal.source_table}`, ` - query fingerprint: ${proposal.change_set.evidence.query_fingerprint}`, " - rows captured: 0"];
11013
+ const receiptLines = replay2.receipts.length > 0 ? replay2.receipts.flatMap((receipt) => [
11014
+ `- receipt: ${receipt.receipt_id}`,
11015
+ ` - status: ${receipt.status}`,
11016
+ ` - affected rows: ${receipt.receipt.rows_affected}`,
11017
+ ` - idempotency key: ${receipt.idempotency_key}`,
11018
+ ` - source database mutated: ${receipt.source_database_mutated ? "yes" : "no"}`,
11019
+ ...receipt.receipt.safe_error_code ? [` - safe error: ${receipt.receipt.safe_error_code}`] : []
11020
+ ]) : ["- no writeback receipt recorded yet"];
11021
+ return [
11022
+ "# Synapsor Replay",
11023
+ "",
11024
+ `Proposal: ${proposal.proposal_id}`,
11025
+ `Capability: ${proposal.action}`,
11026
+ `Tenant: ${proposal.tenant_id}`,
11027
+ `Object: ${proposal.business_object}:${proposal.object_id}`,
11028
+ `Status: ${proposal.state}`,
11029
+ "",
11030
+ "## What The Agent Requested",
11031
+ "",
11032
+ `The model-facing capability requested \`${proposal.action}\` for ${proposal.business_object}:${proposal.object_id}.`,
11033
+ "The source database was not mutated when the proposal was created.",
11034
+ "",
11035
+ "## Trusted Context",
11036
+ "",
11037
+ `tenant_id = ${proposal.tenant_id}`,
11038
+ `principal = ${principal}`,
11039
+ `principal_source = ${proposal.change_set.principal.source}`,
11040
+ "",
11041
+ "## Evidence",
11042
+ "",
11043
+ ...evidenceLines,
11044
+ "",
11045
+ "## Proposed Diff",
11046
+ "",
11047
+ ...Object.keys(proposal.change_set.patch).map((column) => `- ${column}: ${JSON.stringify(proposal.change_set.before[column])} -> ${JSON.stringify(proposal.change_set.after[column])}`),
11048
+ "",
11049
+ "## Approval",
11050
+ "",
11051
+ ...approvalEvents.length > 0 ? approvalEvents.map((event) => `- ${event.kind} by ${event.actor} at ${event.created_at}`) : [`- ${proposal.change_set.approval.status}${proposal.change_set.approval.required_role ? `; required role: ${proposal.change_set.approval.required_role}` : ""}`],
11052
+ "",
11053
+ "## Guarded Writeback",
11054
+ "",
11055
+ ...receiptLines,
11056
+ "",
11057
+ "## Query Audit",
11058
+ "",
11059
+ ...replay2.query_audit.map((record) => `- audit ${record.audit_id}: ${record.source_id}/${record.table_name} rows ${record.row_count} fingerprint ${record.query_fingerprint}`),
11060
+ "",
11061
+ "## Replay Note",
11062
+ "",
11063
+ "This is local captured interaction replay, not external database time travel. It reconstructs what the runner recorded: trusted context, evidence handles, proposal diff, approval events, query audit, and writeback receipts."
11064
+ ].join("\n") + "\n";
11065
+ }
11066
+ function activityFromProposal(proposal) {
11067
+ return {
11068
+ kind: "proposal",
11069
+ created_at: proposal.created_at,
11070
+ capability: proposal.action,
11071
+ tenant: proposal.tenant_id,
11072
+ principal: proposal.principal ?? proposal.change_set.principal.id,
11073
+ object: `${proposal.business_object}:${proposal.object_id}`,
11074
+ proposal: proposal.proposal_id,
11075
+ evidence: proposal.change_set.evidence.bundle_id,
11076
+ status: proposal.state,
11077
+ replay: `replay_${proposal.proposal_id}`,
11078
+ source: proposal.source_id,
11079
+ table: `${proposal.source_schema}.${proposal.source_table}`
11080
+ };
11081
+ }
11082
+ function activityFromEvidence(evidence2) {
11083
+ return {
11084
+ kind: "evidence",
11085
+ created_at: evidence2.created_at,
11086
+ capability: evidence2.capability,
11087
+ tenant: evidence2.tenant_id,
11088
+ principal: evidence2.principal,
11089
+ object: evidence2.business_object && evidence2.object_id ? `${evidence2.business_object}:${evidence2.object_id}` : void 0,
11090
+ proposal: evidence2.proposal_id,
11091
+ evidence: evidence2.evidence_bundle_id,
11092
+ status: "evidence_recorded",
11093
+ source: evidence2.source_id,
11094
+ table: evidence2.source_table
11095
+ };
11096
+ }
11097
+ function activityFromQueryAudit(audit2) {
11098
+ const businessObject = stringField(audit2, "business_object");
11099
+ const objectId = stringField(audit2, "object_id") ?? stringField(audit2, "primary_key_value");
11100
+ return {
11101
+ kind: "query-audit",
11102
+ created_at: stringField(audit2, "created_at"),
11103
+ capability: stringField(audit2, "capability"),
11104
+ tenant: stringField(audit2, "tenant_id"),
11105
+ principal: stringField(audit2, "principal"),
11106
+ object: businessObject && objectId ? `${businessObject}:${objectId}` : void 0,
11107
+ proposal: stringField(audit2, "proposal_id"),
11108
+ evidence: stringField(audit2, "evidence_bundle_id"),
11109
+ status: "query_audited",
11110
+ source: stringField(audit2, "source_id"),
11111
+ table: stringField(audit2, "table_name"),
11112
+ query_audit: stringField(audit2, "audit_id"),
11113
+ query_fingerprint: stringField(audit2, "query_fingerprint")
11114
+ };
11115
+ }
11116
+ function activityFromReceipt(receipt) {
11117
+ return {
11118
+ kind: "receipt",
11119
+ created_at: receipt.created_at,
11120
+ proposal: receipt.proposal_id,
11121
+ receipt: receipt.receipt_id,
11122
+ status: receipt.status,
11123
+ replay: `replay_${receipt.proposal_id}`,
11124
+ source_database_mutated: receipt.source_database_mutated
11125
+ };
11126
+ }
11127
+ function formatActivityItem(item, index, details = false) {
11128
+ const lines = [
11129
+ `${index}. ${item.created_at}`,
11130
+ ` kind: ${item.kind}`,
11131
+ ...item.capability ? [` capability: ${item.capability}`] : [],
11132
+ ...item.tenant ? [` tenant: ${item.tenant}`] : [],
11133
+ ...item.object ? [` object: ${item.object}`] : [],
11134
+ ...item.proposal ? [` proposal: ${item.proposal}`] : [],
11135
+ ...item.evidence ? [` evidence: ${item.evidence}`] : [],
11136
+ ...item.query_audit ? [` query audit: ${item.query_audit}`] : [],
11137
+ ...details && item.query_fingerprint ? [` query fingerprint: ${item.query_fingerprint}`] : [],
11138
+ ...item.receipt ? [` receipt: ${item.receipt}`] : [],
11139
+ ...item.status ? [` status: ${humanStatus(String(item.status))}`] : [],
11140
+ ...item.replay ? [` replay: ${item.replay}`] : [],
11141
+ ""
11142
+ ];
11143
+ return lines.join("\n");
11144
+ }
11145
+ function formatActivityNext(items, storeSuffix) {
11146
+ const first = items[0];
11147
+ if (!first) return "";
11148
+ const proposal = stringField(first, "proposal");
11149
+ const replayId = stringField(first, "replay");
11150
+ const evidence2 = stringField(first, "evidence");
11151
+ const lines = ["Next:"];
11152
+ if (proposal) {
11153
+ lines.push(`${cliCommandName()} proposals show ${proposal}${storeSuffix}`);
11154
+ lines.push(`${cliCommandName()} replay show --proposal ${proposal}${storeSuffix}`);
11155
+ } else if (replayId) {
11156
+ lines.push(`${cliCommandName()} replay show --replay ${replayId}${storeSuffix}`);
11157
+ } else if (evidence2) {
11158
+ lines.push(`${cliCommandName()} evidence show ${evidence2}${storeSuffix}`);
11159
+ } else {
11160
+ lines.push(`${cliCommandName()} activity search --details${storeSuffix}`);
11161
+ }
11162
+ lines.push("");
11163
+ return `${lines.join("\n")}
11164
+ `;
11165
+ }
11166
+ function formatStoreStats(stats) {
11167
+ return [
11168
+ `Local store: ${stats.path}`,
11169
+ `Approx size: ${stats.approx_bytes} bytes`,
11170
+ `Proposals: ${stats.proposals}`,
11171
+ `Evidence bundles: ${stats.evidence_bundles}`,
11172
+ `Evidence items: ${stats.evidence_items}`,
11173
+ `Query audit records: ${stats.query_audit}`,
11174
+ `Writeback receipts: ${stats.writeback_receipts}`,
11175
+ `Writeback jobs: ${stats.writeback_jobs}`,
11176
+ `Idempotency receipts: ${stats.idempotency_receipts}`,
11177
+ `Replay records: ${stats.replay_records}`,
11178
+ `Approvals: ${stats.approvals}`,
11179
+ `Proposal events: ${stats.proposal_events}`,
11180
+ `Shadow human actions: ${stats.shadow_human_actions}`
11181
+ ].join("\n") + "\n";
11182
+ }
11183
+ function formatStorePrune(result) {
11184
+ const lines = [
11185
+ `Local store prune ${result.dry_run ? "dry run" : "complete"}`,
11186
+ `Cutoff: ${result.cutoff}`,
11187
+ "",
11188
+ "Rows:",
11189
+ ...Object.entries(result.deleted).map(([table, count]) => ` ${table}: ${count}`)
11190
+ ];
11191
+ if (result.dry_run) {
11192
+ lines.push("", "No rows were deleted. Rerun with --yes to apply this prune.");
11193
+ }
11194
+ return `${lines.join("\n")}
11195
+ `;
11196
+ }
11197
+ function cutoffFromOlderThan(value) {
11198
+ const match = value.match(/^(\d+)([smhd])$/i);
11199
+ if (!match) throw new Error("--older-than must use a duration such as 30d, 12h, 90m, or 0d");
11200
+ const amount = Number(match[1]);
11201
+ const unit = (match[2] ?? "d").toLowerCase();
11202
+ const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
11203
+ return new Date(Date.now() - amount * multiplier).toISOString();
11204
+ }
11205
+ function formatMcpAuditMarkdown(report) {
11206
+ const lines = [
11207
+ "# Synapsor MCP Database Risk Review",
11208
+ "",
11209
+ `- Target: ${report.target}`,
11210
+ `- Generated at: ${report.generated_at}`,
11211
+ `- Tools inspected: ${report.summary.tools_inspected}`,
11212
+ `- Findings: HIGH ${report.summary.high} | MEDIUM ${report.summary.medium} | LOW ${report.summary.low}`,
11213
+ `- Total findings: ${report.summary.total_findings}`,
11214
+ "",
11215
+ `> ${report.disclaimer}`,
11216
+ ""
11217
+ ];
11218
+ if (report.findings.length === 0) {
11219
+ lines.push("No obvious database-commit risks were detected in the static manifest.", "");
11220
+ lines.push("This does not prove the MCP server or its tools are secure.", "");
11221
+ } else {
11222
+ lines.push("## Findings", "");
11223
+ for (const finding of report.findings) {
11224
+ lines.push(`### ${finding.severity}: ${finding.code}${finding.tool ? ` (${finding.tool})` : ""}`);
11225
+ lines.push("");
11226
+ lines.push(finding.message);
11227
+ lines.push("");
11228
+ if (finding.evidence.length > 0) {
11229
+ lines.push("Evidence:");
11230
+ for (const evidence2 of finding.evidence) lines.push(`- ${evidence2}`);
11231
+ lines.push("");
11232
+ }
11233
+ lines.push(`Recommendation: ${finding.recommendation}`);
11234
+ lines.push("");
11235
+ }
11236
+ }
11237
+ lines.push("## Safer Shape", "");
11238
+ lines.push("- expose semantic inspect/propose tools instead of raw SQL;");
11239
+ lines.push("- bind tenant/principal from trusted context;");
11240
+ lines.push("- keep approval outside MCP;");
11241
+ lines.push("- apply approved changes through guarded writeback;");
11242
+ lines.push("- keep replay/evidence handles for later review.");
11243
+ lines.push("");
11244
+ return `${lines.join("\n")}
11245
+ `;
11246
+ }
11247
+ function stringField(record, key) {
11248
+ if (!isRecord6(record)) return void 0;
11249
+ const value = record[key];
11250
+ if (typeof value === "string") return value;
11251
+ if (typeof value === "number") return String(value);
11252
+ return void 0;
11253
+ }
8569
11254
  function currentApprovalStatus(proposal) {
8570
11255
  if (proposal.state === "rejected") return "rejected";
8571
11256
  if (proposal.state === "canceled") return "canceled";
@@ -8579,6 +11264,60 @@ function currentWritebackStatus(proposal) {
8579
11264
  if (proposal.state === "failed") return "failed";
8580
11265
  return proposal.change_set.writeback.status;
8581
11266
  }
11267
+ function showDetails(args) {
11268
+ return args.includes("--details") || args.includes("--debug");
11269
+ }
11270
+ function storeOptionSuffix(args) {
11271
+ const storePath = optionalArg(args, "--store");
11272
+ return storePath ? ` --store ${storePath}` : "";
11273
+ }
11274
+ function humanStatus(value) {
11275
+ const normalized = value.replace(/_/g, " ");
11276
+ if (normalized === "pending review") return "pending review";
11277
+ if (normalized === "not applied") return "not applied";
11278
+ return normalized;
11279
+ }
11280
+ function plural(count, label) {
11281
+ return `${count} ${label}${count === 1 ? "" : "s"}`;
11282
+ }
11283
+ function formatReceiptId(receiptId) {
11284
+ return `rct_${String(receiptId).padStart(6, "0")}`;
11285
+ }
11286
+ function approvalBoundary(proposal) {
11287
+ if (proposal.state === "pending_review") return "required outside MCP";
11288
+ if (proposal.state === "approved" || proposal.state === "pending_worker") return "approved outside MCP; waiting for trusted worker";
11289
+ if (proposal.state === "applied") return "approved outside MCP; writeback applied";
11290
+ if (proposal.state === "conflict") return "approved outside MCP; writeback blocked by conflict guard";
11291
+ if (proposal.state === "failed") return "approved outside MCP; writeback failed safely";
11292
+ if (proposal.state === "rejected") return "rejected outside MCP";
11293
+ return humanStatus(proposal.state);
11294
+ }
11295
+ function proposalNextCommands(proposal, proposalRef, storeSuffix) {
11296
+ if (proposal.state === "pending_review") {
11297
+ return [
11298
+ `${cliCommandName()} proposals approve ${proposalRef} --yes${storeSuffix}`,
11299
+ `${cliCommandName()} replay show ${proposalRef === "latest" ? "latest" : `--proposal ${proposal.proposal_id}`}${storeSuffix}`
11300
+ ];
11301
+ }
11302
+ if (proposal.state === "approved" || proposal.state === "pending_worker") {
11303
+ return [
11304
+ `${cliCommandName()} replay show --proposal ${proposal.proposal_id}${storeSuffix}`
11305
+ ];
11306
+ }
11307
+ return [
11308
+ `${cliCommandName()} replay show --proposal ${proposal.proposal_id}${storeSuffix}`
11309
+ ];
11310
+ }
11311
+ function formatChangeLines(proposal) {
11312
+ const changeSet = proposal.change_set;
11313
+ const columns = Object.keys(changeSet.patch);
11314
+ if (columns.length === 0) return [" (no changed columns)"];
11315
+ return columns.map((column) => {
11316
+ const before = changeSet.before[column];
11317
+ const proposed = changeSet.after[column];
11318
+ return ` ${column}: ${formatScalar(before)} -> ${formatScalar(proposed)}`;
11319
+ });
11320
+ }
8582
11321
  function formatShadowComparison(comparison) {
8583
11322
  return [
8584
11323
  `shadow comparison: ${comparison.proposal_id}`,
@@ -8696,7 +11435,7 @@ function starterCloudConfig() {
8696
11435
  base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
8697
11436
  runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
8698
11437
  runner_id: "synapsor_runner_local",
8699
- runner_version: "0.1.0-alpha.3",
11438
+ runner_version: "0.1.0-alpha.5",
8700
11439
  project_id: "token_scope",
8701
11440
  adapter_id: "mcp.your_adapter",
8702
11441
  source_id: "src_replace_me",
@@ -8719,6 +11458,37 @@ function normalizeCapabilities(value) {
8719
11458
  function isHelpRequest(args) {
8720
11459
  return args.includes("--help") || args.includes("-h");
8721
11460
  }
11461
+ function isKnownTopLevelCommand(command) {
11462
+ return (/* @__PURE__ */ new Set([
11463
+ "help",
11464
+ "init",
11465
+ "inspect",
11466
+ "config",
11467
+ "doctor",
11468
+ "validate",
11469
+ "apply",
11470
+ "propose",
11471
+ "audit",
11472
+ "start",
11473
+ "runner",
11474
+ "cloud",
11475
+ "mcp",
11476
+ "tools",
11477
+ "onboard",
11478
+ "demo",
11479
+ "recipes",
11480
+ "benchmark",
11481
+ "proposals",
11482
+ "replay",
11483
+ "evidence",
11484
+ "query-audit",
11485
+ "receipts",
11486
+ "activity",
11487
+ "store",
11488
+ "shadow",
11489
+ "ui"
11490
+ ])).has(command);
11491
+ }
8722
11492
  function cliCommandName() {
8723
11493
  if (process2.env.SYNAPSOR_RUNNER_COMMAND_NAME) return process2.env.SYNAPSOR_RUNNER_COMMAND_NAME;
8724
11494
  const invoked = path3.basename(process2.argv[1] ?? "");
@@ -8743,6 +11513,11 @@ Commands:
8743
11513
  propose Create a local evidence-backed proposal
8744
11514
  audit Review MCP/database tool risk
8745
11515
  proposals Review, approve, or reject proposals
11516
+ evidence Inspect local evidence bundles
11517
+ query-audit Inspect local query audit records
11518
+ receipts Inspect guarded writeback receipts
11519
+ activity Search local evidence/replay ledger
11520
+ store Inspect and maintain the local SQLite ledger
8746
11521
  apply Apply an approved proposal with guarded writeback
8747
11522
  replay Show what happened
8748
11523
  demo Start the local commit-safety demo
@@ -8769,16 +11544,31 @@ Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizar
8769
11544
  `,
8770
11545
  mcp: `Usage:
8771
11546
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
11547
+ ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
8772
11548
  ${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
8773
11549
  ${cmd} mcp audit --example dangerous-db-mcp
8774
11550
  ${cmd} mcp audit ./tools-list.json
8775
11551
 
11552
+ Use stdio for local MCP clients that launch the runner. Use authenticated HTTP for app/server deployments.
8776
11553
  MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
8777
11554
  `,
8778
11555
  "mcp serve": `Usage:
8779
11556
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--read-only] [--local]
8780
11557
 
8781
- Start the stdio MCP server. Startup logs stay off stdout so the MCP protocol remains clean.
11558
+ Start the stdio MCP server for local MCP clients such as Claude Desktop, Cursor, or local agent tools. Startup logs stay off stdout so the MCP protocol remains clean.
11559
+ `,
11560
+ "mcp serve-http": `Usage:
11561
+ export SYNAPSOR_RUNNER_HTTP_TOKEN=...
11562
+ ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8765] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
11563
+
11564
+ Start the HTTP MCP server for app/server deployments. Bearer auth is required by default.
11565
+
11566
+ Security:
11567
+ - Defaults to 127.0.0.1:8765.
11568
+ - Refuses to start if the auth token env var is missing.
11569
+ - Use --dev-no-auth only for localhost development.
11570
+ - If binding to 0.0.0.0, use TLS, private networking, authentication, and rate limits.
11571
+ - Optional CORS: --cors-origin http://localhost:3000
8782
11572
  `,
8783
11573
  "mcp config": `Usage:
8784
11574
  ${cmd} mcp config [claude-desktop|cursor|generic|vscode] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -8790,24 +11580,64 @@ Print MCP client configuration that references the local runner command, not dat
8790
11580
  ${cmd} propose <capability-name> --input ./input.json
8791
11581
  ${cmd} propose <capability-name> --json '{"invoice_id":"INV-3001","reason":"support-approved waiver"}'
8792
11582
 
11583
+ Examples after running ${cmd} demo:
11584
+ ${cmd} propose billing.propose_late_fee_waiver --sample
11585
+ ${cmd} propose support.propose_plan_credit --sample
11586
+ ${cmd} propose orders.propose_status_change --sample
11587
+
8793
11588
  Create the same evidence-backed proposal the MCP tool would create. The source database is not mutated.
8794
11589
  `,
8795
11590
  audit: `Usage:
8796
11591
  ${cmd} audit --example dangerous-db-mcp
11592
+ ${cmd} audit --example dangerous-db-mcp --format json
11593
+ ${cmd} audit --example dangerous-db-mcp --format markdown
8797
11594
  ${cmd} audit ./synapsor.runner.json
8798
11595
  ${cmd} audit --mcp-config ./claude_desktop_config.json
8799
11596
  ${cmd} audit --stdio "node ./server.js"
8800
11597
  ${cmd} audit --url http://localhost:3000/mcp
8801
11598
 
8802
11599
  Static MCP/database risk review only. This is not a security guarantee.
11600
+ `,
11601
+ doctor: `Usage:
11602
+ ${cmd} doctor --config synapsor.runner.json
11603
+ ${cmd} doctor --config synapsor.runner.json --json
11604
+ ${cmd} doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
11605
+ ${cmd} doctor --first-run
11606
+
11607
+ Validate local config, environment bindings, semantic tool boundary, source metadata when reachable, and local store stats. Reports are redacted; do not paste secrets into issues.
8803
11608
  `,
8804
11609
  proposals: `Usage:
8805
- ${cmd} proposals list [--store ./.synapsor/local.db]
8806
- ${cmd} proposals show latest
8807
- ${cmd} proposals approve latest --yes
8808
- ${cmd} proposals reject latest --reason "..."
11610
+ ${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
11611
+ ${cmd} proposals show latest
11612
+ ${cmd} proposals show latest --details
11613
+ ${cmd} proposals approve latest --yes
11614
+ ${cmd} proposals reject latest --reason "..."
11615
+
11616
+ Review decisions happen outside the model-facing MCP tool surface. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
11617
+ `,
11618
+ evidence: `Usage:
11619
+ ${cmd} evidence list [--tenant acme] [--capability billing.inspect_invoice] [--object invoice:INV-3001]
11620
+ ${cmd} evidence show ev_...
11621
+ ${cmd} evidence show ev_... --details
11622
+ ${cmd} evidence export ev_... --format json --output evidence.json
11623
+ ${cmd} evidence export ev_... --format markdown --output evidence.md
11624
+
11625
+ Inspect captured local evidence bundles and query-audit links without rerunning external DB reads.
11626
+ `,
11627
+ "query-audit": `Usage:
11628
+ ${cmd} query-audit list [--evidence ev_...] [--source app_postgres] [--table invoices]
11629
+ ${cmd} query-audit show <audit_id>
11630
+ ${cmd} query-audit show <audit_id> --details
11631
+ ${cmd} query-audit export <audit_id> --format json --output audit.json
8809
11632
 
8810
- Review decisions happen outside the model-facing MCP tool surface.
11633
+ Inspect local query fingerprints, table names, row counts, and redacted-parameter metadata.
11634
+ `,
11635
+ receipts: `Usage:
11636
+ ${cmd} receipts list [--proposal wrp_...] [--status applied]
11637
+ ${cmd} receipts show <receipt_id>
11638
+ ${cmd} receipts show <receipt_id> --details
11639
+
11640
+ Inspect guarded writeback receipts recorded by the trusted runner path. Use --details for idempotency keys, receipt hashes, and runner metadata.
8811
11641
  `,
8812
11642
  apply: `Usage:
8813
11643
  ${cmd} apply latest [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -8816,18 +11646,43 @@ Review decisions happen outside the model-facing MCP tool surface.
8816
11646
  Apply an approved proposal through guarded writeback. Requires a trusted write credential.
8817
11647
  `,
8818
11648
  replay: `Usage:
8819
- ${cmd} replay latest [--store ./.synapsor/local.db]
8820
- ${cmd} replay show latest
8821
- ${cmd} replay export latest --output replay.json
11649
+ ${cmd} replay list [--tenant acme] [--object invoice:INV-3001]
11650
+ ${cmd} replay show latest
11651
+ ${cmd} replay show latest --details
11652
+ ${cmd} replay show --proposal wrp_...
11653
+ ${cmd} replay show --replay replay_wrp_...
11654
+ ${cmd} replay show --evidence ev_...
11655
+ ${cmd} replay export --proposal wrp_... --format json --output replay.json
11656
+ ${cmd} replay export --proposal wrp_... --format markdown --output replay.md
11657
+
11658
+ Show evidence, proposal events, receipts, and replay state without rerunning side effects. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
11659
+ `,
11660
+ activity: `Usage:
11661
+ ${cmd} activity search --tenant acme --object invoice:INV-3001
11662
+ ${cmd} activity search --tenant acme --object invoice:INV-3001 --details
11663
+ ${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
11664
+
11665
+ Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
11666
+ `,
11667
+ store: `Usage:
11668
+ ${cmd} store stats --store ./.synapsor/local.db
11669
+ ${cmd} store vacuum --store ./.synapsor/local.db
11670
+ ${cmd} store prune --store ./.synapsor/local.db --older-than 30d --dry-run
11671
+ ${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes
8822
11672
 
8823
- Show evidence, proposal events, receipts, and replay state without rerunning side effects.
11673
+ Local store maintenance only. Prune defaults to dry-run and never touches your source Postgres/MySQL database.
8824
11674
  `,
8825
11675
  demo: `Usage:
8826
11676
  ${cmd} demo [--force]
8827
11677
  ${cmd} demo --quick
11678
+ ${cmd} demo --quick --guided
11679
+ ${cmd} demo --quick --no-interactive
11680
+ ${cmd} demo --quick --details
11681
+ ${cmd} demo inspect
11682
+ ${cmd} demo inspect --npx
8828
11683
 
8829
11684
  Start a disposable local Postgres demo and write ./synapsor.runner.json for the first-run flow.
8830
- Use --quick for a fixture-only 15-second explanation with no Docker startup.
11685
+ Use --quick for a fixture-only guided walkthrough and local ledger seed with no Docker startup. Use demo inspect to print follow-up commands for the quick-demo fixture.
8831
11686
  `,
8832
11687
  ui: `Usage:
8833
11688
  ${cmd} ui [--tour] [--config synapsor.runner.json] [--store ./.synapsor/local.db]