@synapsor/client 0.1.4 → 0.1.6

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 CHANGED
@@ -101,6 +101,30 @@ await run.complete({ decision: "waiver_proposed" }, { status: "waiting_approval"
101
101
  const graph = await run.explain();
102
102
  ```
103
103
 
104
+ Find the persisted run/evidence/proposal later by business object, workflow,
105
+ tenant, query fingerprint, or time window:
106
+
107
+ ```js
108
+ const activity = await db.agentActivity.search({
109
+ tenantId: "acme",
110
+ businessObjectId: "T-1042",
111
+ workflow: "support.ticket_refund_flow",
112
+ timeRange: { from: 123, to: 456 },
113
+ limit: 50,
114
+ });
115
+ ```
116
+
117
+ This helper sends the native lookup form:
118
+
119
+ ```sql
120
+ SELECT *
121
+ FROM AGENT ACTIVITY
122
+ WHERE tenant_id = 'acme'
123
+ AND business_object_id = 'T-1042'
124
+ ORDER BY created_at DESC
125
+ LIMIT 50;
126
+ ```
127
+
104
128
  Replay a stored capability run by using the numeric `agent_run_id` returned by
105
129
  Synapsor. Deterministic replay returns the captured persisted run; comparison
106
130
  modes can inspect the original snapshot, current state, a commit version, a
@@ -157,12 +181,33 @@ The worker validates Synapsor-provided mapping metadata, only writes allowlisted
157
181
  columns, adds tenant and primary-key guards, checks optional conflict columns,
158
182
  and reports `applied`, `conflict`, or `failed` back to Synapsor idempotently.
159
183
 
184
+ ## CLI MCP Risk Review
185
+
186
+ The package installs a `synapsor` CLI. Use `synapsor mcp audit <target>` for a
187
+ static MCP database risk review of exported tool manifests, remote `tools/list`
188
+ endpoints, or stdio MCP servers.
189
+
190
+ ```bash
191
+ synapsor mcp audit ./tools-manifest.json
192
+ synapsor mcp audit https://mcp.example.com --bearer-env MCP_AUDIT_TOKEN --json
193
+ synapsor mcp audit 'stdio:node ./server.mjs' --timeout-ms 5000
194
+ ```
195
+
196
+ The audit flags patterns such as generic `execute_sql` tools, model-controlled
197
+ tenant/table/column inputs, model-callable approval or commit tools, missing
198
+ proposal boundaries, missing structured output schemas, and missing idempotency
199
+ or conflict-guard metadata. It never calls business tools during the audit.
200
+
201
+ This is a static MCP database risk review, not proof that an MCP server is
202
+ secure. MCP annotations are treated as descriptive hints, not enforcement.
203
+
160
204
  ## API Surface
161
205
 
162
206
  - `execute(sql)` and `query(sql)`
163
207
  - `setSession({...})`
164
208
  - `invokeAgentCapability(name, args, options)`
165
209
  - `agentRuns.start(...)`, `run.invokeCapability(...)`, `run.checkpoint(...)`, `run.complete(...)`, `run.explain(...)`
210
+ - `agentActivity.search({ tenantId, businessObjectId, workflow, timeRange, limit })`
166
211
  - `replayAgentRun(id, { mode, version, timestamp, branchName })`
167
212
  - `externalActions.claim(...)` and `externalActions.confirm(...)`
168
213
  - `createAgentEval(...)` / `evals.create(...)` for run-history or `sourceTable` dataset evals
package/bin/synapsor.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
4
5
  import { homedir } from "node:os";
5
6
  import { createRequire } from "node:module";
6
7
  import { dirname, join } from "node:path";
@@ -11,6 +12,7 @@ import { stdin as input, stdout as output } from "node:process";
11
12
  const require = createRequire(import.meta.url);
12
13
  const { version: PACKAGE_VERSION } = require("../package.json");
13
14
  const DEFAULT_BASE_URL = "https://synapsor.ai";
15
+ const MCP_AUDIT_DISCLAIMER = "This is a static MCP database risk review, not proof that an MCP server is secure.";
14
16
  const ERROR_HINTS = new Map([
15
17
  ["AUTH_REQUIRED", "Set SYNAPSOR_API_KEY or run `synapsor config set api-key`."],
16
18
  ["BILLING_REQUIRED", "Builder resources require active or trialing Builder billing."],
@@ -172,6 +174,17 @@ function csvFlag(args, name) {
172
174
  return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : [];
173
175
  }
174
176
 
177
+ function boolFlag(args, name) {
178
+ return args.includes(name);
179
+ }
180
+
181
+ function intFlag(args, name, fallback) {
182
+ const value = flagValue(args, name, "");
183
+ if (!value) return fallback;
184
+ const parsed = Number.parseInt(value, 10);
185
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
186
+ }
187
+
175
188
  function resolveSecretRef(value) {
176
189
  const text = String(value || "").trim();
177
190
  if (text.startsWith("env:")) {
@@ -217,6 +230,250 @@ async function readStdinSecret() {
217
230
  return value.trim();
218
231
  }
219
232
 
233
+ async function loadMcpAuditTarget(target, args) {
234
+ if (!target) throw new Error("MCP_AUDIT_TARGET_REQUIRED: usage `synapsor mcp audit <target>`");
235
+ const timeoutMs = intFlag(args, "--timeout-ms", 5000);
236
+ if (/^https?:\/\//i.test(target)) {
237
+ return { target, kind: "remote_tools_list", payload: await fetchRemoteMcpTools(target, args, timeoutMs) };
238
+ }
239
+ if (target.startsWith("stdio:")) {
240
+ const command = target.slice("stdio:".length).trim();
241
+ if (!command) throw new Error("MCP_STDIO_COMMAND_REQUIRED: usage `synapsor mcp audit 'stdio:node server.js'`");
242
+ return { target, kind: "stdio_tools_list", payload: await fetchStdioMcpTools(command, timeoutMs) };
243
+ }
244
+ const raw = await readFile(target, "utf8");
245
+ return { target, kind: "manifest", payload: JSON.parse(raw) };
246
+ }
247
+
248
+ async function fetchRemoteMcpTools(url, args, timeoutMs) {
249
+ const bearerEnv = flagValue(args, "--bearer-env", "SYNAPSOR_MCP_AUDIT_BEARER");
250
+ const bearer = bearerEnv ? process.env[bearerEnv] || "" : "";
251
+ const controller = new AbortController();
252
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
253
+ try {
254
+ const response = await fetch(url, {
255
+ method: "POST",
256
+ signal: controller.signal,
257
+ headers: {
258
+ accept: "application/json",
259
+ "content-type": "application/json",
260
+ ...(bearer ? { authorization: `Bearer ${bearer}` } : {}),
261
+ },
262
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
263
+ });
264
+ const text = await response.text();
265
+ const payload = text.trim() ? JSON.parse(text) : {};
266
+ if (!response.ok) throw new Error(`MCP_AUDIT_REMOTE_FAILED: HTTP ${response.status}`);
267
+ return payload;
268
+ } finally {
269
+ clearTimeout(timer);
270
+ }
271
+ }
272
+
273
+ function splitCommand(command) {
274
+ const parts = [];
275
+ const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
276
+ let match;
277
+ while ((match = matcher.exec(command)) !== null) {
278
+ parts.push(match[1] ?? match[2] ?? match[3]);
279
+ }
280
+ return parts;
281
+ }
282
+
283
+ async function fetchStdioMcpTools(command, timeoutMs) {
284
+ const [bin, ...args] = splitCommand(command);
285
+ if (!bin) throw new Error("MCP_STDIO_COMMAND_REQUIRED: empty stdio command");
286
+ return await new Promise((resolve, reject) => {
287
+ const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"] });
288
+ let stdout = "";
289
+ let stderr = "";
290
+ const timer = setTimeout(() => {
291
+ child.kill("SIGTERM");
292
+ reject(new Error(`MCP_AUDIT_TIMEOUT: tools/list did not complete within ${timeoutMs}ms`));
293
+ }, timeoutMs);
294
+ child.stdout.on("data", (chunk) => {
295
+ stdout += String(chunk);
296
+ const response = parseJsonRpcLine(stdout, 2);
297
+ if (response) {
298
+ clearTimeout(timer);
299
+ child.kill("SIGTERM");
300
+ resolve(response);
301
+ }
302
+ });
303
+ child.stderr.on("data", (chunk) => {
304
+ stderr += String(chunk);
305
+ });
306
+ child.on("error", (error) => {
307
+ clearTimeout(timer);
308
+ reject(error);
309
+ });
310
+ child.on("close", () => {
311
+ const response = parseJsonRpcLine(stdout, 2);
312
+ if (response) return;
313
+ clearTimeout(timer);
314
+ reject(new Error(`MCP_AUDIT_STDIO_FAILED: tools/list response not found${stderr ? `: ${stderr.slice(0, 240)}` : ""}`));
315
+ });
316
+ child.stdin.write(`${JSON.stringify({
317
+ jsonrpc: "2.0",
318
+ id: 1,
319
+ method: "initialize",
320
+ params: {
321
+ protocolVersion: "2025-11-25",
322
+ capabilities: {},
323
+ clientInfo: { name: "synapsor-mcp-audit", version: PACKAGE_VERSION },
324
+ },
325
+ })}\n`);
326
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })}\n`);
327
+ });
328
+ }
329
+
330
+ function parseJsonRpcLine(text, id) {
331
+ for (const line of text.split(/\r?\n/)) {
332
+ if (!line.trim()) continue;
333
+ try {
334
+ const parsed = JSON.parse(line);
335
+ if (parsed.id === id) return parsed;
336
+ } catch {
337
+ // Ignore non-JSON logs from a local stdio server.
338
+ }
339
+ }
340
+ return null;
341
+ }
342
+
343
+ function extractMcpTools(payload) {
344
+ if (Array.isArray(payload)) return { tools: payload, source: "array_manifest" };
345
+ if (!payload || typeof payload !== "object") return { tools: [], source: "unknown" };
346
+ if (Array.isArray(payload.tools)) return { tools: payload.tools, source: "tool_manifest" };
347
+ if (Array.isArray(payload.result?.tools)) return { tools: payload.result.tools, source: "json_rpc_tools_list" };
348
+ if (Array.isArray(payload.tool_manifest?.tools)) return { tools: payload.tool_manifest.tools, source: "tool_manifest" };
349
+ if (payload.mcpServers && typeof payload.mcpServers === "object") {
350
+ return { tools: [], source: "mcp_client_config", serverCount: Object.keys(payload.mcpServers).length };
351
+ }
352
+ return { tools: [], source: "unknown" };
353
+ }
354
+
355
+ function propNames(schema) {
356
+ if (!schema || typeof schema !== "object") return [];
357
+ const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
358
+ return Object.keys(properties);
359
+ }
360
+
361
+ function schemaText(schema) {
362
+ try {
363
+ return JSON.stringify(schema || {}).toLowerCase();
364
+ } catch {
365
+ return "";
366
+ }
367
+ }
368
+
369
+ function hasAny(text, patterns) {
370
+ return patterns.some((pattern) => pattern.test(text));
371
+ }
372
+
373
+ function buildMcpAuditReport(targetInfo) {
374
+ const { tools, source, serverCount } = extractMcpTools(targetInfo.payload);
375
+ const findings = [];
376
+ const add = (severity, code, message, details = {}) => findings.push({ severity, code, message, ...details });
377
+ if (source === "mcp_client_config") {
378
+ add(
379
+ "LOW",
380
+ "MCP_CLIENT_CONFIG_ONLY",
381
+ "MCP client config found; export or query tools/list for schema-level database risk review.",
382
+ { evidence: `${serverCount || 0} configured MCP server(s)` },
383
+ );
384
+ }
385
+ if (!tools.length && source !== "mcp_client_config") {
386
+ add("MEDIUM", "NO_TOOLS_FOUND", "No MCP tools were found in the target manifest or tools/list response.");
387
+ }
388
+
389
+ for (const tool of tools) {
390
+ const name = String(tool.name || tool.id || tool.title || "unnamed_tool");
391
+ const description = String(tool.description || "");
392
+ const inputSchema = tool.inputSchema || tool.input_schema || tool.parameters || {};
393
+ const outputSchema = tool.outputSchema || tool.output_schema || tool.resultSchema || tool.result_schema || null;
394
+ const props = propNames(inputSchema);
395
+ const propsLower = props.map((prop) => prop.toLowerCase());
396
+ const text = `${name} ${description} ${schemaText(inputSchema)} ${schemaText(outputSchema)}`.toLowerCase();
397
+ const writeTool = hasAny(text, [
398
+ /\b(writes?|written|updates?|updated|deletes?|deleted|inserts?|inserted|upserts?|upserted|mutates?|mutated|refunds?|refunded|waives?|waived|charges?|charged|credits?|credited|commits?|committed|settles?|settled|merges?|merged)\b/,
399
+ /(^|[._-])(approve|close|resolve|cancel)([._-]|$)/,
400
+ ]);
401
+ const readTool = hasAny(text, [/\b(read|get|list|query|inspect|fetch|search)\b/]);
402
+ const proposalBoundary = hasAny(text, [/\b(proposal|propose|approval|review_required|writeback|change[-_ ]?set|diff|evidence)\b/]);
403
+ const idempotency = hasAny(text, [/\b(idempotency|idempotent|request_id|requestid|idempotency_key|idempotencykey)\b/]);
404
+ const conflictGuard = hasAny(text, [/\b(expected_version|row_version|updated_at|etag|version|conflict_guard|conflict guard|optimistic)\b/]);
405
+
406
+ if (/(^|[._-])(execute_sql|exec_sql|run_query|raw_sql|sql_query|query_sql|execute_query)([._-]|$)/.test(name.toLowerCase())) {
407
+ add("HIGH", "GENERIC_SQL_TOOL", "Generic execute_sql or run_query-style database tool exposed.", { tool: name });
408
+ }
409
+ if (propsLower.some((prop) => ["sql", "query", "statement", "raw_sql"].includes(prop)) && writeTool) {
410
+ add("HIGH", "WRITE_ACCEPTS_SQL", "Write-capable tool accepts arbitrary SQL/query/statement input.", { tool: name, evidence: props.join(", ") });
411
+ }
412
+ if (propsLower.some((prop) => ["schema", "table", "table_name", "tablename", "column", "columns", "database", "db"].includes(prop))) {
413
+ add("HIGH", "MODEL_CONTROLLED_IDENTIFIER", "Tool accepts schema/table/column identifiers as ordinary model input.", { tool: name, evidence: props.join(", ") });
414
+ }
415
+ if (propsLower.some((prop) => ["tenant", "tenant_id", "tenantid"].includes(prop))) {
416
+ add("HIGH", "MODEL_CONTROLLED_TENANT", "Tool accepts tenant_id as ordinary model input instead of trusted session context.", { tool: name, evidence: props.join(", ") });
417
+ }
418
+ if (/(^|[._-])(approve|commit|settle|merge)[._-]?(proposal|write|change)|(^|[._-])(proposal|write|change)[._-]?(approve|commit|settle|merge)([._-]|$)/.test(name.toLowerCase())) {
419
+ add("HIGH", "MODEL_CALLABLE_APPROVAL", "Approval or commit operation is exposed as a model-callable MCP tool.", { tool: name });
420
+ }
421
+ if (writeTool && !proposalBoundary) {
422
+ add("HIGH", "NO_VISIBLE_PROPOSAL_BOUNDARY", "Write-capable tool has no visible proposal, approval, evidence, or writeback boundary.", { tool: name });
423
+ }
424
+ if (!outputSchema) {
425
+ add("MEDIUM", "NO_STRUCTURED_OUTPUT_SCHEMA", "Tool has no structured output schema visible in the manifest.", { tool: name });
426
+ }
427
+ if (writeTool && !idempotency) {
428
+ add("MEDIUM", "NO_IDEMPOTENCY_KEY", "Write/proposal tool has no visible idempotency or request-key metadata.", { tool: name });
429
+ }
430
+ if (writeTool && !conflictGuard) {
431
+ add("MEDIUM", "NO_CONFLICT_GUARD", "Write/proposal tool has no visible row-version or conflict-guard metadata.", { tool: name });
432
+ }
433
+ if (readTool && writeTool) {
434
+ add("MEDIUM", "AMBIGUOUS_READ_WRITE_TOOL", "Tool mixes read and write behavior ambiguously.", { tool: name });
435
+ }
436
+ if (!tool.annotations) {
437
+ add("LOW", "MISSING_TOOL_ANNOTATIONS", "Tool is missing descriptive MCP annotations. Annotations are hints, not enforcement.", { tool: name });
438
+ }
439
+ }
440
+
441
+ const severityRank = { HIGH: 0, MEDIUM: 1, LOW: 2 };
442
+ findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || String(a.tool || "").localeCompare(String(b.tool || "")) || a.code.localeCompare(b.code));
443
+ const summary = { high: 0, medium: 0, low: 0 };
444
+ for (const finding of findings) summary[finding.severity.toLowerCase()] += 1;
445
+ return {
446
+ ok: true,
447
+ target: targetInfo.target,
448
+ source,
449
+ inspected_at: new Date().toISOString(),
450
+ disclaimer: MCP_AUDIT_DISCLAIMER,
451
+ tool_count: tools.length,
452
+ summary,
453
+ findings,
454
+ };
455
+ }
456
+
457
+ function printMcpAudit(report, globals) {
458
+ if (globals.json) return print(report, globals);
459
+ console.log("Static MCP database risk review");
460
+ console.log(`Target: ${report.target}`);
461
+ console.log(`Tools inspected: ${report.tool_count}`);
462
+ console.log(`Summary: HIGH=${report.summary.high} MEDIUM=${report.summary.medium} LOW=${report.summary.low}`);
463
+ console.log(MCP_AUDIT_DISCLAIMER);
464
+ if (!report.findings.length) {
465
+ console.log("No matching database MCP risk patterns found. This is not a security guarantee.");
466
+ return;
467
+ }
468
+ console.log("\nFindings:");
469
+ for (const finding of report.findings) {
470
+ const tool = finding.tool ? ` tool=${finding.tool}` : "";
471
+ const evidence = finding.evidence ? ` evidence=${finding.evidence}` : "";
472
+ console.log(`${finding.severity.padEnd(6)} ${finding.code}${tool}${evidence}`);
473
+ console.log(` ${finding.message}`);
474
+ }
475
+ }
476
+
220
477
  async function main(argv = process.argv.slice(2)) {
221
478
  const { globals, args } = parse(argv);
222
479
  const [cmd, sub, third, ...rest] = args;
@@ -282,24 +539,35 @@ async function main(argv = process.argv.slice(2)) {
282
539
  const envHint = kind === "mysql" ? "APP_MYSQL_URL" : "APP_POSTGRES_URL";
283
540
  const name = flagValue(rest, "--name", defaultName);
284
541
  const url = resolveSecretRef(flagValue(rest, "--url", ""));
542
+ const cdcUrl = resolveSecretRef(flagValue(rest, "--cdc-url", ""));
285
543
  const ssl = flagValue(rest, "--ssl", "require");
286
- const mode = flagValue(rest, "--mode", "read-only");
544
+ const mode = flagValue(rest, "--mode", "live_read");
287
545
  const projectId = scopedProject(globals);
288
546
  const databaseId = scopedDatabase(globals);
289
547
  if (!projectId || !databaseId) throw new Error("PROJECT_AND_DATABASE_REQUIRED: use --project <project_id> --db <database_id> or SYNAPSOR_PROJECT_ID/SYNAPSOR_DATABASE_ID");
290
548
  if (!url) throw new Error(`${kind.toUpperCase()}_URL_REQUIRED: usage \`synapsor sources create ${kind} --name ${defaultName} --url env:${envHint} --project <project> --db <database>\``);
291
- return print(await request(globals, "POST", "/v1/control/external-sources", {
549
+ const body = {
292
550
  project_id: projectId,
293
551
  database_id: databaseId,
294
552
  kind,
295
553
  name,
296
554
  connection: { url, ssl_mode: ssl },
297
555
  mode,
298
- }), globals);
556
+ };
557
+ if (cdcUrl) body.cdc_connection = { url: cdcUrl, ssl_mode: ssl };
558
+ if (mode === "cdc_mirror" || boolFlag(rest, "--cdc-ack")) body.cdc_acknowledged = true;
559
+ return print(await request(globals, "POST", "/v1/control/external-sources", body), globals);
299
560
  }
300
- if (["test", "inspect", "generate", "doctor", "disable"].includes(sub)) {
561
+ if (["test", "inspect", "generate", "doctor", "disable", "cdc-status", "cdc-snapshot", "cdc-poll"].includes(sub)) {
301
562
  const sourceId = await resolveSourceId(globals, third);
302
563
  if (sub === "test") return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/test`, {}), globals);
564
+ if (sub === "cdc-status") return print(await request(globals, "GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/cdc/status`), globals);
565
+ if (sub === "cdc-snapshot" || sub === "cdc-poll") {
566
+ const tables = csvFlag(rest, "--tables");
567
+ const body = { cdc_acknowledged: true };
568
+ if (tables.length) body.tables = tables;
569
+ return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/cdc/${sub === "cdc-snapshot" ? "snapshot" : "poll"}`, body), globals);
570
+ }
303
571
  if (sub === "inspect") {
304
572
  const schema = flagValue(rest, "--schema", "public");
305
573
  const database = flagValue(rest, "--database", "");
@@ -340,10 +608,10 @@ async function main(argv = process.argv.slice(2)) {
340
608
  const database = flagValue(rest, "--database", "");
341
609
  const tables = csvFlag(rest, "--tables");
342
610
  const tenantColumn = flagValue(rest, "--tenant-column", "");
343
- const mode = flagValue(rest, "--mode", "live-read");
611
+ const mode = flagValue(rest, "--mode", "live_read");
344
612
  if (!tables.length) throw new Error("TABLES_REQUIRED: pass --tables tickets,customers,policy_chunks");
345
613
  if (!tenantColumn && !rest.includes("--single-tenant")) throw new Error("TENANT_COLUMN_REQUIRED: pass --tenant-column tenant_id or --single-tenant");
346
- return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/import`, {
614
+ const body = {
347
615
  tables: tables.map((table) => ({
348
616
  ...(database ? { database } : {}),
349
617
  schema,
@@ -354,10 +622,17 @@ async function main(argv = process.argv.slice(2)) {
354
622
  single_tenant: rest.includes("--single-tenant"),
355
623
  mode,
356
624
  })),
357
- }), globals);
625
+ };
626
+ if (mode === "cdc_mirror" || boolFlag(rest, "--cdc-ack")) body.cdc_acknowledged = true;
627
+ return print(await request(globals, "POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/import`, body), globals);
358
628
  }
359
629
  }
360
630
 
631
+ if (cmd === "mcp" && sub === "audit") {
632
+ const targetInfo = await loadMcpAuditTarget(third, rest);
633
+ return printMcpAudit(buildMcpAuditReport(targetInfo), globals);
634
+ }
635
+
361
636
  if (cmd === "sql") {
362
637
  const db = globals.db || "";
363
638
  let sql = args.slice(1).join(" ").trim();
@@ -408,8 +683,8 @@ Usage:
408
683
  synapsor projects create <name>
409
684
  synapsor db list --project <project>
410
685
  synapsor db create <name> --project <project>
411
- synapsor sources create postgres --name app_postgres --url env:APP_POSTGRES_URL --ssl require --mode read-only --project <project> --db <database>
412
- synapsor sources create mysql --name app_mysql --url env:APP_MYSQL_URL --ssl require --mode read-only --project <project> --db <database>
686
+ synapsor sources create postgres --name app_postgres --url env:APP_POSTGRES_URL --ssl require --mode live_read --project <project> --db <database>
687
+ synapsor sources create mysql --name app_mysql --url env:APP_MYSQL_URL --ssl require --mode live_read --project <project> --db <database>
413
688
  synapsor sources test app_postgres --project <project>
414
689
  synapsor sources inspect app_postgres --schema public --project <project>
415
690
  synapsor sources inspect app_mysql --database shopdb --project <project>
@@ -421,6 +696,9 @@ Usage:
421
696
  synapsor sources show app_postgres --project <project>
422
697
  synapsor sources doctor app_postgres --project <project>
423
698
  synapsor sources disable app_postgres --project <project> --yes
699
+ synapsor mcp audit ./tools-manifest.json
700
+ synapsor mcp audit https://mcp.example.com --bearer-env MCP_AUDIT_TOKEN --json
701
+ synapsor mcp audit 'stdio:node ./server.mjs' --timeout-ms 5000
424
702
  synapsor sql --db <database> "SELECT 1;"
425
703
  synapsor sql --db <database> --file ./query.sql
426
704
  synapsor invoke <capability> --db <database> --json '{"ticket_id":"TICK-1001"}'
@@ -430,6 +708,13 @@ Usage:
430
708
  synapsor proposal reject <proposal-handle> --db <database> --yes
431
709
  synapsor replay <run-id> --db <database>
432
710
 
711
+ Preview/dev CDC commands:
712
+ These are not the current public customer connector path. Use live_read unless you are running a reviewed dev/test CDC smoke.
713
+ synapsor sources import app_postgres --schema public --tables tickets --tenant-column tenant_id --mode cdc_mirror --cdc-ack --project <project>
714
+ synapsor sources cdc-snapshot app_postgres --tables external_support.tickets --project <project>
715
+ synapsor sources cdc-poll app_postgres --tables external_support.tickets --project <project>
716
+ synapsor sources cdc-status app_postgres --project <project>
717
+
433
718
  Environment:
434
719
  SYNAPSOR_API_KEY, SYNAPSOR_BASE_URL
435
720
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapsor/client",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Node.js SDK for Synapsor, the agent-native database for auditable AI applications",
5
5
  "type": "module",
6
6
  "main": "./synapsor.mjs",
package/synapsor.mjs CHANGED
@@ -19,6 +19,17 @@ export {
19
19
  PROTOCOL_VERSION,
20
20
  };
21
21
 
22
+ export const WRITEBACK_LEASE_EXPIRED = "WRITEBACK_LEASE_EXPIRED";
23
+ export const WRITEBACK_ALREADY_COMPLETED = "WRITEBACK_ALREADY_COMPLETED";
24
+ export const WRITEBACK_CONFLICT = "WRITEBACK_CONFLICT";
25
+ export const WRITEBACK_SAFETY_FAILURE = "WRITEBACK_SAFETY_FAILURE";
26
+ export const WRITEBACK_ERROR_CODES = Object.freeze([
27
+ WRITEBACK_LEASE_EXPIRED,
28
+ WRITEBACK_ALREADY_COMPLETED,
29
+ WRITEBACK_CONFLICT,
30
+ WRITEBACK_SAFETY_FAILURE,
31
+ ]);
32
+
22
33
  export class SynapsorError extends Error {
23
34
  constructor(
24
35
  message,
@@ -80,6 +91,7 @@ export class Synapsor {
80
91
  this.apiKey = apiKey;
81
92
  this.process = process;
82
93
  this.timeoutMs = timeoutMs;
94
+ this.agentActivity = new AgentActivityNamespace(this);
83
95
  this.agentRuns = new AgentRunsNamespace(this);
84
96
  this.externalActions = new ExternalActionsNamespace(this);
85
97
  this.evals = new AgentEvalsNamespace(this);
@@ -268,6 +280,13 @@ export class Synapsor {
268
280
  return results.at(-1).result?.rows ?? [];
269
281
  }
270
282
 
283
+ async searchAgentActivity(filters = {}, options = {}) {
284
+ return this.query(buildAgentActivitySearchSql(filters, options), {
285
+ session: options.session,
286
+ asOf: options.asOf ?? options.as_of,
287
+ });
288
+ }
289
+
271
290
  async invokeAgentCapability(capability, args = {}, {
272
291
  session = undefined,
273
292
  traceId = undefined,
@@ -636,6 +655,10 @@ export class Synapsor {
636
655
  });
637
656
  }
638
657
 
658
+ async listAdapterTools(adapter, options = {}) {
659
+ return this.adapterTools(adapter, options);
660
+ }
661
+
639
662
  async callAdapterTool(adapter, tool, {
640
663
  arguments: args = {},
641
664
  runId = undefined,
@@ -661,6 +684,138 @@ export class Synapsor {
661
684
  return this.request("POST", "/v1/agent/adapters/call-tool", payload);
662
685
  }
663
686
 
687
+ async createWritebackRunnerToken(sourceId, {
688
+ runnerId = undefined,
689
+ runner_id = undefined,
690
+ tokenId = undefined,
691
+ token_id = undefined,
692
+ permissions = undefined,
693
+ expiresAt = undefined,
694
+ expires_at = undefined,
695
+ } = {}) {
696
+ const payload = {};
697
+ const runner = runnerId ?? runner_id;
698
+ const token = tokenId ?? token_id;
699
+ const expiry = expiresAt ?? expires_at;
700
+ if (runner !== undefined) payload.runner_id = runner;
701
+ if (token !== undefined) payload.token_id = token;
702
+ if (permissions !== undefined) payload.permissions = permissions;
703
+ if (expiry !== undefined) payload.expires_at = expiry;
704
+ return this.request("POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens`, payload);
705
+ }
706
+
707
+ async createRunnerToken(sourceId, options = {}) {
708
+ return this.createWritebackRunnerToken(sourceId, options);
709
+ }
710
+
711
+ async listWritebackRunnerTokens(sourceId) {
712
+ return this.request("GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens`);
713
+ }
714
+
715
+ async listWritebackRunners(sourceId) {
716
+ return this.request("GET", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runners`);
717
+ }
718
+
719
+ async listRunners(sourceId) {
720
+ return this.listWritebackRunners(sourceId);
721
+ }
722
+
723
+ async revokeWritebackRunnerToken(sourceId, tokenId, { reason = undefined } = {}) {
724
+ const payload = {};
725
+ if (reason !== undefined) payload.reason = reason;
726
+ return this.request("POST", `/v1/control/external-sources/${encodeURIComponent(sourceId)}/writeback-runner-tokens/${encodeURIComponent(tokenId)}/revoke`, payload);
727
+ }
728
+
729
+ async listExternalWritebackProposals({
730
+ projectId = undefined,
731
+ project_id = undefined,
732
+ sourceId = undefined,
733
+ source_id = undefined,
734
+ status = undefined,
735
+ limit = undefined,
736
+ } = {}) {
737
+ return this.request("GET", withQuery("/v1/control/external-writebacks/proposals", {
738
+ project_id: projectId ?? project_id,
739
+ source_id: sourceId ?? source_id,
740
+ status,
741
+ limit,
742
+ }));
743
+ }
744
+
745
+ async getExternalWritebackProposal(proposalId) {
746
+ return this.request("GET", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}`);
747
+ }
748
+
749
+ async getProposal(proposalId) {
750
+ return this.getExternalWritebackProposal(proposalId);
751
+ }
752
+
753
+ async createExternalWritebackProposal(payload) {
754
+ return this.request("POST", "/v1/control/external-writebacks/proposals", payload);
755
+ }
756
+
757
+ async transitionExternalWritebackProposal(proposalId, action, payload = {}) {
758
+ return this.request("POST", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}/${encodeURIComponent(action)}`, payload);
759
+ }
760
+
761
+ async approveExternalWritebackProposal(proposalId, payload = {}) {
762
+ return this.transitionExternalWritebackProposal(proposalId, "approve", payload);
763
+ }
764
+
765
+ async rejectExternalWritebackProposal(proposalId, payload = {}) {
766
+ return this.transitionExternalWritebackProposal(proposalId, "reject", payload);
767
+ }
768
+
769
+ async recordExternalWritebackApplyResult(proposalId, payload) {
770
+ return this.request("POST", `/v1/control/external-writebacks/proposals/${encodeURIComponent(proposalId)}/apply-result`, payload);
771
+ }
772
+
773
+ async doctorWritebackRunner() {
774
+ return this.request("GET", "/v1/writeback/runner/doctor");
775
+ }
776
+
777
+ async registerRunner(payload) {
778
+ return this.request("POST", "/v1/runner/register", payload);
779
+ }
780
+
781
+ async heartbeatRunner(payload) {
782
+ return this.request("POST", "/v1/runner/heartbeat", payload);
783
+ }
784
+
785
+ async claimWritebackJob({
786
+ sourceId = undefined,
787
+ source_id = undefined,
788
+ limit = undefined,
789
+ leaseSeconds = undefined,
790
+ lease_seconds = undefined,
791
+ } = {}) {
792
+ const payload = {};
793
+ const source = sourceId ?? source_id;
794
+ const lease = leaseSeconds ?? lease_seconds;
795
+ if (source !== undefined) payload.source_id = source;
796
+ if (limit !== undefined) payload.limit = Number(limit);
797
+ if (lease !== undefined) payload.lease_seconds = Number(lease);
798
+ return this.request("POST", "/v1/writeback/jobs/claim", payload);
799
+ }
800
+
801
+ async renewWritebackLease(jobId, {
802
+ leaseSeconds = undefined,
803
+ lease_seconds = undefined,
804
+ } = {}) {
805
+ const payload = {};
806
+ const lease = leaseSeconds ?? lease_seconds;
807
+ if (lease !== undefined) payload.lease_seconds = Number(lease);
808
+ return this.request("POST", `/v1/writeback/jobs/${encodeURIComponent(jobId)}/heartbeat`, payload);
809
+ }
810
+
811
+ async completeWritebackJob(jobId, result) {
812
+ return this.request("POST", `/v1/writeback/jobs/${encodeURIComponent(jobId)}/result`, result);
813
+ }
814
+
815
+ async recordWritebackJobResult(jobId, result) {
816
+ return this.completeWritebackJob(jobId, result);
817
+ }
818
+
664
819
  async traceAgentRun(runId, { session = undefined } = {}) {
665
820
  return this.request("POST", "/v1/agent/runs/trace", {
666
821
  run_id: runId,
@@ -1045,6 +1200,10 @@ export class Synapsor {
1045
1200
  return this.request("POST", "/v1/agent/runs/replay", payload);
1046
1201
  }
1047
1202
 
1203
+ async getReplay(runId, options = {}) {
1204
+ return this.replayAgentRun(runId, options);
1205
+ }
1206
+
1048
1207
  async proposalLifecycle(action, proposal, { session = undefined, promoteBranch = undefined, promote_branch = undefined, targetBranch = undefined, target_branch = undefined, settlementPolicy = undefined, settlement_policy = undefined } = {}) {
1049
1208
  const payload = {
1050
1209
  proposal,
@@ -1131,6 +1290,16 @@ export class Synapsor {
1131
1290
  }
1132
1291
  }
1133
1292
 
1293
+ class AgentActivityNamespace {
1294
+ constructor(client) {
1295
+ this.client = client;
1296
+ }
1297
+
1298
+ async search(filters = {}, options = {}) {
1299
+ return this.client.searchAgentActivity(filters, options);
1300
+ }
1301
+ }
1302
+
1134
1303
  class AgentRunsNamespace {
1135
1304
  constructor(client) {
1136
1305
  this.client = client;
@@ -1396,6 +1565,60 @@ function typedSql(value) {
1396
1565
  return `${identifier(ref.type)}(${sqlLiteral(String(ref.id ?? ""))})`;
1397
1566
  }
1398
1567
 
1568
+ function buildAgentActivitySearchSql(filters = {}, options = {}) {
1569
+ const where = [];
1570
+ const addFilter = (column, ...keys) => {
1571
+ for (const key of keys) {
1572
+ if (filters[key] !== undefined && filters[key] !== null && filters[key] !== "") {
1573
+ where.push(`${column} = ${sqlScalar(filters[key])}`);
1574
+ return;
1575
+ }
1576
+ }
1577
+ };
1578
+ addFilter("activity_kind", "activityKind", "activity_kind");
1579
+ addFilter("tenant_id", "tenantId", "tenant_id");
1580
+ addFilter("principal", "principal");
1581
+ addFilter("workflow", "workflow");
1582
+ addFilter("capability", "capability");
1583
+ addFilter("run_id", "runId", "run_id");
1584
+ addFilter("evidence_bundle_id", "evidenceBundleId", "evidence_bundle_id");
1585
+ addFilter("proposal_id", "proposalId", "proposal_id");
1586
+ addFilter("replay_id", "replayId", "replay_id");
1587
+ addFilter("business_object_type", "businessObjectType", "business_object_type");
1588
+ addFilter("business_object_id", "businessObjectId", "business_object_id");
1589
+ addFilter("query_fingerprint", "queryFingerprint", "query_fingerprint");
1590
+
1591
+ const timeRange = filters.timeRange ?? filters.time_range ?? {};
1592
+ const from = timeRange.from ?? filters.createdFrom ?? filters.created_from;
1593
+ const to = timeRange.to ?? filters.createdTo ?? filters.created_to;
1594
+ if (from !== undefined && from !== null && from !== "") {
1595
+ where.push(`created_at >= ${sqlScalar(from)}`);
1596
+ }
1597
+ if (to !== undefined && to !== null && to !== "") {
1598
+ where.push(`created_at <= ${sqlScalar(to)}`);
1599
+ }
1600
+
1601
+ const requestedLimit = options.limit ?? filters.limit ?? 50;
1602
+ const limit = Number.isInteger(Number(requestedLimit)) && Number(requestedLimit) > 0
1603
+ ? Math.min(Number(requestedLimit), 500)
1604
+ : 50;
1605
+ const whereSql = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
1606
+ return `SELECT * FROM AGENT ACTIVITY${whereSql} ORDER BY created_at DESC LIMIT ${limit};`;
1607
+ }
1608
+
1609
+ function sqlScalar(value) {
1610
+ if (typeof value === "number" && Number.isFinite(value)) {
1611
+ return String(Math.trunc(value));
1612
+ }
1613
+ if (typeof value === "bigint") {
1614
+ return String(value);
1615
+ }
1616
+ if (typeof value === "boolean") {
1617
+ return value ? "TRUE" : "FALSE";
1618
+ }
1619
+ return sqlLiteral(String(value));
1620
+ }
1621
+
1399
1622
  function sqlLiteral(value) {
1400
1623
  return `'${String(value).replaceAll("'", "''")}'`;
1401
1624
  }
@@ -1407,6 +1630,17 @@ function identifier(value) {
1407
1630
  return value;
1408
1631
  }
1409
1632
 
1633
+ function withQuery(path, params) {
1634
+ const query = new URLSearchParams();
1635
+ for (const [key, value] of Object.entries(params)) {
1636
+ if (value !== undefined && value !== null && value !== "") {
1637
+ query.set(key, String(value));
1638
+ }
1639
+ }
1640
+ const queryText = query.toString();
1641
+ return queryText ? `${path}?${queryText}` : path;
1642
+ }
1643
+
1410
1644
  function repoRoot() {
1411
1645
  return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
1412
1646
  }