@synapsor/runner 0.1.0-alpha.2 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/runner.mjs +2229 -100
- package/docs/MCP_RUNNER_IMPLEMENTATION_PLAN.md +1 -1
- package/docs/cloud-mode.md +3 -3
- package/docs/config-migrations.md +3 -3
- package/docs/demo-transcript.md +140 -52
- package/docs/first-10-minutes.md +30 -5
- package/docs/getting-started-own-database.md +27 -27
- package/docs/limitations.md +20 -0
- package/docs/local-mode.md +78 -15
- package/docs/local-ui.md +4 -4
- package/docs/mcp-audit.md +28 -7
- package/docs/mcp-client-setup.md +6 -6
- package/docs/mcp-efficiency-benchmark.md +2 -2
- package/docs/open-source-feature-inventory.md +254 -0
- package/docs/operations.md +3 -3
- package/docs/own-db-20-minutes.md +14 -14
- package/docs/recipes.md +6 -6
- package/docs/schema-inspection.md +3 -3
- package/docs/security-boundary.md +17 -0
- package/docs/shadow-mode.md +4 -4
- package/docs/troubleshooting-first-run.md +6 -6
- package/docs/writeback-executors.md +1 -1
- package/examples/reference-support-billing-app/README.md +60 -9
- package/examples/reference-support-billing-app/schema.sql +14 -1
- package/examples/reference-support-billing-app/seed.sql +12 -5
- package/examples/reference-support-billing-app/synapsor.runner.json +105 -0
- package/package.json +1 -1
package/dist/runner.mjs
CHANGED
|
@@ -1196,13 +1196,89 @@ var ProposalStoreError = class extends Error {
|
|
|
1196
1196
|
};
|
|
1197
1197
|
var ProposalStore = class {
|
|
1198
1198
|
db;
|
|
1199
|
+
path;
|
|
1199
1200
|
constructor(path4 = ":memory:") {
|
|
1201
|
+
this.path = path4;
|
|
1200
1202
|
this.db = new DatabaseSync(path4);
|
|
1201
1203
|
this.migrate();
|
|
1202
1204
|
}
|
|
1203
1205
|
close() {
|
|
1204
1206
|
this.db.close();
|
|
1205
1207
|
}
|
|
1208
|
+
stats() {
|
|
1209
|
+
const pageCount = this.numberValue("PRAGMA page_count");
|
|
1210
|
+
const pageSize = this.numberValue("PRAGMA page_size");
|
|
1211
|
+
return {
|
|
1212
|
+
path: this.path,
|
|
1213
|
+
proposals: this.countTable("proposals"),
|
|
1214
|
+
evidence_bundles: this.countTable("evidence_bundles"),
|
|
1215
|
+
evidence_items: this.countTable("evidence_items"),
|
|
1216
|
+
query_audit: this.countTable("query_audit"),
|
|
1217
|
+
writeback_receipts: this.countTable("writeback_receipts"),
|
|
1218
|
+
writeback_jobs: this.countTable("writeback_jobs"),
|
|
1219
|
+
idempotency_receipts: this.countTable("idempotency_receipts"),
|
|
1220
|
+
replay_records: this.countTable("replay_records"),
|
|
1221
|
+
approvals: this.countTable("approvals"),
|
|
1222
|
+
proposal_events: this.countTable("proposal_events"),
|
|
1223
|
+
shadow_human_actions: this.countTable("shadow_human_actions"),
|
|
1224
|
+
page_count: pageCount,
|
|
1225
|
+
page_size: pageSize,
|
|
1226
|
+
approx_bytes: pageCount * pageSize
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
vacuum() {
|
|
1230
|
+
this.db.exec("VACUUM");
|
|
1231
|
+
}
|
|
1232
|
+
pruneBefore(cutoffIso, options = {}) {
|
|
1233
|
+
const dryRun = options.dryRun !== false;
|
|
1234
|
+
const proposalIds = this.stringColumn("SELECT proposal_id FROM proposals WHERE created_at < ?", [cutoffIso], "proposal_id");
|
|
1235
|
+
const evidenceIds = this.evidenceIdsForPrune(cutoffIso, proposalIds);
|
|
1236
|
+
const deleted = {};
|
|
1237
|
+
const run = (table, where, params) => {
|
|
1238
|
+
deleted[table] = this.countWhere(table, where, params);
|
|
1239
|
+
if (!dryRun && deleted[table] > 0) this.db.prepare(`DELETE FROM ${table} WHERE ${where}`).run(...params);
|
|
1240
|
+
};
|
|
1241
|
+
const proposalWhere = inWhere("proposal_id", proposalIds);
|
|
1242
|
+
const evidenceWhere = inWhere("evidence_bundle_id", evidenceIds);
|
|
1243
|
+
this.transaction(() => {
|
|
1244
|
+
if (proposalWhere) {
|
|
1245
|
+
run("idempotency_receipts", proposalWhere.sql, proposalWhere.params);
|
|
1246
|
+
run("writeback_receipts", proposalWhere.sql, proposalWhere.params);
|
|
1247
|
+
run("writeback_jobs", proposalWhere.sql, proposalWhere.params);
|
|
1248
|
+
run("approvals", proposalWhere.sql, proposalWhere.params);
|
|
1249
|
+
run("proposal_events", proposalWhere.sql, proposalWhere.params);
|
|
1250
|
+
run("shadow_human_actions", proposalWhere.sql, proposalWhere.params);
|
|
1251
|
+
run("replay_records", proposalWhere.sql, proposalWhere.params);
|
|
1252
|
+
} else {
|
|
1253
|
+
for (const table of ["idempotency_receipts", "writeback_receipts", "writeback_jobs", "approvals", "proposal_events", "shadow_human_actions", "replay_records"]) {
|
|
1254
|
+
deleted[table] = 0;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const auditClauses = [];
|
|
1258
|
+
const auditParams = [];
|
|
1259
|
+
if (proposalWhere) {
|
|
1260
|
+
auditClauses.push(proposalWhere.sql);
|
|
1261
|
+
auditParams.push(...proposalWhere.params);
|
|
1262
|
+
}
|
|
1263
|
+
if (evidenceWhere) {
|
|
1264
|
+
auditClauses.push(evidenceWhere.sql);
|
|
1265
|
+
auditParams.push(...evidenceWhere.params);
|
|
1266
|
+
}
|
|
1267
|
+
auditClauses.push("(proposal_id IS NULL AND evidence_bundle_id IS NULL AND created_at < ?)");
|
|
1268
|
+
auditParams.push(cutoffIso);
|
|
1269
|
+
run("query_audit", auditClauses.map((clause) => `(${clause})`).join(" OR "), auditParams);
|
|
1270
|
+
if (evidenceWhere) {
|
|
1271
|
+
run("evidence_items", evidenceWhere.sql, evidenceWhere.params);
|
|
1272
|
+
run("evidence_bundles", evidenceWhere.sql, evidenceWhere.params);
|
|
1273
|
+
} else {
|
|
1274
|
+
deleted.evidence_items = 0;
|
|
1275
|
+
deleted.evidence_bundles = 0;
|
|
1276
|
+
}
|
|
1277
|
+
if (proposalWhere) run("proposals", proposalWhere.sql, proposalWhere.params);
|
|
1278
|
+
else deleted.proposals = 0;
|
|
1279
|
+
});
|
|
1280
|
+
return { cutoff: cutoffIso, dry_run: dryRun, deleted };
|
|
1281
|
+
}
|
|
1206
1282
|
migrate() {
|
|
1207
1283
|
this.db.exec(`
|
|
1208
1284
|
PRAGMA foreign_keys = ON;
|
|
@@ -1353,6 +1429,154 @@ var ProposalStore = class {
|
|
|
1353
1429
|
INSERT OR IGNORE INTO proposal_store_schema(version, applied_at)
|
|
1354
1430
|
VALUES (1, datetime('now'));
|
|
1355
1431
|
`);
|
|
1432
|
+
this.ensureSearchColumns();
|
|
1433
|
+
this.backfillSearchColumns();
|
|
1434
|
+
this.ensureSearchIndexes();
|
|
1435
|
+
}
|
|
1436
|
+
ensureSearchColumns() {
|
|
1437
|
+
this.ensureColumn("proposals", "principal", "TEXT");
|
|
1438
|
+
this.ensureColumn("proposals", "capability", "TEXT");
|
|
1439
|
+
this.ensureColumn("proposals", "interaction_id", "TEXT");
|
|
1440
|
+
this.ensureColumn("proposals", "tool_call_id", "TEXT");
|
|
1441
|
+
this.ensureColumn("evidence_bundles", "principal", "TEXT");
|
|
1442
|
+
this.ensureColumn("evidence_bundles", "capability", "TEXT");
|
|
1443
|
+
this.ensureColumn("evidence_bundles", "source_id", "TEXT");
|
|
1444
|
+
this.ensureColumn("evidence_bundles", "source_table", "TEXT");
|
|
1445
|
+
this.ensureColumn("evidence_bundles", "business_object", "TEXT");
|
|
1446
|
+
this.ensureColumn("evidence_bundles", "object_id", "TEXT");
|
|
1447
|
+
this.ensureColumn("evidence_bundles", "query_fingerprint", "TEXT");
|
|
1448
|
+
this.ensureColumn("query_audit", "tenant_id", "TEXT");
|
|
1449
|
+
this.ensureColumn("query_audit", "principal", "TEXT");
|
|
1450
|
+
this.ensureColumn("query_audit", "capability", "TEXT");
|
|
1451
|
+
this.ensureColumn("query_audit", "business_object", "TEXT");
|
|
1452
|
+
this.ensureColumn("query_audit", "object_id", "TEXT");
|
|
1453
|
+
this.ensureColumn("query_audit", "primary_key_value", "TEXT");
|
|
1454
|
+
}
|
|
1455
|
+
ensureSearchIndexes() {
|
|
1456
|
+
this.db.exec(`
|
|
1457
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_tenant_created ON proposals(tenant_id, created_at);
|
|
1458
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_action_created ON proposals(action, created_at);
|
|
1459
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_capability_created ON proposals(capability, created_at);
|
|
1460
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_principal_created ON proposals(principal, created_at);
|
|
1461
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_object_created ON proposals(business_object, object_id, created_at);
|
|
1462
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_state_created ON proposals(state, created_at);
|
|
1463
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_source_table_created ON proposals(source_id, source_table, created_at);
|
|
1464
|
+
|
|
1465
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_tenant_created ON evidence_bundles(tenant_id, created_at);
|
|
1466
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_proposal_id ON evidence_bundles(proposal_id);
|
|
1467
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_created ON evidence_bundles(created_at);
|
|
1468
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_capability_created ON evidence_bundles(capability, created_at);
|
|
1469
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_principal_created ON evidence_bundles(principal, created_at);
|
|
1470
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_object_created ON evidence_bundles(business_object, object_id, created_at);
|
|
1471
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_source_table_created ON evidence_bundles(source_id, source_table, created_at);
|
|
1472
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_bundles_fingerprint_created ON evidence_bundles(query_fingerprint, created_at);
|
|
1473
|
+
|
|
1474
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_items_bundle_id ON evidence_items(evidence_bundle_id);
|
|
1475
|
+
|
|
1476
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_evidence_id ON query_audit(evidence_bundle_id);
|
|
1477
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_source_table_created ON query_audit(source_id, table_name, created_at);
|
|
1478
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_fingerprint_created ON query_audit(query_fingerprint, created_at);
|
|
1479
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_created ON query_audit(created_at);
|
|
1480
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_tenant_created ON query_audit(tenant_id, created_at);
|
|
1481
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_capability_created ON query_audit(capability, created_at);
|
|
1482
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_principal_created ON query_audit(principal, created_at);
|
|
1483
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_object_created ON query_audit(business_object, object_id, created_at);
|
|
1484
|
+
CREATE INDEX IF NOT EXISTS idx_query_audit_primary_key_created ON query_audit(primary_key_value, created_at);
|
|
1485
|
+
|
|
1486
|
+
CREATE INDEX IF NOT EXISTS idx_writeback_receipts_writeback_job ON writeback_receipts(writeback_job_id);
|
|
1487
|
+
CREATE INDEX IF NOT EXISTS idx_writeback_receipts_idempotency_key ON writeback_receipts(idempotency_key);
|
|
1488
|
+
CREATE INDEX IF NOT EXISTS idx_writeback_receipts_status_created ON writeback_receipts(status, created_at);
|
|
1489
|
+
|
|
1490
|
+
CREATE INDEX IF NOT EXISTS idx_replay_records_created ON replay_records(created_at);
|
|
1491
|
+
|
|
1492
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_proposal_id ON approvals(proposal_id);
|
|
1493
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_status_created ON approvals(status, created_at);
|
|
1494
|
+
|
|
1495
|
+
CREATE INDEX IF NOT EXISTS idx_proposal_events_kind_created ON proposal_events(kind, created_at);
|
|
1496
|
+
`);
|
|
1497
|
+
}
|
|
1498
|
+
ensureColumn(table, column, definition) {
|
|
1499
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
1500
|
+
if (columns.some((row) => isRecord2(row) && row.name === column)) return;
|
|
1501
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
1502
|
+
}
|
|
1503
|
+
backfillSearchColumns() {
|
|
1504
|
+
const proposals2 = this.db.prepare("SELECT proposal_id, action, change_set_json FROM proposals").all();
|
|
1505
|
+
for (const row of proposals2) {
|
|
1506
|
+
if (!isRecord2(row)) continue;
|
|
1507
|
+
try {
|
|
1508
|
+
const changeSet = parseChangeSet(JSON.parse(String(row.change_set_json)));
|
|
1509
|
+
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));
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const evidenceRows = this.db.prepare("SELECT evidence_bundle_id, proposal_id, payload_json FROM evidence_bundles").all();
|
|
1514
|
+
for (const row of evidenceRows) {
|
|
1515
|
+
if (!isRecord2(row)) continue;
|
|
1516
|
+
const proposal = row.proposal_id == null ? void 0 : this.getProposal(String(row.proposal_id));
|
|
1517
|
+
let payload = {};
|
|
1518
|
+
try {
|
|
1519
|
+
payload = JSON.parse(String(row.payload_json));
|
|
1520
|
+
} catch {
|
|
1521
|
+
payload = {};
|
|
1522
|
+
}
|
|
1523
|
+
const metadata = this.evidenceMetadata({
|
|
1524
|
+
proposal,
|
|
1525
|
+
payload,
|
|
1526
|
+
items: this.evidenceItems(String(row.evidence_bundle_id)).map((item) => item.item)
|
|
1527
|
+
});
|
|
1528
|
+
this.db.prepare(`
|
|
1529
|
+
UPDATE evidence_bundles
|
|
1530
|
+
SET principal = COALESCE(principal, ?),
|
|
1531
|
+
capability = COALESCE(capability, ?),
|
|
1532
|
+
source_id = COALESCE(source_id, ?),
|
|
1533
|
+
source_table = COALESCE(source_table, ?),
|
|
1534
|
+
business_object = COALESCE(business_object, ?),
|
|
1535
|
+
object_id = COALESCE(object_id, ?),
|
|
1536
|
+
query_fingerprint = COALESCE(query_fingerprint, ?)
|
|
1537
|
+
WHERE evidence_bundle_id = ?
|
|
1538
|
+
`).run(
|
|
1539
|
+
metadata.principal ?? null,
|
|
1540
|
+
metadata.capability ?? null,
|
|
1541
|
+
metadata.source_id ?? null,
|
|
1542
|
+
metadata.source_table ?? null,
|
|
1543
|
+
metadata.business_object ?? null,
|
|
1544
|
+
metadata.object_id ?? null,
|
|
1545
|
+
metadata.query_fingerprint ?? null,
|
|
1546
|
+
String(row.evidence_bundle_id)
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
const auditRows = this.db.prepare("SELECT audit_id, proposal_id, evidence_bundle_id, payload_json FROM query_audit").all();
|
|
1550
|
+
for (const row of auditRows) {
|
|
1551
|
+
if (!isRecord2(row)) continue;
|
|
1552
|
+
const proposal = row.proposal_id == null ? void 0 : this.getProposal(String(row.proposal_id));
|
|
1553
|
+
const evidence2 = row.evidence_bundle_id == null ? void 0 : this.getEvidenceBundle(String(row.evidence_bundle_id));
|
|
1554
|
+
let payload = {};
|
|
1555
|
+
try {
|
|
1556
|
+
payload = JSON.parse(String(row.payload_json));
|
|
1557
|
+
} catch {
|
|
1558
|
+
payload = {};
|
|
1559
|
+
}
|
|
1560
|
+
const metadata = this.queryAuditMetadata({ proposal, evidence: evidence2, payload });
|
|
1561
|
+
this.db.prepare(`
|
|
1562
|
+
UPDATE query_audit
|
|
1563
|
+
SET tenant_id = COALESCE(tenant_id, ?),
|
|
1564
|
+
principal = COALESCE(principal, ?),
|
|
1565
|
+
capability = COALESCE(capability, ?),
|
|
1566
|
+
business_object = COALESCE(business_object, ?),
|
|
1567
|
+
object_id = COALESCE(object_id, ?),
|
|
1568
|
+
primary_key_value = COALESCE(primary_key_value, ?)
|
|
1569
|
+
WHERE audit_id = ?
|
|
1570
|
+
`).run(
|
|
1571
|
+
metadata.tenant_id ?? null,
|
|
1572
|
+
metadata.principal ?? null,
|
|
1573
|
+
metadata.capability ?? null,
|
|
1574
|
+
metadata.business_object ?? null,
|
|
1575
|
+
metadata.object_id ?? null,
|
|
1576
|
+
metadata.primary_key_value ?? null,
|
|
1577
|
+
Number(row.audit_id)
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1356
1580
|
}
|
|
1357
1581
|
createProposal(input) {
|
|
1358
1582
|
const changeSet = parseChangeSet(input);
|
|
@@ -1377,6 +1601,10 @@ var ProposalStore = class {
|
|
|
1377
1601
|
action,
|
|
1378
1602
|
state,
|
|
1379
1603
|
tenant_id,
|
|
1604
|
+
principal,
|
|
1605
|
+
capability,
|
|
1606
|
+
interaction_id,
|
|
1607
|
+
tool_call_id,
|
|
1380
1608
|
business_object,
|
|
1381
1609
|
object_id,
|
|
1382
1610
|
source_kind,
|
|
@@ -1387,7 +1615,7 @@ var ProposalStore = class {
|
|
|
1387
1615
|
change_set_json,
|
|
1388
1616
|
created_at,
|
|
1389
1617
|
updated_at
|
|
1390
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1618
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1391
1619
|
`);
|
|
1392
1620
|
this.transaction(() => {
|
|
1393
1621
|
insert.run(
|
|
@@ -1397,6 +1625,10 @@ var ProposalStore = class {
|
|
|
1397
1625
|
changeSet.action,
|
|
1398
1626
|
state,
|
|
1399
1627
|
changeSet.scope.tenant_id,
|
|
1628
|
+
changeSet.principal.id,
|
|
1629
|
+
changeSet.action,
|
|
1630
|
+
null,
|
|
1631
|
+
null,
|
|
1400
1632
|
changeSet.scope.business_object,
|
|
1401
1633
|
changeSet.scope.object_id,
|
|
1402
1634
|
changeSet.source.kind,
|
|
@@ -1424,10 +1656,44 @@ var ProposalStore = class {
|
|
|
1424
1656
|
const row = this.db.prepare("SELECT * FROM proposals WHERE proposal_id = ?").get(proposalId);
|
|
1425
1657
|
return rowToProposal(row);
|
|
1426
1658
|
}
|
|
1427
|
-
listProposals(
|
|
1428
|
-
|
|
1659
|
+
listProposals(filters) {
|
|
1660
|
+
if (typeof filters === "string") filters = { state: filters };
|
|
1661
|
+
const query = buildProposalQuery(filters ?? {});
|
|
1662
|
+
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
1429
1663
|
return rows.map((row) => rowToProposal(row)).filter((proposal) => proposal !== void 0);
|
|
1430
1664
|
}
|
|
1665
|
+
listEvidenceBundles(filters = {}) {
|
|
1666
|
+
const query = buildEvidenceQuery(filters);
|
|
1667
|
+
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
1668
|
+
return rows.map((row) => this.rowToEvidenceBundle(row)).filter((evidence2) => evidence2 !== void 0);
|
|
1669
|
+
}
|
|
1670
|
+
listQueryAudit(filters = {}) {
|
|
1671
|
+
const query = buildQueryAuditQuery(filters);
|
|
1672
|
+
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
1673
|
+
return rows.map(rowToQueryAudit).filter((record) => record !== void 0);
|
|
1674
|
+
}
|
|
1675
|
+
getQueryAudit(auditId) {
|
|
1676
|
+
return rowToQueryAudit(this.db.prepare("SELECT * FROM query_audit WHERE audit_id = ?").get(auditId));
|
|
1677
|
+
}
|
|
1678
|
+
listReceipts(filters = {}) {
|
|
1679
|
+
const query = buildReceiptQuery(filters);
|
|
1680
|
+
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
1681
|
+
return rows.map(rowToReceipt).filter((receipt) => receipt !== void 0);
|
|
1682
|
+
}
|
|
1683
|
+
getReceipt(receiptId) {
|
|
1684
|
+
return rowToReceipt(this.db.prepare("SELECT * FROM writeback_receipts WHERE receipt_id = ?").get(receiptId));
|
|
1685
|
+
}
|
|
1686
|
+
getReplayByReplayId(replayId) {
|
|
1687
|
+
const prefix = "replay_";
|
|
1688
|
+
const proposalId = replayId.startsWith(prefix) ? replayId.slice(prefix.length) : replayId;
|
|
1689
|
+
return this.replay(proposalId);
|
|
1690
|
+
}
|
|
1691
|
+
proposalIdForEvidence(evidenceBundleId) {
|
|
1692
|
+
const evidence2 = this.getEvidenceBundle(evidenceBundleId);
|
|
1693
|
+
if (evidence2?.proposal_id) return evidence2.proposal_id;
|
|
1694
|
+
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);
|
|
1695
|
+
return isRecord2(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
|
|
1696
|
+
}
|
|
1431
1697
|
approveProposal(proposalId, options) {
|
|
1432
1698
|
const proposal = this.requireProposal(proposalId);
|
|
1433
1699
|
assertWritebackAllowed(proposal, "approved");
|
|
@@ -1691,17 +1957,38 @@ var ProposalStore = class {
|
|
|
1691
1957
|
recordEvidenceBundle(input) {
|
|
1692
1958
|
assertNoSecretMaterial({ payload: input.payload, items: input.items ?? [] }, "evidence_bundle");
|
|
1693
1959
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1694
|
-
|
|
1960
|
+
const proposal = input.proposal_id ? this.requireProposal(input.proposal_id) : void 0;
|
|
1961
|
+
const metadata = this.evidenceMetadata({ proposal, payload: input.payload, items: input.items ?? [] });
|
|
1695
1962
|
this.transaction(() => {
|
|
1696
1963
|
this.db.prepare(`
|
|
1697
1964
|
INSERT OR REPLACE INTO evidence_bundles (
|
|
1698
1965
|
evidence_bundle_id,
|
|
1699
1966
|
proposal_id,
|
|
1700
1967
|
tenant_id,
|
|
1968
|
+
principal,
|
|
1969
|
+
capability,
|
|
1970
|
+
source_id,
|
|
1971
|
+
source_table,
|
|
1972
|
+
business_object,
|
|
1973
|
+
object_id,
|
|
1974
|
+
query_fingerprint,
|
|
1701
1975
|
payload_json,
|
|
1702
1976
|
created_at
|
|
1703
|
-
) VALUES (?, ?, ?, ?, ?)
|
|
1704
|
-
`).run(
|
|
1977
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1978
|
+
`).run(
|
|
1979
|
+
input.evidence_bundle_id,
|
|
1980
|
+
input.proposal_id ?? null,
|
|
1981
|
+
input.tenant_id,
|
|
1982
|
+
metadata.principal ?? null,
|
|
1983
|
+
metadata.capability ?? null,
|
|
1984
|
+
metadata.source_id ?? null,
|
|
1985
|
+
metadata.source_table ?? null,
|
|
1986
|
+
metadata.business_object ?? null,
|
|
1987
|
+
metadata.object_id ?? null,
|
|
1988
|
+
metadata.query_fingerprint ?? null,
|
|
1989
|
+
JSON.stringify(input.payload),
|
|
1990
|
+
now
|
|
1991
|
+
);
|
|
1705
1992
|
for (const item of input.items ?? []) {
|
|
1706
1993
|
this.db.prepare(`
|
|
1707
1994
|
INSERT INTO evidence_items (evidence_bundle_id, item_json, created_at)
|
|
@@ -1719,21 +2006,35 @@ var ProposalStore = class {
|
|
|
1719
2006
|
recordQueryAudit(input) {
|
|
1720
2007
|
assertNoSecretMaterial(input.payload, "query_audit");
|
|
1721
2008
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1722
|
-
|
|
2009
|
+
const proposal = input.proposal_id ? this.requireProposal(input.proposal_id) : void 0;
|
|
2010
|
+
const evidence2 = input.evidence_bundle_id ? this.getEvidenceBundle(input.evidence_bundle_id) : void 0;
|
|
2011
|
+
const metadata = this.queryAuditMetadata({ proposal, evidence: evidence2, payload: input.payload });
|
|
1723
2012
|
this.db.prepare(`
|
|
1724
2013
|
INSERT INTO query_audit (
|
|
1725
2014
|
proposal_id,
|
|
1726
2015
|
evidence_bundle_id,
|
|
2016
|
+
tenant_id,
|
|
2017
|
+
principal,
|
|
2018
|
+
capability,
|
|
2019
|
+
business_object,
|
|
2020
|
+
object_id,
|
|
2021
|
+
primary_key_value,
|
|
1727
2022
|
source_id,
|
|
1728
2023
|
query_fingerprint,
|
|
1729
2024
|
table_name,
|
|
1730
2025
|
row_count,
|
|
1731
2026
|
payload_json,
|
|
1732
2027
|
created_at
|
|
1733
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2028
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1734
2029
|
`).run(
|
|
1735
2030
|
input.proposal_id ?? null,
|
|
1736
2031
|
input.evidence_bundle_id ?? null,
|
|
2032
|
+
metadata.tenant_id ?? null,
|
|
2033
|
+
metadata.principal ?? null,
|
|
2034
|
+
metadata.capability ?? null,
|
|
2035
|
+
metadata.business_object ?? null,
|
|
2036
|
+
metadata.object_id ?? null,
|
|
2037
|
+
metadata.primary_key_value ?? null,
|
|
1737
2038
|
input.source_id,
|
|
1738
2039
|
input.query_fingerprint,
|
|
1739
2040
|
input.table_name,
|
|
@@ -1744,14 +2045,24 @@ var ProposalStore = class {
|
|
|
1744
2045
|
}
|
|
1745
2046
|
getEvidenceBundle(evidenceBundleId) {
|
|
1746
2047
|
const row = this.db.prepare("SELECT * FROM evidence_bundles WHERE evidence_bundle_id = ?").get(evidenceBundleId);
|
|
2048
|
+
return this.rowToEvidenceBundle(row);
|
|
2049
|
+
}
|
|
2050
|
+
rowToEvidenceBundle(row) {
|
|
1747
2051
|
if (!isRecord2(row)) return void 0;
|
|
1748
2052
|
return {
|
|
1749
2053
|
evidence_bundle_id: String(row.evidence_bundle_id),
|
|
1750
2054
|
proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
|
|
1751
2055
|
tenant_id: String(row.tenant_id),
|
|
2056
|
+
principal: row.principal == null ? void 0 : String(row.principal),
|
|
2057
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
2058
|
+
source_id: row.source_id == null ? void 0 : String(row.source_id),
|
|
2059
|
+
source_table: row.source_table == null ? void 0 : String(row.source_table),
|
|
2060
|
+
business_object: row.business_object == null ? void 0 : String(row.business_object),
|
|
2061
|
+
object_id: row.object_id == null ? void 0 : String(row.object_id),
|
|
2062
|
+
query_fingerprint: row.query_fingerprint == null ? void 0 : String(row.query_fingerprint),
|
|
1752
2063
|
payload: JSON.parse(String(row.payload_json)),
|
|
1753
|
-
items: this.evidenceItems(
|
|
1754
|
-
query_audit: this.queryAuditByEvidence(
|
|
2064
|
+
items: this.evidenceItems(String(row.evidence_bundle_id)),
|
|
2065
|
+
query_audit: this.queryAuditByEvidence(String(row.evidence_bundle_id)),
|
|
1755
2066
|
created_at: String(row.created_at)
|
|
1756
2067
|
};
|
|
1757
2068
|
}
|
|
@@ -1896,6 +2207,8 @@ var ProposalStore = class {
|
|
|
1896
2207
|
proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
|
|
1897
2208
|
tenant_id: String(row.tenant_id),
|
|
1898
2209
|
payload: JSON.parse(String(row.payload_json)),
|
|
2210
|
+
items: this.evidenceItems(String(row.evidence_bundle_id)),
|
|
2211
|
+
query_audit: this.queryAuditByEvidence(String(row.evidence_bundle_id)),
|
|
1899
2212
|
created_at: String(row.created_at)
|
|
1900
2213
|
});
|
|
1901
2214
|
}
|
|
@@ -1915,6 +2228,81 @@ var ProposalStore = class {
|
|
|
1915
2228
|
}
|
|
1916
2229
|
return records;
|
|
1917
2230
|
}
|
|
2231
|
+
evidenceMetadata(input) {
|
|
2232
|
+
if (input.proposal) {
|
|
2233
|
+
return {
|
|
2234
|
+
principal: input.proposal.change_set.principal.id,
|
|
2235
|
+
capability: input.proposal.action,
|
|
2236
|
+
source_id: input.proposal.source_id,
|
|
2237
|
+
source_table: `${input.proposal.source_schema}.${input.proposal.source_table}`,
|
|
2238
|
+
business_object: input.proposal.business_object,
|
|
2239
|
+
object_id: input.proposal.object_id,
|
|
2240
|
+
query_fingerprint: input.proposal.change_set.evidence.query_fingerprint
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
const firstItem = input.items.find(isRecord2);
|
|
2244
|
+
const primaryKey = isRecord2(firstItem?.primary_key) ? firstItem.primary_key : void 0;
|
|
2245
|
+
const principal = stringFromPrincipal(input.payload.principal);
|
|
2246
|
+
const table = stringFromUnknown(input.payload.target) ?? stringFromUnknown(firstItem?.table);
|
|
2247
|
+
return {
|
|
2248
|
+
principal,
|
|
2249
|
+
capability: stringFromUnknown(input.payload.capability),
|
|
2250
|
+
source_id: stringFromUnknown(input.payload.source_id) ?? stringFromUnknown(firstItem?.source_id),
|
|
2251
|
+
source_table: table,
|
|
2252
|
+
business_object: table ? lastIdentifier(table) : void 0,
|
|
2253
|
+
object_id: primaryKey ? stringFromUnknown(primaryKey.value) : void 0,
|
|
2254
|
+
query_fingerprint: stringFromUnknown(input.payload.query_fingerprint)
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
queryAuditMetadata(input) {
|
|
2258
|
+
if (input.proposal) {
|
|
2259
|
+
return {
|
|
2260
|
+
tenant_id: input.proposal.tenant_id,
|
|
2261
|
+
principal: input.proposal.change_set.principal.id,
|
|
2262
|
+
capability: input.proposal.action,
|
|
2263
|
+
business_object: input.proposal.business_object,
|
|
2264
|
+
object_id: input.proposal.object_id,
|
|
2265
|
+
primary_key_value: String(input.proposal.change_set.source.primary_key.value)
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
const firstItem = input.evidence?.items.find((item) => isRecord2(item.item))?.item;
|
|
2269
|
+
const primaryKey = isRecord2(firstItem?.primary_key) ? firstItem.primary_key : void 0;
|
|
2270
|
+
return {
|
|
2271
|
+
tenant_id: input.evidence?.tenant_id ?? stringFromUnknown(input.payload.tenant_id),
|
|
2272
|
+
principal: input.evidence?.principal ?? stringFromPrincipal(input.payload.principal),
|
|
2273
|
+
capability: input.evidence?.capability ?? stringFromUnknown(input.payload.capability),
|
|
2274
|
+
business_object: input.evidence?.business_object ?? stringFromUnknown(input.payload.business_object),
|
|
2275
|
+
object_id: input.evidence?.object_id ?? stringFromUnknown(input.payload.object_id),
|
|
2276
|
+
primary_key_value: primaryKey ? stringFromUnknown(primaryKey.value) : input.evidence?.object_id ?? stringFromUnknown(input.payload.primary_key_value)
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
countTable(table) {
|
|
2280
|
+
return this.countWhere(table, "1 = 1", []);
|
|
2281
|
+
}
|
|
2282
|
+
countWhere(table, where, params) {
|
|
2283
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get(...params);
|
|
2284
|
+
return isRecord2(row) ? Number(row.count ?? 0) : 0;
|
|
2285
|
+
}
|
|
2286
|
+
numberValue(sql) {
|
|
2287
|
+
const row = this.db.prepare(sql).get();
|
|
2288
|
+
if (!isRecord2(row)) return 0;
|
|
2289
|
+
const value = Object.values(row)[0];
|
|
2290
|
+
return typeof value === "number" ? value : Number(value ?? 0);
|
|
2291
|
+
}
|
|
2292
|
+
stringColumn(sql, params, column) {
|
|
2293
|
+
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);
|
|
2294
|
+
}
|
|
2295
|
+
evidenceIdsForPrune(cutoffIso, proposalIds) {
|
|
2296
|
+
const proposalWhere = inWhere("proposal_id", proposalIds);
|
|
2297
|
+
if (!proposalWhere) {
|
|
2298
|
+
return this.stringColumn("SELECT evidence_bundle_id FROM evidence_bundles WHERE created_at < ?", [cutoffIso], "evidence_bundle_id");
|
|
2299
|
+
}
|
|
2300
|
+
return this.stringColumn(
|
|
2301
|
+
`SELECT evidence_bundle_id FROM evidence_bundles WHERE created_at < ? OR ${proposalWhere.sql}`,
|
|
2302
|
+
[cutoffIso, ...proposalWhere.params],
|
|
2303
|
+
"evidence_bundle_id"
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
1918
2306
|
transaction(fn) {
|
|
1919
2307
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1920
2308
|
try {
|
|
@@ -1933,6 +2321,133 @@ function stateFromChangeSet(changeSet) {
|
|
|
1933
2321
|
if (changeSet.approval.status === "canceled") return "canceled";
|
|
1934
2322
|
return "pending_review";
|
|
1935
2323
|
}
|
|
2324
|
+
function inWhere(column, values) {
|
|
2325
|
+
if (values.length === 0) return void 0;
|
|
2326
|
+
return {
|
|
2327
|
+
sql: `${column} IN (${values.map(() => "?").join(", ")})`,
|
|
2328
|
+
params: values
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
function buildProposalQuery(filters) {
|
|
2332
|
+
const clauses = [];
|
|
2333
|
+
const params = [];
|
|
2334
|
+
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
2335
|
+
addEqual(clauses, params, "tenant_id", filters.tenant);
|
|
2336
|
+
addEqual(clauses, params, "principal", filters.principal);
|
|
2337
|
+
addEqual(clauses, params, "source_id", filters.source);
|
|
2338
|
+
addTableFilter(clauses, params, "source_table", filters.table);
|
|
2339
|
+
addEqual(clauses, params, "state", filters.status ?? filters.state);
|
|
2340
|
+
addEqual(clauses, params, "action", filters.capability ?? filters.action);
|
|
2341
|
+
addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
|
|
2342
|
+
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2343
|
+
return finishQuery("SELECT * FROM proposals", clauses, params, filters.limit);
|
|
2344
|
+
}
|
|
2345
|
+
function buildEvidenceQuery(filters) {
|
|
2346
|
+
const clauses = [];
|
|
2347
|
+
const params = [];
|
|
2348
|
+
addEqual(clauses, params, "evidence_bundle_id", filters.evidence);
|
|
2349
|
+
addEqual(clauses, params, "tenant_id", filters.tenant);
|
|
2350
|
+
addEqual(clauses, params, "principal", filters.principal);
|
|
2351
|
+
addEqual(clauses, params, "capability", filters.capability);
|
|
2352
|
+
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
2353
|
+
addEqual(clauses, params, "source_id", filters.source);
|
|
2354
|
+
addTableFilter(clauses, params, "source_table", filters.table);
|
|
2355
|
+
addEqual(clauses, params, "query_fingerprint", filters.queryFingerprint);
|
|
2356
|
+
addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
|
|
2357
|
+
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2358
|
+
return finishQuery("SELECT * FROM evidence_bundles", clauses, params, filters.limit);
|
|
2359
|
+
}
|
|
2360
|
+
function buildQueryAuditQuery(filters) {
|
|
2361
|
+
const clauses = [];
|
|
2362
|
+
const params = [];
|
|
2363
|
+
addEqual(clauses, params, "tenant_id", filters.tenant);
|
|
2364
|
+
addEqual(clauses, params, "principal", filters.principal);
|
|
2365
|
+
addEqual(clauses, params, "capability", filters.capability);
|
|
2366
|
+
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
2367
|
+
addEqual(clauses, params, "evidence_bundle_id", filters.evidence);
|
|
2368
|
+
addEqual(clauses, params, "source_id", filters.source);
|
|
2369
|
+
addTableFilter(clauses, params, "table_name", filters.table);
|
|
2370
|
+
addObjectFilter(clauses, params, "business_object", "table_name", "object_id", filters.objectType, filters.objectId);
|
|
2371
|
+
addEqual(clauses, params, "primary_key_value", filters.primaryKey);
|
|
2372
|
+
addEqual(clauses, params, "query_fingerprint", filters.queryFingerprint);
|
|
2373
|
+
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2374
|
+
return finishQuery("SELECT * FROM query_audit", clauses, params, filters.limit);
|
|
2375
|
+
}
|
|
2376
|
+
function buildReceiptQuery(filters) {
|
|
2377
|
+
const clauses = [];
|
|
2378
|
+
const params = [];
|
|
2379
|
+
addEqual(clauses, params, "receipt_id", filters.receipt);
|
|
2380
|
+
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
2381
|
+
addEqual(clauses, params, "writeback_job_id", filters.writebackJob);
|
|
2382
|
+
addEqual(clauses, params, "idempotency_key", filters.idempotencyKey);
|
|
2383
|
+
addEqual(clauses, params, "status", filters.status);
|
|
2384
|
+
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2385
|
+
return finishQuery("SELECT * FROM writeback_receipts", clauses, params, filters.limit);
|
|
2386
|
+
}
|
|
2387
|
+
function addEqual(clauses, params, column, value) {
|
|
2388
|
+
if (!value) return;
|
|
2389
|
+
clauses.push(`${column} = ?`);
|
|
2390
|
+
params.push(value);
|
|
2391
|
+
}
|
|
2392
|
+
function addTableFilter(clauses, params, column, value) {
|
|
2393
|
+
if (!value) return;
|
|
2394
|
+
if (value.includes(".")) {
|
|
2395
|
+
clauses.push(`${column} = ?`);
|
|
2396
|
+
params.push(value);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
clauses.push(`(${column} = ? OR ${column} = ?)`);
|
|
2400
|
+
params.push(value, `public.${value}`);
|
|
2401
|
+
}
|
|
2402
|
+
function addObjectFilter(clauses, params, typeColumn, tableColumn, idColumn, objectType, objectId) {
|
|
2403
|
+
if (objectId) {
|
|
2404
|
+
clauses.push(`${idColumn} = ?`);
|
|
2405
|
+
params.push(objectId);
|
|
2406
|
+
}
|
|
2407
|
+
if (!objectType) return;
|
|
2408
|
+
const variants = objectTypeVariants(objectType);
|
|
2409
|
+
const placeholders = variants.map(() => "?").join(", ");
|
|
2410
|
+
clauses.push(`(${typeColumn} IN (${placeholders}) OR ${tableColumn} IN (${placeholders}))`);
|
|
2411
|
+
params.push(...variants, ...variants);
|
|
2412
|
+
}
|
|
2413
|
+
function addTimeRange(clauses, params, column, from, to) {
|
|
2414
|
+
if (from) {
|
|
2415
|
+
clauses.push(`${column} >= ?`);
|
|
2416
|
+
params.push(from);
|
|
2417
|
+
}
|
|
2418
|
+
if (to) {
|
|
2419
|
+
clauses.push(`${column} <= ?`);
|
|
2420
|
+
params.push(to);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function finishQuery(base, clauses, params, limit) {
|
|
2424
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
2425
|
+
const sql = `${base}${where} ORDER BY created_at DESC${limit ? " LIMIT ?" : ""}`;
|
|
2426
|
+
return { sql, params: limit ? [...params, limit] : params };
|
|
2427
|
+
}
|
|
2428
|
+
function objectTypeVariants(value) {
|
|
2429
|
+
const variants = /* @__PURE__ */ new Set([value]);
|
|
2430
|
+
if (value.endsWith("s")) variants.add(value.slice(0, -1));
|
|
2431
|
+
else variants.add(`${value}s`);
|
|
2432
|
+
for (const variant of [...variants]) {
|
|
2433
|
+
if (!variant.includes(".")) variants.add(`public.${variant}`);
|
|
2434
|
+
}
|
|
2435
|
+
return [...variants];
|
|
2436
|
+
}
|
|
2437
|
+
function stringFromUnknown(value) {
|
|
2438
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2439
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2440
|
+
return void 0;
|
|
2441
|
+
}
|
|
2442
|
+
function stringFromPrincipal(value) {
|
|
2443
|
+
if (typeof value === "string") return value;
|
|
2444
|
+
if (isRecord2(value)) return stringFromUnknown(value.id);
|
|
2445
|
+
return void 0;
|
|
2446
|
+
}
|
|
2447
|
+
function lastIdentifier(value) {
|
|
2448
|
+
const parts = value.split(".");
|
|
2449
|
+
return parts[parts.length - 1] ?? value;
|
|
2450
|
+
}
|
|
1936
2451
|
function stateFromReceipt(receipt) {
|
|
1937
2452
|
if (receipt.status === "applied" || receipt.status === "already_applied") return "applied";
|
|
1938
2453
|
if (receipt.status === "conflict") return "conflict";
|
|
@@ -2007,6 +2522,10 @@ function rowToProposal(row) {
|
|
|
2007
2522
|
action: String(row.action),
|
|
2008
2523
|
state: String(row.state),
|
|
2009
2524
|
tenant_id: String(row.tenant_id),
|
|
2525
|
+
principal: row.principal == null ? void 0 : String(row.principal),
|
|
2526
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
2527
|
+
interaction_id: row.interaction_id == null ? void 0 : String(row.interaction_id),
|
|
2528
|
+
tool_call_id: row.tool_call_id == null ? void 0 : String(row.tool_call_id),
|
|
2010
2529
|
business_object: String(row.business_object),
|
|
2011
2530
|
object_id: String(row.object_id),
|
|
2012
2531
|
source_kind: String(row.source_kind),
|
|
@@ -2033,6 +2552,7 @@ function rowToEvent(row) {
|
|
|
2033
2552
|
function rowToReceipt(row) {
|
|
2034
2553
|
if (!isRecord2(row)) return void 0;
|
|
2035
2554
|
return {
|
|
2555
|
+
receipt_id: Number(row.receipt_id),
|
|
2036
2556
|
writeback_job_id: String(row.writeback_job_id),
|
|
2037
2557
|
proposal_id: String(row.proposal_id),
|
|
2038
2558
|
runner_id: String(row.runner_id),
|
|
@@ -2049,6 +2569,12 @@ function rowToQueryAudit(row) {
|
|
|
2049
2569
|
audit_id: Number(row.audit_id),
|
|
2050
2570
|
proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
|
|
2051
2571
|
evidence_bundle_id: row.evidence_bundle_id == null ? void 0 : String(row.evidence_bundle_id),
|
|
2572
|
+
tenant_id: row.tenant_id == null ? void 0 : String(row.tenant_id),
|
|
2573
|
+
principal: row.principal == null ? void 0 : String(row.principal),
|
|
2574
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
2575
|
+
business_object: row.business_object == null ? void 0 : String(row.business_object),
|
|
2576
|
+
object_id: row.object_id == null ? void 0 : String(row.object_id),
|
|
2577
|
+
primary_key_value: row.primary_key_value == null ? void 0 : String(row.primary_key_value),
|
|
2052
2578
|
source_id: String(row.source_id),
|
|
2053
2579
|
query_fingerprint: String(row.query_fingerprint),
|
|
2054
2580
|
table_name: String(row.table_name),
|
|
@@ -2158,7 +2684,7 @@ function createMcpRuntime(config, options = {}) {
|
|
|
2158
2684
|
}
|
|
2159
2685
|
function createSynapsorMcpServer(runtime) {
|
|
2160
2686
|
const server = new McpServer(
|
|
2161
|
-
{ name: "synapsor-runner", version: "0.1.0-alpha.
|
|
2687
|
+
{ name: "synapsor-runner", version: "0.1.0-alpha.4" },
|
|
2162
2688
|
{ capabilities: { tools: {}, resources: {} } }
|
|
2163
2689
|
);
|
|
2164
2690
|
if (runtime.config.mode === "cloud") {
|
|
@@ -2798,9 +3324,9 @@ function readLocalResource(store, uri) {
|
|
|
2798
3324
|
return { proposal, events: store.events(id), receipts: store.receipts(id) };
|
|
2799
3325
|
}
|
|
2800
3326
|
if (collection === "evidence") {
|
|
2801
|
-
const
|
|
2802
|
-
if (!
|
|
2803
|
-
return
|
|
3327
|
+
const evidence2 = store.getEvidenceBundle(id);
|
|
3328
|
+
if (!evidence2) throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Evidence bundle not found: ${id}`);
|
|
3329
|
+
return evidence2;
|
|
2804
3330
|
}
|
|
2805
3331
|
if (collection === "replay") {
|
|
2806
3332
|
const proposalId = id.startsWith("replay_") ? id.slice("replay_".length) : id;
|
|
@@ -4392,7 +4918,7 @@ async function handleRequest(input) {
|
|
|
4392
4918
|
}
|
|
4393
4919
|
if (request.method === "GET" && url.pathname === "/") {
|
|
4394
4920
|
response.setHeader("set-cookie", `synapsor_ui_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/`);
|
|
4395
|
-
sendHtml(response, renderShell(csrfToken, tour || url.searchParams.get("tour") === "1"));
|
|
4921
|
+
sendHtml(response, renderShell(csrfToken, tour || url.searchParams.get("tour") === "1", configPath, storePath));
|
|
4396
4922
|
return;
|
|
4397
4923
|
}
|
|
4398
4924
|
if (request.method === "GET" && url.pathname === "/api/summary") {
|
|
@@ -4691,8 +5217,10 @@ function sendHtml(response, html) {
|
|
|
4691
5217
|
response.setHeader("cache-control", "no-store");
|
|
4692
5218
|
response.end(html);
|
|
4693
5219
|
}
|
|
4694
|
-
function renderShell(csrfToken, tour = false) {
|
|
5220
|
+
function renderShell(csrfToken, tour = false, configPath = "synapsor.runner.json", storePath = "./.synapsor/local.db") {
|
|
4695
5221
|
const escapedCsrf = escapeScriptString(csrfToken);
|
|
5222
|
+
const escapedConfigPath = escapeScriptString(configPath);
|
|
5223
|
+
const escapedStorePath = escapeScriptString(storePath);
|
|
4696
5224
|
const tourHtml = tour ? `
|
|
4697
5225
|
<div class="card full tour">
|
|
4698
5226
|
<h2>Commit-safe MCP in one loop</h2>
|
|
@@ -4822,6 +5350,8 @@ details.raw > summary { cursor:pointer; color:var(--blue); font-weight:600; font
|
|
|
4822
5350
|
</main>
|
|
4823
5351
|
<script>
|
|
4824
5352
|
const csrfToken = "${escapedCsrf}";
|
|
5353
|
+
const configPath = "${escapedConfigPath}";
|
|
5354
|
+
const storePath = "${escapedStorePath}";
|
|
4825
5355
|
const state = { selected: null, firstId: null };
|
|
4826
5356
|
const byId = (id) => document.getElementById(id);
|
|
4827
5357
|
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 +5445,13 @@ function guardDrawer(gc) {
|
|
|
4915
5445
|
d.append(kv);
|
|
4916
5446
|
return d;
|
|
4917
5447
|
}
|
|
5448
|
+
function shellQuote(value) {
|
|
5449
|
+
const text = String(value || "");
|
|
5450
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : "'" + text.replace(/'/g, "'\\\\''") + "'";
|
|
5451
|
+
}
|
|
5452
|
+
function trustedApplyCommand(proposalId) {
|
|
5453
|
+
return "synapsor apply " + shellQuote(proposalId) + " --config " + shellQuote(configPath) + " --store " + shellQuote(storePath);
|
|
5454
|
+
}
|
|
4918
5455
|
async function loadSummary() {
|
|
4919
5456
|
const payload = await api("/api/summary");
|
|
4920
5457
|
const root = byId("summary"); root.replaceChildren(text("h2", "Setup summary"));
|
|
@@ -5109,6 +5646,24 @@ async function loadDetail(proposalId) {
|
|
|
5109
5646
|
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
5647
|
actions.append(approve, reject);
|
|
5111
5648
|
reviewPane.append(el("div", { class: "callout", text: "You are the approval authority here \u2014 the model cannot reach these controls." }), actor, reason, actions);
|
|
5649
|
+
} else if (proposal.state === "approved" || proposal.state === "pending_worker") {
|
|
5650
|
+
const command = trustedApplyCommand(proposalId);
|
|
5651
|
+
const commandBox = el("div", { class: "mono", text: command, style: "display:block;margin-top:8px" });
|
|
5652
|
+
const copied = el("span", { class: "status-line", text: "" });
|
|
5653
|
+
const copy = el("button", { text: "Copy guarded apply command", onclick: async () => {
|
|
5654
|
+
try {
|
|
5655
|
+
await navigator.clipboard.writeText(command);
|
|
5656
|
+
copied.textContent = "Copied. Run this from a trusted terminal with write credentials.";
|
|
5657
|
+
} catch {
|
|
5658
|
+
copied.textContent = "Copy this guarded apply command and run it from a trusted terminal with write credentials.";
|
|
5659
|
+
}
|
|
5660
|
+
} });
|
|
5661
|
+
reviewPane.append(
|
|
5662
|
+
el("div", { class: "callout", text: "Apply guarded writeback from a trusted terminal. This remains outside MCP, so the model still cannot commit." }),
|
|
5663
|
+
commandBox,
|
|
5664
|
+
el("div", { class: "actions" }, [copy]),
|
|
5665
|
+
copied,
|
|
5666
|
+
);
|
|
5112
5667
|
}
|
|
5113
5668
|
|
|
5114
5669
|
jsonPane.append(
|
|
@@ -5162,8 +5717,97 @@ function stringOrDefault(value, fallback) {
|
|
|
5162
5717
|
// apps/runner/src/cli.ts
|
|
5163
5718
|
var adapters = { postgres: postgresAdapter, mysql: mysqlAdapter };
|
|
5164
5719
|
var handlerReceiptStatuses = /* @__PURE__ */ new Set(["applied", "already_applied", "conflict", "failed"]);
|
|
5720
|
+
var dangerousDatabaseMcpAuditExample = {
|
|
5721
|
+
tools: [
|
|
5722
|
+
{
|
|
5723
|
+
name: "execute_sql",
|
|
5724
|
+
description: "Execute arbitrary SQL against the application database.",
|
|
5725
|
+
inputSchema: {
|
|
5726
|
+
type: "object",
|
|
5727
|
+
properties: {
|
|
5728
|
+
sql: { type: "string" }
|
|
5729
|
+
},
|
|
5730
|
+
required: ["sql"]
|
|
5731
|
+
}
|
|
5732
|
+
},
|
|
5733
|
+
{
|
|
5734
|
+
name: "run_query",
|
|
5735
|
+
description: "Run any query and return database rows.",
|
|
5736
|
+
inputSchema: {
|
|
5737
|
+
type: "object",
|
|
5738
|
+
properties: {
|
|
5739
|
+
query: { type: "string" },
|
|
5740
|
+
table: { type: "string" },
|
|
5741
|
+
columns: {
|
|
5742
|
+
type: "array",
|
|
5743
|
+
items: { type: "string" }
|
|
5744
|
+
}
|
|
5745
|
+
},
|
|
5746
|
+
required: ["query"]
|
|
5747
|
+
}
|
|
5748
|
+
},
|
|
5749
|
+
{
|
|
5750
|
+
name: "approve_refund",
|
|
5751
|
+
description: "Approve and issue a customer refund immediately.",
|
|
5752
|
+
inputSchema: {
|
|
5753
|
+
type: "object",
|
|
5754
|
+
properties: {
|
|
5755
|
+
refund_id: { type: "string" },
|
|
5756
|
+
tenant_id: { type: "string" },
|
|
5757
|
+
amount_cents: { type: "number" }
|
|
5758
|
+
},
|
|
5759
|
+
required: ["refund_id", "tenant_id", "amount_cents"]
|
|
5760
|
+
}
|
|
5761
|
+
},
|
|
5762
|
+
{
|
|
5763
|
+
name: "update_customer",
|
|
5764
|
+
description: "Update a customer record directly.",
|
|
5765
|
+
inputSchema: {
|
|
5766
|
+
type: "object",
|
|
5767
|
+
properties: {
|
|
5768
|
+
customer_id: { type: "string" },
|
|
5769
|
+
tenant_id: { type: "string" },
|
|
5770
|
+
column: { type: "string" },
|
|
5771
|
+
value: { type: "string" }
|
|
5772
|
+
},
|
|
5773
|
+
required: ["customer_id", "tenant_id", "column", "value"]
|
|
5774
|
+
}
|
|
5775
|
+
},
|
|
5776
|
+
{
|
|
5777
|
+
name: "delete_order",
|
|
5778
|
+
description: "Delete an order from the database.",
|
|
5779
|
+
inputSchema: {
|
|
5780
|
+
type: "object",
|
|
5781
|
+
properties: {
|
|
5782
|
+
order_id: { type: "string" },
|
|
5783
|
+
tenant_id: { type: "string" }
|
|
5784
|
+
},
|
|
5785
|
+
required: ["order_id", "tenant_id"]
|
|
5786
|
+
}
|
|
5787
|
+
},
|
|
5788
|
+
{
|
|
5789
|
+
name: "query_database",
|
|
5790
|
+
description: "Query arbitrary tables and columns from the database.",
|
|
5791
|
+
inputSchema: {
|
|
5792
|
+
type: "object",
|
|
5793
|
+
properties: {
|
|
5794
|
+
database: { type: "string" },
|
|
5795
|
+
schema: { type: "string" },
|
|
5796
|
+
table: { type: "string" },
|
|
5797
|
+
columns: {
|
|
5798
|
+
type: "array",
|
|
5799
|
+
items: { type: "string" }
|
|
5800
|
+
},
|
|
5801
|
+
where: { type: "string" }
|
|
5802
|
+
},
|
|
5803
|
+
required: ["table"]
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
]
|
|
5807
|
+
};
|
|
5165
5808
|
var defaultConfigPath = "synapsor.runner.json";
|
|
5166
5809
|
var defaultStorePath = "./.synapsor/local.db";
|
|
5810
|
+
var quickDemoStorePath = "./.synapsor/quick-demo.db";
|
|
5167
5811
|
var referenceDemoDir = "examples/reference-support-billing-app";
|
|
5168
5812
|
var referenceDemoConfigPath = `${referenceDemoDir}/synapsor.runner.json`;
|
|
5169
5813
|
var referenceDemoContainer = "synapsor_runner_reference_support_billing";
|
|
@@ -5186,6 +5830,14 @@ async function main(argv) {
|
|
|
5186
5830
|
usage([]);
|
|
5187
5831
|
return 0;
|
|
5188
5832
|
}
|
|
5833
|
+
if (!isKnownTopLevelCommand(command)) {
|
|
5834
|
+
process2.stderr.write(`Unknown command: synapsor ${command}
|
|
5835
|
+
|
|
5836
|
+
Try:
|
|
5837
|
+
synapsor --help
|
|
5838
|
+
`);
|
|
5839
|
+
return 2;
|
|
5840
|
+
}
|
|
5189
5841
|
if (isHelpRequest(rest)) {
|
|
5190
5842
|
usage([command, ...rest.filter((arg) => arg !== "--help" && arg !== "-h")]);
|
|
5191
5843
|
return 0;
|
|
@@ -5213,9 +5865,18 @@ async function main(argv) {
|
|
|
5213
5865
|
if (command === "benchmark") return benchmark(rest);
|
|
5214
5866
|
if (command === "proposals") return proposals(rest);
|
|
5215
5867
|
if (command === "replay") return replay(rest);
|
|
5868
|
+
if (command === "evidence") return evidence(rest);
|
|
5869
|
+
if (command === "query-audit") return queryAudit(rest);
|
|
5870
|
+
if (command === "receipts") return receipts(rest);
|
|
5871
|
+
if (command === "activity") return activity(rest);
|
|
5872
|
+
if (command === "store") return storeCommand(rest);
|
|
5216
5873
|
if (command === "shadow") return shadow(rest);
|
|
5217
5874
|
if (command === "ui") return ui(rest);
|
|
5218
|
-
|
|
5875
|
+
process2.stderr.write(`Unknown command: synapsor ${command}
|
|
5876
|
+
|
|
5877
|
+
Try:
|
|
5878
|
+
synapsor --help
|
|
5879
|
+
`);
|
|
5219
5880
|
return 2;
|
|
5220
5881
|
}
|
|
5221
5882
|
async function init(args) {
|
|
@@ -6039,6 +6700,49 @@ function formatLocalDoctorReport(report) {
|
|
|
6039
6700
|
return `${lines.join("\n")}
|
|
6040
6701
|
`;
|
|
6041
6702
|
}
|
|
6703
|
+
function formatLocalDoctorMarkdown(report) {
|
|
6704
|
+
const store = report.store_stats;
|
|
6705
|
+
const boundaryOk = report.checks.find((check) => check.name === "mcp-tool-boundary")?.ok === true;
|
|
6706
|
+
const lines = [
|
|
6707
|
+
"# Synapsor Runner Doctor Report",
|
|
6708
|
+
"",
|
|
6709
|
+
`- Runner package: @synapsor/runner`,
|
|
6710
|
+
`- Node version: ${process2.versions.node}`,
|
|
6711
|
+
`- Config: ${report.config_path}`,
|
|
6712
|
+
`- Mode: ${report.mode}`,
|
|
6713
|
+
`- Status: ${report.ok ? "ok" : "needs attention"}`,
|
|
6714
|
+
"",
|
|
6715
|
+
"## Semantic Tools",
|
|
6716
|
+
"",
|
|
6717
|
+
...report.tools.length ? report.tools.map((tool) => `- ${tool}`) : ["- none listed"],
|
|
6718
|
+
"",
|
|
6719
|
+
"## Safety Boundary",
|
|
6720
|
+
"",
|
|
6721
|
+
`- Raw SQL / commit tools exposed: ${boundaryOk ? "no obvious forbidden tools detected" : "needs review"}`,
|
|
6722
|
+
"- Database URLs, passwords, bearer tokens, and private keys are intentionally not included in this report.",
|
|
6723
|
+
"",
|
|
6724
|
+
"## Local Store",
|
|
6725
|
+
"",
|
|
6726
|
+
`- Path: ${store?.path ?? "not configured"}`,
|
|
6727
|
+
`- Exists: ${store?.exists ? "yes" : "no"}`,
|
|
6728
|
+
...store?.exists ? [
|
|
6729
|
+
`- Proposals: ${store.proposals ?? 0}`,
|
|
6730
|
+
`- Evidence bundles: ${store.evidence ?? 0}`,
|
|
6731
|
+
`- Query audit records: ${store.query_audit ?? 0}`,
|
|
6732
|
+
`- Receipts: ${store.receipts ?? 0}`
|
|
6733
|
+
] : [],
|
|
6734
|
+
"",
|
|
6735
|
+
"## Checks",
|
|
6736
|
+
"",
|
|
6737
|
+
...report.checks.map((check) => `- ${check.level.toUpperCase()} ${check.name}: ${check.message}`),
|
|
6738
|
+
"",
|
|
6739
|
+
"## Redaction Note",
|
|
6740
|
+
"",
|
|
6741
|
+
"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."
|
|
6742
|
+
];
|
|
6743
|
+
return `${lines.join("\n")}
|
|
6744
|
+
`;
|
|
6745
|
+
}
|
|
6042
6746
|
var moduleDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
6043
6747
|
var packageAssetRoot = path3.resolve(moduleDir, "..");
|
|
6044
6748
|
var sourceAssetRoot = path3.resolve(moduleDir, "../../..");
|
|
@@ -6391,9 +7095,16 @@ async function localDoctor(args) {
|
|
|
6391
7095
|
mode: String(parsed.mode),
|
|
6392
7096
|
config_path: configPath,
|
|
6393
7097
|
checks,
|
|
6394
|
-
tools: tools2
|
|
7098
|
+
tools: tools2,
|
|
7099
|
+
store_stats: await localDoctorStoreStats(optionalArg(args, "--store") ?? parsed.storage?.sqlite_path)
|
|
6395
7100
|
};
|
|
6396
|
-
if (args.includes("--
|
|
7101
|
+
if (args.includes("--report")) {
|
|
7102
|
+
const output = outputArg(args) ?? "synapsor-doctor.md";
|
|
7103
|
+
await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
|
|
7104
|
+
await fs3.writeFile(output, formatLocalDoctorMarkdown(report), "utf8");
|
|
7105
|
+
process2.stdout.write(`wrote redacted doctor report: ${output}
|
|
7106
|
+
`);
|
|
7107
|
+
} else if (args.includes("--json")) {
|
|
6397
7108
|
process2.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
6398
7109
|
`);
|
|
6399
7110
|
} else {
|
|
@@ -6401,6 +7112,23 @@ async function localDoctor(args) {
|
|
|
6401
7112
|
}
|
|
6402
7113
|
return report.ok ? 0 : 1;
|
|
6403
7114
|
}
|
|
7115
|
+
async function localDoctorStoreStats(storePath) {
|
|
7116
|
+
if (!storePath || storePath === ":memory:") return { path: storePath ?? "not configured", exists: storePath === ":memory:" };
|
|
7117
|
+
if (!await fileExists(storePath)) return { path: storePath, exists: false };
|
|
7118
|
+
const store = new ProposalStore(storePath);
|
|
7119
|
+
try {
|
|
7120
|
+
return {
|
|
7121
|
+
path: storePath,
|
|
7122
|
+
exists: true,
|
|
7123
|
+
proposals: store.listProposals({ limit: 1e6 }).length,
|
|
7124
|
+
evidence: store.listEvidenceBundles({ limit: 1e6 }).length,
|
|
7125
|
+
query_audit: store.listQueryAudit({ limit: 1e6 }).length,
|
|
7126
|
+
receipts: store.listReceipts({ limit: 1e6 }).length
|
|
7127
|
+
};
|
|
7128
|
+
} finally {
|
|
7129
|
+
store.close();
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
6404
7132
|
async function validate(args) {
|
|
6405
7133
|
const job = await readJob(args);
|
|
6406
7134
|
parseWritebackJob(job);
|
|
@@ -6479,21 +7207,21 @@ async function applyProposal(args, proposalId) {
|
|
|
6479
7207
|
lease_seconds: Number(optionalArg(args, "--lease-seconds") ?? "300")
|
|
6480
7208
|
});
|
|
6481
7209
|
const result = await applySqlJob(job, configPath, storePath, dryRun, envWithDemoDefaults(config, configPath));
|
|
6482
|
-
process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6483
|
-
`);
|
|
7210
|
+
process2.stdout.write(args.includes("--json") ? `${JSON.stringify(result, null, 2)}
|
|
7211
|
+
` : formatApplyResult(parseWritebackJob(job), result, dryRun, storePath));
|
|
6484
7212
|
return result.status === "failed" ? 1 : 0;
|
|
6485
7213
|
}
|
|
6486
7214
|
const executor = executorConfig(config, executorName);
|
|
6487
7215
|
if (executor.type === "http_handler") {
|
|
6488
7216
|
const result = await applyHttpHandlerProposal({ store, proposalId: resolvedProposalId, proposal, executorName, executor, runnerId, dryRun, env: envWithDemoDefaults(config, configPath) });
|
|
6489
|
-
process2.stdout.write(`${JSON.stringify(redactConfig(result), null, 2)}
|
|
6490
|
-
`);
|
|
7217
|
+
process2.stdout.write(args.includes("--json") ? `${JSON.stringify(redactConfig(result), null, 2)}
|
|
7218
|
+
` : formatHandlerApplyResult(result, resolvedProposalId, storePath));
|
|
6491
7219
|
return result.status === "failed" ? 1 : 0;
|
|
6492
7220
|
}
|
|
6493
7221
|
if (executor.type === "command_handler") {
|
|
6494
7222
|
const result = await applyCommandHandlerProposal({ store, proposalId: resolvedProposalId, proposal, executorName, executor, runnerId, dryRun, env: envWithDemoDefaults(config, configPath) });
|
|
6495
|
-
process2.stdout.write(`${JSON.stringify(redactConfig(result), null, 2)}
|
|
6496
|
-
`);
|
|
7223
|
+
process2.stdout.write(args.includes("--json") ? `${JSON.stringify(redactConfig(result), null, 2)}
|
|
7224
|
+
` : formatHandlerApplyResult(result, resolvedProposalId, storePath));
|
|
6497
7225
|
return result.status === "failed" ? 1 : 0;
|
|
6498
7226
|
}
|
|
6499
7227
|
throw new Error(`unsupported executor type for ${executorName}`);
|
|
@@ -6669,8 +7397,8 @@ function prepareHandlerProposal(store, proposal, runnerId) {
|
|
|
6669
7397
|
};
|
|
6670
7398
|
}
|
|
6671
7399
|
function duplicateHandlerReceipt(store, proposalId) {
|
|
6672
|
-
const
|
|
6673
|
-
const existing =
|
|
7400
|
+
const receipts2 = store.receipts(proposalId);
|
|
7401
|
+
const existing = receipts2.find((receipt) => receipt.writeback_job_id.startsWith("hwb_"));
|
|
6674
7402
|
return existing ? { receipt: existing.receipt } : void 0;
|
|
6675
7403
|
}
|
|
6676
7404
|
function alreadyAppliedReceipt(receipt, runnerId) {
|
|
@@ -6749,6 +7477,107 @@ function buildHandlerReceipt(input) {
|
|
|
6749
7477
|
function hashReceipt(input) {
|
|
6750
7478
|
return `sha256:${crypto5.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
|
|
6751
7479
|
}
|
|
7480
|
+
function formatApplyResult(job, result, dryRun, storePath) {
|
|
7481
|
+
const status = writebackResultStatus(result);
|
|
7482
|
+
const affectedRows = writebackAffectedRows(result);
|
|
7483
|
+
const errorCode = writebackErrorCode(result);
|
|
7484
|
+
const receiptHash = writebackReceiptHash(result);
|
|
7485
|
+
const conflictGuardPassed = status === "conflict" && errorCode === "VERSION_CONFLICT" ? "no" : status === "applied" ? "yes" : "not completed";
|
|
7486
|
+
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.";
|
|
7487
|
+
const lines = [
|
|
7488
|
+
title,
|
|
7489
|
+
"",
|
|
7490
|
+
"Checks:",
|
|
7491
|
+
"* proposal approved: yes",
|
|
7492
|
+
`* primary key matched: ${status === "conflict" && errorCode === "ROW_NOT_FOUND" ? "no" : status === "failed" ? "not completed" : "yes"}`,
|
|
7493
|
+
`* tenant guard matched: ${status === "conflict" && errorCode === "ROW_NOT_FOUND" ? "no" : status === "failed" ? "not completed" : "yes"}`,
|
|
7494
|
+
"* allowed columns only: yes",
|
|
7495
|
+
`* conflict guard passed: ${conflictGuardPassed}`,
|
|
7496
|
+
`* affected rows: ${affectedRows}`,
|
|
7497
|
+
`* idempotency key: ${job.idempotency_key}`,
|
|
7498
|
+
""
|
|
7499
|
+
];
|
|
7500
|
+
if (status === "conflict") {
|
|
7501
|
+
lines.push(
|
|
7502
|
+
errorCode === "VERSION_CONFLICT" ? "The row changed after the agent saw it." : "The target row was not available under the primary-key and tenant guard.",
|
|
7503
|
+
"",
|
|
7504
|
+
"Result:",
|
|
7505
|
+
"conflict",
|
|
7506
|
+
"",
|
|
7507
|
+
"Source DB changed by Synapsor:",
|
|
7508
|
+
"no",
|
|
7509
|
+
"",
|
|
7510
|
+
"Why:",
|
|
7511
|
+
errorCode === "VERSION_CONFLICT" ? "conflict/version guard did not match" : errorCode || "guarded writeback returned conflict",
|
|
7512
|
+
""
|
|
7513
|
+
);
|
|
7514
|
+
}
|
|
7515
|
+
if (status === "failed") {
|
|
7516
|
+
lines.push("Error:", errorCode || "writeback failed", "");
|
|
7517
|
+
}
|
|
7518
|
+
lines.push(
|
|
7519
|
+
"Receipt:",
|
|
7520
|
+
receiptHash || "(stored locally)",
|
|
7521
|
+
"",
|
|
7522
|
+
"Replay:",
|
|
7523
|
+
`${cliCommandName()} replay ${job.proposal_id} --store ${storePath}`,
|
|
7524
|
+
""
|
|
7525
|
+
);
|
|
7526
|
+
return `${lines.join("\n")}
|
|
7527
|
+
`;
|
|
7528
|
+
}
|
|
7529
|
+
function formatHandlerApplyResult(receipt, proposalId, storePath) {
|
|
7530
|
+
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.";
|
|
7531
|
+
const lines = [
|
|
7532
|
+
title,
|
|
7533
|
+
"",
|
|
7534
|
+
"Checks:",
|
|
7535
|
+
"* proposal approved: yes",
|
|
7536
|
+
"* execution authority: app-owned handler outside MCP",
|
|
7537
|
+
`* source database changed by handler: ${receipt.source_database_mutated ? "yes" : "no"}`,
|
|
7538
|
+
`* affected rows: ${receipt.rows_affected}`,
|
|
7539
|
+
`* idempotency key: ${receipt.idempotency_key}`,
|
|
7540
|
+
""
|
|
7541
|
+
];
|
|
7542
|
+
if (receipt.status === "conflict") {
|
|
7543
|
+
lines.push(
|
|
7544
|
+
"Result:",
|
|
7545
|
+
"conflict",
|
|
7546
|
+
"",
|
|
7547
|
+
"Why:",
|
|
7548
|
+
receipt.safe_error_code || "handler returned conflict",
|
|
7549
|
+
""
|
|
7550
|
+
);
|
|
7551
|
+
}
|
|
7552
|
+
if (receipt.status === "failed") {
|
|
7553
|
+
lines.push("Error:", receipt.safe_error_code || "handler writeback failed", "");
|
|
7554
|
+
}
|
|
7555
|
+
lines.push(
|
|
7556
|
+
"Receipt:",
|
|
7557
|
+
receipt.receipt_hash,
|
|
7558
|
+
"",
|
|
7559
|
+
"Replay:",
|
|
7560
|
+
`${cliCommandName()} replay ${proposalId} --store ${storePath}`,
|
|
7561
|
+
""
|
|
7562
|
+
);
|
|
7563
|
+
return `${lines.join("\n")}
|
|
7564
|
+
`;
|
|
7565
|
+
}
|
|
7566
|
+
function writebackResultStatus(result) {
|
|
7567
|
+
return String(result.status ?? "unknown");
|
|
7568
|
+
}
|
|
7569
|
+
function writebackAffectedRows(result) {
|
|
7570
|
+
const value = result.affected_rows ?? result.rows_affected ?? 0;
|
|
7571
|
+
return Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
7572
|
+
}
|
|
7573
|
+
function writebackErrorCode(result) {
|
|
7574
|
+
const value = result.error_code ?? result.safe_error_code;
|
|
7575
|
+
return typeof value === "string" && value ? value : void 0;
|
|
7576
|
+
}
|
|
7577
|
+
function writebackReceiptHash(result) {
|
|
7578
|
+
const value = result.result_hash ?? result.receipt_hash;
|
|
7579
|
+
return typeof value === "string" && value ? value : void 0;
|
|
7580
|
+
}
|
|
6752
7581
|
function parseOptionalJson(text) {
|
|
6753
7582
|
if (!text.trim()) return {};
|
|
6754
7583
|
try {
|
|
@@ -6912,7 +7741,7 @@ async function cloudConnect(args) {
|
|
|
6912
7741
|
return 1;
|
|
6913
7742
|
}
|
|
6914
7743
|
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
6915
|
-
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.
|
|
7744
|
+
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.4").trim();
|
|
6916
7745
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
6917
7746
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
6918
7747
|
const client = new ControlPlaneClient({
|
|
@@ -7015,8 +7844,16 @@ async function demo(args) {
|
|
|
7015
7844
|
return prepareReferenceDemo(args);
|
|
7016
7845
|
}
|
|
7017
7846
|
async function quickDemo() {
|
|
7847
|
+
const seeded = await seedQuickDemoStore(quickDemoStorePath);
|
|
7018
7848
|
process2.stdout.write([
|
|
7019
|
-
"Synapsor Runner quick demo
|
|
7849
|
+
"Synapsor Runner quick demo",
|
|
7850
|
+
"",
|
|
7851
|
+
"This is a fixture-only first look. It does not start Docker, connect a database,",
|
|
7852
|
+
"or require an MCP client. It writes an inspectable local ledger fixture to:",
|
|
7853
|
+
quickDemoStorePath,
|
|
7854
|
+
"",
|
|
7855
|
+
"If you ran this through one-off npx and did not install the package, prefix",
|
|
7856
|
+
"follow-up commands with: npx -y -p @synapsor/runner@alpha synapsor",
|
|
7020
7857
|
"",
|
|
7021
7858
|
"Raw MCP shape:",
|
|
7022
7859
|
"execute_sql(sql: string)",
|
|
@@ -7027,7 +7864,7 @@ async function quickDemo() {
|
|
|
7027
7864
|
"billing.propose_late_fee_waiver(invoice_id, reason)",
|
|
7028
7865
|
"",
|
|
7029
7866
|
"Agent requested:",
|
|
7030
|
-
'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
|
|
7867
|
+
'billing.propose_late_fee_waiver(invoice_id="INV-3001", reason="approved support waiver")',
|
|
7031
7868
|
"",
|
|
7032
7869
|
"Trusted context:",
|
|
7033
7870
|
"tenant_id = acme",
|
|
@@ -7043,47 +7880,215 @@ async function quickDemo() {
|
|
|
7043
7880
|
"required outside MCP",
|
|
7044
7881
|
"",
|
|
7045
7882
|
"Replay:",
|
|
7046
|
-
|
|
7883
|
+
`${seeded.replay_id} captures the local proposal, evidence handle, query audit, and events.`,
|
|
7884
|
+
"",
|
|
7885
|
+
"Inspect this local fixture:",
|
|
7886
|
+
`${cliCommandName()} proposals show latest --store ${quickDemoStorePath}`,
|
|
7887
|
+
`${cliCommandName()} evidence show ${seeded.evidence_bundle_id} --store ${quickDemoStorePath}`,
|
|
7888
|
+
`${cliCommandName()} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`,
|
|
7889
|
+
`${cliCommandName()} replay show --proposal ${seeded.proposal_id} --store ${quickDemoStorePath}`,
|
|
7047
7890
|
"",
|
|
7048
|
-
"
|
|
7891
|
+
"Approve the fixture outside MCP:",
|
|
7892
|
+
`${cliCommandName()} proposals approve latest --yes --store ${quickDemoStorePath}`,
|
|
7893
|
+
`${cliCommandName()} replay show latest --store ${quickDemoStorePath}`,
|
|
7894
|
+
"",
|
|
7895
|
+
"For real guarded writeback against disposable Postgres:",
|
|
7049
7896
|
`${cliCommandName()} demo`,
|
|
7050
|
-
|
|
7897
|
+
"",
|
|
7898
|
+
"Audit risky MCP database tools:",
|
|
7899
|
+
`${cliCommandName()} audit --example dangerous-db-mcp`,
|
|
7051
7900
|
""
|
|
7052
7901
|
].join("\n"));
|
|
7053
7902
|
return 0;
|
|
7054
7903
|
}
|
|
7055
|
-
async function
|
|
7056
|
-
const
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7904
|
+
async function seedQuickDemoStore(storePath) {
|
|
7905
|
+
const resolved = path3.resolve(storePath);
|
|
7906
|
+
await fs3.mkdir(path3.dirname(resolved), { recursive: true });
|
|
7907
|
+
await fs3.rm(resolved, { force: true });
|
|
7908
|
+
const store = new ProposalStore(resolved);
|
|
7909
|
+
try {
|
|
7910
|
+
const changeSet = quickDemoChangeSet();
|
|
7911
|
+
const proposal = store.createProposal(changeSet);
|
|
7912
|
+
store.recordEvidenceBundle({
|
|
7913
|
+
evidence_bundle_id: changeSet.evidence.bundle_id,
|
|
7914
|
+
proposal_id: proposal.proposal_id,
|
|
7915
|
+
tenant_id: changeSet.scope.tenant_id,
|
|
7916
|
+
payload: {
|
|
7917
|
+
capability: changeSet.action,
|
|
7918
|
+
proposal_id: proposal.proposal_id,
|
|
7919
|
+
source_id: changeSet.source.source_id,
|
|
7920
|
+
target: `${changeSet.source.schema}.${changeSet.source.table}`,
|
|
7921
|
+
principal: changeSet.principal.id,
|
|
7922
|
+
tenant_id: changeSet.scope.tenant_id,
|
|
7923
|
+
business_object: changeSet.scope.business_object,
|
|
7924
|
+
object_id: changeSet.scope.object_id,
|
|
7925
|
+
query_fingerprint: changeSet.evidence.query_fingerprint,
|
|
7926
|
+
source_database_changed: false
|
|
7927
|
+
},
|
|
7928
|
+
items: [
|
|
7929
|
+
{
|
|
7930
|
+
kind: "external_row",
|
|
7931
|
+
source_id: changeSet.source.source_id,
|
|
7932
|
+
table: `${changeSet.source.schema}.${changeSet.source.table}`,
|
|
7933
|
+
primary_key: changeSet.source.primary_key,
|
|
7934
|
+
tenant: changeSet.guards.tenant,
|
|
7935
|
+
visible_row: changeSet.before
|
|
7936
|
+
},
|
|
7937
|
+
{
|
|
7938
|
+
kind: "proposal_diff",
|
|
7939
|
+
before: changeSet.before,
|
|
7940
|
+
patch: changeSet.patch,
|
|
7941
|
+
after: changeSet.after
|
|
7942
|
+
}
|
|
7943
|
+
]
|
|
7944
|
+
});
|
|
7945
|
+
store.recordQueryAudit({
|
|
7946
|
+
proposal_id: proposal.proposal_id,
|
|
7947
|
+
evidence_bundle_id: changeSet.evidence.bundle_id,
|
|
7948
|
+
source_id: changeSet.source.source_id,
|
|
7949
|
+
query_fingerprint: changeSet.evidence.query_fingerprint,
|
|
7950
|
+
table_name: `${changeSet.source.schema}.${changeSet.source.table}`,
|
|
7951
|
+
row_count: 1,
|
|
7952
|
+
payload: {
|
|
7953
|
+
capability: changeSet.action,
|
|
7954
|
+
tenant_bound: true,
|
|
7955
|
+
statement_template: "SELECT id, tenant_id, updated_at, late_fee_cents FROM invoices WHERE id = ? AND tenant_id = ? LIMIT 1",
|
|
7956
|
+
parameters_redacted: true
|
|
7957
|
+
}
|
|
7958
|
+
});
|
|
7959
|
+
store.replay(proposal.proposal_id);
|
|
7960
|
+
return {
|
|
7961
|
+
proposal_id: proposal.proposal_id,
|
|
7962
|
+
evidence_bundle_id: changeSet.evidence.bundle_id,
|
|
7963
|
+
replay_id: `replay_${proposal.proposal_id}`
|
|
7964
|
+
};
|
|
7965
|
+
} finally {
|
|
7966
|
+
store.close();
|
|
7071
7967
|
}
|
|
7072
|
-
const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "5000");
|
|
7073
|
-
const payload = await readMcpAuditTarget(target2, args, timeoutMs);
|
|
7074
|
-
const report = auditMcpManifest(payload, { target: target2 });
|
|
7075
|
-
process2.stdout.write(json ? `${JSON.stringify(report, null, 2)}
|
|
7076
|
-
` : formatMcpAuditReport(report));
|
|
7077
|
-
return 0;
|
|
7078
7968
|
}
|
|
7079
|
-
|
|
7080
|
-
const
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7969
|
+
function quickDemoChangeSet() {
|
|
7970
|
+
const base = {
|
|
7971
|
+
schema_version: protocolVersions.changeSet,
|
|
7972
|
+
proposal_id: "wrp_quick_INV_3001",
|
|
7973
|
+
proposal_version: 1,
|
|
7974
|
+
action: "billing.propose_late_fee_waiver",
|
|
7975
|
+
mode: "review_required",
|
|
7976
|
+
principal: {
|
|
7977
|
+
id: "support.agent",
|
|
7978
|
+
source: "trusted_session"
|
|
7979
|
+
},
|
|
7980
|
+
scope: {
|
|
7981
|
+
tenant_id: "acme",
|
|
7982
|
+
business_object: "invoice",
|
|
7983
|
+
object_id: "INV-3001"
|
|
7984
|
+
},
|
|
7985
|
+
source: {
|
|
7986
|
+
kind: "external_postgres",
|
|
7987
|
+
source_id: "app_postgres",
|
|
7988
|
+
schema: "public",
|
|
7989
|
+
table: "invoices",
|
|
7990
|
+
primary_key: {
|
|
7991
|
+
column: "id",
|
|
7992
|
+
value: "INV-3001"
|
|
7993
|
+
}
|
|
7994
|
+
},
|
|
7995
|
+
before: {
|
|
7996
|
+
id: "INV-3001",
|
|
7997
|
+
tenant_id: "acme",
|
|
7998
|
+
updated_at: "2026-06-23T09:00:00Z",
|
|
7999
|
+
late_fee_cents: 5500
|
|
8000
|
+
},
|
|
8001
|
+
patch: {
|
|
8002
|
+
late_fee_cents: 0
|
|
8003
|
+
},
|
|
8004
|
+
after: {
|
|
8005
|
+
id: "INV-3001",
|
|
8006
|
+
tenant_id: "acme",
|
|
8007
|
+
updated_at: "2026-06-23T09:00:00Z",
|
|
8008
|
+
late_fee_cents: 0
|
|
8009
|
+
},
|
|
8010
|
+
guards: {
|
|
8011
|
+
tenant: {
|
|
8012
|
+
column: "tenant_id",
|
|
8013
|
+
value: "acme"
|
|
8014
|
+
},
|
|
8015
|
+
allowed_columns: ["late_fee_cents"],
|
|
8016
|
+
expected_version: {
|
|
8017
|
+
column: "updated_at",
|
|
8018
|
+
value: "2026-06-23T09:00:00Z"
|
|
8019
|
+
}
|
|
8020
|
+
},
|
|
8021
|
+
evidence: {
|
|
8022
|
+
bundle_id: "ev_quick_INV_3001",
|
|
8023
|
+
query_fingerprint: "sha256:quick-demo-invoice-read",
|
|
8024
|
+
items: [
|
|
8025
|
+
{
|
|
8026
|
+
kind: "external_row",
|
|
8027
|
+
source_id: "app_postgres",
|
|
8028
|
+
table: "public.invoices",
|
|
8029
|
+
primary_key: { column: "id", value: "INV-3001" }
|
|
8030
|
+
}
|
|
8031
|
+
]
|
|
8032
|
+
},
|
|
8033
|
+
approval: {
|
|
8034
|
+
status: "pending",
|
|
8035
|
+
required_role: "local_operator"
|
|
8036
|
+
},
|
|
8037
|
+
writeback: {
|
|
8038
|
+
status: "not_applied",
|
|
8039
|
+
mode: "trusted_worker_required"
|
|
8040
|
+
},
|
|
8041
|
+
source_database_mutated: false,
|
|
8042
|
+
integrity: {
|
|
8043
|
+
proposal_hash: "sha256:placeholder"
|
|
8044
|
+
},
|
|
8045
|
+
created_at: "2026-06-23T09:00:00Z"
|
|
8046
|
+
};
|
|
8047
|
+
return {
|
|
8048
|
+
...base,
|
|
8049
|
+
integrity: {
|
|
8050
|
+
proposal_hash: hashReceipt({ ...base, integrity: void 0 })
|
|
8051
|
+
}
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
async function mcpServe(args) {
|
|
8055
|
+
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
8056
|
+
const readOnly = args.includes("--read-only");
|
|
8057
|
+
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
8058
|
+
await serveStdio({
|
|
8059
|
+
configPath,
|
|
8060
|
+
storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
|
|
8061
|
+
config
|
|
8062
|
+
});
|
|
8063
|
+
return 0;
|
|
8064
|
+
}
|
|
8065
|
+
async function mcpAudit(args) {
|
|
8066
|
+
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
8067
|
+
if (!["text", "json", "markdown"].includes(format)) {
|
|
8068
|
+
throw new Error("audit --format must be text, json, or markdown");
|
|
8069
|
+
}
|
|
8070
|
+
const example = optionalArg(args, "--example");
|
|
8071
|
+
const target2 = example ? `example:${example}` : firstPositional(args);
|
|
8072
|
+
if (!target2) {
|
|
8073
|
+
throw new Error("mcp audit requires <target> or --example dangerous-db-mcp");
|
|
8074
|
+
}
|
|
8075
|
+
const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "5000");
|
|
8076
|
+
const payload = example ? builtInMcpAuditExample(example) : await readMcpAuditTarget(target2, args, timeoutMs);
|
|
8077
|
+
const report = auditMcpManifest(payload, { target: target2 });
|
|
8078
|
+
if (format === "json") process2.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
8079
|
+
`);
|
|
8080
|
+
else if (format === "markdown") process2.stdout.write(formatMcpAuditMarkdown(report));
|
|
8081
|
+
else process2.stdout.write(formatMcpAuditReport(report));
|
|
8082
|
+
return 0;
|
|
8083
|
+
}
|
|
8084
|
+
async function propose(args) {
|
|
8085
|
+
const capabilityName = firstPositional(args);
|
|
8086
|
+
if (!capabilityName) throw new Error("propose requires <capability-name>");
|
|
8087
|
+
const configPath = optionalArg(args, "--config") ?? defaultConfigPath;
|
|
8088
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? defaultStorePath;
|
|
8089
|
+
const config = await readRuntimeConfig(configPath);
|
|
8090
|
+
const capability = (config.capabilities ?? []).find((item) => item.name === capabilityName);
|
|
8091
|
+
if (!capability) throw new Error(`proposal capability not found: ${capabilityName}`);
|
|
7087
8092
|
if (capability.kind !== "proposal") throw new Error(`${capabilityName} is a ${capability.kind} capability. Use a proposal capability with ${cliCommandName()} propose.`);
|
|
7088
8093
|
const input = await proposalInput(args, capability);
|
|
7089
8094
|
const env = envWithDemoDefaults(config, configPath);
|
|
@@ -7106,8 +8111,9 @@ async function audit(args) {
|
|
|
7106
8111
|
const url = optionalArg(args, "--url");
|
|
7107
8112
|
const stdio = optionalArg(args, "--stdio");
|
|
7108
8113
|
const mcpConfig2 = optionalArg(args, "--mcp-config");
|
|
7109
|
-
const
|
|
7110
|
-
|
|
8114
|
+
const example = optionalArg(args, "--example");
|
|
8115
|
+
const target2 = example ? `example:${example}` : url ?? (stdio ? `stdio:${stdio}` : mcpConfig2 ?? firstPositional(args));
|
|
8116
|
+
if (!target2) throw new Error("audit requires <target>, --example dangerous-db-mcp, --mcp-config <path>, --stdio <command>, or --url <url>");
|
|
7111
8117
|
const forwarded = args.filter((arg, index) => {
|
|
7112
8118
|
const previous = args[index - 1];
|
|
7113
8119
|
return !["--url", "--stdio", "--mcp-config"].includes(arg) && !["--url", "--stdio", "--mcp-config"].includes(previous ?? "");
|
|
@@ -7137,13 +8143,13 @@ function sampleInputForCapability(capability) {
|
|
|
7137
8143
|
const input = {};
|
|
7138
8144
|
for (const [name, spec] of Object.entries(capability.args)) {
|
|
7139
8145
|
if (name === capability.lookup.id_from_arg) input[name] = sampleIdForCapability(capability, name);
|
|
7140
|
-
else if (/reason/i.test(name)) input[name] =
|
|
8146
|
+
else if (/reason/i.test(name)) input[name] = sampleReasonForCapability(capability);
|
|
7141
8147
|
else if (/resolution/i.test(name)) input[name] = "Resolved after reviewing policy evidence.";
|
|
8148
|
+
else if (spec.enum?.length) input[name] = spec.enum[0];
|
|
7142
8149
|
else if (/status/i.test(name)) input[name] = "pending_review";
|
|
7143
8150
|
else if (/amount|cents|fee|credit|balance/i.test(name)) input[name] = typeof spec.maximum === "number" ? Math.min(spec.maximum, 1e3) : 0;
|
|
7144
8151
|
else if (spec.type === "number") input[name] = spec.minimum ?? 1;
|
|
7145
8152
|
else if (spec.type === "boolean") input[name] = true;
|
|
7146
|
-
else if (spec.enum?.length) input[name] = spec.enum[0];
|
|
7147
8153
|
else input[name] = `sample_${name}`;
|
|
7148
8154
|
}
|
|
7149
8155
|
const missing = Object.entries(capability.args).filter(([, spec]) => spec.required !== false).filter(([name]) => input[name] === void 0).map(([name]) => name);
|
|
@@ -7154,12 +8160,20 @@ function sampleInputForCapability(capability) {
|
|
|
7154
8160
|
}
|
|
7155
8161
|
function sampleIdForCapability(capability, argName) {
|
|
7156
8162
|
const text = `${capability.name} ${capability.target.table} ${argName}`.toLowerCase();
|
|
8163
|
+
const arg = argName.toLowerCase();
|
|
7157
8164
|
if (/invoice|billing/.test(text)) return "INV-3001";
|
|
8165
|
+
if (/account|customer/.test(arg) || /accounts|customers/.test(text)) return "cust_acme_1";
|
|
7158
8166
|
if (/ticket|support/.test(text)) return "T-1042";
|
|
7159
|
-
if (/order/.test(text)) return "
|
|
7160
|
-
if (/account|customer/.test(text)) return "cust_acme_1";
|
|
8167
|
+
if (/order/.test(text)) return "O-1001";
|
|
7161
8168
|
return "sample_1";
|
|
7162
8169
|
}
|
|
8170
|
+
function sampleReasonForCapability(capability) {
|
|
8171
|
+
const text = `${capability.name} ${capability.target.table}`.toLowerCase();
|
|
8172
|
+
if (/order|status_change/.test(text)) return "payment cleared and ready for the next status";
|
|
8173
|
+
if (/credit|customer|account/.test(text)) return "support goodwill credit";
|
|
8174
|
+
if (/late_fee|waiver|billing|invoice/.test(text)) return "approved support waiver";
|
|
8175
|
+
return "reviewed and approved by support";
|
|
8176
|
+
}
|
|
7163
8177
|
function formatProposeResult(capabilityName, result, storePath) {
|
|
7164
8178
|
const proposalId = String(result.proposal_id ?? "");
|
|
7165
8179
|
const evidenceId = String(result.evidence_bundle_id ?? "");
|
|
@@ -7723,6 +8737,10 @@ async function readMcpAuditTarget(target2, args, timeoutMs) {
|
|
|
7723
8737
|
}
|
|
7724
8738
|
return parsed;
|
|
7725
8739
|
}
|
|
8740
|
+
function builtInMcpAuditExample(example) {
|
|
8741
|
+
if (example === "dangerous-db-mcp") return dangerousDatabaseMcpAuditExample;
|
|
8742
|
+
throw new Error(`unknown audit example: ${example}. Available examples: dangerous-db-mcp`);
|
|
8743
|
+
}
|
|
7726
8744
|
function isRunnerConfigLike(value) {
|
|
7727
8745
|
return isRecord6(value) && value.version === 1 && Array.isArray(value.capabilities);
|
|
7728
8746
|
}
|
|
@@ -7813,12 +8831,50 @@ async function proposals(args) {
|
|
|
7813
8831
|
}
|
|
7814
8832
|
async function replay(args) {
|
|
7815
8833
|
const [subcommand, ...rest] = args;
|
|
8834
|
+
if (subcommand === "list") return replayList(rest);
|
|
7816
8835
|
if (subcommand && !["show", "export"].includes(subcommand)) return replayShow(args);
|
|
7817
8836
|
if (subcommand === "show") return replayShow(rest);
|
|
7818
8837
|
if (subcommand === "export") return replayExport(rest);
|
|
7819
8838
|
usage(["replay"]);
|
|
7820
8839
|
return 2;
|
|
7821
8840
|
}
|
|
8841
|
+
async function evidence(args) {
|
|
8842
|
+
const [subcommand, ...rest] = args;
|
|
8843
|
+
if (subcommand === "show") return evidenceShow(rest);
|
|
8844
|
+
if (subcommand === "list") return evidenceList(rest);
|
|
8845
|
+
if (subcommand === "export") return evidenceExport(rest);
|
|
8846
|
+
usage(["evidence"]);
|
|
8847
|
+
return 2;
|
|
8848
|
+
}
|
|
8849
|
+
async function queryAudit(args) {
|
|
8850
|
+
const [subcommand, ...rest] = args;
|
|
8851
|
+
if (subcommand === "list") return queryAuditList(rest);
|
|
8852
|
+
if (subcommand === "show") return queryAuditShow(rest);
|
|
8853
|
+
if (subcommand === "export") return queryAuditExport(rest);
|
|
8854
|
+
usage(["query-audit"]);
|
|
8855
|
+
return 2;
|
|
8856
|
+
}
|
|
8857
|
+
async function receipts(args) {
|
|
8858
|
+
const [subcommand, ...rest] = args;
|
|
8859
|
+
if (subcommand === "list") return receiptsList(rest);
|
|
8860
|
+
if (subcommand === "show") return receiptsShow(rest);
|
|
8861
|
+
usage(["receipts"]);
|
|
8862
|
+
return 2;
|
|
8863
|
+
}
|
|
8864
|
+
async function activity(args) {
|
|
8865
|
+
const [subcommand, ...rest] = args;
|
|
8866
|
+
if (subcommand === "search") return activitySearch(rest);
|
|
8867
|
+
usage(["activity"]);
|
|
8868
|
+
return 2;
|
|
8869
|
+
}
|
|
8870
|
+
async function storeCommand(args) {
|
|
8871
|
+
const [subcommand, ...rest] = args;
|
|
8872
|
+
if (subcommand === "stats") return storeStats(rest);
|
|
8873
|
+
if (subcommand === "vacuum") return storeVacuum(rest);
|
|
8874
|
+
if (subcommand === "prune") return storePrune(rest);
|
|
8875
|
+
usage(["store"]);
|
|
8876
|
+
return 2;
|
|
8877
|
+
}
|
|
7822
8878
|
async function shadow(args) {
|
|
7823
8879
|
const [subcommand, ...rest] = args;
|
|
7824
8880
|
if (subcommand === "list") return shadowList(rest);
|
|
@@ -7939,10 +8995,11 @@ async function shadowReport(args) {
|
|
|
7939
8995
|
}
|
|
7940
8996
|
}
|
|
7941
8997
|
async function proposalsList(args) {
|
|
8998
|
+
assertKnownOptions(args, proposalListAllowedOptions, "proposals list");
|
|
7942
8999
|
const store = await openLocalStore(args);
|
|
7943
9000
|
try {
|
|
7944
|
-
const
|
|
7945
|
-
const rows = store.listProposals(
|
|
9001
|
+
const filters = proposalFiltersFromArgs(args);
|
|
9002
|
+
const rows = store.listProposals(filters);
|
|
7946
9003
|
if (args.includes("--json")) {
|
|
7947
9004
|
process2.stdout.write(`${JSON.stringify({ proposals: rows }, null, 2)}
|
|
7948
9005
|
`);
|
|
@@ -7968,12 +9025,13 @@ async function proposalsShow(args) {
|
|
|
7968
9025
|
const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
|
|
7969
9026
|
const proposal = store.getProposal(resolvedProposalId);
|
|
7970
9027
|
if (!proposal) throw new Error(`proposal not found: ${resolvedProposalId}`);
|
|
7971
|
-
const
|
|
9028
|
+
const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
|
|
9029
|
+
const payload = { proposal, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId), evidence: evidence2 };
|
|
7972
9030
|
if (args.includes("--json")) {
|
|
7973
9031
|
process2.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
7974
9032
|
`);
|
|
7975
9033
|
} else {
|
|
7976
|
-
process2.stdout.write(formatProposalDetail(proposal));
|
|
9034
|
+
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
7977
9035
|
for (const event of payload.events) {
|
|
7978
9036
|
process2.stdout.write(`event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}
|
|
7979
9037
|
`);
|
|
@@ -7992,7 +9050,8 @@ async function proposalsApprove(args) {
|
|
|
7992
9050
|
const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
|
|
7993
9051
|
const proposal = requireLocalProposal(store, resolvedProposalId);
|
|
7994
9052
|
if (!args.includes("--json")) {
|
|
7995
|
-
|
|
9053
|
+
const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
|
|
9054
|
+
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
7996
9055
|
}
|
|
7997
9056
|
await confirmDangerousAction(args, `Approve proposal ${resolvedProposalId} for guarded writeback?`);
|
|
7998
9057
|
const updated = store.approveProposal(resolvedProposalId, {
|
|
@@ -8019,7 +9078,8 @@ async function proposalsReject(args) {
|
|
|
8019
9078
|
const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
|
|
8020
9079
|
const proposal = requireLocalProposal(store, resolvedProposalId);
|
|
8021
9080
|
if (!args.includes("--json")) {
|
|
8022
|
-
|
|
9081
|
+
const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
|
|
9082
|
+
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
8023
9083
|
}
|
|
8024
9084
|
await confirmDangerousAction(args, `Reject proposal ${resolvedProposalId}?`);
|
|
8025
9085
|
const updated = store.rejectProposal(resolvedProposalId, {
|
|
@@ -8063,28 +9123,180 @@ async function proposalsWritebackJob(args) {
|
|
|
8063
9123
|
store.close();
|
|
8064
9124
|
}
|
|
8065
9125
|
}
|
|
8066
|
-
async function
|
|
8067
|
-
|
|
8068
|
-
if (!proposalId) throw new Error("replay show requires <proposal_id>");
|
|
9126
|
+
async function evidenceList(args) {
|
|
9127
|
+
assertKnownOptions(args, evidenceListAllowedOptions, "evidence list");
|
|
8069
9128
|
const store = await openLocalStore(args);
|
|
8070
9129
|
try {
|
|
8071
|
-
const
|
|
8072
|
-
const replayRecord = store.replay(resolvedProposalId);
|
|
9130
|
+
const rows = store.listEvidenceBundles(evidenceFiltersFromArgs(args));
|
|
8073
9131
|
if (args.includes("--json")) {
|
|
8074
|
-
process2.stdout.write(`${JSON.stringify(
|
|
9132
|
+
process2.stdout.write(`${JSON.stringify({ evidence: rows }, null, 2)}
|
|
8075
9133
|
`);
|
|
9134
|
+
} else if (rows.length === 0) {
|
|
9135
|
+
process2.stdout.write("No evidence bundles found.\n");
|
|
8076
9136
|
} else {
|
|
8077
|
-
process2.stdout.write(
|
|
9137
|
+
for (const bundle of rows) process2.stdout.write(formatEvidenceSummary(bundle));
|
|
9138
|
+
}
|
|
9139
|
+
return 0;
|
|
9140
|
+
} finally {
|
|
9141
|
+
store.close();
|
|
9142
|
+
}
|
|
9143
|
+
}
|
|
9144
|
+
async function evidenceShow(args) {
|
|
9145
|
+
assertKnownOptions(args, showAllowedOptions, "evidence show");
|
|
9146
|
+
const evidenceId = positional(args, 0);
|
|
9147
|
+
if (!evidenceId) throw new Error("evidence show requires <evidence_bundle_id>");
|
|
9148
|
+
const store = await openLocalStore(args);
|
|
9149
|
+
try {
|
|
9150
|
+
const evidence2 = store.getEvidenceBundle(evidenceId);
|
|
9151
|
+
if (!evidence2) throw new Error(`evidence bundle not found: ${evidenceId}`);
|
|
9152
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(evidence2, null, 2)}
|
|
9153
|
+
`);
|
|
9154
|
+
else process2.stdout.write(formatEvidenceDetail(evidence2));
|
|
9155
|
+
return 0;
|
|
9156
|
+
} finally {
|
|
9157
|
+
store.close();
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9160
|
+
async function evidenceExport(args) {
|
|
9161
|
+
assertKnownOptions(args, exportAllowedOptions, "evidence export");
|
|
9162
|
+
const evidenceId = positional(args, 0) ?? optionalArg(args, "--evidence");
|
|
9163
|
+
if (!evidenceId) throw new Error("evidence export requires <evidence_bundle_id>");
|
|
9164
|
+
const output = outputArg(args);
|
|
9165
|
+
if (!output) throw new Error("evidence export requires --output <path>");
|
|
9166
|
+
const format = exportFormat(args);
|
|
9167
|
+
const store = await openLocalStore(args);
|
|
9168
|
+
try {
|
|
9169
|
+
const evidence2 = store.getEvidenceBundle(evidenceId);
|
|
9170
|
+
if (!evidence2) throw new Error(`evidence bundle not found: ${evidenceId}`);
|
|
9171
|
+
const text = format === "json" ? `${JSON.stringify(evidence2, null, 2)}
|
|
9172
|
+
` : formatEvidenceMarkdown(evidence2);
|
|
9173
|
+
await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
|
|
9174
|
+
await fs3.writeFile(output, text, "utf8");
|
|
9175
|
+
process2.stdout.write(`exported ${evidence2.evidence_bundle_id} to ${output}
|
|
9176
|
+
`);
|
|
9177
|
+
return 0;
|
|
9178
|
+
} finally {
|
|
9179
|
+
store.close();
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
async function queryAuditList(args) {
|
|
9183
|
+
assertKnownOptions(args, queryAuditListAllowedOptions, "query-audit list");
|
|
9184
|
+
const store = await openLocalStore(args);
|
|
9185
|
+
try {
|
|
9186
|
+
const rows = store.listQueryAudit(queryAuditFiltersFromArgs(args));
|
|
9187
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ query_audit: rows }, null, 2)}
|
|
9188
|
+
`);
|
|
9189
|
+
else if (rows.length === 0) process2.stdout.write("No query audit records found.\n");
|
|
9190
|
+
else for (const row of rows) process2.stdout.write(formatQueryAuditSummary(row));
|
|
9191
|
+
return 0;
|
|
9192
|
+
} finally {
|
|
9193
|
+
store.close();
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
async function queryAuditShow(args) {
|
|
9197
|
+
assertKnownOptions(args, showAllowedOptions, "query-audit show");
|
|
9198
|
+
const auditId = Number(positional(args, 0));
|
|
9199
|
+
if (!Number.isInteger(auditId) || auditId <= 0) throw new Error("query-audit show requires <audit_id>");
|
|
9200
|
+
const store = await openLocalStore(args);
|
|
9201
|
+
try {
|
|
9202
|
+
const row = store.getQueryAudit(auditId);
|
|
9203
|
+
if (!row) throw new Error(`query audit record not found: ${auditId}`);
|
|
9204
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(row, null, 2)}
|
|
8078
9205
|
`);
|
|
8079
|
-
|
|
8080
|
-
|
|
9206
|
+
else process2.stdout.write(formatQueryAuditDetail(row));
|
|
9207
|
+
return 0;
|
|
9208
|
+
} finally {
|
|
9209
|
+
store.close();
|
|
9210
|
+
}
|
|
9211
|
+
}
|
|
9212
|
+
async function queryAuditExport(args) {
|
|
9213
|
+
assertKnownOptions(args, exportAllowedOptions, "query-audit export");
|
|
9214
|
+
const auditId = Number(positional(args, 0) ?? optionalArg(args, "--audit"));
|
|
9215
|
+
if (!Number.isInteger(auditId) || auditId <= 0) throw new Error("query-audit export requires <audit_id>");
|
|
9216
|
+
const output = outputArg(args);
|
|
9217
|
+
if (!output) throw new Error("query-audit export requires --output <path>");
|
|
9218
|
+
const format = exportFormat(args, ["json"]);
|
|
9219
|
+
const store = await openLocalStore(args);
|
|
9220
|
+
try {
|
|
9221
|
+
const row = store.getQueryAudit(auditId);
|
|
9222
|
+
if (!row) throw new Error(`query audit record not found: ${auditId}`);
|
|
9223
|
+
await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
|
|
9224
|
+
await fs3.writeFile(output, `${JSON.stringify(row, null, 2)}
|
|
9225
|
+
`, "utf8");
|
|
9226
|
+
process2.stdout.write(`exported query audit ${auditId} to ${output}
|
|
8081
9227
|
`);
|
|
8082
|
-
|
|
9228
|
+
return 0;
|
|
9229
|
+
} finally {
|
|
9230
|
+
store.close();
|
|
9231
|
+
}
|
|
9232
|
+
}
|
|
9233
|
+
async function receiptsList(args) {
|
|
9234
|
+
assertKnownOptions(args, receiptListAllowedOptions, "receipts list");
|
|
9235
|
+
const store = await openLocalStore(args);
|
|
9236
|
+
try {
|
|
9237
|
+
const rows = store.listReceipts(receiptFiltersFromArgs(args));
|
|
9238
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ receipts: rows }, null, 2)}
|
|
9239
|
+
`);
|
|
9240
|
+
else if (rows.length === 0) process2.stdout.write("No writeback receipts found.\n");
|
|
9241
|
+
else for (const receipt of rows) process2.stdout.write(formatReceiptSummary(receipt));
|
|
9242
|
+
return 0;
|
|
9243
|
+
} finally {
|
|
9244
|
+
store.close();
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
async function receiptsShow(args) {
|
|
9248
|
+
assertKnownOptions(args, showAllowedOptions, "receipts show");
|
|
9249
|
+
const receiptId = Number(positional(args, 0));
|
|
9250
|
+
if (!Number.isInteger(receiptId) || receiptId <= 0) throw new Error("receipts show requires <receipt_id>");
|
|
9251
|
+
const store = await openLocalStore(args);
|
|
9252
|
+
try {
|
|
9253
|
+
const receipt = store.getReceipt(receiptId);
|
|
9254
|
+
if (!receipt) throw new Error(`writeback receipt not found: ${receiptId}`);
|
|
9255
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(receipt, null, 2)}
|
|
8083
9256
|
`);
|
|
8084
|
-
|
|
9257
|
+
else process2.stdout.write(formatReceiptDetail(receipt));
|
|
9258
|
+
return 0;
|
|
9259
|
+
} finally {
|
|
9260
|
+
store.close();
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
async function replayList(args) {
|
|
9264
|
+
assertKnownOptions(args, replayListAllowedOptions, "replay list");
|
|
9265
|
+
const store = await openLocalStore(args);
|
|
9266
|
+
try {
|
|
9267
|
+
const filters = proposalFiltersFromReplayArgs(args, store);
|
|
9268
|
+
const proposals2 = store.listProposals(filters);
|
|
9269
|
+
const rows = proposals2.map((proposal) => ({
|
|
9270
|
+
replay_id: `replay_${proposal.proposal_id}`,
|
|
9271
|
+
proposal_id: proposal.proposal_id,
|
|
9272
|
+
created_at: proposal.created_at,
|
|
9273
|
+
state: proposal.state,
|
|
9274
|
+
tenant_id: proposal.tenant_id,
|
|
9275
|
+
principal: proposal.principal ?? proposal.change_set.principal.id,
|
|
9276
|
+
capability: proposal.action,
|
|
9277
|
+
business_object: proposal.business_object,
|
|
9278
|
+
object_id: proposal.object_id
|
|
9279
|
+
}));
|
|
9280
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ replays: rows }, null, 2)}
|
|
8085
9281
|
`);
|
|
8086
|
-
|
|
9282
|
+
else if (rows.length === 0) process2.stdout.write("No replay records found.\n");
|
|
9283
|
+
else for (const row of rows) process2.stdout.write(formatReplaySummary(row));
|
|
9284
|
+
return 0;
|
|
9285
|
+
} finally {
|
|
9286
|
+
store.close();
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
async function replayShow(args) {
|
|
9290
|
+
assertKnownOptions(args, replayShowAllowedOptions, "replay show");
|
|
9291
|
+
const store = await openLocalStore(args);
|
|
9292
|
+
try {
|
|
9293
|
+
const resolvedProposalId = resolveReplayProposalId(args, store);
|
|
9294
|
+
const replayRecord = store.replay(resolvedProposalId);
|
|
9295
|
+
if (args.includes("--json")) {
|
|
9296
|
+
process2.stdout.write(`${JSON.stringify(replayRecord, null, 2)}
|
|
8087
9297
|
`);
|
|
9298
|
+
} else {
|
|
9299
|
+
process2.stdout.write(formatReplayDetail(replayRecord));
|
|
8088
9300
|
}
|
|
8089
9301
|
return 0;
|
|
8090
9302
|
} finally {
|
|
@@ -8092,17 +9304,18 @@ async function replayShow(args) {
|
|
|
8092
9304
|
}
|
|
8093
9305
|
}
|
|
8094
9306
|
async function replayExport(args) {
|
|
8095
|
-
|
|
8096
|
-
if (!proposalId) throw new Error("replay export requires <proposal_id>");
|
|
9307
|
+
assertKnownOptions(args, replayExportAllowedOptions, "replay export");
|
|
8097
9308
|
const output = outputArg(args);
|
|
8098
9309
|
if (!output) throw new Error("replay export requires --output <path>");
|
|
9310
|
+
const format = exportFormat(args);
|
|
8099
9311
|
const store = await openLocalStore(args);
|
|
8100
9312
|
try {
|
|
8101
|
-
const resolvedProposalId =
|
|
9313
|
+
const resolvedProposalId = resolveReplayProposalId(args, store);
|
|
8102
9314
|
const replayRecord = store.replay(resolvedProposalId);
|
|
9315
|
+
const text = format === "json" ? `${JSON.stringify(replayRecord, null, 2)}
|
|
9316
|
+
` : formatReplayMarkdown(replayRecord);
|
|
8103
9317
|
await fs3.mkdir(path3.dirname(path3.resolve(output)), { recursive: true });
|
|
8104
|
-
await fs3.writeFile(output,
|
|
8105
|
-
`, "utf8");
|
|
9318
|
+
await fs3.writeFile(output, text, "utf8");
|
|
8106
9319
|
process2.stdout.write(`exported ${replayRecord.replay_id} to ${output}
|
|
8107
9320
|
`);
|
|
8108
9321
|
return 0;
|
|
@@ -8110,9 +9323,421 @@ async function replayExport(args) {
|
|
|
8110
9323
|
store.close();
|
|
8111
9324
|
}
|
|
8112
9325
|
}
|
|
9326
|
+
async function activitySearch(args) {
|
|
9327
|
+
assertKnownOptions(args, activitySearchAllowedOptions, "activity search");
|
|
9328
|
+
const store = await openLocalStore(args);
|
|
9329
|
+
try {
|
|
9330
|
+
const proposalFilters = proposalFiltersFromActivityArgs(args, store);
|
|
9331
|
+
const evidenceFilters = evidenceFiltersFromActivityArgs(args, store);
|
|
9332
|
+
const queryAuditFilters = queryAuditFiltersFromActivityArgs(args, store);
|
|
9333
|
+
const receiptFilters = receiptFiltersFromActivityArgs(args, store);
|
|
9334
|
+
const proposals2 = store.listProposals(proposalFilters);
|
|
9335
|
+
const evidenceRows = store.listEvidenceBundles(evidenceFilters);
|
|
9336
|
+
const queryAuditRows = store.listQueryAudit(queryAuditFilters);
|
|
9337
|
+
const receiptsRows = store.listReceipts(receiptFilters);
|
|
9338
|
+
const proposalIds = new Set(proposals2.map((proposal) => proposal.proposal_id));
|
|
9339
|
+
const evidenceIds = new Set(evidenceRows.map((evidence2) => evidence2.evidence_bundle_id));
|
|
9340
|
+
const results = proposals2.map((proposal) => activityFromProposal(proposal));
|
|
9341
|
+
for (const evidence2 of evidenceRows) {
|
|
9342
|
+
if (evidence2.proposal_id && proposalIds.has(evidence2.proposal_id)) continue;
|
|
9343
|
+
results.push(activityFromEvidence(evidence2));
|
|
9344
|
+
}
|
|
9345
|
+
for (const audit2 of queryAuditRows) {
|
|
9346
|
+
const proposalId = stringField(audit2, "proposal_id");
|
|
9347
|
+
const evidenceId = stringField(audit2, "evidence_bundle_id");
|
|
9348
|
+
if (proposalId && proposalIds.has(proposalId)) continue;
|
|
9349
|
+
if (evidenceId && evidenceIds.has(evidenceId)) continue;
|
|
9350
|
+
results.push(activityFromQueryAudit(audit2));
|
|
9351
|
+
}
|
|
9352
|
+
for (const receipt of receiptsRows) {
|
|
9353
|
+
if (proposalIds.has(receipt.proposal_id)) continue;
|
|
9354
|
+
results.push(activityFromReceipt(receipt));
|
|
9355
|
+
}
|
|
9356
|
+
const sorted = results.sort((left, right) => String(right.created_at ?? "").localeCompare(String(left.created_at ?? ""))).slice(0, limitFromArgs(args));
|
|
9357
|
+
if (args.includes("--json")) {
|
|
9358
|
+
process2.stdout.write(`${JSON.stringify({ interactions: sorted }, null, 2)}
|
|
9359
|
+
`);
|
|
9360
|
+
} else if (sorted.length === 0) {
|
|
9361
|
+
process2.stdout.write("No local interactions found.\n");
|
|
9362
|
+
} else {
|
|
9363
|
+
process2.stdout.write(`Found ${sorted.length} local interaction${sorted.length === 1 ? "" : "s"}
|
|
9364
|
+
|
|
9365
|
+
`);
|
|
9366
|
+
sorted.forEach((item, index) => process2.stdout.write(formatActivityItem(item, index + 1)));
|
|
9367
|
+
}
|
|
9368
|
+
return 0;
|
|
9369
|
+
} finally {
|
|
9370
|
+
store.close();
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
async function storeStats(args) {
|
|
9374
|
+
assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
|
|
9375
|
+
const store = await openLocalStore(args);
|
|
9376
|
+
try {
|
|
9377
|
+
const stats = store.stats();
|
|
9378
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(stats, null, 2)}
|
|
9379
|
+
`);
|
|
9380
|
+
else process2.stdout.write(formatStoreStats(stats));
|
|
9381
|
+
return 0;
|
|
9382
|
+
} finally {
|
|
9383
|
+
store.close();
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
async function storeVacuum(args) {
|
|
9387
|
+
assertKnownOptions(args, storeVacuumAllowedOptions, "store vacuum");
|
|
9388
|
+
const store = await openLocalStore(args);
|
|
9389
|
+
try {
|
|
9390
|
+
const before = store.stats();
|
|
9391
|
+
store.vacuum();
|
|
9392
|
+
const after = store.stats();
|
|
9393
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ before, after }, null, 2)}
|
|
9394
|
+
`);
|
|
9395
|
+
else process2.stdout.write(`vacuumed local store ${before.path}
|
|
9396
|
+
approx bytes: ${before.approx_bytes} -> ${after.approx_bytes}
|
|
9397
|
+
`);
|
|
9398
|
+
return 0;
|
|
9399
|
+
} finally {
|
|
9400
|
+
store.close();
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
async function storePrune(args) {
|
|
9404
|
+
assertKnownOptions(args, storePruneAllowedOptions, "store prune");
|
|
9405
|
+
const olderThan = optionalArg(args, "--older-than");
|
|
9406
|
+
if (!olderThan) throw new Error("store prune requires --older-than <duration>, for example --older-than 30d");
|
|
9407
|
+
if (args.includes("--yes") && args.includes("--dry-run")) throw new Error("store prune accepts either --dry-run or --yes, not both");
|
|
9408
|
+
const cutoff = cutoffFromOlderThan(olderThan);
|
|
9409
|
+
const dryRun = !args.includes("--yes");
|
|
9410
|
+
const store = await openLocalStore(args);
|
|
9411
|
+
try {
|
|
9412
|
+
const result = store.pruneBefore(cutoff, { dryRun });
|
|
9413
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
9414
|
+
`);
|
|
9415
|
+
else process2.stdout.write(formatStorePrune(result));
|
|
9416
|
+
return 0;
|
|
9417
|
+
} finally {
|
|
9418
|
+
store.close();
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json"]);
|
|
9422
|
+
var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
9423
|
+
var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
|
|
9424
|
+
var proposalListAllowedOptions = /* @__PURE__ */ new Set([
|
|
9425
|
+
...commonReadOptions,
|
|
9426
|
+
"--tenant",
|
|
9427
|
+
"--principal",
|
|
9428
|
+
"--capability",
|
|
9429
|
+
"--action",
|
|
9430
|
+
"--object",
|
|
9431
|
+
"--object-type",
|
|
9432
|
+
"--object-id",
|
|
9433
|
+
"--status",
|
|
9434
|
+
"--state",
|
|
9435
|
+
"--source",
|
|
9436
|
+
"--table",
|
|
9437
|
+
"--from",
|
|
9438
|
+
"--to",
|
|
9439
|
+
"--limit"
|
|
9440
|
+
]);
|
|
9441
|
+
var evidenceListAllowedOptions = /* @__PURE__ */ new Set([
|
|
9442
|
+
...commonReadOptions,
|
|
9443
|
+
"--tenant",
|
|
9444
|
+
"--principal",
|
|
9445
|
+
"--capability",
|
|
9446
|
+
"--proposal",
|
|
9447
|
+
"--object",
|
|
9448
|
+
"--object-type",
|
|
9449
|
+
"--object-id",
|
|
9450
|
+
"--source",
|
|
9451
|
+
"--table",
|
|
9452
|
+
"--query-fingerprint",
|
|
9453
|
+
"--from",
|
|
9454
|
+
"--to",
|
|
9455
|
+
"--limit"
|
|
9456
|
+
]);
|
|
9457
|
+
var queryAuditListAllowedOptions = /* @__PURE__ */ new Set([
|
|
9458
|
+
...commonReadOptions,
|
|
9459
|
+
"--tenant",
|
|
9460
|
+
"--proposal",
|
|
9461
|
+
"--evidence",
|
|
9462
|
+
"--source",
|
|
9463
|
+
"--table",
|
|
9464
|
+
"--primary-key",
|
|
9465
|
+
"--query-fingerprint",
|
|
9466
|
+
"--from",
|
|
9467
|
+
"--to",
|
|
9468
|
+
"--limit"
|
|
9469
|
+
]);
|
|
9470
|
+
var receiptListAllowedOptions = /* @__PURE__ */ new Set([
|
|
9471
|
+
...commonReadOptions,
|
|
9472
|
+
"--proposal",
|
|
9473
|
+
"--writeback-job",
|
|
9474
|
+
"--idempotency-key",
|
|
9475
|
+
"--status",
|
|
9476
|
+
"--from",
|
|
9477
|
+
"--to",
|
|
9478
|
+
"--limit"
|
|
9479
|
+
]);
|
|
9480
|
+
var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
|
|
9481
|
+
var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
|
|
9482
|
+
var replayListAllowedOptions = /* @__PURE__ */ new Set([
|
|
9483
|
+
...commonReadOptions,
|
|
9484
|
+
"--tenant",
|
|
9485
|
+
"--principal",
|
|
9486
|
+
"--capability",
|
|
9487
|
+
"--proposal",
|
|
9488
|
+
"--evidence",
|
|
9489
|
+
"--receipt",
|
|
9490
|
+
"--object",
|
|
9491
|
+
"--object-type",
|
|
9492
|
+
"--object-id",
|
|
9493
|
+
"--status",
|
|
9494
|
+
"--state",
|
|
9495
|
+
"--from",
|
|
9496
|
+
"--to",
|
|
9497
|
+
"--limit"
|
|
9498
|
+
]);
|
|
9499
|
+
var activitySearchAllowedOptions = /* @__PURE__ */ new Set([
|
|
9500
|
+
...commonReadOptions,
|
|
9501
|
+
"--tenant",
|
|
9502
|
+
"--principal",
|
|
9503
|
+
"--capability",
|
|
9504
|
+
"--object",
|
|
9505
|
+
"--object-type",
|
|
9506
|
+
"--object-id",
|
|
9507
|
+
"--proposal",
|
|
9508
|
+
"--evidence",
|
|
9509
|
+
"--replay",
|
|
9510
|
+
"--receipt",
|
|
9511
|
+
"--source",
|
|
9512
|
+
"--table",
|
|
9513
|
+
"--query-fingerprint",
|
|
9514
|
+
"--status",
|
|
9515
|
+
"--state",
|
|
9516
|
+
"--from",
|
|
9517
|
+
"--to",
|
|
9518
|
+
"--limit"
|
|
9519
|
+
]);
|
|
9520
|
+
var storeStatsAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
9521
|
+
var storeVacuumAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
9522
|
+
var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes"]);
|
|
9523
|
+
function assertKnownOptions(args, allowed, commandName) {
|
|
9524
|
+
for (const arg of args) {
|
|
9525
|
+
if (!arg.startsWith("--")) continue;
|
|
9526
|
+
const option = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
9527
|
+
if (option === "--help" || option === "-h") continue;
|
|
9528
|
+
if (!allowed.has(option)) throw new Error(`Unknown option for ${commandName}: ${option}`);
|
|
9529
|
+
}
|
|
9530
|
+
}
|
|
9531
|
+
function proposalFiltersFromArgs(args) {
|
|
9532
|
+
const object = objectFilterFromArgs(args);
|
|
9533
|
+
return {
|
|
9534
|
+
proposal: optionalArg(args, "--proposal"),
|
|
9535
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9536
|
+
principal: optionalArg(args, "--principal"),
|
|
9537
|
+
capability: optionalArg(args, "--capability"),
|
|
9538
|
+
action: optionalArg(args, "--action"),
|
|
9539
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9540
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9541
|
+
status: optionalArg(args, "--status"),
|
|
9542
|
+
state: optionalArg(args, "--state"),
|
|
9543
|
+
source: optionalArg(args, "--source"),
|
|
9544
|
+
table: optionalArg(args, "--table"),
|
|
9545
|
+
from: optionalArg(args, "--from"),
|
|
9546
|
+
to: optionalArg(args, "--to"),
|
|
9547
|
+
limit: limitFromArgs(args)
|
|
9548
|
+
};
|
|
9549
|
+
}
|
|
9550
|
+
function evidenceFiltersFromArgs(args) {
|
|
9551
|
+
const object = objectFilterFromArgs(args);
|
|
9552
|
+
return {
|
|
9553
|
+
evidence: optionalArg(args, "--evidence"),
|
|
9554
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9555
|
+
principal: optionalArg(args, "--principal"),
|
|
9556
|
+
capability: optionalArg(args, "--capability"),
|
|
9557
|
+
proposal: optionalArg(args, "--proposal"),
|
|
9558
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9559
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9560
|
+
source: optionalArg(args, "--source"),
|
|
9561
|
+
table: optionalArg(args, "--table"),
|
|
9562
|
+
queryFingerprint: optionalArg(args, "--query-fingerprint"),
|
|
9563
|
+
from: optionalArg(args, "--from"),
|
|
9564
|
+
to: optionalArg(args, "--to"),
|
|
9565
|
+
limit: limitFromArgs(args)
|
|
9566
|
+
};
|
|
9567
|
+
}
|
|
9568
|
+
function queryAuditFiltersFromArgs(args) {
|
|
9569
|
+
const object = objectFilterFromArgs(args);
|
|
9570
|
+
return {
|
|
9571
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9572
|
+
principal: optionalArg(args, "--principal"),
|
|
9573
|
+
capability: optionalArg(args, "--capability"),
|
|
9574
|
+
proposal: optionalArg(args, "--proposal"),
|
|
9575
|
+
evidence: optionalArg(args, "--evidence"),
|
|
9576
|
+
source: optionalArg(args, "--source"),
|
|
9577
|
+
table: optionalArg(args, "--table"),
|
|
9578
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9579
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9580
|
+
primaryKey: optionalArg(args, "--primary-key"),
|
|
9581
|
+
queryFingerprint: optionalArg(args, "--query-fingerprint"),
|
|
9582
|
+
from: optionalArg(args, "--from"),
|
|
9583
|
+
to: optionalArg(args, "--to"),
|
|
9584
|
+
limit: limitFromArgs(args)
|
|
9585
|
+
};
|
|
9586
|
+
}
|
|
9587
|
+
function receiptFiltersFromArgs(args) {
|
|
9588
|
+
return {
|
|
9589
|
+
receipt: optionalArg(args, "--receipt"),
|
|
9590
|
+
proposal: optionalArg(args, "--proposal"),
|
|
9591
|
+
writebackJob: optionalArg(args, "--writeback-job"),
|
|
9592
|
+
idempotencyKey: optionalArg(args, "--idempotency-key"),
|
|
9593
|
+
status: optionalArg(args, "--status"),
|
|
9594
|
+
from: optionalArg(args, "--from"),
|
|
9595
|
+
to: optionalArg(args, "--to"),
|
|
9596
|
+
limit: limitFromArgs(args)
|
|
9597
|
+
};
|
|
9598
|
+
}
|
|
9599
|
+
function proposalFiltersFromReplayArgs(args, store) {
|
|
9600
|
+
return proposalFiltersFromActivityArgs(args, store);
|
|
9601
|
+
}
|
|
9602
|
+
function proposalFiltersFromActivityArgs(args, store) {
|
|
9603
|
+
const object = objectFilterFromArgs(args);
|
|
9604
|
+
const linkedProposal = linkedProposalFilter(args, store);
|
|
9605
|
+
return {
|
|
9606
|
+
proposal: optionalArg(args, "--proposal") ?? linkedProposal,
|
|
9607
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9608
|
+
principal: optionalArg(args, "--principal"),
|
|
9609
|
+
capability: optionalArg(args, "--capability"),
|
|
9610
|
+
action: optionalArg(args, "--capability"),
|
|
9611
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9612
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9613
|
+
status: optionalArg(args, "--status"),
|
|
9614
|
+
state: optionalArg(args, "--state"),
|
|
9615
|
+
source: optionalArg(args, "--source"),
|
|
9616
|
+
table: optionalArg(args, "--table"),
|
|
9617
|
+
from: optionalArg(args, "--from"),
|
|
9618
|
+
to: optionalArg(args, "--to"),
|
|
9619
|
+
limit: limitFromArgs(args)
|
|
9620
|
+
};
|
|
9621
|
+
}
|
|
9622
|
+
function evidenceFiltersFromActivityArgs(args, store) {
|
|
9623
|
+
const object = objectFilterFromArgs(args);
|
|
9624
|
+
const linkedProposal = linkedProposalFilter(args, store, { includeEvidence: false });
|
|
9625
|
+
return {
|
|
9626
|
+
evidence: optionalArg(args, "--evidence"),
|
|
9627
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9628
|
+
principal: optionalArg(args, "--principal"),
|
|
9629
|
+
capability: optionalArg(args, "--capability"),
|
|
9630
|
+
proposal: optionalArg(args, "--proposal") ?? linkedProposal,
|
|
9631
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9632
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9633
|
+
source: optionalArg(args, "--source"),
|
|
9634
|
+
table: optionalArg(args, "--table"),
|
|
9635
|
+
queryFingerprint: optionalArg(args, "--query-fingerprint"),
|
|
9636
|
+
from: optionalArg(args, "--from"),
|
|
9637
|
+
to: optionalArg(args, "--to"),
|
|
9638
|
+
limit: limitFromArgs(args)
|
|
9639
|
+
};
|
|
9640
|
+
}
|
|
9641
|
+
function queryAuditFiltersFromActivityArgs(args, store) {
|
|
9642
|
+
const object = objectFilterFromArgs(args);
|
|
9643
|
+
const linkedProposal = linkedProposalFilter(args, store, { includeEvidence: false });
|
|
9644
|
+
return {
|
|
9645
|
+
tenant: optionalArg(args, "--tenant"),
|
|
9646
|
+
principal: optionalArg(args, "--principal"),
|
|
9647
|
+
capability: optionalArg(args, "--capability"),
|
|
9648
|
+
proposal: optionalArg(args, "--proposal") ?? linkedProposal,
|
|
9649
|
+
evidence: optionalArg(args, "--evidence"),
|
|
9650
|
+
source: optionalArg(args, "--source"),
|
|
9651
|
+
table: optionalArg(args, "--table"),
|
|
9652
|
+
objectType: optionalArg(args, "--object-type") ?? object.type,
|
|
9653
|
+
objectId: optionalArg(args, "--object-id") ?? object.id,
|
|
9654
|
+
queryFingerprint: optionalArg(args, "--query-fingerprint"),
|
|
9655
|
+
from: optionalArg(args, "--from"),
|
|
9656
|
+
to: optionalArg(args, "--to"),
|
|
9657
|
+
limit: limitFromArgs(args)
|
|
9658
|
+
};
|
|
9659
|
+
}
|
|
9660
|
+
function receiptFiltersFromActivityArgs(args, store) {
|
|
9661
|
+
const linkedProposal = linkedProposalFilter(args, store, { includeReceipt: false });
|
|
9662
|
+
return {
|
|
9663
|
+
receipt: optionalArg(args, "--receipt"),
|
|
9664
|
+
proposal: optionalArg(args, "--proposal") ?? linkedProposal,
|
|
9665
|
+
status: optionalArg(args, "--status") ?? optionalArg(args, "--state"),
|
|
9666
|
+
from: optionalArg(args, "--from"),
|
|
9667
|
+
to: optionalArg(args, "--to"),
|
|
9668
|
+
limit: limitFromArgs(args)
|
|
9669
|
+
};
|
|
9670
|
+
}
|
|
9671
|
+
function linkedProposalFilter(args, store, options = {}) {
|
|
9672
|
+
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
9673
|
+
const replay2 = optionalArg(args, "--replay");
|
|
9674
|
+
if (replay2) return proposalIdFromReplayId(replay2);
|
|
9675
|
+
if (!store) return void 0;
|
|
9676
|
+
if (options.includeEvidence !== false) {
|
|
9677
|
+
const evidence2 = optionalArg(args, "--evidence");
|
|
9678
|
+
if (evidence2) return store.proposalIdForEvidence(evidence2) ?? noLinkedProposal;
|
|
9679
|
+
}
|
|
9680
|
+
if (options.includeReceipt !== false) {
|
|
9681
|
+
const receiptValue = optionalArg(args, "--receipt");
|
|
9682
|
+
if (receiptValue) {
|
|
9683
|
+
const receiptId = Number(receiptValue);
|
|
9684
|
+
if (!Number.isInteger(receiptId) || receiptId <= 0) throw new Error("--receipt must be a positive receipt id");
|
|
9685
|
+
return store.getReceipt(receiptId)?.proposal_id ?? noLinkedProposal;
|
|
9686
|
+
}
|
|
9687
|
+
}
|
|
9688
|
+
return void 0;
|
|
9689
|
+
}
|
|
9690
|
+
function objectFilterFromArgs(args) {
|
|
9691
|
+
const value = optionalArg(args, "--object");
|
|
9692
|
+
if (!value) return {};
|
|
9693
|
+
const separator = value.indexOf(":");
|
|
9694
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
9695
|
+
throw new Error("--object must use type:id, for example invoice:INV-3001");
|
|
9696
|
+
}
|
|
9697
|
+
return { type: value.slice(0, separator), id: value.slice(separator + 1) };
|
|
9698
|
+
}
|
|
9699
|
+
function limitFromArgs(args) {
|
|
9700
|
+
const value = optionalArg(args, "--limit");
|
|
9701
|
+
if (!value) return 20;
|
|
9702
|
+
const parsed = Number(value);
|
|
9703
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error("--limit must be a positive integer");
|
|
9704
|
+
return Math.min(parsed, 200);
|
|
9705
|
+
}
|
|
9706
|
+
function exportFormat(args, supported = ["json", "markdown"]) {
|
|
9707
|
+
const format = optionalArg(args, "--format") ?? "json";
|
|
9708
|
+
if (!supported.includes(format)) {
|
|
9709
|
+
throw new Error(`unsupported export format: ${format}. Supported formats: ${supported.join(", ")}`);
|
|
9710
|
+
}
|
|
9711
|
+
return format;
|
|
9712
|
+
}
|
|
9713
|
+
function resolveReplayProposalId(args, store) {
|
|
9714
|
+
const explicitProposal = optionalArg(args, "--proposal");
|
|
9715
|
+
if (explicitProposal) return resolveProposalIdFromStore(explicitProposal, store);
|
|
9716
|
+
const explicitReplay = optionalArg(args, "--replay");
|
|
9717
|
+
if (explicitReplay) return proposalIdFromReplayId(explicitReplay);
|
|
9718
|
+
const explicitEvidence = optionalArg(args, "--evidence");
|
|
9719
|
+
if (explicitEvidence) {
|
|
9720
|
+
const proposalId = store.proposalIdForEvidence(explicitEvidence);
|
|
9721
|
+
if (!proposalId) throw new Error(`evidence bundle ${explicitEvidence} is not linked to a replayable proposal`);
|
|
9722
|
+
return proposalId;
|
|
9723
|
+
}
|
|
9724
|
+
const value = positional(args, 0);
|
|
9725
|
+
if (!value) throw new Error("replay show requires <proposal_id>, --proposal <proposal_id>, --replay <replay_id>, or --evidence <evidence_bundle_id>");
|
|
9726
|
+
if (value === "latest") return resolveProposalIdFromStore(value, store);
|
|
9727
|
+
if (value.startsWith("replay_")) return proposalIdFromReplayId(value);
|
|
9728
|
+
if (value.startsWith("ev_")) throw new Error(`Use --evidence ${value} to replay from an evidence bundle.`);
|
|
9729
|
+
return resolveProposalIdFromStore(value, store);
|
|
9730
|
+
}
|
|
9731
|
+
function proposalIdFromReplayId(replayId) {
|
|
9732
|
+
if (!replayId.startsWith("replay_")) throw new Error(`invalid replay id: ${replayId}`);
|
|
9733
|
+
const proposalId = replayId.slice("replay_".length);
|
|
9734
|
+
if (!proposalId) throw new Error(`invalid replay id: ${replayId}`);
|
|
9735
|
+
return proposalId;
|
|
9736
|
+
}
|
|
8113
9737
|
async function openLocalStore(args) {
|
|
8114
9738
|
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
8115
9739
|
if (storePath !== ":memory:") {
|
|
9740
|
+
if (!await fileExists(storePath)) throw missingLocalStoreError(storePath);
|
|
8116
9741
|
await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
|
|
8117
9742
|
}
|
|
8118
9743
|
return new ProposalStore(storePath);
|
|
@@ -8157,6 +9782,7 @@ function requireLocalProposal(store, proposalId) {
|
|
|
8157
9782
|
}
|
|
8158
9783
|
async function resolveProposalId(proposalId, storePath) {
|
|
8159
9784
|
if (proposalId !== "latest") return proposalId;
|
|
9785
|
+
if (storePath !== ":memory:" && !await fileExists(storePath)) throw missingLocalStoreError(storePath);
|
|
8160
9786
|
const store = new ProposalStore(storePath);
|
|
8161
9787
|
try {
|
|
8162
9788
|
return resolveProposalIdFromStore(proposalId, store);
|
|
@@ -8170,6 +9796,15 @@ function resolveProposalIdFromStore(proposalId, store) {
|
|
|
8170
9796
|
if (!latest) throw new Error("no proposals found in the local store");
|
|
8171
9797
|
return latest.proposal_id;
|
|
8172
9798
|
}
|
|
9799
|
+
function missingLocalStoreError(storePath) {
|
|
9800
|
+
return new Error([
|
|
9801
|
+
`No local Synapsor proposal store was found at ${storePath}.`,
|
|
9802
|
+
"Run:",
|
|
9803
|
+
`${cliCommandName()} demo`,
|
|
9804
|
+
"or pass:",
|
|
9805
|
+
"--store /path/to/local.db"
|
|
9806
|
+
].join("\n"));
|
|
9807
|
+
}
|
|
8173
9808
|
async function readRuntimeConfig(configPath) {
|
|
8174
9809
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
8175
9810
|
return parsed;
|
|
@@ -8320,24 +9955,36 @@ function firstPositional(args) {
|
|
|
8320
9955
|
"--allowed-columns",
|
|
8321
9956
|
"--approval-role",
|
|
8322
9957
|
"--actor",
|
|
9958
|
+
"--action",
|
|
9959
|
+
"--audit",
|
|
8323
9960
|
"--bearer-env",
|
|
9961
|
+
"--capability",
|
|
8324
9962
|
"--config",
|
|
8325
9963
|
"--conflict-column",
|
|
8326
9964
|
"--database-url-env",
|
|
8327
9965
|
"--destination",
|
|
8328
9966
|
"--engine",
|
|
9967
|
+
"--evidence",
|
|
9968
|
+
"--example",
|
|
9969
|
+
"--format",
|
|
8329
9970
|
"--from",
|
|
8330
9971
|
"--from-env",
|
|
8331
9972
|
"--host",
|
|
9973
|
+
"--idempotency-key",
|
|
8332
9974
|
"--input",
|
|
8333
9975
|
"--job",
|
|
8334
9976
|
"--lease-seconds",
|
|
9977
|
+
"--limit",
|
|
8335
9978
|
"--lookup-arg",
|
|
8336
9979
|
"--mode",
|
|
8337
9980
|
"--mcp-config",
|
|
8338
9981
|
"--namespace",
|
|
8339
9982
|
"--numeric-bound",
|
|
9983
|
+
"--object",
|
|
9984
|
+
"--object-id",
|
|
9985
|
+
"--object-type",
|
|
8340
9986
|
"--object-name",
|
|
9987
|
+
"--older-than",
|
|
8341
9988
|
"--output",
|
|
8342
9989
|
"--out",
|
|
8343
9990
|
"--patch-fixed",
|
|
@@ -8345,22 +9992,31 @@ function firstPositional(args) {
|
|
|
8345
9992
|
"--port",
|
|
8346
9993
|
"--primary-key",
|
|
8347
9994
|
"--principal-env",
|
|
9995
|
+
"--proposal",
|
|
8348
9996
|
"--project",
|
|
9997
|
+
"--query-fingerprint",
|
|
8349
9998
|
"--reason",
|
|
8350
9999
|
"--recipe",
|
|
10000
|
+
"--receipt",
|
|
10001
|
+
"--replay",
|
|
8351
10002
|
"--runner",
|
|
8352
10003
|
"--schema",
|
|
8353
10004
|
"--source-name",
|
|
10005
|
+
"--source",
|
|
8354
10006
|
"--state",
|
|
10007
|
+
"--status",
|
|
8355
10008
|
"--stdio",
|
|
8356
10009
|
"--store",
|
|
8357
10010
|
"--table",
|
|
10011
|
+
"--tenant",
|
|
8358
10012
|
"--tenant-env",
|
|
8359
10013
|
"--tenant-key",
|
|
8360
10014
|
"--timeout-ms",
|
|
10015
|
+
"--to",
|
|
8361
10016
|
"--transition-guard",
|
|
8362
10017
|
"--url",
|
|
8363
10018
|
"--visible-columns",
|
|
10019
|
+
"--writeback-job",
|
|
8364
10020
|
"--write-url-env"
|
|
8365
10021
|
]);
|
|
8366
10022
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -8436,15 +10092,16 @@ function parseJsonRpcResponse(stdout, id) {
|
|
|
8436
10092
|
}
|
|
8437
10093
|
function formatProposalSummary(proposal) {
|
|
8438
10094
|
return [
|
|
8439
|
-
`${proposal.proposal_id} ${proposal.state} ${proposal.action}`,
|
|
10095
|
+
`${proposal.created_at} ${proposal.proposal_id} ${proposal.state} ${proposal.action}`,
|
|
10096
|
+
` object: ${proposal.business_object}:${proposal.object_id}`,
|
|
8440
10097
|
` target: ${proposal.source_kind}:${proposal.source_id}/${proposal.source_schema}.${proposal.source_table}/${proposal.object_id}`,
|
|
8441
10098
|
` tenant: ${proposal.tenant_id} source changed: ${proposal.source_database_mutated ? "yes" : "no"}`
|
|
8442
10099
|
].join("\n") + "\n";
|
|
8443
10100
|
}
|
|
8444
|
-
function formatProposalDetail(proposal) {
|
|
10101
|
+
function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
8445
10102
|
const changeSet = proposal.change_set;
|
|
8446
10103
|
const conflictGuard = changeSet.guards.expected_version;
|
|
8447
|
-
const evidenceItems = changeSet.evidence.items?.length ?? 0;
|
|
10104
|
+
const evidenceItems = storedEvidenceItemCount ?? changeSet.evidence.items?.length ?? 0;
|
|
8448
10105
|
const approvalStatus = currentApprovalStatus(proposal);
|
|
8449
10106
|
const writebackStatus = currentWritebackStatus(proposal);
|
|
8450
10107
|
return [
|
|
@@ -8471,6 +10128,386 @@ function formatProposalDetail(proposal) {
|
|
|
8471
10128
|
})
|
|
8472
10129
|
].join("\n") + "\n";
|
|
8473
10130
|
}
|
|
10131
|
+
function formatEvidenceSummary(evidence2) {
|
|
10132
|
+
return [
|
|
10133
|
+
`${evidence2.created_at} ${evidence2.evidence_bundle_id}`,
|
|
10134
|
+
` tenant: ${evidence2.tenant_id} capability: ${evidence2.capability ?? "unknown"} proposal: ${evidence2.proposal_id ?? "none"}`,
|
|
10135
|
+
` source: ${evidence2.source_id ?? "unknown"}/${evidence2.source_table ?? "unknown"} object: ${evidence2.business_object ?? "object"}:${evidence2.object_id ?? "unknown"}`
|
|
10136
|
+
].join("\n") + "\n";
|
|
10137
|
+
}
|
|
10138
|
+
function formatEvidenceDetail(evidence2) {
|
|
10139
|
+
const audit2 = evidence2.query_audit[0];
|
|
10140
|
+
const lines = [
|
|
10141
|
+
`Evidence bundle: ${evidence2.evidence_bundle_id}`,
|
|
10142
|
+
`Tenant: ${evidence2.tenant_id}`,
|
|
10143
|
+
`Proposal: ${evidence2.proposal_id ?? "none"}`,
|
|
10144
|
+
`Principal: ${evidence2.principal ?? "unknown"}`,
|
|
10145
|
+
`Capability: ${evidence2.capability ?? "unknown"}`,
|
|
10146
|
+
`Source: ${evidence2.source_id ?? "unknown"}`,
|
|
10147
|
+
`Table: ${evidence2.source_table ?? "unknown"}`,
|
|
10148
|
+
`Query fingerprint: ${evidence2.query_fingerprint ?? stringField(audit2, "query_fingerprint") ?? "unknown"}`,
|
|
10149
|
+
`Rows captured: ${evidence2.items.length}`,
|
|
10150
|
+
`Created at: ${evidence2.created_at}`,
|
|
10151
|
+
"Projection: captured visible fields only; credentials and secret-looking values are rejected before persistence.",
|
|
10152
|
+
"",
|
|
10153
|
+
"Items:",
|
|
10154
|
+
...evidence2.items.flatMap((item, index) => formatEvidenceItem(item, index + 1)),
|
|
10155
|
+
"",
|
|
10156
|
+
"Related:",
|
|
10157
|
+
...evidence2.proposal_id ? [` ${cliCommandName()} proposals show ${evidence2.proposal_id}`, ` ${cliCommandName()} replay show --proposal ${evidence2.proposal_id}`] : [],
|
|
10158
|
+
` ${cliCommandName()} query-audit list --evidence ${evidence2.evidence_bundle_id}`
|
|
10159
|
+
];
|
|
10160
|
+
return `${lines.join("\n")}
|
|
10161
|
+
`;
|
|
10162
|
+
}
|
|
10163
|
+
function formatEvidenceItem(item, index) {
|
|
10164
|
+
const payload = isRecord6(item.item) ? item.item : item;
|
|
10165
|
+
const visibleRow = isRecord6(payload.visible_row) ? payload.visible_row : payload;
|
|
10166
|
+
const title = stringField(payload, "kind") ?? "item";
|
|
10167
|
+
const primaryKey = isRecord6(payload.primary_key) ? payload.primary_key : void 0;
|
|
10168
|
+
const heading = primaryKey ? `* ${title} ${formatScalar(primaryKey.value)}` : `* ${title} ${index}`;
|
|
10169
|
+
const rows = Object.entries(visibleRow).filter(([key]) => !["kind", "source_id", "table", "primary_key", "tenant"].includes(key)).slice(0, 12).map(([key, value]) => ` ${key}: ${formatScalar(value)}`);
|
|
10170
|
+
return [heading, ...rows.length ? rows : [" (no scalar preview fields)"]];
|
|
10171
|
+
}
|
|
10172
|
+
function formatEvidenceMarkdown(evidence2) {
|
|
10173
|
+
return [
|
|
10174
|
+
`# Evidence ${evidence2.evidence_bundle_id}`,
|
|
10175
|
+
"",
|
|
10176
|
+
`- Tenant: ${evidence2.tenant_id}`,
|
|
10177
|
+
`- Proposal: ${evidence2.proposal_id ?? "none"}`,
|
|
10178
|
+
`- Principal: ${evidence2.principal ?? "unknown"}`,
|
|
10179
|
+
`- Capability: ${evidence2.capability ?? "unknown"}`,
|
|
10180
|
+
`- Source: ${evidence2.source_id ?? "unknown"}`,
|
|
10181
|
+
`- Table: ${evidence2.source_table ?? "unknown"}`,
|
|
10182
|
+
`- Query fingerprint: ${evidence2.query_fingerprint ?? "unknown"}`,
|
|
10183
|
+
`- Created at: ${evidence2.created_at}`,
|
|
10184
|
+
"",
|
|
10185
|
+
"## Captured Items",
|
|
10186
|
+
"",
|
|
10187
|
+
"```json",
|
|
10188
|
+
JSON.stringify(evidence2.items, null, 2),
|
|
10189
|
+
"```",
|
|
10190
|
+
"",
|
|
10191
|
+
"## Query Audit",
|
|
10192
|
+
"",
|
|
10193
|
+
"```json",
|
|
10194
|
+
JSON.stringify(evidence2.query_audit, null, 2),
|
|
10195
|
+
"```"
|
|
10196
|
+
].join("\n") + "\n";
|
|
10197
|
+
}
|
|
10198
|
+
function formatQueryAuditSummary(row) {
|
|
10199
|
+
return [
|
|
10200
|
+
`${row.created_at} audit ${row.audit_id}`,
|
|
10201
|
+
` source: ${row.source_id}/${row.table_name} rows: ${row.row_count} query: ${row.query_fingerprint}`,
|
|
10202
|
+
` proposal: ${row.proposal_id ?? "none"} evidence: ${row.evidence_bundle_id ?? "none"}`
|
|
10203
|
+
].join("\n") + "\n";
|
|
10204
|
+
}
|
|
10205
|
+
function formatQueryAuditDetail(row) {
|
|
10206
|
+
const payload = isRecord6(row.payload) ? row.payload : {};
|
|
10207
|
+
return [
|
|
10208
|
+
`Query audit: ${row.audit_id}`,
|
|
10209
|
+
`Created at: ${row.created_at}`,
|
|
10210
|
+
`Source: ${row.source_id}`,
|
|
10211
|
+
`Table: ${row.table_name}`,
|
|
10212
|
+
`Rows: ${row.row_count}`,
|
|
10213
|
+
`Query fingerprint: ${row.query_fingerprint}`,
|
|
10214
|
+
`Proposal: ${row.proposal_id ?? "none"}`,
|
|
10215
|
+
`Evidence: ${row.evidence_bundle_id ?? "none"}`,
|
|
10216
|
+
`Tenant: ${row.tenant_id ?? "unknown"}`,
|
|
10217
|
+
`Capability: ${row.capability ?? payload.capability ?? "unknown"}`,
|
|
10218
|
+
`Parameters redacted: ${payload.parameters_redacted === true ? "yes" : "unknown"}`,
|
|
10219
|
+
"",
|
|
10220
|
+
"Payload:",
|
|
10221
|
+
JSON.stringify(payload, null, 2)
|
|
10222
|
+
].join("\n") + "\n";
|
|
10223
|
+
}
|
|
10224
|
+
function formatReceiptSummary(receipt) {
|
|
10225
|
+
return [
|
|
10226
|
+
`${receipt.created_at} receipt ${receipt.receipt_id} ${receipt.status}`,
|
|
10227
|
+
` proposal: ${receipt.proposal_id} job: ${receipt.writeback_job_id}`,
|
|
10228
|
+
` idempotency: ${receipt.idempotency_key} source changed: ${receipt.source_database_mutated ? "yes" : "no"}`
|
|
10229
|
+
].join("\n") + "\n";
|
|
10230
|
+
}
|
|
10231
|
+
function formatReceiptDetail(receipt) {
|
|
10232
|
+
return [
|
|
10233
|
+
`Receipt: ${receipt.receipt_id}`,
|
|
10234
|
+
`Proposal: ${receipt.proposal_id}`,
|
|
10235
|
+
`Writeback job: ${receipt.writeback_job_id}`,
|
|
10236
|
+
`Runner: ${receipt.runner_id}`,
|
|
10237
|
+
`Status: ${receipt.status}`,
|
|
10238
|
+
`Idempotency key: ${receipt.idempotency_key}`,
|
|
10239
|
+
`Source database mutated: ${receipt.source_database_mutated ? "yes" : "no"}`,
|
|
10240
|
+
`Rows affected: ${receipt.receipt.rows_affected}`,
|
|
10241
|
+
`Safe error: ${receipt.receipt.safe_error_code ?? "none"}`,
|
|
10242
|
+
`Receipt hash: ${receipt.receipt.receipt_hash}`,
|
|
10243
|
+
`Created at: ${receipt.created_at}`,
|
|
10244
|
+
"",
|
|
10245
|
+
"Related:",
|
|
10246
|
+
` ${cliCommandName()} replay show --proposal ${receipt.proposal_id}`
|
|
10247
|
+
].join("\n") + "\n";
|
|
10248
|
+
}
|
|
10249
|
+
function formatReplaySummary(row) {
|
|
10250
|
+
return [
|
|
10251
|
+
`${row.created_at} ${row.replay_id}`,
|
|
10252
|
+
` proposal: ${row.proposal_id} status: ${row.state}`,
|
|
10253
|
+
` tenant: ${row.tenant_id} capability: ${row.capability} object: ${row.business_object}:${row.object_id}`
|
|
10254
|
+
].join("\n") + "\n";
|
|
10255
|
+
}
|
|
10256
|
+
function formatReplayDetail(replay2) {
|
|
10257
|
+
const evidenceItems = replay2.evidence.reduce((count, item) => {
|
|
10258
|
+
const evidence2 = item;
|
|
10259
|
+
return count + (Array.isArray(evidence2.items) ? evidence2.items.length : 0);
|
|
10260
|
+
}, 0);
|
|
10261
|
+
return [
|
|
10262
|
+
`Replay ${replay2.replay_id}`,
|
|
10263
|
+
formatProposalDetail(replay2.proposal, evidenceItems).trimEnd(),
|
|
10264
|
+
`events: ${replay2.events.length}`,
|
|
10265
|
+
...replay2.events.map((event) => ` ${event.kind} by ${event.actor} at ${event.created_at}`),
|
|
10266
|
+
`receipts: ${replay2.receipts.length}`,
|
|
10267
|
+
...replay2.receipts.map((receipt) => ` receipt ${receipt.receipt_id}: ${receipt.status} job ${receipt.writeback_job_id}`),
|
|
10268
|
+
`evidence bundles: ${replay2.evidence.length}`,
|
|
10269
|
+
...replay2.evidence.map((evidence2) => ` ${evidence2.evidence_bundle_id ?? "unknown"}`),
|
|
10270
|
+
`query audit records: ${replay2.query_audit.length}`,
|
|
10271
|
+
...replay2.query_audit.map((record) => ` audit ${record.audit_id}: ${record.source_id}/${record.table_name} rows ${record.row_count}`)
|
|
10272
|
+
].join("\n") + "\n";
|
|
10273
|
+
}
|
|
10274
|
+
function formatReplayMarkdown(replay2) {
|
|
10275
|
+
const proposal = replay2.proposal;
|
|
10276
|
+
const principal = proposal.change_set.principal.id;
|
|
10277
|
+
const approvalEvents = replay2.events.filter((event) => /approved|rejected|canceled/i.test(event.kind));
|
|
10278
|
+
const evidenceLines = replay2.evidence.length > 0 ? replay2.evidence.flatMap((evidence2) => {
|
|
10279
|
+
const record = evidence2;
|
|
10280
|
+
const payload = isRecord6(record.payload) ? record.payload : {};
|
|
10281
|
+
const sourceId = stringField(payload, "source_id") ?? proposal.source_id;
|
|
10282
|
+
const table = stringField(payload, "target") ?? `${proposal.source_schema}.${proposal.source_table}`;
|
|
10283
|
+
const queryFingerprint = stringField(payload, "query_fingerprint") ?? proposal.change_set.evidence.query_fingerprint;
|
|
10284
|
+
return [
|
|
10285
|
+
`- evidence: ${record.evidence_bundle_id ?? proposal.change_set.evidence.bundle_id}`,
|
|
10286
|
+
` - source: ${sourceId}.${table}`,
|
|
10287
|
+
` - query fingerprint: ${queryFingerprint}`,
|
|
10288
|
+
` - rows captured: ${Array.isArray(record.items) ? record.items.length : 0}`
|
|
10289
|
+
];
|
|
10290
|
+
}) : [`- 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"];
|
|
10291
|
+
const receiptLines = replay2.receipts.length > 0 ? replay2.receipts.flatMap((receipt) => [
|
|
10292
|
+
`- receipt: ${receipt.receipt_id}`,
|
|
10293
|
+
` - status: ${receipt.status}`,
|
|
10294
|
+
` - affected rows: ${receipt.receipt.rows_affected}`,
|
|
10295
|
+
` - idempotency key: ${receipt.idempotency_key}`,
|
|
10296
|
+
` - source database mutated: ${receipt.source_database_mutated ? "yes" : "no"}`,
|
|
10297
|
+
...receipt.receipt.safe_error_code ? [` - safe error: ${receipt.receipt.safe_error_code}`] : []
|
|
10298
|
+
]) : ["- no writeback receipt recorded yet"];
|
|
10299
|
+
return [
|
|
10300
|
+
"# Synapsor Replay",
|
|
10301
|
+
"",
|
|
10302
|
+
`Proposal: ${proposal.proposal_id}`,
|
|
10303
|
+
`Capability: ${proposal.action}`,
|
|
10304
|
+
`Tenant: ${proposal.tenant_id}`,
|
|
10305
|
+
`Object: ${proposal.business_object}:${proposal.object_id}`,
|
|
10306
|
+
`Status: ${proposal.state}`,
|
|
10307
|
+
"",
|
|
10308
|
+
"## What The Agent Requested",
|
|
10309
|
+
"",
|
|
10310
|
+
`The model-facing capability requested \`${proposal.action}\` for ${proposal.business_object}:${proposal.object_id}.`,
|
|
10311
|
+
"The source database was not mutated when the proposal was created.",
|
|
10312
|
+
"",
|
|
10313
|
+
"## Trusted Context",
|
|
10314
|
+
"",
|
|
10315
|
+
`tenant_id = ${proposal.tenant_id}`,
|
|
10316
|
+
`principal = ${principal}`,
|
|
10317
|
+
`principal_source = ${proposal.change_set.principal.source}`,
|
|
10318
|
+
"",
|
|
10319
|
+
"## Evidence",
|
|
10320
|
+
"",
|
|
10321
|
+
...evidenceLines,
|
|
10322
|
+
"",
|
|
10323
|
+
"## Proposed Diff",
|
|
10324
|
+
"",
|
|
10325
|
+
...Object.keys(proposal.change_set.patch).map((column) => `- ${column}: ${JSON.stringify(proposal.change_set.before[column])} -> ${JSON.stringify(proposal.change_set.after[column])}`),
|
|
10326
|
+
"",
|
|
10327
|
+
"## Approval",
|
|
10328
|
+
"",
|
|
10329
|
+
...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}` : ""}`],
|
|
10330
|
+
"",
|
|
10331
|
+
"## Guarded Writeback",
|
|
10332
|
+
"",
|
|
10333
|
+
...receiptLines,
|
|
10334
|
+
"",
|
|
10335
|
+
"## Query Audit",
|
|
10336
|
+
"",
|
|
10337
|
+
...replay2.query_audit.map((record) => `- audit ${record.audit_id}: ${record.source_id}/${record.table_name} rows ${record.row_count} fingerprint ${record.query_fingerprint}`),
|
|
10338
|
+
"",
|
|
10339
|
+
"## Replay Note",
|
|
10340
|
+
"",
|
|
10341
|
+
"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."
|
|
10342
|
+
].join("\n") + "\n";
|
|
10343
|
+
}
|
|
10344
|
+
function activityFromProposal(proposal) {
|
|
10345
|
+
return {
|
|
10346
|
+
kind: "proposal",
|
|
10347
|
+
created_at: proposal.created_at,
|
|
10348
|
+
capability: proposal.action,
|
|
10349
|
+
tenant: proposal.tenant_id,
|
|
10350
|
+
principal: proposal.principal ?? proposal.change_set.principal.id,
|
|
10351
|
+
object: `${proposal.business_object}:${proposal.object_id}`,
|
|
10352
|
+
proposal: proposal.proposal_id,
|
|
10353
|
+
evidence: proposal.change_set.evidence.bundle_id,
|
|
10354
|
+
status: proposal.state,
|
|
10355
|
+
replay: `replay_${proposal.proposal_id}`,
|
|
10356
|
+
source: proposal.source_id,
|
|
10357
|
+
table: `${proposal.source_schema}.${proposal.source_table}`
|
|
10358
|
+
};
|
|
10359
|
+
}
|
|
10360
|
+
function activityFromEvidence(evidence2) {
|
|
10361
|
+
return {
|
|
10362
|
+
kind: "evidence",
|
|
10363
|
+
created_at: evidence2.created_at,
|
|
10364
|
+
capability: evidence2.capability,
|
|
10365
|
+
tenant: evidence2.tenant_id,
|
|
10366
|
+
principal: evidence2.principal,
|
|
10367
|
+
object: evidence2.business_object && evidence2.object_id ? `${evidence2.business_object}:${evidence2.object_id}` : void 0,
|
|
10368
|
+
proposal: evidence2.proposal_id,
|
|
10369
|
+
evidence: evidence2.evidence_bundle_id,
|
|
10370
|
+
status: "evidence_recorded",
|
|
10371
|
+
source: evidence2.source_id,
|
|
10372
|
+
table: evidence2.source_table
|
|
10373
|
+
};
|
|
10374
|
+
}
|
|
10375
|
+
function activityFromQueryAudit(audit2) {
|
|
10376
|
+
const businessObject = stringField(audit2, "business_object");
|
|
10377
|
+
const objectId = stringField(audit2, "object_id") ?? stringField(audit2, "primary_key_value");
|
|
10378
|
+
return {
|
|
10379
|
+
kind: "query-audit",
|
|
10380
|
+
created_at: stringField(audit2, "created_at"),
|
|
10381
|
+
capability: stringField(audit2, "capability"),
|
|
10382
|
+
tenant: stringField(audit2, "tenant_id"),
|
|
10383
|
+
principal: stringField(audit2, "principal"),
|
|
10384
|
+
object: businessObject && objectId ? `${businessObject}:${objectId}` : void 0,
|
|
10385
|
+
proposal: stringField(audit2, "proposal_id"),
|
|
10386
|
+
evidence: stringField(audit2, "evidence_bundle_id"),
|
|
10387
|
+
status: "query_audited",
|
|
10388
|
+
source: stringField(audit2, "source_id"),
|
|
10389
|
+
table: stringField(audit2, "table_name"),
|
|
10390
|
+
query_audit: stringField(audit2, "audit_id"),
|
|
10391
|
+
query_fingerprint: stringField(audit2, "query_fingerprint")
|
|
10392
|
+
};
|
|
10393
|
+
}
|
|
10394
|
+
function activityFromReceipt(receipt) {
|
|
10395
|
+
return {
|
|
10396
|
+
kind: "receipt",
|
|
10397
|
+
created_at: receipt.created_at,
|
|
10398
|
+
proposal: receipt.proposal_id,
|
|
10399
|
+
receipt: receipt.receipt_id,
|
|
10400
|
+
status: receipt.status,
|
|
10401
|
+
replay: `replay_${receipt.proposal_id}`,
|
|
10402
|
+
source_database_mutated: receipt.source_database_mutated
|
|
10403
|
+
};
|
|
10404
|
+
}
|
|
10405
|
+
function formatActivityItem(item, index) {
|
|
10406
|
+
const lines = [
|
|
10407
|
+
`${index}. ${item.created_at}`,
|
|
10408
|
+
` kind: ${item.kind}`,
|
|
10409
|
+
...item.capability ? [` capability: ${item.capability}`] : [],
|
|
10410
|
+
...item.tenant ? [` tenant: ${item.tenant}`] : [],
|
|
10411
|
+
...item.object ? [` object: ${item.object}`] : [],
|
|
10412
|
+
...item.proposal ? [` proposal: ${item.proposal}`] : [],
|
|
10413
|
+
...item.evidence ? [` evidence: ${item.evidence}`] : [],
|
|
10414
|
+
...item.query_audit ? [` query audit: ${item.query_audit}`] : [],
|
|
10415
|
+
...item.query_fingerprint ? [` query fingerprint: ${item.query_fingerprint}`] : [],
|
|
10416
|
+
...item.receipt ? [` receipt: ${item.receipt}`] : [],
|
|
10417
|
+
...item.status ? [` status: ${item.status}`] : [],
|
|
10418
|
+
...item.replay ? [` replay: ${item.replay}`] : [],
|
|
10419
|
+
""
|
|
10420
|
+
];
|
|
10421
|
+
return lines.join("\n");
|
|
10422
|
+
}
|
|
10423
|
+
function formatStoreStats(stats) {
|
|
10424
|
+
return [
|
|
10425
|
+
`Local store: ${stats.path}`,
|
|
10426
|
+
`Approx size: ${stats.approx_bytes} bytes`,
|
|
10427
|
+
`Proposals: ${stats.proposals}`,
|
|
10428
|
+
`Evidence bundles: ${stats.evidence_bundles}`,
|
|
10429
|
+
`Evidence items: ${stats.evidence_items}`,
|
|
10430
|
+
`Query audit records: ${stats.query_audit}`,
|
|
10431
|
+
`Writeback receipts: ${stats.writeback_receipts}`,
|
|
10432
|
+
`Writeback jobs: ${stats.writeback_jobs}`,
|
|
10433
|
+
`Idempotency receipts: ${stats.idempotency_receipts}`,
|
|
10434
|
+
`Replay records: ${stats.replay_records}`,
|
|
10435
|
+
`Approvals: ${stats.approvals}`,
|
|
10436
|
+
`Proposal events: ${stats.proposal_events}`,
|
|
10437
|
+
`Shadow human actions: ${stats.shadow_human_actions}`
|
|
10438
|
+
].join("\n") + "\n";
|
|
10439
|
+
}
|
|
10440
|
+
function formatStorePrune(result) {
|
|
10441
|
+
const lines = [
|
|
10442
|
+
`Local store prune ${result.dry_run ? "dry run" : "complete"}`,
|
|
10443
|
+
`Cutoff: ${result.cutoff}`,
|
|
10444
|
+
"",
|
|
10445
|
+
"Rows:",
|
|
10446
|
+
...Object.entries(result.deleted).map(([table, count]) => ` ${table}: ${count}`)
|
|
10447
|
+
];
|
|
10448
|
+
if (result.dry_run) {
|
|
10449
|
+
lines.push("", "No rows were deleted. Rerun with --yes to apply this prune.");
|
|
10450
|
+
}
|
|
10451
|
+
return `${lines.join("\n")}
|
|
10452
|
+
`;
|
|
10453
|
+
}
|
|
10454
|
+
function cutoffFromOlderThan(value) {
|
|
10455
|
+
const match = value.match(/^(\d+)([smhd])$/i);
|
|
10456
|
+
if (!match) throw new Error("--older-than must use a duration such as 30d, 12h, 90m, or 0d");
|
|
10457
|
+
const amount = Number(match[1]);
|
|
10458
|
+
const unit = (match[2] ?? "d").toLowerCase();
|
|
10459
|
+
const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
10460
|
+
return new Date(Date.now() - amount * multiplier).toISOString();
|
|
10461
|
+
}
|
|
10462
|
+
function formatMcpAuditMarkdown(report) {
|
|
10463
|
+
const lines = [
|
|
10464
|
+
"# Synapsor MCP Database Risk Review",
|
|
10465
|
+
"",
|
|
10466
|
+
`- Target: ${report.target}`,
|
|
10467
|
+
`- Generated at: ${report.generated_at}`,
|
|
10468
|
+
`- Tools inspected: ${report.summary.tools_inspected}`,
|
|
10469
|
+
`- Findings: HIGH ${report.summary.high} | MEDIUM ${report.summary.medium} | LOW ${report.summary.low}`,
|
|
10470
|
+
`- Total findings: ${report.summary.total_findings}`,
|
|
10471
|
+
"",
|
|
10472
|
+
`> ${report.disclaimer}`,
|
|
10473
|
+
""
|
|
10474
|
+
];
|
|
10475
|
+
if (report.findings.length === 0) {
|
|
10476
|
+
lines.push("No obvious database-commit risks were detected in the static manifest.", "");
|
|
10477
|
+
lines.push("This does not prove the MCP server or its tools are secure.", "");
|
|
10478
|
+
} else {
|
|
10479
|
+
lines.push("## Findings", "");
|
|
10480
|
+
for (const finding of report.findings) {
|
|
10481
|
+
lines.push(`### ${finding.severity}: ${finding.code}${finding.tool ? ` (${finding.tool})` : ""}`);
|
|
10482
|
+
lines.push("");
|
|
10483
|
+
lines.push(finding.message);
|
|
10484
|
+
lines.push("");
|
|
10485
|
+
if (finding.evidence.length > 0) {
|
|
10486
|
+
lines.push("Evidence:");
|
|
10487
|
+
for (const evidence2 of finding.evidence) lines.push(`- ${evidence2}`);
|
|
10488
|
+
lines.push("");
|
|
10489
|
+
}
|
|
10490
|
+
lines.push(`Recommendation: ${finding.recommendation}`);
|
|
10491
|
+
lines.push("");
|
|
10492
|
+
}
|
|
10493
|
+
}
|
|
10494
|
+
lines.push("## Safer Shape", "");
|
|
10495
|
+
lines.push("- expose semantic inspect/propose tools instead of raw SQL;");
|
|
10496
|
+
lines.push("- bind tenant/principal from trusted context;");
|
|
10497
|
+
lines.push("- keep approval outside MCP;");
|
|
10498
|
+
lines.push("- apply approved changes through guarded writeback;");
|
|
10499
|
+
lines.push("- keep replay/evidence handles for later review.");
|
|
10500
|
+
lines.push("");
|
|
10501
|
+
return `${lines.join("\n")}
|
|
10502
|
+
`;
|
|
10503
|
+
}
|
|
10504
|
+
function stringField(record, key) {
|
|
10505
|
+
if (!isRecord6(record)) return void 0;
|
|
10506
|
+
const value = record[key];
|
|
10507
|
+
if (typeof value === "string") return value;
|
|
10508
|
+
if (typeof value === "number") return String(value);
|
|
10509
|
+
return void 0;
|
|
10510
|
+
}
|
|
8474
10511
|
function currentApprovalStatus(proposal) {
|
|
8475
10512
|
if (proposal.state === "rejected") return "rejected";
|
|
8476
10513
|
if (proposal.state === "canceled") return "canceled";
|
|
@@ -8601,7 +10638,7 @@ function starterCloudConfig() {
|
|
|
8601
10638
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
8602
10639
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
8603
10640
|
runner_id: "synapsor_runner_local",
|
|
8604
|
-
runner_version: "0.1.0-alpha.
|
|
10641
|
+
runner_version: "0.1.0-alpha.4",
|
|
8605
10642
|
project_id: "token_scope",
|
|
8606
10643
|
adapter_id: "mcp.your_adapter",
|
|
8607
10644
|
source_id: "src_replace_me",
|
|
@@ -8624,6 +10661,37 @@ function normalizeCapabilities(value) {
|
|
|
8624
10661
|
function isHelpRequest(args) {
|
|
8625
10662
|
return args.includes("--help") || args.includes("-h");
|
|
8626
10663
|
}
|
|
10664
|
+
function isKnownTopLevelCommand(command) {
|
|
10665
|
+
return (/* @__PURE__ */ new Set([
|
|
10666
|
+
"help",
|
|
10667
|
+
"init",
|
|
10668
|
+
"inspect",
|
|
10669
|
+
"config",
|
|
10670
|
+
"doctor",
|
|
10671
|
+
"validate",
|
|
10672
|
+
"apply",
|
|
10673
|
+
"propose",
|
|
10674
|
+
"audit",
|
|
10675
|
+
"start",
|
|
10676
|
+
"runner",
|
|
10677
|
+
"cloud",
|
|
10678
|
+
"mcp",
|
|
10679
|
+
"tools",
|
|
10680
|
+
"onboard",
|
|
10681
|
+
"demo",
|
|
10682
|
+
"recipes",
|
|
10683
|
+
"benchmark",
|
|
10684
|
+
"proposals",
|
|
10685
|
+
"replay",
|
|
10686
|
+
"evidence",
|
|
10687
|
+
"query-audit",
|
|
10688
|
+
"receipts",
|
|
10689
|
+
"activity",
|
|
10690
|
+
"store",
|
|
10691
|
+
"shadow",
|
|
10692
|
+
"ui"
|
|
10693
|
+
])).has(command);
|
|
10694
|
+
}
|
|
8627
10695
|
function cliCommandName() {
|
|
8628
10696
|
if (process2.env.SYNAPSOR_RUNNER_COMMAND_NAME) return process2.env.SYNAPSOR_RUNNER_COMMAND_NAME;
|
|
8629
10697
|
const invoked = path3.basename(process2.argv[1] ?? "");
|
|
@@ -8648,6 +10716,11 @@ Commands:
|
|
|
8648
10716
|
propose Create a local evidence-backed proposal
|
|
8649
10717
|
audit Review MCP/database tool risk
|
|
8650
10718
|
proposals Review, approve, or reject proposals
|
|
10719
|
+
evidence Inspect local evidence bundles
|
|
10720
|
+
query-audit Inspect local query audit records
|
|
10721
|
+
receipts Inspect guarded writeback receipts
|
|
10722
|
+
activity Search local evidence/replay ledger
|
|
10723
|
+
store Inspect and maintain the local SQLite ledger
|
|
8651
10724
|
apply Apply an approved proposal with guarded writeback
|
|
8652
10725
|
replay Show what happened
|
|
8653
10726
|
demo Start the local commit-safety demo
|
|
@@ -8675,6 +10748,7 @@ Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizar
|
|
|
8675
10748
|
mcp: `Usage:
|
|
8676
10749
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
8677
10750
|
${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
10751
|
+
${cmd} mcp audit --example dangerous-db-mcp
|
|
8678
10752
|
${cmd} mcp audit ./tools-list.json
|
|
8679
10753
|
|
|
8680
10754
|
MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
|
|
@@ -8694,23 +10768,60 @@ Print MCP client configuration that references the local runner command, not dat
|
|
|
8694
10768
|
${cmd} propose <capability-name> --input ./input.json
|
|
8695
10769
|
${cmd} propose <capability-name> --json '{"invoice_id":"INV-3001","reason":"support-approved waiver"}'
|
|
8696
10770
|
|
|
10771
|
+
Examples after running ${cmd} demo:
|
|
10772
|
+
${cmd} propose billing.propose_late_fee_waiver --sample
|
|
10773
|
+
${cmd} propose support.propose_plan_credit --sample
|
|
10774
|
+
${cmd} propose orders.propose_status_change --sample
|
|
10775
|
+
|
|
8697
10776
|
Create the same evidence-backed proposal the MCP tool would create. The source database is not mutated.
|
|
8698
10777
|
`,
|
|
8699
10778
|
audit: `Usage:
|
|
10779
|
+
${cmd} audit --example dangerous-db-mcp
|
|
10780
|
+
${cmd} audit --example dangerous-db-mcp --format json
|
|
10781
|
+
${cmd} audit --example dangerous-db-mcp --format markdown
|
|
8700
10782
|
${cmd} audit ./synapsor.runner.json
|
|
8701
10783
|
${cmd} audit --mcp-config ./claude_desktop_config.json
|
|
8702
10784
|
${cmd} audit --stdio "node ./server.js"
|
|
8703
10785
|
${cmd} audit --url http://localhost:3000/mcp
|
|
8704
10786
|
|
|
8705
10787
|
Static MCP/database risk review only. This is not a security guarantee.
|
|
10788
|
+
`,
|
|
10789
|
+
doctor: `Usage:
|
|
10790
|
+
${cmd} doctor --config synapsor.runner.json
|
|
10791
|
+
${cmd} doctor --config synapsor.runner.json --json
|
|
10792
|
+
${cmd} doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
|
|
10793
|
+
${cmd} doctor --first-run
|
|
10794
|
+
|
|
10795
|
+
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.
|
|
8706
10796
|
`,
|
|
8707
10797
|
proposals: `Usage:
|
|
8708
|
-
${cmd} proposals list [--
|
|
10798
|
+
${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
|
|
8709
10799
|
${cmd} proposals show latest
|
|
8710
10800
|
${cmd} proposals approve latest --yes
|
|
8711
10801
|
${cmd} proposals reject latest --reason "..."
|
|
8712
10802
|
|
|
8713
10803
|
Review decisions happen outside the model-facing MCP tool surface.
|
|
10804
|
+
`,
|
|
10805
|
+
evidence: `Usage:
|
|
10806
|
+
${cmd} evidence list [--tenant acme] [--capability billing.inspect_invoice] [--object invoice:INV-3001]
|
|
10807
|
+
${cmd} evidence show ev_...
|
|
10808
|
+
${cmd} evidence export ev_... --format json --output evidence.json
|
|
10809
|
+
${cmd} evidence export ev_... --format markdown --output evidence.md
|
|
10810
|
+
|
|
10811
|
+
Inspect captured local evidence bundles and query-audit links without rerunning external DB reads.
|
|
10812
|
+
`,
|
|
10813
|
+
"query-audit": `Usage:
|
|
10814
|
+
${cmd} query-audit list [--evidence ev_...] [--source app_postgres] [--table invoices]
|
|
10815
|
+
${cmd} query-audit show <audit_id>
|
|
10816
|
+
${cmd} query-audit export <audit_id> --format json --output audit.json
|
|
10817
|
+
|
|
10818
|
+
Inspect local query fingerprints, table names, row counts, and redacted-parameter metadata.
|
|
10819
|
+
`,
|
|
10820
|
+
receipts: `Usage:
|
|
10821
|
+
${cmd} receipts list [--proposal wrp_...] [--status applied]
|
|
10822
|
+
${cmd} receipts show <receipt_id>
|
|
10823
|
+
|
|
10824
|
+
Inspect guarded writeback receipts recorded by the trusted runner path.
|
|
8714
10825
|
`,
|
|
8715
10826
|
apply: `Usage:
|
|
8716
10827
|
${cmd} apply latest [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -8719,18 +10830,36 @@ Review decisions happen outside the model-facing MCP tool surface.
|
|
|
8719
10830
|
Apply an approved proposal through guarded writeback. Requires a trusted write credential.
|
|
8720
10831
|
`,
|
|
8721
10832
|
replay: `Usage:
|
|
8722
|
-
${cmd} replay
|
|
10833
|
+
${cmd} replay list [--tenant acme] [--object invoice:INV-3001]
|
|
8723
10834
|
${cmd} replay show latest
|
|
8724
|
-
${cmd} replay
|
|
10835
|
+
${cmd} replay show --proposal wrp_...
|
|
10836
|
+
${cmd} replay show --replay replay_wrp_...
|
|
10837
|
+
${cmd} replay show --evidence ev_...
|
|
10838
|
+
${cmd} replay export --proposal wrp_... --format json --output replay.json
|
|
10839
|
+
${cmd} replay export --proposal wrp_... --format markdown --output replay.md
|
|
8725
10840
|
|
|
8726
10841
|
Show evidence, proposal events, receipts, and replay state without rerunning side effects.
|
|
10842
|
+
`,
|
|
10843
|
+
activity: `Usage:
|
|
10844
|
+
${cmd} activity search --tenant acme --object invoice:INV-3001
|
|
10845
|
+
${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
|
|
10846
|
+
|
|
10847
|
+
Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
|
|
10848
|
+
`,
|
|
10849
|
+
store: `Usage:
|
|
10850
|
+
${cmd} store stats --store ./.synapsor/local.db
|
|
10851
|
+
${cmd} store vacuum --store ./.synapsor/local.db
|
|
10852
|
+
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --dry-run
|
|
10853
|
+
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes
|
|
10854
|
+
|
|
10855
|
+
Local store maintenance only. Prune defaults to dry-run and never touches your source Postgres/MySQL database.
|
|
8727
10856
|
`,
|
|
8728
10857
|
demo: `Usage:
|
|
8729
10858
|
${cmd} demo [--force]
|
|
8730
10859
|
${cmd} demo --quick
|
|
8731
10860
|
|
|
8732
10861
|
Start a disposable local Postgres demo and write ./synapsor.runner.json for the first-run flow.
|
|
8733
|
-
Use --quick for a fixture-only 15-second explanation with no Docker startup.
|
|
10862
|
+
Use --quick for a fixture-only 15-second explanation and local ledger seed with no Docker startup.
|
|
8734
10863
|
`,
|
|
8735
10864
|
ui: `Usage:
|
|
8736
10865
|
${cmd} ui [--tour] [--config synapsor.runner.json] [--store ./.synapsor/local.db]
|